From af622dd19b214aeffbefbb7d3831c858e454925b Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Fri, 9 Dec 2022 11:53:20 +0100 Subject: [PATCH] new: [users:view] Added keycloak status showing the potential differences between Cerebrate and Keycloak --- src/Controller/UsersController.php | 23 ++- src/Model/Behavior/AuthKeycloakBehavior.php | 132 +++++++++++++----- src/Model/Table/UsersTable.php | 12 -- templates/Users/index.php | 9 ++ templates/Users/view.php | 20 ++- .../IndexTable/Fields/keycloak_status.php | 14 ++ .../Fields/keycloakStatusField.php | 21 +++ 7 files changed, 178 insertions(+), 53 deletions(-) create mode 100644 templates/element/genericElements/IndexTable/Fields/keycloak_status.php create mode 100644 templates/element/genericElements/SingleViews/Fields/keycloakStatusField.php diff --git a/src/Controller/UsersController.php b/src/Controller/UsersController.php index 78bd5ac..989025c 100644 --- a/src/Controller/UsersController.php +++ b/src/Controller/UsersController.php @@ -21,11 +21,22 @@ class UsersController extends AppController if (empty($currentUser['role']['perm_admin'])) { $conditions['organisation_id'] = $currentUser['organisation_id']; } + $keycloakUsersParsed = null; + if (!empty(Configure::read('keycloak.enabled'))) { + $keycloakUsersParsed = $this->Users->getParsedKeycloakUser(); + } $this->CRUD->index([ 'contain' => $this->containFields, 'filters' => $this->filterFields, 'quickFilters' => $this->quickFilterFields, - 'conditions' => $conditions + 'conditions' => $conditions, + 'afterFind' => function($data) use ($keycloakUsersParsed) { + // if (!empty(Configure::read('keycloak.enabled'))) { + // $keycloakUser = $keycloakUsersParsed[$data->username]; + // $data['keycloak_status'] = array_values($this->Users->checkKeycloakStatus([$data->toArray()], [$keycloakUser]))[0]; + // } + return $data; + } ]); $responsePayload = $this->CRUD->getResponsePayload(); if (!empty($responsePayload)) { @@ -139,10 +150,18 @@ class UsersController extends AppController if (empty($id) || (empty($currentUser['role']['perm_org_admin']) && empty($currentUser['role']['perm_admin']))) { $id = $this->ACL->getUser()['id']; } + $keycloakUsersParsed = null; + if (!empty(Configure::read('keycloak.enabled'))) { + $keycloakUsersParsed = $this->Users->getParsedKeycloakUser(); + } $this->CRUD->view($id, [ 'contain' => ['Individuals' => ['Alignments' => 'Organisations'], 'Roles', 'Organisations'], - 'afterFind' => function($data) { + 'afterFind' => function($data) use ($keycloakUsersParsed) { $data = $this->fetchTable('PermissionLimitations')->attachLimitations($data); + if (!empty(Configure::read('keycloak.enabled'))) { + $keycloakUser = $keycloakUsersParsed[$data->username]; + $data['keycloak_status'] = array_values($this->Users->checkKeycloakStatus([$data->toArray()], [$keycloakUser]))[0]; + } return $data; } ]); diff --git a/src/Model/Behavior/AuthKeycloakBehavior.php b/src/Model/Behavior/AuthKeycloakBehavior.php index 0a485dd..71af07b 100644 --- a/src/Model/Behavior/AuthKeycloakBehavior.php +++ b/src/Model/Behavior/AuthKeycloakBehavior.php @@ -284,27 +284,34 @@ class AuthKeycloakBehavior extends Behavior { $this->updateMappers(); $results = []; - $data['Users'] = $this->_table->find()->contain(['Individuals', 'Organisations', 'Roles'])->select( - [ - 'id', - 'uuid', - 'username', - 'disabled', - 'Individuals.email', - 'Individuals.first_name', - 'Individuals.last_name', - 'Individuals.uuid', - 'Roles.name', - 'Roles.uuid', - 'Organisations.name', - 'Organisations.uuid' - ] - )->disableHydration()->toArray(); + $data['Users'] = $this->getCerebrateUsers(); $clientId = $this->getClientId(); return $this->syncUsers($data['Users'], $clientId); } private function syncUsers(array $users, $clientId): array + { + $keycloakUsersParsed = $this->getParsedKeycloakUser(); + $changes = [ + 'created' => [], + 'modified' => [], + ]; + foreach ($users as &$user) { + $changed = false; + if (empty($keycloakUsersParsed[$user['username']])) { + if ($this->createUser($user, $clientId)) { + $changes['created'][] = $user['username']; + } + } else { + if ($this->checkAndUpdateUser($keycloakUsersParsed[$user['username']], $user)) { + $changes['modified'][] = $user['username']; + } + } + } + return $changes; + } + + public function getParsedKeycloakUser(): array { $response = $this->restApiRequest('%s/admin/realms/%s/users', [], 'get'); $keycloakUsers = json_decode($response->getStringBody(), true); @@ -325,23 +332,25 @@ class AuthKeycloakBehavior extends Behavior ] ]; } - $changes = [ - 'created' => [], - 'modified' => [], - ]; - foreach ($users as &$user) { - $changed = false; - if (empty($keycloakUsersParsed[$user['username']])) { - if ($this->createUser($user, $clientId)) { - $changes['created'][] = $user['username']; - } - } else { - if ($this->checkAndUpdateUser($keycloakUsersParsed[$user['username']], $user)) { - $changes['modified'][] = $user['username']; - } - } - } - return $changes; + return $keycloakUsersParsed; + } + + private function getCerebrateUsers(): array + { + return $this->_table->find()->contain(['Individuals', 'Organisations', 'Roles'])->select([ + 'id', + 'uuid', + 'username', + 'disabled', + 'Individuals.email', + 'Individuals.first_name', + 'Individuals.last_name', + 'Individuals.uuid', + 'Roles.name', + 'Roles.uuid', + 'Organisations.name', + 'Organisations.uuid' + ])->disableHydration()->toArray(); } private function checkAndUpdateUser(array $keycloakUser, array $user): bool @@ -387,6 +396,63 @@ class AuthKeycloakBehavior extends Behavior return false; } + public function checkKeycloakStatus(array $users, array $keycloakUsers): array + { + $users = Hash::combine($users, '{n}.username', '{n}'); + $keycloakUsersParsed = Hash::combine($keycloakUsers, '{n}.username', '{n}'); + $status = []; + foreach ($users as $username => $user) { + $differences = []; + $requireUpdate = $this->checkKeycloakUserRequiresUpdate($keycloakUsersParsed[$username], $user, $differences); + $status[$user['id']] = [ + 'require_update' => $requireUpdate, + 'differences' => $differences, + ]; + } + return $status; + } + + private function checkKeycloakUserRequiresUpdate(array $keycloakUser, array $user, array &$differences = []): bool + { + + $cEnabled = $keycloakUser['enabled'] == $user['disabled']; + $cFn = $keycloakUser['firstName'] !== $user['individual']['first_name']; + $Ln = $keycloakUser['lastName'] !== $user['individual']['last_name']; + $cEmail = $keycloakUser['email'] !== $user['individual']['email']; + $cRolename = (empty($keycloakUser['attributes']['role_name']) || $keycloakUser['attributes']['role_name'] !== $user['role']['name']); + $cRoleuuid = (empty($keycloakUser['attributes']['role_uuid']) || $keycloakUser['attributes']['role_uuid'] !== $user['role']['uuid']); + $cOrgname = (empty($keycloakUser['attributes']['org_name']) || $keycloakUser['attributes']['org_name'] !== $user['organisation']['name']); + $cOrguuid = (empty($keycloakUser['attributes']['org_uuid']) || $keycloakUser['attributes']['org_uuid'] !== $user['organisation']['uuid']); + if ($cEnabled || $cFn || $Ln || $cEmail || $cRolename || $cRoleuuid || $cOrgname || $cOrguuid) { + if ($cEnabled) { + $differences['enabled'] = ['kc' => $keycloakUser['enabled'], 'cerebrate' => $user['disabled']]; + } + if ($cFn) { + $differences['first_name'] = ['kc' => $keycloakUser['firstName'], 'cerebrate' => $user['individual']['first_name']]; + } + if ($Ln) { + $differences['last_name'] = ['kc' => $keycloakUser['lastName'], 'cerebrate' => $user['individual']['last_name']]; + } + if ($cEmail) { + $differences['email'] = ['kc' => $keycloakUser['email'], 'cerebrate' => $user['individual']['email']]; + } + if ($cRolename) { + $differences['role_name'] = ['kc' => $keycloakUser['attributes']['role_name'], 'cerebrate' => $user['role']['name']]; + } + if ($cRoleuuid) { + $differences['role_uuid'] = ['kc' => $keycloakUser['attributes']['role_uuid'], 'cerebrate' => $user['role']['uuid']]; + } + if ($cOrgname) { + $differences['org_name'] = ['kc' => $keycloakUser['attributes']['org_name'], 'cerebrate' => $user['organisation']['name']]; + } + if ($cOrguuid) { + $differences['org_uuid'] = ['kc' => $keycloakUser['attributes']['org_uuid'], 'cerebrate' => $user['organisation']['uuid']]; + } + return true; + } + return false; + } + private function createUser(array $user, string $clientId) { $newUser = [ diff --git a/src/Model/Table/UsersTable.php b/src/Model/Table/UsersTable.php index a32db0b..8f74390 100644 --- a/src/Model/Table/UsersTable.php +++ b/src/Model/Table/UsersTable.php @@ -183,18 +183,6 @@ class UsersTable extends AppTable return $rules; } - public function test() - { - $this->Roles = TableRegistry::get('Roles'); - $role = $this->Roles->newEntity([ - 'name' => 'admin', - 'perm_admin' => 1, - 'perm_org_admin' => 1, - 'perm_sync' => 1 - ]); - $this->Roles->save($role); - } - public function checkForNewInstance(): bool { if (empty($this->find()->first())) { diff --git a/templates/Users/index.php b/templates/Users/index.php index 826ae5e..6c28dd2 100644 --- a/templates/Users/index.php +++ b/templates/Users/index.php @@ -1,4 +1,7 @@ element('genericElements/IndexTable/index_table', [ 'data' => [ 'data' => $data, @@ -92,6 +95,12 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'url' => '/user-settings/index?Users.id={{url_data}}', 'url_data_path' => 'id' ], + // [ + // 'name' => __('Keycloak status'), + // 'element' => 'keycloak_status', + // 'data_path' => 'keycloak_status', + // 'requirements' => Configure::read('keycloak.enabled', false), + // ], ], 'title' => __('User index'), 'description' => __('The list of enrolled users in this Cerebrate instance. All of the users have or at one point had access to the system.'), diff --git a/templates/Users/view.php b/templates/Users/view.php index c6ca881..4e2bda7 100644 --- a/templates/Users/view.php +++ b/templates/Users/view.php @@ -51,14 +51,22 @@ $fields = [ 'scope' => 'individuals' ] ]; -if ($keycloakConfig['enabled'] && $loggedUser['id'] == $entity['id']) { +if ($keycloakConfig['enabled']) { $fields[] = [ - 'type' => 'generic', - 'key' => __('Modify keycloak profile'), - 'path' => 'username', - 'url' => $kcurl, - 'requirements' => false + 'key' => __('Keycloak status'), + 'type' => 'keycloakStatus', + 'path' => 'keycloak_status', + 'requirements' => !empty($keycloakConfig['enabled']), ]; + if ($loggedUser['id'] == $entity['id']) { + $fields[] = [ + 'type' => 'generic', + 'key' => __('Modify keycloak profile'), + 'path' => 'username', + 'url' => $kcurl, + 'requirements' => false + ]; + } } echo $this->element( '/genericElements/SingleViews/single_view', diff --git a/templates/element/genericElements/IndexTable/Fields/keycloak_status.php b/templates/element/genericElements/IndexTable/Fields/keycloak_status.php new file mode 100644 index 0000000..bc2b95b --- /dev/null +++ b/templates/element/genericElements/IndexTable/Fields/keycloak_status.php @@ -0,0 +1,14 @@ +Hash->get($row, $field['data_path']); + if (is_null($data)) { + echo ''; + } else if (!empty($data['require_update'])) { + echo sprintf( + '%s', + sprintf('Fields having differences: %s', (implode(', ', array_keys($data['differences'])))), + $this->Bootstrap->icon('times', ['class' => 'text-danger', ]) + ); + } else { + echo $this->Bootstrap->icon('check', ['class' => 'text-success',]); + } +?> diff --git a/templates/element/genericElements/SingleViews/Fields/keycloakStatusField.php b/templates/element/genericElements/SingleViews/Fields/keycloakStatusField.php new file mode 100644 index 0000000..efac939 --- /dev/null +++ b/templates/element/genericElements/SingleViews/Fields/keycloakStatusField.php @@ -0,0 +1,21 @@ +
%s
%s', + $this->Bootstrap->icon('exclamation-triangle') . __(' This user is not synchronise with Keycloak. Differences:'), + $this->Html->nestedList($differencesRearranged, ['class' => '']) + ); + } else { + echo $this->Bootstrap->icon('check', ['class' => 'text-success',]); + } +?>