new [keycloak]: WiP user enrollment added
- also moved the keycloak specific functionalities to a behaviour - added new role permission (org admin)keycloak
parent
efe3765609
commit
f60e411af1
|
@ -0,0 +1,18 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
|
||||||
|
class RolesPermOrgAdmin extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function change()
|
||||||
|
{
|
||||||
|
$table = $this->table('roles')
|
||||||
|
->addColumn('perm_org_admin', 'boolean', [
|
||||||
|
'default' => 0,
|
||||||
|
'null' => false,
|
||||||
|
])
|
||||||
|
->update();
|
||||||
|
}
|
||||||
|
}
|
|
@ -140,8 +140,14 @@ class CRUDComponent extends Component
|
||||||
$patchEntityParams['fields'] = $params['fields'];
|
$patchEntityParams['fields'] = $params['fields'];
|
||||||
}
|
}
|
||||||
$data = $this->Table->patchEntity($data, $input, $patchEntityParams);
|
$data = $this->Table->patchEntity($data, $input, $patchEntityParams);
|
||||||
|
if (isset($params['beforeSave'])) {
|
||||||
|
$data = $params['beforeSave']($data);
|
||||||
|
}
|
||||||
$savedData = $this->Table->save($data);
|
$savedData = $this->Table->save($data);
|
||||||
if ($savedData !== false) {
|
if ($savedData !== false) {
|
||||||
|
if (isset($params['afterSave'])) {
|
||||||
|
$params['afterSave']($data);
|
||||||
|
}
|
||||||
$message = __('{0} added.', $this->ObjectAlias);
|
$message = __('{0} added.', $this->ObjectAlias);
|
||||||
if (!empty($input['metaFields'])) {
|
if (!empty($input['metaFields'])) {
|
||||||
$this->saveMetaFields($data->id, $input);
|
$this->saveMetaFields($data->id, $input);
|
||||||
|
@ -257,8 +263,14 @@ class CRUDComponent extends Component
|
||||||
$patchEntityParams['fields'] = $params['fields'];
|
$patchEntityParams['fields'] = $params['fields'];
|
||||||
}
|
}
|
||||||
$data = $this->Table->patchEntity($data, $input, $patchEntityParams);
|
$data = $this->Table->patchEntity($data, $input, $patchEntityParams);
|
||||||
|
if (isset($params['beforeSave'])) {
|
||||||
|
$data = $params['beforeSave']($data);
|
||||||
|
}
|
||||||
$savedData = $this->Table->save($data);
|
$savedData = $this->Table->save($data);
|
||||||
if ($savedData !== false) {
|
if ($savedData !== false) {
|
||||||
|
if (isset($params['afterSave'])) {
|
||||||
|
$params['afterSave']($data);
|
||||||
|
}
|
||||||
$message = __('{0} `{1}` updated.', $this->ObjectAlias, $savedData->{$this->Table->getDisplayField()});
|
$message = __('{0} `{1}` updated.', $this->ObjectAlias, $savedData->{$this->Table->getDisplayField()});
|
||||||
if (!empty($input['metaFields'])) {
|
if (!empty($input['metaFields'])) {
|
||||||
$this->MetaFields->deleteAll(['scope' => $this->Table->metaFields, 'parent_id' => $savedData->id]);
|
$this->MetaFields->deleteAll(['scope' => $this->Table->metaFields, 'parent_id' => $savedData->id]);
|
||||||
|
|
|
@ -15,7 +15,7 @@ class RolesController extends AppController
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
$this->CRUD->index([
|
$this->CRUD->index([
|
||||||
'filters' => ['name', 'uuid', 'perm_admin', 'Users.id'],
|
'filters' => ['name', 'uuid', 'perm_admin', 'perm_org_admin', 'Users.id'],
|
||||||
'quickFilters' => ['name']
|
'quickFilters' => ['name']
|
||||||
]);
|
]);
|
||||||
$responsePayload = $this->CRUD->getResponsePayload();
|
$responsePayload = $this->CRUD->getResponsePayload();
|
||||||
|
|
|
@ -25,7 +25,12 @@ class UsersController extends AppController
|
||||||
|
|
||||||
public function add()
|
public function add()
|
||||||
{
|
{
|
||||||
$this->CRUD->add();
|
$this->CRUD->add([
|
||||||
|
'beforeSave' => function($data) {
|
||||||
|
$this->Users->enrollUserRouter($data);
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
]);
|
||||||
$responsePayload = $this->CRUD->getResponsePayload();
|
$responsePayload = $this->CRUD->getResponsePayload();
|
||||||
if (!empty($responsePayload)) {
|
if (!empty($responsePayload)) {
|
||||||
return $responsePayload;
|
return $responsePayload;
|
||||||
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
<?php
|
||||||
|
namespace App\Event;
|
||||||
|
|
||||||
|
use ADmad\SocialAuth\Middleware\SocialAuthMiddleware;
|
||||||
|
use Cake\Datasource\EntityInterface;
|
||||||
|
use Cake\Event\EventInterface;
|
||||||
|
use Cake\Event\EventListenerInterface;
|
||||||
|
use Cake\Http\ServerRequest;
|
||||||
|
use Cake\I18n\FrozenTime;
|
||||||
|
use Cake\ORM\Locator\LocatorAwareTrait;
|
||||||
|
|
||||||
|
class SocialAuthListener implements EventListenerInterface
|
||||||
|
{
|
||||||
|
use LocatorAwareTrait;
|
||||||
|
|
||||||
|
public function implementedEvents(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
SocialAuthMiddleware::EVENT_AFTER_IDENTIFY => 'afterIdentify',
|
||||||
|
SocialAuthMiddleware::EVENT_BEFORE_REDIRECT => 'beforeRedirect',
|
||||||
|
// Uncomment below if you want to use the event listener to return
|
||||||
|
// an entity for a new user instead of directly using `createUser()` table method.
|
||||||
|
// SocialAuthMiddleware::EVENT_CREATE_USER => 'createUser',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function afterIdentify(EventInterface $event, EntityInterface $user): EntityInterface
|
||||||
|
{
|
||||||
|
// Update last login time
|
||||||
|
// $user->set('last_login', new FrozenTime());
|
||||||
|
|
||||||
|
// You can access the profile using $user->social_profile
|
||||||
|
|
||||||
|
$this->getTableLocator()->get('Users')->saveOrFail($user);
|
||||||
|
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param \Cake\Event\EventInterface $event
|
||||||
|
* @param string|array $url
|
||||||
|
* @param string $status
|
||||||
|
* @param \Cake\Http\ServerRequest $request
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function beforeRedirect(EventInterface $event, $url, string $status, ServerRequest $request): void
|
||||||
|
{
|
||||||
|
$messages = (array)$request->getSession()->read('Flash.flash');
|
||||||
|
|
||||||
|
// Set flash message
|
||||||
|
switch ($status) {
|
||||||
|
case SocialAuthMiddleware::AUTH_STATUS_SUCCESS:
|
||||||
|
$messages[] = [
|
||||||
|
'message' => __('You are now logged in'),
|
||||||
|
'key' => 'flash',
|
||||||
|
'element' => 'flash/success',
|
||||||
|
'params' => [],
|
||||||
|
];
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Auth through provider failed. Details will be logged in
|
||||||
|
// `error.log` if `logErrors` option is set to `true`.
|
||||||
|
case SocialAuthMiddleware::AUTH_STATUS_PROVIDER_FAILURE:
|
||||||
|
|
||||||
|
// Table finder failed to return user record. An e.g. of this is a
|
||||||
|
// user has been authenticated through provider but your finder has
|
||||||
|
// a condition to not return an inactivated user.
|
||||||
|
case SocialAuthMiddleware::AUTH_STATUS_FINDER_FAILURE:
|
||||||
|
$messages[] = [
|
||||||
|
'message' => __('Authentication failed'),
|
||||||
|
'key' => 'flash',
|
||||||
|
'element' => 'flash/error',
|
||||||
|
'params' => [],
|
||||||
|
];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$request->getSession()->write('Flash.flash', $messages);
|
||||||
|
|
||||||
|
// You can return a modified redirect URL if needed.
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createUser(EventInterface $event, EntityInterface $profile, Session $session): EntityInterface
|
||||||
|
{
|
||||||
|
// Create and save entity for new user as shown in "createUser()" method above
|
||||||
|
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,168 @@
|
||||||
|
<?php
|
||||||
|
namespace App\Model\Behavior;
|
||||||
|
|
||||||
|
use ArrayObject;
|
||||||
|
use Cake\Datasource\EntityInterface;
|
||||||
|
use Cake\Event\EventInterface;
|
||||||
|
use Cake\ORM\Behavior;
|
||||||
|
use Cake\ORM\Entity;
|
||||||
|
use Cake\ORM\Query;
|
||||||
|
use Cake\Utility\Text;
|
||||||
|
use \Cake\Http\Session;
|
||||||
|
use Cake\Core\Configure;
|
||||||
|
use Cake\Http\Client;
|
||||||
|
use Cake\Http\Client\FormData;
|
||||||
|
|
||||||
|
class AuthKeycloakBehavior extends Behavior
|
||||||
|
{
|
||||||
|
|
||||||
|
public function getUser(EntityInterface $profile, Session $session)
|
||||||
|
{
|
||||||
|
$userId = $session->read('Auth.User.id');
|
||||||
|
if ($userId) {
|
||||||
|
return $this->_table->get($userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
$raw_profile_payload = $profile->access_token->getJwt()->getPayload();
|
||||||
|
$user = $this->extractProfileData($raw_profile_payload);
|
||||||
|
if (!$user) {
|
||||||
|
throw new \RuntimeException('Unable to save new user');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function extractProfileData($profile_payload)
|
||||||
|
{
|
||||||
|
$mapping = Configure::read('keycloak.mapping');
|
||||||
|
$fields = [
|
||||||
|
'org_uuid' => 'org_uuid',
|
||||||
|
'role_name' => 'role_name',
|
||||||
|
'username' => 'preferred_username',
|
||||||
|
'email' => 'email',
|
||||||
|
'first_name' => 'given_name',
|
||||||
|
'last_name' => 'family_name'
|
||||||
|
];
|
||||||
|
foreach ($fields as $field => $default) {
|
||||||
|
if (!empty($mapping[$field])) {
|
||||||
|
$fields[$field] = $mapping[$field];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$user = [
|
||||||
|
'individual' => [
|
||||||
|
'email' => $profile_payload[$fields['email']],
|
||||||
|
'first_name' => $profile_payload[$fields['first_name']],
|
||||||
|
'last_name' => $profile_payload[$fields['last_name']]
|
||||||
|
],
|
||||||
|
'user' => [
|
||||||
|
'username' => $profile_payload[$fields['username']],
|
||||||
|
],
|
||||||
|
'organisation' => [
|
||||||
|
'uuid' => $profile_payload[$fields['org_uuid']],
|
||||||
|
],
|
||||||
|
'role' => [
|
||||||
|
'name' => $profile_payload[$fields['role_name']],
|
||||||
|
]
|
||||||
|
];
|
||||||
|
$user['user']['individual_id'] = $this->_table->captureIndividual($user);
|
||||||
|
$user['user']['role_id'] = $this->_table->captureRole($user);
|
||||||
|
$existingUser = $this->_table->find()->where(['username' => $user['user']['username']])->first();
|
||||||
|
if (empty($existingUser)) {
|
||||||
|
$user['user']['password'] = Security::randomString(16);
|
||||||
|
$existingUser = $this->_table->newEntity($user['user']);
|
||||||
|
if (!$this->save($existingUser)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$dirty = false;
|
||||||
|
if ($user['user']['individual_id'] != $existingUser['individual_id']) {
|
||||||
|
$existingUser['individual_id'] = $user['user']['individual_id'];
|
||||||
|
$dirty = true;
|
||||||
|
}
|
||||||
|
if ($user['user']['role_id'] != $existingUser['role_id']) {
|
||||||
|
$existingUser['role_id'] = $user['user']['role_id'];
|
||||||
|
$dirty = true;
|
||||||
|
}
|
||||||
|
$existingUser;
|
||||||
|
if ($dirty) {
|
||||||
|
if (!$this->_table->save($existingUser)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $existingUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function enrollUser($data): bool
|
||||||
|
{
|
||||||
|
$individual = $this->_table->Individuals->find()->where(
|
||||||
|
['id' => $data['individual_id']]
|
||||||
|
)->contain(['Organisations'])->first();
|
||||||
|
$roleConditions = [
|
||||||
|
'id' => $data['role_id']
|
||||||
|
];
|
||||||
|
if (!empty(Configure::read('keycloak.user_management.actions'))) {
|
||||||
|
$roleConditions['name'] = Configure::read('keycloak.default_role_name');
|
||||||
|
}
|
||||||
|
$role = $this->_table->Roles->find()->where($roleConditions)->first();
|
||||||
|
$orgs = [];
|
||||||
|
foreach ($individual['organisations'] as $org) {
|
||||||
|
$orgs[] = $org['uuid'];
|
||||||
|
}
|
||||||
|
$token = $this->getAdminAccessToken();
|
||||||
|
$keyCloakUser = [
|
||||||
|
'firstName' => $individual['first_name'],
|
||||||
|
'lastName' => $individual['last_name'],
|
||||||
|
'username' => $data['username'],
|
||||||
|
'email' => $individual['email'],
|
||||||
|
'attributes' => [
|
||||||
|
'role_name' => empty($role['name']) ? Configure::read('keycloak.default_role_name') : $role['name'],
|
||||||
|
'org_uuid' => empty($orgs[0]) ? '' : $orgs[0]
|
||||||
|
]
|
||||||
|
];
|
||||||
|
$keycloakConfig = Configure::read('keycloak');
|
||||||
|
$http = new Client();
|
||||||
|
$url = sprintf(
|
||||||
|
'%s/admin/realms/%s/users',
|
||||||
|
$keycloakConfig['provider']['baseUrl'],
|
||||||
|
$keycloakConfig['provider']['realm']
|
||||||
|
);
|
||||||
|
$response = $http->post(
|
||||||
|
$url,
|
||||||
|
json_encode($keyCloakUser),
|
||||||
|
[
|
||||||
|
'headers' => [
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
'Authorization' => 'Bearer ' . $token
|
||||||
|
]
|
||||||
|
]
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getAdminAccessToken()
|
||||||
|
{
|
||||||
|
$keycloakConfig = Configure::read('keycloak');
|
||||||
|
$http = new Client();
|
||||||
|
$tokenUrl = sprintf(
|
||||||
|
'%s/realms/%s/protocol/openid-connect/token',
|
||||||
|
$keycloakConfig['provider']['baseUrl'],
|
||||||
|
$keycloakConfig['provider']['realm']
|
||||||
|
);
|
||||||
|
$response = $http->post(
|
||||||
|
$tokenUrl,
|
||||||
|
sprintf(
|
||||||
|
'grant_type=client_credentials&client_id=%s&client_secret=%s',
|
||||||
|
urlencode(Configure::read('keycloak.provider.applicationId')),
|
||||||
|
urlencode(Configure::read('keycloak.provider.applicationSecret'))
|
||||||
|
),
|
||||||
|
[
|
||||||
|
'headers' => [
|
||||||
|
'Content-Type' => 'application/x-www-form-urlencoded'
|
||||||
|
]
|
||||||
|
]
|
||||||
|
);
|
||||||
|
$parsedResponse = json_decode($response->getStringBody(), true);
|
||||||
|
return $parsedResponse['access_token'];
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,6 +19,7 @@ class UsersTable extends AppTable
|
||||||
{
|
{
|
||||||
parent::initialize($config);
|
parent::initialize($config);
|
||||||
$this->addBehavior('UUID');
|
$this->addBehavior('UUID');
|
||||||
|
$this->initAuthBehaviors();
|
||||||
$this->belongsTo(
|
$this->belongsTo(
|
||||||
'Individuals',
|
'Individuals',
|
||||||
[
|
[
|
||||||
|
@ -36,6 +37,13 @@ class UsersTable extends AppTable
|
||||||
$this->setDisplayField('username');
|
$this->setDisplayField('username');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function initAuthBehaviors()
|
||||||
|
{
|
||||||
|
if (!empty(Configure::read('keycloak'))) {
|
||||||
|
$this->addBehavior('AuthKeycloak');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function validationDefault(Validator $validator): Validator
|
public function validationDefault(Validator $validator): Validator
|
||||||
{
|
{
|
||||||
$validator
|
$validator
|
||||||
|
@ -99,68 +107,7 @@ class UsersTable extends AppTable
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function extractKeycloakProfileData($profile_payload)
|
public function captureIndividual($user): int
|
||||||
{
|
|
||||||
$mapping = Configure::read('keycloak.mapping');
|
|
||||||
$fields = [
|
|
||||||
'org_uuid' => 'org_uuid',
|
|
||||||
'role_name' => 'role_name',
|
|
||||||
'username' => 'preferred_username',
|
|
||||||
'email' => 'email',
|
|
||||||
'first_name' => 'given_name',
|
|
||||||
'last_name' => 'family_name'
|
|
||||||
];
|
|
||||||
foreach ($fields as $field => $default) {
|
|
||||||
if (!empty($mapping[$field])) {
|
|
||||||
$fields[$field] = $mapping[$field];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$user = [
|
|
||||||
'individual' => [
|
|
||||||
'email' => $profile_payload[$fields['email']],
|
|
||||||
'first_name' => $profile_payload[$fields['first_name']],
|
|
||||||
'last_name' => $profile_payload[$fields['last_name']]
|
|
||||||
],
|
|
||||||
'user' => [
|
|
||||||
'username' => $profile_payload[$fields['username']],
|
|
||||||
],
|
|
||||||
'organisation' => [
|
|
||||||
'uuid' => $profile_payload[$fields['org_uuid']],
|
|
||||||
],
|
|
||||||
'role' => [
|
|
||||||
'name' => $profile_payload[$fields['role_name']],
|
|
||||||
]
|
|
||||||
];
|
|
||||||
$user['user']['individual_id'] = $this->captureIndividual($user);
|
|
||||||
$user['user']['role_id'] = $this->captureRole($user);
|
|
||||||
$existingUser = $this->find()->where(['username' => $user['user']['username']])->first();
|
|
||||||
if (empty($existingUser)) {
|
|
||||||
$user['user']['password'] = Security::randomString(16);
|
|
||||||
$existingUser = $this->newEntity($user['user']);
|
|
||||||
if (!$this->save($existingUser)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$dirty = false;
|
|
||||||
if ($user['user']['individual_id'] != $existingUser['individual_id']) {
|
|
||||||
$existingUser['individual_id'] = $user['user']['individual_id'];
|
|
||||||
$dirty = true;
|
|
||||||
}
|
|
||||||
if ($user['user']['role_id'] != $existingUser['role_id']) {
|
|
||||||
$existingUser['role_id'] = $user['user']['role_id'];
|
|
||||||
$dirty = true;
|
|
||||||
}
|
|
||||||
$existingUser;
|
|
||||||
if ($dirty) {
|
|
||||||
if (!$this->save($existingUser)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $existingUser;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function captureIndividual($user)
|
|
||||||
{
|
{
|
||||||
$individual = $this->Individuals->find()->where(['email' => $user['individual']['email']])->first();
|
$individual = $this->Individuals->find()->where(['email' => $user['individual']['email']])->first();
|
||||||
if (empty($individual)) {
|
if (empty($individual)) {
|
||||||
|
@ -172,7 +119,7 @@ class UsersTable extends AppTable
|
||||||
return $individual->id;
|
return $individual->id;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function captureOrganisation($user)
|
public function captureOrganisation($user): int
|
||||||
{
|
{
|
||||||
$organisation = $this->Organisations->find()->where(['uuid' => $user['organisation']['uuid']])->first();
|
$organisation = $this->Organisations->find()->where(['uuid' => $user['organisation']['uuid']])->first();
|
||||||
if (empty($organisation)) {
|
if (empty($organisation)) {
|
||||||
|
@ -185,7 +132,7 @@ class UsersTable extends AppTable
|
||||||
return $organisation->id;
|
return $organisation->id;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function captureRole($user)
|
public function captureRole($user): int
|
||||||
{
|
{
|
||||||
$role = $this->Roles->find()->where(['name' => $user['role']['name']])->first();
|
$role = $this->Roles->find()->where(['name' => $user['role']['name']])->first();
|
||||||
if (empty($role)) {
|
if (empty($role)) {
|
||||||
|
@ -200,19 +147,10 @@ class UsersTable extends AppTable
|
||||||
return $role->id;
|
return $role->id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getUser(EntityInterface $profile, Session $session)
|
public function enrollUserRouter($data): void
|
||||||
{
|
{
|
||||||
$userId = $session->read('Auth.User.id');
|
if (!empty(Configure::read('keycloak'))) {
|
||||||
if ($userId) {
|
$this->enrollUser($data);
|
||||||
return $this->get($userId);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
$raw_profile_payload = $profile->access_token->getJwt()->getPayload();
|
|
||||||
$user = $this->extractKeycloakProfileData($raw_profile_payload);
|
|
||||||
if (!$user) {
|
|
||||||
throw new \RuntimeException('Unable to save new user');
|
|
||||||
}
|
|
||||||
|
|
||||||
return $user;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,11 @@
|
||||||
'type' => 'checkbox',
|
'type' => 'checkbox',
|
||||||
'label' => 'Full admin privilege'
|
'label' => 'Full admin privilege'
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'field' => 'perm_org_admin',
|
||||||
|
'type' => 'checkbox',
|
||||||
|
'label' => 'Organisation admin privilege'
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'field' => 'perm_sync',
|
'field' => 'perm_sync',
|
||||||
'type' => 'checkbox',
|
'type' => 'checkbox',
|
||||||
|
|
|
@ -47,6 +47,12 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
||||||
'data_path' => 'perm_admin',
|
'data_path' => 'perm_admin',
|
||||||
'element' => 'boolean'
|
'element' => 'boolean'
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'name' => __('Org Admin'),
|
||||||
|
'sort' => 'perm_org_admin',
|
||||||
|
'data_path' => 'perm_org_admin',
|
||||||
|
'element' => 'boolean'
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'name' => __('Sync'),
|
'name' => __('Sync'),
|
||||||
'sort' => 'perm_sync',
|
'sort' => 'perm_sync',
|
||||||
|
|
|
@ -17,6 +17,11 @@ echo $this->element(
|
||||||
'path' => 'perm_admin',
|
'path' => 'perm_admin',
|
||||||
'type' => 'boolean'
|
'type' => 'boolean'
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'key' => __('Organisation admin permission'),
|
||||||
|
'path' => 'perm_org_admin',
|
||||||
|
'type' => 'boolean'
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'key' => __('Sync permission'),
|
'key' => __('Sync permission'),
|
||||||
'path' => 'perm_sync',
|
'path' => 'perm_sync',
|
||||||
|
|
Loading…
Reference in New Issue