new: [users:view] Added keycloak status showing the potential differences between Cerebrate and Keycloak
parent
21c5601c29
commit
af622dd19b
|
@ -21,11 +21,22 @@ class UsersController extends AppController
|
||||||
if (empty($currentUser['role']['perm_admin'])) {
|
if (empty($currentUser['role']['perm_admin'])) {
|
||||||
$conditions['organisation_id'] = $currentUser['organisation_id'];
|
$conditions['organisation_id'] = $currentUser['organisation_id'];
|
||||||
}
|
}
|
||||||
|
$keycloakUsersParsed = null;
|
||||||
|
if (!empty(Configure::read('keycloak.enabled'))) {
|
||||||
|
$keycloakUsersParsed = $this->Users->getParsedKeycloakUser();
|
||||||
|
}
|
||||||
$this->CRUD->index([
|
$this->CRUD->index([
|
||||||
'contain' => $this->containFields,
|
'contain' => $this->containFields,
|
||||||
'filters' => $this->filterFields,
|
'filters' => $this->filterFields,
|
||||||
'quickFilters' => $this->quickFilterFields,
|
'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();
|
$responsePayload = $this->CRUD->getResponsePayload();
|
||||||
if (!empty($responsePayload)) {
|
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']))) {
|
if (empty($id) || (empty($currentUser['role']['perm_org_admin']) && empty($currentUser['role']['perm_admin']))) {
|
||||||
$id = $this->ACL->getUser()['id'];
|
$id = $this->ACL->getUser()['id'];
|
||||||
}
|
}
|
||||||
|
$keycloakUsersParsed = null;
|
||||||
|
if (!empty(Configure::read('keycloak.enabled'))) {
|
||||||
|
$keycloakUsersParsed = $this->Users->getParsedKeycloakUser();
|
||||||
|
}
|
||||||
$this->CRUD->view($id, [
|
$this->CRUD->view($id, [
|
||||||
'contain' => ['Individuals' => ['Alignments' => 'Organisations'], 'Roles', 'Organisations'],
|
'contain' => ['Individuals' => ['Alignments' => 'Organisations'], 'Roles', 'Organisations'],
|
||||||
'afterFind' => function($data) {
|
'afterFind' => function($data) use ($keycloakUsersParsed) {
|
||||||
$data = $this->fetchTable('PermissionLimitations')->attachLimitations($data);
|
$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;
|
return $data;
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -284,27 +284,34 @@ class AuthKeycloakBehavior extends Behavior
|
||||||
{
|
{
|
||||||
$this->updateMappers();
|
$this->updateMappers();
|
||||||
$results = [];
|
$results = [];
|
||||||
$data['Users'] = $this->_table->find()->contain(['Individuals', 'Organisations', 'Roles'])->select(
|
$data['Users'] = $this->getCerebrateUsers();
|
||||||
[
|
|
||||||
'id',
|
|
||||||
'uuid',
|
|
||||||
'username',
|
|
||||||
'disabled',
|
|
||||||
'Individuals.email',
|
|
||||||
'Individuals.first_name',
|
|
||||||
'Individuals.last_name',
|
|
||||||
'Individuals.uuid',
|
|
||||||
'Roles.name',
|
|
||||||
'Roles.uuid',
|
|
||||||
'Organisations.name',
|
|
||||||
'Organisations.uuid'
|
|
||||||
]
|
|
||||||
)->disableHydration()->toArray();
|
|
||||||
$clientId = $this->getClientId();
|
$clientId = $this->getClientId();
|
||||||
return $this->syncUsers($data['Users'], $clientId);
|
return $this->syncUsers($data['Users'], $clientId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function syncUsers(array $users, $clientId): array
|
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');
|
$response = $this->restApiRequest('%s/admin/realms/%s/users', [], 'get');
|
||||||
$keycloakUsers = json_decode($response->getStringBody(), true);
|
$keycloakUsers = json_decode($response->getStringBody(), true);
|
||||||
|
@ -325,23 +332,25 @@ class AuthKeycloakBehavior extends Behavior
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
$changes = [
|
return $keycloakUsersParsed;
|
||||||
'created' => [],
|
}
|
||||||
'modified' => [],
|
|
||||||
];
|
private function getCerebrateUsers(): array
|
||||||
foreach ($users as &$user) {
|
{
|
||||||
$changed = false;
|
return $this->_table->find()->contain(['Individuals', 'Organisations', 'Roles'])->select([
|
||||||
if (empty($keycloakUsersParsed[$user['username']])) {
|
'id',
|
||||||
if ($this->createUser($user, $clientId)) {
|
'uuid',
|
||||||
$changes['created'][] = $user['username'];
|
'username',
|
||||||
}
|
'disabled',
|
||||||
} else {
|
'Individuals.email',
|
||||||
if ($this->checkAndUpdateUser($keycloakUsersParsed[$user['username']], $user)) {
|
'Individuals.first_name',
|
||||||
$changes['modified'][] = $user['username'];
|
'Individuals.last_name',
|
||||||
}
|
'Individuals.uuid',
|
||||||
}
|
'Roles.name',
|
||||||
}
|
'Roles.uuid',
|
||||||
return $changes;
|
'Organisations.name',
|
||||||
|
'Organisations.uuid'
|
||||||
|
])->disableHydration()->toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
private function checkAndUpdateUser(array $keycloakUser, array $user): bool
|
private function checkAndUpdateUser(array $keycloakUser, array $user): bool
|
||||||
|
@ -387,6 +396,63 @@ class AuthKeycloakBehavior extends Behavior
|
||||||
return false;
|
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)
|
private function createUser(array $user, string $clientId)
|
||||||
{
|
{
|
||||||
$newUser = [
|
$newUser = [
|
||||||
|
|
|
@ -183,18 +183,6 @@ class UsersTable extends AppTable
|
||||||
return $rules;
|
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
|
public function checkForNewInstance(): bool
|
||||||
{
|
{
|
||||||
if (empty($this->find()->first())) {
|
if (empty($this->find()->first())) {
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use Cake\Core\Configure;
|
||||||
|
|
||||||
echo $this->element('genericElements/IndexTable/index_table', [
|
echo $this->element('genericElements/IndexTable/index_table', [
|
||||||
'data' => [
|
'data' => [
|
||||||
'data' => $data,
|
'data' => $data,
|
||||||
|
@ -92,6 +95,12 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
||||||
'url' => '/user-settings/index?Users.id={{url_data}}',
|
'url' => '/user-settings/index?Users.id={{url_data}}',
|
||||||
'url_data_path' => 'id'
|
'url_data_path' => 'id'
|
||||||
],
|
],
|
||||||
|
// [
|
||||||
|
// 'name' => __('Keycloak status'),
|
||||||
|
// 'element' => 'keycloak_status',
|
||||||
|
// 'data_path' => 'keycloak_status',
|
||||||
|
// 'requirements' => Configure::read('keycloak.enabled', false),
|
||||||
|
// ],
|
||||||
],
|
],
|
||||||
'title' => __('User index'),
|
'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.'),
|
'description' => __('The list of enrolled users in this Cerebrate instance. All of the users have or at one point had access to the system.'),
|
||||||
|
|
|
@ -51,14 +51,22 @@ $fields = [
|
||||||
'scope' => 'individuals'
|
'scope' => 'individuals'
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
if ($keycloakConfig['enabled'] && $loggedUser['id'] == $entity['id']) {
|
if ($keycloakConfig['enabled']) {
|
||||||
$fields[] = [
|
$fields[] = [
|
||||||
'type' => 'generic',
|
'key' => __('Keycloak status'),
|
||||||
'key' => __('Modify keycloak profile'),
|
'type' => 'keycloakStatus',
|
||||||
'path' => 'username',
|
'path' => 'keycloak_status',
|
||||||
'url' => $kcurl,
|
'requirements' => !empty($keycloakConfig['enabled']),
|
||||||
'requirements' => false
|
|
||||||
];
|
];
|
||||||
|
if ($loggedUser['id'] == $entity['id']) {
|
||||||
|
$fields[] = [
|
||||||
|
'type' => 'generic',
|
||||||
|
'key' => __('Modify keycloak profile'),
|
||||||
|
'path' => 'username',
|
||||||
|
'url' => $kcurl,
|
||||||
|
'requirements' => false
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
echo $this->element(
|
echo $this->element(
|
||||||
'/genericElements/SingleViews/single_view',
|
'/genericElements/SingleViews/single_view',
|
||||||
|
|
|
@ -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',]);
|
||||||
|
}
|
||||||
|
?>
|
|
@ -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',]);
|
||||||
|
}
|
||||||
|
?>
|
Loading…
Reference in New Issue