new: [users:view] Added keycloak status showing the potential differences between Cerebrate and Keycloak

pull/121/head
Sami Mokaddem 2022-12-09 11:53:20 +01:00
parent 21c5601c29
commit af622dd19b
No known key found for this signature in database
GPG Key ID: 164C473F627A06FA
7 changed files with 178 additions and 53 deletions

View File

@ -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;
}
]);

View File

@ -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 = [

View File

@ -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())) {

View File

@ -1,4 +1,7 @@
<?php
use Cake\Core\Configure;
echo $this->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.'),

View File

@ -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',

View File

@ -0,0 +1,14 @@
<?php
$data = $this->Hash->get($row, $field['data_path']);
if (is_null($data)) {
echo '';
} else if (!empty($data['require_update'])) {
echo sprintf(
'<span data-bs-toggle="tooltip" data-bs-title="%s">%s</span>',
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',]);
}
?>

View File

@ -0,0 +1,21 @@
<?php
use Cake\Utility\Hash;
$value = Hash::get($data, $field['path']);
$differencesRearranged = array_map(function($difference) {
return [
__('Local: {0}', h($difference['cerebrate'])),
__('Keycloak: {0}', h($difference['kc'])),
];
}, $value['differences']);
if (!empty($value['require_update'])) {
echo sprintf(
'<div class="alert alert-warning"><div>%s</div>%s</div>',
$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',]);
}
?>