commit
f69456d8f3
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Migrations\AbstractMigration;
|
||||
|
||||
final class PermissionRestrictions extends AbstractMigration
|
||||
{
|
||||
/**
|
||||
* Change Method.
|
||||
*
|
||||
* Write your reversible migrations using this method.
|
||||
*
|
||||
* More information on writing migrations is available here:
|
||||
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
|
||||
*
|
||||
* Remember to call "create()" or "update()" and NOT "save()" when working
|
||||
* with the Table class.
|
||||
*/
|
||||
public function change(): void
|
||||
{
|
||||
$exists = $this->hasTable('permission_limitations');
|
||||
if (!$exists) {
|
||||
$table = $this->table('permission_limitations', [
|
||||
'signed' => false,
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
]);
|
||||
$table
|
||||
->addColumn('scope', 'string', [
|
||||
'null' => false,
|
||||
'length' => 20,
|
||||
'collation' => 'ascii_general_ci'
|
||||
])
|
||||
->addColumn('permission', 'string', [
|
||||
'null' => false,
|
||||
'length' => 40,
|
||||
'collation' => 'utf8mb4_unicode_ci'
|
||||
])
|
||||
->addColumn('max_occurrence', 'integer', [
|
||||
'null' => false,
|
||||
'signed' => false
|
||||
])
|
||||
->addColumn('comment', 'blob', [])
|
||||
->addIndex('scope')
|
||||
->addIndex('permission');
|
||||
$table->create();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"name": "CSIRT network permissions",
|
||||
"namespace": "csn",
|
||||
"description": "Template of additional tooling permissions for the CSIRT network",
|
||||
"version": 1,
|
||||
"scope": "user",
|
||||
"uuid": "447ded8b-314b-41c7-a913-4ce32535b28d",
|
||||
"source": "CSIRT network",
|
||||
"metaFields": [
|
||||
{
|
||||
"field": "perm_mattermost",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"field": "perm_bigbluebutton",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"field": "perm_sharepoint",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"field": "perm_misp",
|
||||
"type": "boolean"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -16,10 +16,10 @@ class KeycloakSyncCommand extends Command
|
|||
$this->loadModel('Users');
|
||||
$results = $this->fetchTable()->syncWithKeycloak();
|
||||
$tableData = [
|
||||
['Changes to', 'Count']
|
||||
['Modification type', 'Count', 'Affected users']
|
||||
];
|
||||
foreach ($results as $k => $v) {
|
||||
$tableData[] = [$k, '<text-right>' . $v . '</text-right>'];
|
||||
$tableData[] = [$k, '<text-right>' . count($v) . '</text-right>', '<text-right>' . implode(', ', $v) . '</text-right>'];
|
||||
}
|
||||
$io->out(__('Sync done. See the results below.'));
|
||||
$io->helper('Table')->output($tableData);
|
||||
|
|
|
@ -87,7 +87,7 @@ class ACLComponent extends Component
|
|||
'Individuals' => [
|
||||
'add' => ['perm_admin'],
|
||||
'delete' => ['perm_admin'],
|
||||
'edit' => ['perm_admin'],
|
||||
'edit' => ['perm_admin', 'perm_org_admin'],
|
||||
'filtering' => ['*'],
|
||||
'index' => ['*'],
|
||||
'tag' => ['perm_tagger'],
|
||||
|
@ -169,6 +169,13 @@ class ACLComponent extends Component
|
|||
'Pages' => [
|
||||
'display' => ['*']
|
||||
],
|
||||
'PermissionLimitations' => [
|
||||
"index" => ['*'],
|
||||
"add" => ['perm_admin'],
|
||||
"view" => ['*'],
|
||||
"edit" => ['perm_admin'],
|
||||
"delete" => ['perm_admin']
|
||||
],
|
||||
'Roles' => [
|
||||
'add' => ['perm_admin'],
|
||||
'delete' => ['perm_admin'],
|
||||
|
|
|
@ -12,15 +12,19 @@ use Cake\Core\Configure;
|
|||
use Cake\Core\Configure\Engine\PhpConfig;
|
||||
use Cake\Utility\Inflector;
|
||||
use Cake\Routing\Router;
|
||||
use Cake\Collection\Collection;
|
||||
|
||||
class APIRearrangeComponent extends Component
|
||||
{
|
||||
public function rearrangeForAPI(object $data): object
|
||||
public function rearrangeForAPI(object $data)
|
||||
{
|
||||
if (is_subclass_of($data, 'Iterator')) {
|
||||
$data->each(function ($value, $key) {
|
||||
$newData = [];
|
||||
$data->each(function ($value, $key) use (&$newData) {
|
||||
$value->rearrangeForAPI();
|
||||
$newData[] = $value;
|
||||
});
|
||||
return new Collection($newData);
|
||||
} else {
|
||||
$data->rearrangeForAPI();
|
||||
}
|
||||
|
|
|
@ -73,6 +73,9 @@ class CRUDComponent extends Component
|
|||
$query->order($options['order']);
|
||||
}
|
||||
if ($this->Controller->ParamHandler->isRest()) {
|
||||
if ($this->metaFieldsSupported()) {
|
||||
$query = $this->includeRequestedMetaFields($query);
|
||||
}
|
||||
$data = $query->all();
|
||||
if (isset($options['hidden'])) {
|
||||
$data->each(function($value, $key) use ($options) {
|
||||
|
@ -98,6 +101,12 @@ class CRUDComponent extends Component
|
|||
});
|
||||
}
|
||||
}
|
||||
if ($this->metaFieldsSupported()) {
|
||||
$metaTemplates = $this->getMetaTemplates()->toArray();
|
||||
$data = $data->map(function($value, $key) use ($metaTemplates) {
|
||||
return $this->attachMetaTemplatesIfNeeded($value, $metaTemplates);
|
||||
});
|
||||
}
|
||||
$this->Controller->restResponsePayload = $this->RestResponse->viewData($data, 'json');
|
||||
} else {
|
||||
if ($this->metaFieldsSupported()) {
|
||||
|
@ -545,7 +554,14 @@ class CRUDComponent extends Component
|
|||
$savedData = $this->Table->save($data);
|
||||
if ($savedData !== false) {
|
||||
if ($this->metaFieldsSupported() && !empty($metaFieldsToDelete)) {
|
||||
$this->Table->MetaFields->unlink($savedData, $metaFieldsToDelete);
|
||||
foreach ($metaFieldsToDelete as $k => $v) {
|
||||
if ($v === null) {
|
||||
unset($metaFieldsToDelete[$k]);
|
||||
}
|
||||
}
|
||||
if (!empty($metaFieldsToDelete)) {
|
||||
$this->Table->MetaFields->unlink($savedData, $metaFieldsToDelete);
|
||||
}
|
||||
}
|
||||
if (isset($params['afterSave'])) {
|
||||
$params['afterSave']($data);
|
||||
|
@ -663,7 +679,9 @@ class CRUDComponent extends Component
|
|||
if (!empty($newestTemplate) && !empty($metaTemplates[$i])) {
|
||||
$metaTemplates[$i]['hasNewerVersion'] = $newestTemplate;
|
||||
}
|
||||
$metaTemplates[$metaTemplate->id]['meta_template_fields'] = $metaTemplates[$metaTemplate->id]['meta_template_fields'];
|
||||
}
|
||||
$metaTemplates = $metaTemplates;
|
||||
$data['MetaTemplates'] = $metaTemplates;
|
||||
return $data;
|
||||
}
|
||||
|
|
|
@ -123,6 +123,11 @@ class Sidemenu {
|
|||
'url' => '/auditLogs/index',
|
||||
'icon' => 'history',
|
||||
],
|
||||
'PermissionLimitations' => [
|
||||
'label' => __('Permission Limitations'),
|
||||
'url' => '/permissionLimitations/index',
|
||||
'icon' => 'jedi',
|
||||
],
|
||||
]
|
||||
],
|
||||
'API' => [
|
||||
|
|
|
@ -20,6 +20,8 @@ class IndividualsController extends AppController
|
|||
|
||||
public function index()
|
||||
{
|
||||
$currentUser = $this->ACL->getUser();
|
||||
$orgAdmin = !$currentUser['role']['perm_admin'] && $currentUser['role']['perm_org_admin'];
|
||||
$this->CRUD->index([
|
||||
'filters' => $this->filterFields,
|
||||
'quickFilters' => $this->quickFilterFields,
|
||||
|
@ -31,6 +33,11 @@ class IndividualsController extends AppController
|
|||
if (!empty($responsePayload)) {
|
||||
return $responsePayload;
|
||||
}
|
||||
$editableIds = null;
|
||||
if ($orgAdmin) {
|
||||
$editableIds = $this->Individuals->getValidIndividualsToEdit($currentUser);
|
||||
}
|
||||
$this->set('editableIds', $editableIds);
|
||||
$this->set('alignmentScope', 'individuals');
|
||||
}
|
||||
|
||||
|
@ -59,6 +66,14 @@ class IndividualsController extends AppController
|
|||
|
||||
public function edit($id)
|
||||
{
|
||||
$currentUser = $this->ACL->getUser();
|
||||
$validIndividualIds = [];
|
||||
if ($currentUser['role']['perm_admin']) {
|
||||
$validIndividualIds = $this->Individuals->getValidIndividualsToEdit($currentUser);
|
||||
if (!isset($validIndividualIds[$id])) {
|
||||
throw new NotFoundException(__('Invalid individual.'));
|
||||
}
|
||||
}
|
||||
$this->CRUD->edit($id);
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
if (!empty($responsePayload)) {
|
||||
|
|
|
@ -99,6 +99,13 @@ class OrganisationsController extends AppController
|
|||
|
||||
public function edit($id)
|
||||
{
|
||||
$currentUser = $this->ACL->getUser();
|
||||
if (
|
||||
!($currentUser['organisation']['id'] == $id && $currentUser['role']['perm_org_admin']) &&
|
||||
!$currentUser['role']['perm_admin']
|
||||
) {
|
||||
throw new MethodNotAllowedException(__('You cannot modify that organisation.'));
|
||||
}
|
||||
$this->CRUD->edit($id);
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
if (!empty($responsePayload)) {
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Controller\AppController;
|
||||
use Cake\Utility\Hash;
|
||||
use Cake\Utility\Text;
|
||||
use \Cake\Database\Expression\QueryExpression;
|
||||
use Cake\Http\Exception\NotFoundException;
|
||||
use Cake\Http\Exception\MethodNotAllowedException;
|
||||
use Cake\Http\Exception\ForbiddenException;
|
||||
|
||||
class PermissionLimitationsController extends AppController
|
||||
{
|
||||
public $filterFields = ['scope', 'permission'];
|
||||
public $quickFilterFields = ['name'];
|
||||
public $containFields = [];
|
||||
|
||||
public function index()
|
||||
{
|
||||
$this->CRUD->index([
|
||||
'filters' => $this->filterFields,
|
||||
'quickFilters' => $this->quickFilterFields,
|
||||
'afterFind' => function($data) {
|
||||
$data['comment'] = is_resource($data['comment']) ? stream_get_contents($data['comment']) : $data['comment'];
|
||||
return $data;
|
||||
}
|
||||
]);
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
if (!empty($responsePayload)) {
|
||||
return $responsePayload;
|
||||
}
|
||||
$this->set('metaGroup', 'PermissionLimitations');
|
||||
}
|
||||
|
||||
public function add()
|
||||
{
|
||||
$this->CRUD->add([
|
||||
'afterFind' => function($data) {
|
||||
$data['comment'] = is_resource($data['comment']) ? stream_get_contents($data['comment']) : $data['comment'];
|
||||
return $data;
|
||||
}
|
||||
]);
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
if (!empty($responsePayload)) {
|
||||
return $responsePayload;
|
||||
}
|
||||
$this->set('metaGroup', 'PermissionLimitations');
|
||||
}
|
||||
|
||||
public function view($id)
|
||||
{
|
||||
$this->CRUD->view($id, [
|
||||
'afterFind' => function($data) {
|
||||
$data['comment'] = is_resource($data['comment']) ? stream_get_contents($data['comment']) : $data['comment'];
|
||||
return $data;
|
||||
}
|
||||
]);
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
if (!empty($responsePayload)) {
|
||||
return $responsePayload;
|
||||
}
|
||||
$this->set('metaGroup', 'PermissionLimitations');
|
||||
}
|
||||
|
||||
public function edit($id)
|
||||
{
|
||||
$this->CRUD->edit($id, [
|
||||
'afterFind' => function($data) {
|
||||
$data['comment'] = is_resource($data['comment']) ? stream_get_contents($data['comment']) : $data['comment'];
|
||||
return $data;
|
||||
}
|
||||
]);
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
if (!empty($responsePayload)) {
|
||||
return $responsePayload;
|
||||
}
|
||||
$this->set('metaGroup', 'PermissionLimitations');
|
||||
$this->render('add');
|
||||
}
|
||||
|
||||
public function delete($id)
|
||||
{
|
||||
$this->CRUD->delete($id);
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
if (!empty($responsePayload)) {
|
||||
return $responsePayload;
|
||||
}
|
||||
$this->set('metaGroup', 'PermissionLimitations');
|
||||
}
|
||||
}
|
|
@ -16,6 +16,7 @@ class UsersController extends AppController
|
|||
|
||||
public function index()
|
||||
{
|
||||
$this->Users->updateMappers();
|
||||
$currentUser = $this->ACL->getUser();
|
||||
$conditions = [];
|
||||
if (empty($currentUser['role']['perm_admin'])) {
|
||||
|
@ -136,7 +137,11 @@ class UsersController extends AppController
|
|||
$id = $this->ACL->getUser()['id'];
|
||||
}
|
||||
$this->CRUD->view($id, [
|
||||
'contain' => ['Individuals' => ['Alignments' => 'Organisations'], 'Roles', 'Organisations']
|
||||
'contain' => ['Individuals' => ['Alignments' => 'Organisations'], 'Roles', 'Organisations'],
|
||||
'afterFind' => function($data) {
|
||||
$data = $this->fetchTable('PermissionLimitations')->attachLimitations($data);
|
||||
return $data;
|
||||
}
|
||||
]);
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
if (!empty($responsePayload)) {
|
||||
|
@ -354,6 +359,9 @@ class UsersController extends AppController
|
|||
]);
|
||||
$this->Authentication->logout();
|
||||
$this->Flash->success(__('Goodbye.'));
|
||||
if (Configure::read('keycloak.enabled')) {
|
||||
$this->redirect($this->Users->keyCloaklogout());
|
||||
}
|
||||
return $this->redirect(\Cake\Routing\Router::url('/users/login'));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@ class SocialAuthListener implements EventListenerInterface
|
|||
|
||||
// You can access the profile using $user->social_profile
|
||||
|
||||
$this->getTableLocator()->get('Users')->saveOrFail($user);
|
||||
// $this->getTableLocator()->get('Users')->saveOrFail($user);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
|
|
@ -18,11 +18,9 @@ use Cake\Http\Exception\NotFoundException;
|
|||
|
||||
class AuthKeycloakBehavior extends Behavior
|
||||
{
|
||||
|
||||
public function getUser(EntityInterface $profile, Session $session)
|
||||
{
|
||||
$userId = $session->read('Auth.User.id');
|
||||
$userId = null;
|
||||
if ($userId) {
|
||||
return $this->_table->get($userId);
|
||||
}
|
||||
|
@ -110,12 +108,7 @@ class AuthKeycloakBehavior extends Behavior
|
|||
)->first()
|
||||
];
|
||||
$clientId = $this->getClientId();
|
||||
$roles = $this->getAllRoles($clientId);
|
||||
$rolesParsed = [];
|
||||
foreach ($roles as $role) {
|
||||
$rolesParsed[$role['name']] = $role['id'];
|
||||
}
|
||||
$newUserId = $this->createUser($user, $clientId, $rolesParsed);
|
||||
$newUserId = $this->createUser($user, $clientId);
|
||||
if (!$newUserId) {
|
||||
$logChange = [
|
||||
'username' => $user['username'],
|
||||
|
@ -165,9 +158,9 @@ class AuthKeycloakBehavior extends Behavior
|
|||
* handleUserUpdate
|
||||
*
|
||||
* @param \App\Model\Entity\User $user
|
||||
* @return boolean If the update was a success
|
||||
* @return array Containing changes if successful
|
||||
*/
|
||||
public function handleUserUpdate(\App\Model\Entity\User $user): bool
|
||||
public function handleUserUpdate(\App\Model\Entity\User $user): array
|
||||
{
|
||||
$user['individual'] = $this->_table->Individuals->find()->where([
|
||||
'id' => $user['individual_id']
|
||||
|
@ -182,7 +175,19 @@ class AuthKeycloakBehavior extends Behavior
|
|||
$users = [$user->toArray()];
|
||||
$clientId = $this->getClientId();
|
||||
$changes = $this->syncUsers($users, $clientId);
|
||||
return !empty($changes);
|
||||
return $changes;
|
||||
}
|
||||
|
||||
public function keyCloaklogout(): string
|
||||
{
|
||||
$keycloakConfig = Configure::read('keycloak');
|
||||
$logoutUrl = sprintf(
|
||||
'%s/realms/%s/protocol/openid-connect/logout?redirect_uri=%s',
|
||||
$keycloakConfig['provider']['baseUrl'],
|
||||
$keycloakConfig['provider']['realm'],
|
||||
urlencode(Configure::read('App.fullBaseUrl'))
|
||||
);
|
||||
return $logoutUrl;
|
||||
}
|
||||
|
||||
private function getAdminAccessToken()
|
||||
|
@ -224,9 +229,8 @@ class AuthKeycloakBehavior extends Behavior
|
|||
|
||||
public function syncWithKeycloak(): array
|
||||
{
|
||||
$this->updateMappers();
|
||||
$results = [];
|
||||
$data['Roles'] = $this->_table->Roles->find()->disableHydration()->toArray();
|
||||
$data['Organisations'] = $this->_table->Organisations->find()->disableHydration()->toArray();
|
||||
$data['Users'] = $this->_table->find()->contain(['Individuals', 'Organisations', 'Roles'])->select(
|
||||
[
|
||||
'id',
|
||||
|
@ -244,86 +248,15 @@ class AuthKeycloakBehavior extends Behavior
|
|||
]
|
||||
)->disableHydration()->toArray();
|
||||
$clientId = $this->getClientId();
|
||||
$results = [];
|
||||
$results['roles'] = $this->syncRoles(Hash::extract($data['Roles'], '{n}.name'), $clientId, 'Role');
|
||||
$results['organisations'] = $this->syncRoles(Hash::extract($data['Organisations'], '{n}.name'), $clientId, 'Organisation');
|
||||
$results['users'] = $this->syncUsers($data['Users'], $clientId);
|
||||
return $results;
|
||||
return $this->syncUsers($data['Users'], $clientId);
|
||||
}
|
||||
|
||||
private function syncRoles(array $roles, string $clientId, string $scope = 'Role'): int
|
||||
private function syncUsers(array $users, $clientId): array
|
||||
{
|
||||
$keycloakRoles = $this->getAllRoles($clientId);
|
||||
$keycloakRolesParsed = Hash::extract($keycloakRoles, '{n}.name');
|
||||
$scopeString = $scope . ':';
|
||||
$modified = 0;
|
||||
foreach ($roles as $role) {
|
||||
if (!in_array($scopeString . $role, $keycloakRolesParsed)) {
|
||||
$roleToPush = [
|
||||
'name' => $scopeString . $role,
|
||||
'clientRole' => true
|
||||
];
|
||||
$url = '%s/admin/realms/%s/clients/' . $clientId . '/roles';
|
||||
$response = $this->restApiRequest($url, $roleToPush, 'post');
|
||||
if (!$response->isOk()) {
|
||||
$this->_table->auditLogs()->insert([
|
||||
'request_action' => 'keycloakCreateRole',
|
||||
'model' => 'User',
|
||||
'model_id' => 0,
|
||||
'model_title' => __('Failed to create role ({0}) in keycloak', $scopeString . $role),
|
||||
'changed' => [
|
||||
'code' => $response->getStatusCode(),
|
||||
'error_body' => $response->getStringBody()
|
||||
]
|
||||
]);
|
||||
}
|
||||
$modified += 1;
|
||||
}
|
||||
$keycloakRolesParsed = array_diff($keycloakRolesParsed, [$scopeString . $role]);
|
||||
}
|
||||
foreach ($keycloakRolesParsed as $roleToRemove) {
|
||||
if (substr($roleToRemove, 0, strlen($scopeString)) === $scopeString) {
|
||||
$url = '%s/admin/realms/%s/clients/' . $clientId . '/roles/' . $roleToRemove;
|
||||
$response = $this->restApiRequest($url, [], 'delete');
|
||||
if (!$response->isOk()) {
|
||||
$this->_table->auditLogs()->insert([
|
||||
'request_action' => 'keycloakRemoveRole',
|
||||
'model' => 'User',
|
||||
'model_id' => 0,
|
||||
'model_title' => __('Failed to remove role ({0}) in keycloak', $roleToRemove),
|
||||
'changed' => [
|
||||
'code' => $response->getStatusCode(),
|
||||
'error_body' => $response->getStringBody()
|
||||
]
|
||||
]);
|
||||
}
|
||||
$modified += 1;
|
||||
}
|
||||
}
|
||||
return $modified;
|
||||
}
|
||||
|
||||
private function getAllRoles(string $clientId): array
|
||||
{
|
||||
$response = $this->restApiRequest('%s/admin/realms/%s/clients/' . $clientId . '/roles', [], 'get');
|
||||
return json_decode($response->getStringBody(), true);
|
||||
}
|
||||
|
||||
private function syncUsers(array $users, $clientId, $roles = null): int
|
||||
{
|
||||
if ($roles === null) {
|
||||
$roles = $this->getAllRoles($clientId);
|
||||
}
|
||||
$rolesParsed = [];
|
||||
foreach ($roles as $role) {
|
||||
$rolesParsed[$role['name']] = $role['id'];
|
||||
}
|
||||
$response = $this->restApiRequest('%s/admin/realms/%s/users', [], 'get');
|
||||
$keycloakUsers = json_decode($response->getStringBody(), true);
|
||||
$keycloakUsersParsed = [];
|
||||
foreach ($keycloakUsers as $u) {
|
||||
$response = $this->restApiRequest('%s/admin/realms/%s/users/' . $u['id'] . '/role-mappings/clients/' . $clientId, [], 'get');
|
||||
$roleMappings = json_decode($response->getStringBody(), true);
|
||||
$keycloakUsersParsed[$u['username']] = [
|
||||
'id' => $u['id'],
|
||||
'username' => $u['username'],
|
||||
|
@ -331,26 +264,28 @@ class AuthKeycloakBehavior extends Behavior
|
|||
'firstName' => $u['firstName'],
|
||||
'lastName' => $u['lastName'],
|
||||
'email' => $u['email'],
|
||||
'roles' => $roleMappings
|
||||
'attributes' => [
|
||||
'role_name' => $u['attributes']['role_name'][0] ?? '',
|
||||
'role_uuid' => $u['attributes']['role_uuid'][0] ?? '',
|
||||
'org_uuid' => $u['attributes']['org_uuid'][0] ?? '',
|
||||
'org_name' => $u['attributes']['org_name'][0] ?? ''
|
||||
]
|
||||
];
|
||||
}
|
||||
$changes = 0;
|
||||
$changes = [
|
||||
'created' => [],
|
||||
'modified' => [],
|
||||
];
|
||||
foreach ($users as &$user) {
|
||||
$changed = false;
|
||||
if (empty($keycloakUsersParsed[$user['username']])) {
|
||||
if ($this->createUser($user, $clientId, $rolesParsed)) {
|
||||
$changes = true;
|
||||
if ($this->createUser($user, $clientId)) {
|
||||
$changes['created'][] = $user['username'];
|
||||
}
|
||||
} else {
|
||||
if ($this->checkAndUpdateUser($keycloakUsersParsed[$user['username']], $user)) {
|
||||
$changes = true;
|
||||
$changes['modified'][] = $user['username'];
|
||||
}
|
||||
if ($this->checkAndUpdateUserRoles($keycloakUsersParsed[$user['username']], $user, $clientId, $rolesParsed)) {
|
||||
$changes = true;
|
||||
}
|
||||
}
|
||||
if ($changed) {
|
||||
$changes += 1;
|
||||
}
|
||||
}
|
||||
return $changes;
|
||||
|
@ -362,13 +297,23 @@ class AuthKeycloakBehavior extends Behavior
|
|||
$keycloakUser['enabled'] == $user['disabled'] ||
|
||||
$keycloakUser['firstName'] !== $user['individual']['first_name'] ||
|
||||
$keycloakUser['lastName'] !== $user['individual']['last_name'] ||
|
||||
$keycloakUser['email'] !== $user['individual']['email']
|
||||
$keycloakUser['email'] !== $user['individual']['email'] ||
|
||||
(empty($keycloakUser['attributes']['role_name']) || $keycloakUser['attributes']['role_name'] !== $user['role']['name']) ||
|
||||
(empty($keycloakUser['attributes']['role_uuid']) || $keycloakUser['attributes']['role_uuid'] !== $user['role']['uuid']) ||
|
||||
(empty($keycloakUser['attributes']['org_name']) || $keycloakUser['attributes']['org_name'] !== $user['organisation']['name']) ||
|
||||
(empty($keycloakUser['attributes']['org_uuid']) || $keycloakUser['attributes']['org_uuid'] !== $user['organisation']['uuid'])
|
||||
) {
|
||||
$change = [
|
||||
'enabled' => !$user['disabled'],
|
||||
'firstName' => $user['individual']['first_name'],
|
||||
'lastName' => $user['individual']['last_name'],
|
||||
'email' => $user['individual']['email'],
|
||||
'attributes' => [
|
||||
'role_name' => $user['role']['name'],
|
||||
'role_uuid' => $user['role']['uuid'],
|
||||
'org_name' => $user['organisation']['name'],
|
||||
'org_uuid' => $user['organisation']['uuid']
|
||||
]
|
||||
];
|
||||
$response = $this->restApiRequest('%s/admin/realms/%s/users/' . $keycloakUser['id'], $change, 'put');
|
||||
if (!$response->isOk()) {
|
||||
|
@ -389,14 +334,20 @@ class AuthKeycloakBehavior extends Behavior
|
|||
return false;
|
||||
}
|
||||
|
||||
private function createUser(array $user, string $clientId, array $rolesParsed)
|
||||
private function createUser(array $user, string $clientId)
|
||||
{
|
||||
$newUser = [
|
||||
'username' => $user['username'],
|
||||
'enabled' => !$user['disabled'],
|
||||
'firstName' => $user['individual']['first_name'],
|
||||
'lastName' => $user['individual']['last_name'],
|
||||
'email' => $user['individual']['email']
|
||||
'email' => $user['individual']['email'],
|
||||
'attributes' => [
|
||||
'role_name' => $user['role']['name'],
|
||||
'role_uuid' => $user['role']['uuid'],
|
||||
'org_name' => $user['organisation']['name'],
|
||||
'org_uuid' => $user['organisation']['uuid']
|
||||
]
|
||||
];
|
||||
$response = $this->restApiRequest('%s/admin/realms/%s/users', $newUser, 'post');
|
||||
if (!$response->isOk()) {
|
||||
|
@ -424,119 +375,78 @@ class AuthKeycloakBehavior extends Behavior
|
|||
$users[0]['id'] = $users[0]['id'][0];
|
||||
}
|
||||
$user['id'] = $users[0]['id'];
|
||||
$this->assignRolesToUser($user, $rolesParsed, $clientId);
|
||||
return $user['id'];
|
||||
}
|
||||
|
||||
private function assignRolesToUser(array $user, array $rolesParsed, string $clientId): bool
|
||||
{
|
||||
$roles = [
|
||||
[
|
||||
'id' => $rolesParsed['Role:' . $user['role']['name']],
|
||||
'name' => 'Role:' . $user['role']['name'],
|
||||
'clientRole' => true,
|
||||
'containerId' => $clientId
|
||||
],
|
||||
[
|
||||
'id' => $rolesParsed['Organisation:' . $user['organisation']['name']],
|
||||
'name' => 'Organisation:' . $user['organisation']['name'],
|
||||
'clientRole' => true,
|
||||
'containerId' => $clientId
|
||||
]
|
||||
];
|
||||
$response = $this->restApiRequest('%s/admin/realms/%s/users/' . $user['id'] . '/role-mappings/clients/' . $clientId, $roles, 'post');
|
||||
if (!$response->isOk()) {
|
||||
$this->_table->auditLogs()->insert([
|
||||
'request_action' => 'keycloakAssignRoles',
|
||||
'model' => 'User',
|
||||
'model_id' => 0,
|
||||
'model_title' => __('Failed to create assign role ({0}) in keycloak to user {1}', $user['role']['name'], $user['username']),
|
||||
'changed' => [
|
||||
'code' => $response->getStatusCode(),
|
||||
'error_body' => $response->getStringBody()
|
||||
]
|
||||
]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private function checkAndUpdateUserRoles(array $keycloakUser, array $user, string $clientId, array $rolesParsed): bool
|
||||
{
|
||||
$assignedRoles = $this->restApiRequest('%s/admin/realms/%s/users/' . $keycloakUser['id'] . '/role-mappings/clients/' . $clientId, [], 'get');
|
||||
$assignedRoles = json_decode($assignedRoles->getStringBody(), true);
|
||||
$keycloakUserRoles = Hash::extract($assignedRoles, '{n}.name');
|
||||
$assignedRolesParsed = [];
|
||||
foreach ($assignedRoles as $k => $v) {
|
||||
$assignedRolesParsed[$v['name']] = $v;
|
||||
}
|
||||
$userRoles = [
|
||||
'Organisation:' . $user['organisation']['name'] => [
|
||||
'id' => $rolesParsed['Organisation:' . $user['organisation']['name']],
|
||||
'name' => 'Organisation:' . $user['organisation']['name'],
|
||||
'clientRole' => true,
|
||||
'containerId' => $clientId
|
||||
],
|
||||
'Role:' . $user['role']['name'] => [
|
||||
'id' => $rolesParsed['Role:' . $user['role']['name']],
|
||||
'name' => 'Role:' . $user['role']['name'],
|
||||
'clientRole' => true,
|
||||
'containerId' => $clientId
|
||||
]
|
||||
];
|
||||
$toAdd = array_diff(array_keys($userRoles), $keycloakUserRoles);
|
||||
$toRemove = array_diff($keycloakUserRoles, array_keys($userRoles));
|
||||
$changed = false;
|
||||
foreach ($toRemove as $k => $role) {
|
||||
if (substr($role, 0, strlen('Organisation:')) !== 'Organisation:' && substr($role, 0, strlen('Role:')) !== 'Role:') {
|
||||
unset($toRemove[$k]);
|
||||
} else {
|
||||
$toRemove[$k] = $assignedRolesParsed[$role];
|
||||
}
|
||||
}
|
||||
if (!empty($toRemove)) {
|
||||
$toRemove = array_values($toRemove);
|
||||
$response = $this->restApiRequest('%s/admin/realms/%s/users/' . $keycloakUser['id'] . '/role-mappings/clients/' . $clientId, $toRemove, 'delete');
|
||||
if (!$response->isOk()) {
|
||||
$this->_table->auditLogs()->insert([
|
||||
'request_action' => 'keycloakDetachRole',
|
||||
'model' => 'User',
|
||||
'model_id' => 0,
|
||||
'model_title' => __('Failed to detach role ({0}) in keycloak from user {1}', $user['role']['name'], $user['username']),
|
||||
'changed' => [
|
||||
'code' => $response->getStatusCode(),
|
||||
'error_body' => $response->getStringBody()
|
||||
]
|
||||
]);
|
||||
} else {
|
||||
$changed = true;
|
||||
}
|
||||
}
|
||||
foreach ($toAdd as $k => $name) {
|
||||
$toAdd[$k] = $userRoles[$name];
|
||||
}
|
||||
if (!empty($toAdd)) {
|
||||
$toAdd = array_values($toAdd);
|
||||
$response = $this->restApiRequest('%s/admin/realms/%s/users/' . $keycloakUser['id'] . '/role-mappings/clients/' . $clientId, $toAdd, 'post');
|
||||
if (!$response->isOk()) {
|
||||
$this->_table->auditLogs()->insert([
|
||||
'request_action' => 'keycloakAttachRoles',
|
||||
'model' => 'User',
|
||||
'model_id' => 0,
|
||||
'model_title' => __('Failed to attach role ({0}) in keycloak to user {1}', $user['role']['name'], $user['username']),
|
||||
'changed' => [
|
||||
'code' => $response->getStatusCode(),
|
||||
'error_body' => $response->getStringBody()
|
||||
]
|
||||
]);
|
||||
} else {
|
||||
$changed = true;
|
||||
}
|
||||
}
|
||||
return $changed;
|
||||
}
|
||||
|
||||
private function urlencodeEscapeForSprintf(string $input): string
|
||||
{
|
||||
return str_replace('%', '%%', $input);
|
||||
}
|
||||
|
||||
public function updateMappers(): bool
|
||||
{
|
||||
$clientId = $this->getClientId();
|
||||
$response = $this->restApiRequest('%s/admin/realms/%s/clients/' . $clientId . '/protocol-mappers/models?protocolMapper=oidc-usermodel-attribute-mapper', [], 'get');
|
||||
if ($response->isOk()) {
|
||||
$mappers = json_decode($response->getStringBody(), true);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
$enabledMappers = [];
|
||||
$defaultMappers = [
|
||||
'org_name' => 0,
|
||||
'org_uuid' => 0,
|
||||
'role_name' => 0,
|
||||
'role_uuid' => 0
|
||||
];
|
||||
$mappersToEnable = explode(',', Configure::read('keycloak.user_meta_mapping'));
|
||||
foreach ($mappers as $mapper) {
|
||||
if ($mapper['protocolMapper'] !== 'oidc-usermodel-attribute-mapper') {
|
||||
continue;
|
||||
}
|
||||
if (in_array($mapper['name'], array_keys($defaultMappers))) {
|
||||
$defaultMappers[$mapper['name']] = 1;
|
||||
continue;
|
||||
}
|
||||
$enabledMappers[$mapper['name']] = $mapper;
|
||||
}
|
||||
$payload = [];
|
||||
foreach ($mappersToEnable as $mapperToEnable) {
|
||||
$payload[] = [
|
||||
'protocol' => 'openid-connect',
|
||||
'name' => $mapperToEnable,
|
||||
'protocolMapper' => 'oidc-usermodel-attribute-mapper',
|
||||
'config' => [
|
||||
'id.token.claim' => true,
|
||||
'access.token.claim' => true,
|
||||
'userinfo.token.claim' => true,
|
||||
'user.attribute' => $mapperToEnable,
|
||||
'claim.name' => $mapperToEnable
|
||||
]
|
||||
];
|
||||
}
|
||||
foreach ($defaultMappers as $defaultMapper => $enabled) {
|
||||
if (!$enabled) {
|
||||
$payload[] = [
|
||||
'protocol' => 'openid-connect',
|
||||
'name' => $defaultMapper,
|
||||
'protocolMapper' => 'oidc-usermodel-attribute-mapper',
|
||||
'config' => [
|
||||
'id.token.claim' => true,
|
||||
'access.token.claim' => true,
|
||||
'userinfo.token.claim' => true,
|
||||
'user.attribute' => $defaultMapper,
|
||||
'claim.name' => $defaultMapper
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
if (!empty($payload)) {
|
||||
$response = $this->restApiRequest('%s/admin/realms/%s/clients/' . $clientId . '/protocol-mappers/add-models', $payload, 'post');
|
||||
if (!$response->isOk()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -61,7 +61,11 @@ class AppModel extends Entity
|
|||
public function rearrangeTags(array $tags): array
|
||||
{
|
||||
foreach ($tags as &$tag) {
|
||||
unset($tag['_joinData']);
|
||||
$tag = [
|
||||
'id' => $tag['id'],
|
||||
'name' => $tag['name'],
|
||||
'colour' => $tag['colour']
|
||||
];
|
||||
}
|
||||
return $tags;
|
||||
}
|
||||
|
@ -70,14 +74,47 @@ class AppModel extends Entity
|
|||
{
|
||||
$rearrangedAlignments = [];
|
||||
$validAlignmentTypes = ['individual', 'organisation'];
|
||||
$alignmentDataToKeep = [
|
||||
'individual' => [
|
||||
'id',
|
||||
'email'
|
||||
],
|
||||
'organisation' => [
|
||||
'id',
|
||||
'uuid',
|
||||
'name'
|
||||
]
|
||||
];
|
||||
foreach ($alignments as $alignment) {
|
||||
foreach ($validAlignmentTypes as $type) {
|
||||
foreach (array_keys($alignmentDataToKeep) as $type) {
|
||||
if (isset($alignment[$type])) {
|
||||
$alignment[$type]['type'] = $alignment['type'];
|
||||
$rearrangedAlignments[$type][] = $alignment[$type];
|
||||
$temp = [];
|
||||
foreach ($alignmentDataToKeep[$type] as $field) {
|
||||
$temp[$field] = $alignment[$type][$field];
|
||||
}
|
||||
$rearrangedAlignments[$type][] = $temp;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $rearrangedAlignments;
|
||||
}
|
||||
|
||||
public function rearrangeSimplify(array $typesToRearrange): void
|
||||
{
|
||||
if (in_array('organisation', $typesToRearrange) && isset($this->organisation)) {
|
||||
$this->organisation = [
|
||||
'id' => $this->organisation['id'],
|
||||
'name' => $this->organisation['name'],
|
||||
'uuid' => $this->organisation['uuid']
|
||||
];
|
||||
}
|
||||
if (in_array('individual', $typesToRearrange) && isset($this->individual)) {
|
||||
$this->individual = [
|
||||
'id' => $this->individual['id'],
|
||||
'email' => $this->individual['email'],
|
||||
'uuid' => $this->individual['uuid']
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,4 +7,9 @@ use Cake\ORM\Entity;
|
|||
|
||||
class EncryptionKey extends AppModel
|
||||
{
|
||||
|
||||
public function rearrangeForAPI(): void
|
||||
{
|
||||
$this->rearrangeSimplify(['organisation', 'individual']);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,12 +33,28 @@ class Individual extends AppModel
|
|||
{
|
||||
$emails = [];
|
||||
if (!empty($this->meta_fields)) {
|
||||
foreach ($this->meta_fields as $metaField) {
|
||||
if (str_contains($metaField->field, 'email')) {
|
||||
$emails[] = $metaField;
|
||||
}
|
||||
}
|
||||
foreach ($this->meta_fields as $metaField) {
|
||||
if (!empty($metaField->field) && str_contains($metaField->field, 'email')) {
|
||||
$emails[] = $metaField;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $emails;
|
||||
}
|
||||
|
||||
public function rearrangeForAPI(): void
|
||||
{
|
||||
if (!empty($this->tags)) {
|
||||
$this->tags = $this->rearrangeTags($this->tags);
|
||||
}
|
||||
if (!empty($this->alignments)) {
|
||||
$this->alignments = $this->rearrangeAlignments($this->alignments);
|
||||
}
|
||||
if (!empty($this->meta_fields)) {
|
||||
$this->rearrangeMetaFields();
|
||||
}
|
||||
if (!empty($this->MetaTemplates)) {
|
||||
unset($this->MetaTemplates);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
namespace App\Model\Entity;
|
||||
|
||||
use App\Model\Entity\AppModel;
|
||||
use Cake\ORM\Entity;
|
||||
|
||||
class PermissionLimitation extends AppModel
|
||||
{
|
||||
|
||||
}
|
|
@ -11,7 +11,7 @@ use App\Settings\SettingsProvider\UserSettingsProvider;
|
|||
|
||||
class User extends AppModel
|
||||
{
|
||||
protected $_hidden = ['password', 'confirm_password'];
|
||||
protected $_hidden = ['password', 'confirm_password', 'user_settings_by_name', 'user_settings_by_name_with_fallback', 'SettingsProvider', 'user_settings'];
|
||||
|
||||
protected $_virtual = ['user_settings_by_name', 'user_settings_by_name_with_fallback'];
|
||||
|
||||
|
@ -48,4 +48,39 @@ class User extends AppModel
|
|||
return (new DefaultPasswordHasher())->hash($password);
|
||||
}
|
||||
}
|
||||
|
||||
public function rearrangeForAPI(): void
|
||||
{
|
||||
if (!empty($this->tags)) {
|
||||
$this->tags = $this->rearrangeTags($this->tags);
|
||||
}
|
||||
if (!empty($this->meta_fields)) {
|
||||
$this->rearrangeMetaFields();
|
||||
}
|
||||
if (!empty($this->MetaTemplates)) {
|
||||
unset($this->MetaTemplates);
|
||||
}
|
||||
if (!empty($this->user_settings_by_name)) {
|
||||
$this->rearrangeUserSettings();
|
||||
}
|
||||
$this->rearrangeSimplify(['organisation', 'individual']);
|
||||
}
|
||||
|
||||
private function rearrangeUserSettings()
|
||||
{
|
||||
$settings = [];
|
||||
if (isset($this->user_settings_by_name)) {
|
||||
foreach ($this->user_settings_by_name as $setting => $data) {
|
||||
$settings[$setting] = $data['value'];
|
||||
}
|
||||
}
|
||||
if (isset($this->user_settings_by_name_with_fallback)) {
|
||||
foreach ($this->user_settings_by_name_with_fallback as $setting => $data) {
|
||||
if (!isset($settings[$setting])) {
|
||||
$settings[$setting] = $data['value'];
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->settings = $settings;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -110,4 +110,15 @@ class IndividualsTable extends AppTable
|
|||
}
|
||||
return $query->group(['Individuals.id', 'Individuals.uuid']);
|
||||
}
|
||||
|
||||
public function getValidIndividualsToEdit(object $currentUser): array
|
||||
{
|
||||
$validIndividualIds = $this->Users->find('list')->select(['individual_id'])->where(
|
||||
[
|
||||
'organisation_id' => $currentUser['organisation_id'],
|
||||
'disabled' => 0
|
||||
]
|
||||
)->all()->toArray();
|
||||
return array_keys($validIndividualIds);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,119 @@
|
|||
<?php
|
||||
|
||||
namespace App\Model\Table;
|
||||
|
||||
use App\Model\Table\AppTable;
|
||||
use Cake\ORM\Table;
|
||||
use Cake\Validation\Validator;
|
||||
use Cake\Error\Debugger;
|
||||
use Cake\ORM\TableRegistry;
|
||||
|
||||
class PermissionLimitationsTable extends AppTable
|
||||
{
|
||||
public function initialize(array $config): void
|
||||
{
|
||||
parent::initialize($config);
|
||||
$this->addBehavior('AuditLog');
|
||||
$this->setDisplayField('permission');
|
||||
}
|
||||
|
||||
public function validationDefault(Validator $validator): Validator
|
||||
{
|
||||
$validator
|
||||
->notEmptyString('permission')
|
||||
->notEmptyString('scope')
|
||||
->requirePresence(['permission', 'scope', 'max_occurrence'], 'create');
|
||||
return $validator;
|
||||
}
|
||||
|
||||
public function getListOfLimitations(\App\Model\Entity\User $data)
|
||||
{
|
||||
$Users = TableRegistry::getTableLocator()->get('Users');
|
||||
$ownOrgUserIds = $Users->find('list', [
|
||||
'keyField' => 'id',
|
||||
'valueField' => 'id',
|
||||
'conditions' => [
|
||||
'organisation_id' => $data['organisation_id']
|
||||
]
|
||||
])->all()->toList();
|
||||
$MetaFields = TableRegistry::getTableLocator()->get('MetaFields');
|
||||
$raw = $this->find()->select(['scope', 'permission', 'max_occurrence'])->disableHydration()->toArray();
|
||||
$limitations = [];
|
||||
foreach ($raw as $entry) {
|
||||
$limitations[$entry['permission']][$entry['scope']] = [
|
||||
'limit' => $entry['max_occurrence']
|
||||
];
|
||||
}
|
||||
foreach ($limitations as $field => $data) {
|
||||
if (isset($data['global'])) {
|
||||
$limitations[$field]['global']['current'] = $MetaFields->find('all', [
|
||||
'conditions' => [
|
||||
'scope' => 'user',
|
||||
'field' => $field
|
||||
]
|
||||
])->count();
|
||||
}
|
||||
if (isset($data['global'])) {
|
||||
$limitations[$field]['organisation']['current'] = $MetaFields->find('all', [
|
||||
'conditions' => [
|
||||
'scope' => 'user',
|
||||
'field' => $field,
|
||||
'parent_id IN' => array_values($ownOrgUserIds)
|
||||
]
|
||||
])->count();
|
||||
}
|
||||
}
|
||||
return $limitations;
|
||||
}
|
||||
|
||||
public function attachLimitations(\App\Model\Entity\User $data)
|
||||
{
|
||||
$permissionLimitations = $this->getListOfLimitations($data);
|
||||
$icons = [
|
||||
'global' => 'globe',
|
||||
'organisation' => 'sitemap'
|
||||
|
||||
];
|
||||
if (!empty($data['MetaTemplates'])) {
|
||||
foreach ($data['MetaTemplates'] as &$metaTemplate) {
|
||||
foreach ($metaTemplate['meta_template_fields'] as &$meta_template_field) {
|
||||
$boolean = $meta_template_field['type'] === 'boolean';
|
||||
foreach ($meta_template_field['metaFields'] as &$metaField) {
|
||||
if ($boolean) {
|
||||
$metaField['value'] = '<i class="fas fa-' . ((bool)$metaField['value'] ? 'check' : 'times') . '"></i>';
|
||||
$metaField['no_escaping'] = true;
|
||||
}
|
||||
if (isset($permissionLimitations[$metaField['field']])) {
|
||||
foreach ($permissionLimitations[$metaField['field']] as $scope => $value) {
|
||||
$messageType = 'warning';
|
||||
if ($value['limit'] > $value['current']) {
|
||||
$messageType = 'info';
|
||||
}
|
||||
if ($value['limit'] < $value['current']) {
|
||||
$messageType = 'danger';
|
||||
}
|
||||
if (empty($metaField[$messageType])) {
|
||||
$metaField[$messageType] = '';
|
||||
}
|
||||
$altText = __(
|
||||
'There is a limitation enforced on the number of users with this permission {0}. Currently {1} slot(s) are used up of a maximum of {2} slot(s).',
|
||||
$scope === 'global' ? __('instance wide') : __('for your organisation'),
|
||||
$value['current'],
|
||||
$value['limit']
|
||||
);
|
||||
$metaField[$messageType] .= sprintf(
|
||||
' <span title="%s"><span class="text-dark"><i class="fas fa-%s"></i>: </span>%s/%s</span>',
|
||||
$altText,
|
||||
$icons[$scope],
|
||||
$value['current'],
|
||||
$value['limit']
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
}
|
|
@ -284,6 +284,14 @@ class CerebrateSettingsProvider extends BaseSettingsProvider
|
|||
'description' => __('family_name mapped name in keycloak'),
|
||||
'dependsOn' => 'keycloak.enabled'
|
||||
],
|
||||
'keycloak.user_meta_mapping' => [
|
||||
'name' => 'User Meta-field attribute mapping',
|
||||
'type' => 'string',
|
||||
'severity' => 'info',
|
||||
'default' => '',
|
||||
'description' => __('List of user metafields to push to keycloak as attributes. When using multiple templates, the attribute names have to be unique. Expects a comma separated list.'),
|
||||
'dependsOn' => 'keycloak.enabled'
|
||||
]
|
||||
]
|
||||
]
|
||||
],
|
||||
|
|
|
@ -23,6 +23,7 @@ class UsersTable extends AppTable
|
|||
parent::initialize($config);
|
||||
$this->addBehavior('Timestamp');
|
||||
$this->addBehavior('UUID');
|
||||
$this->addBehavior('MetaFields');
|
||||
$this->addBehavior('AuditLog');
|
||||
$this->initAuthBehaviors();
|
||||
$this->belongsTo(
|
||||
|
@ -58,7 +59,9 @@ class UsersTable extends AppTable
|
|||
|
||||
public function beforeMarshal(EventInterface $event, ArrayObject $data, ArrayObject $options)
|
||||
{
|
||||
$data['username'] = trim(mb_strtolower($data['username']));
|
||||
if (isset($data['username'])) {
|
||||
$data['username'] = trim(mb_strtolower($data['username']));
|
||||
}
|
||||
}
|
||||
|
||||
public function beforeSave(EventInterface $event, EntityInterface $entity, ArrayObject $options)
|
||||
|
@ -66,9 +69,51 @@ class UsersTable extends AppTable
|
|||
if (!$entity->isNew()) {
|
||||
$success = $this->handleUserUpdateRouter($entity);
|
||||
}
|
||||
$permissionRestrictionCheck = $this->checkPermissionRestrictions($entity);
|
||||
if ($permissionRestrictionCheck !== true) {
|
||||
$entity->setErrors($permissionRestrictionCheck);
|
||||
$event->stopPropagation();
|
||||
$event->setResult(false);
|
||||
return false;
|
||||
}
|
||||
return $success;
|
||||
}
|
||||
|
||||
private function checkPermissionRestrictions(EntityInterface $entity)
|
||||
{
|
||||
if (!isset($this->PermissionLimitations)) {
|
||||
$this->PermissionLimitations = TableRegistry::get('PermissionLimitations');
|
||||
}
|
||||
$new = $entity->isNew();
|
||||
$permissions = $this->PermissionLimitations->getListOfLimitations($entity);
|
||||
foreach ($permissions as $permission_name => $permission) {
|
||||
foreach ($permission as $scope => $permission_data) {
|
||||
if (!empty($entity['meta_fields'])) {
|
||||
$enabled = false;
|
||||
foreach ($entity['meta_fields'] as $metaField) {
|
||||
if ($metaField['field'] === $permission_name) {
|
||||
$enabled = true;
|
||||
}
|
||||
}
|
||||
if (!$enabled) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
$valueToCompareTo = $permission_data['current'] + ($new ? 1 : 0);
|
||||
if ($valueToCompareTo > $permission_data['limit']) {
|
||||
return [
|
||||
$permission_name =>
|
||||
__(
|
||||
'{0} limit exceeded.',
|
||||
$scope
|
||||
)
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private function initAuthBehaviors()
|
||||
{
|
||||
if (!empty(Configure::read('keycloak'))) {
|
||||
|
@ -79,6 +124,7 @@ class UsersTable extends AppTable
|
|||
public function validationDefault(Validator $validator): Validator
|
||||
{
|
||||
$validator
|
||||
->setStopOnFailure()
|
||||
->requirePresence(['password'], 'create')
|
||||
->add('password', [
|
||||
'password_complexity' => [
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
"version": "1.6",
|
||||
"version": "1.7",
|
||||
"application": "Cerebrate"
|
||||
}
|
||||
|
|
|
@ -811,7 +811,10 @@ class BoostrapListTable extends BootstrapGeneric
|
|||
private function genCell($field = [])
|
||||
{
|
||||
if (isset($field['raw'])) {
|
||||
$cellContent = h($field['raw']);
|
||||
$cellContent = $field['raw'];
|
||||
if (empty($field['no_escaping'])) {
|
||||
$field['raw'] = h($field['raw']);
|
||||
}
|
||||
} else if (isset($field['formatter'])) {
|
||||
$cellContent = $field['formatter']($this->getValueFromObject($field), $this->item);
|
||||
} else if (isset($field['type'])) {
|
||||
|
@ -822,6 +825,11 @@ class BoostrapListTable extends BootstrapGeneric
|
|||
} else {
|
||||
$cellContent = h($this->getValueFromObject($field));
|
||||
}
|
||||
foreach (['info', 'warning', 'danger'] as $message_type) {
|
||||
if (!empty($field[$message_type])) {
|
||||
$cellContent .= sprintf(' <span class="text-%s">%s</span>', $message_type, $field[$message_type]);
|
||||
}
|
||||
}
|
||||
return $this->genNode('td', [
|
||||
'class' => [
|
||||
'col-8 col-sm-10',
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
array(
|
||||
'field' => 'tag_list',
|
||||
'type' => 'tags',
|
||||
'requirements' => $this->request->getParam('action') === 'edit'
|
||||
'requirements' => ($this->request->getParam('action') === 'edit' && $loggedUser['role']['perm_admin'])
|
||||
),
|
||||
),
|
||||
'submit' => array(
|
||||
|
|
|
@ -81,12 +81,25 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
[
|
||||
'open_modal' => '/individuals/edit/[onclick_params_data_path]',
|
||||
'modal_params_data_path' => 'id',
|
||||
'icon' => 'edit'
|
||||
'icon' => 'edit',
|
||||
'complex_requirement' => [
|
||||
'function' => function ($row, $options) use ($loggedUser, $editableIds) {
|
||||
if ($loggedUser['role']['perm_admin'] || ($editableIds && in_array($row['id'], $editableIds))) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
]
|
||||
],
|
||||
[
|
||||
'open_modal' => '/individuals/delete/[onclick_params_data_path]',
|
||||
'modal_params_data_path' => 'id',
|
||||
'icon' => 'trash'
|
||||
'icon' => 'trash',
|
||||
'complex_requirement' => [
|
||||
'function' => function ($row, $options) use ($loggedUser) {
|
||||
return (bool)$loggedUser['role']['perm_admin'];
|
||||
}
|
||||
]
|
||||
],
|
||||
]
|
||||
]
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
echo $this->element('genericElements/Form/genericForm', [
|
||||
'data' => [
|
||||
'description' => __(
|
||||
'Add a limitation of how many users can have the given permission. The scope applies the limitation globally or for a given organisation.
|
||||
Permissions can be valid role permissions or any user meta field.
|
||||
An example: perm_misp global limit 500, organisation limit 10 would ensure that there are a maximum of 500 MISP admitted users on the instance, limiting the number of users to 10 / org.'
|
||||
),
|
||||
'model' => 'PermissionLimitation',
|
||||
'fields' => [
|
||||
[
|
||||
'field' => 'scope',
|
||||
'type' => 'dropdown',
|
||||
'label' => 'Scope',
|
||||
'options' => [
|
||||
'global' => 'global',
|
||||
'organisation' => 'organisation'
|
||||
]
|
||||
],
|
||||
[
|
||||
'field' => 'permission'
|
||||
],
|
||||
[
|
||||
'field' => 'max_occurrence',
|
||||
'label' => 'Limit'
|
||||
],
|
||||
[
|
||||
'field' => 'comment',
|
||||
'label' => 'Comment'
|
||||
]
|
||||
],
|
||||
'submit' => [
|
||||
'action' => $this->request->getParam('action')
|
||||
]
|
||||
]
|
||||
]);
|
||||
?>
|
||||
</div>
|
|
@ -0,0 +1,79 @@
|
|||
<?php
|
||||
echo $this->element('genericElements/IndexTable/index_table', [
|
||||
'data' => [
|
||||
'data' => $data,
|
||||
'top_bar' => [
|
||||
'children' => [
|
||||
[
|
||||
'type' => 'simple',
|
||||
'children' => [
|
||||
'data' => [
|
||||
'type' => 'simple',
|
||||
'text' => __('Add permission limitation'),
|
||||
'class' => 'btn btn-primary',
|
||||
'popover_url' => '/PermissionLimitations/add'
|
||||
]
|
||||
]
|
||||
],
|
||||
[
|
||||
'type' => 'search',
|
||||
'button' => __('Search'),
|
||||
'placeholder' => __('Enter value to search'),
|
||||
'data' => '',
|
||||
'searchKey' => 'value'
|
||||
]
|
||||
]
|
||||
],
|
||||
'fields' => [
|
||||
[
|
||||
'name' => '#',
|
||||
'sort' => 'id',
|
||||
'data_path' => 'id',
|
||||
],
|
||||
[
|
||||
'name' => __('Scope'),
|
||||
'sort' => 'scope',
|
||||
'data_path' => 'scope',
|
||||
],
|
||||
[
|
||||
'name' => __('Permission'),
|
||||
'sort' => 'permission',
|
||||
'data_path' => 'permission'
|
||||
],
|
||||
[
|
||||
'name' => __('Limit'),
|
||||
'sort' => 'max_occurrence',
|
||||
'data_path' => 'max_occurrence'
|
||||
],
|
||||
[
|
||||
'name' => __('Comment'),
|
||||
'sort' => 'comment',
|
||||
'data_path' => 'comment'
|
||||
]
|
||||
],
|
||||
'title' => __('Permission Limitations Index'),
|
||||
'description' => __('A list of configurable user roles. Create or modify user access roles based on the settings below.'),
|
||||
'pull' => 'right',
|
||||
'actions' => [
|
||||
[
|
||||
'url' => '/permissionLimitations/view',
|
||||
'url_params_data_paths' => ['id'],
|
||||
'icon' => 'eye'
|
||||
],
|
||||
[
|
||||
'open_modal' => '/permissionLimitations/edit/[onclick_params_data_path]',
|
||||
'modal_params_data_path' => 'id',
|
||||
'icon' => 'edit',
|
||||
'requirement' => !empty($loggedUser['role']['perm_admin'])
|
||||
],
|
||||
[
|
||||
'open_modal' => '/permissionLimitations/delete/[onclick_params_data_path]',
|
||||
'modal_params_data_path' => 'id',
|
||||
'icon' => 'trash',
|
||||
'requirement' => !empty($loggedUser['role']['perm_admin'])
|
||||
],
|
||||
]
|
||||
]
|
||||
]);
|
||||
echo '</div>';
|
||||
?>
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
echo $this->element(
|
||||
'/genericElements/SingleViews/single_view',
|
||||
[
|
||||
'data' => $entity,
|
||||
'fields' => [
|
||||
[
|
||||
'key' => __('ID'),
|
||||
'path' => 'id'
|
||||
],
|
||||
[
|
||||
'key' => __('Scope'),
|
||||
'path' => 'scope'
|
||||
],
|
||||
[
|
||||
'key' => __('Permission'),
|
||||
'path' => 'permission'
|
||||
],
|
||||
[
|
||||
'key' => __('Limit'),
|
||||
'path' => 'limit'
|
||||
],
|
||||
[
|
||||
'key' => __('Comment'),
|
||||
'path' => 'comment'
|
||||
]
|
||||
],
|
||||
'children' => []
|
||||
]
|
||||
);
|
|
@ -112,6 +112,7 @@
|
|||
echo $this->Bootstrap->modal([
|
||||
'title' => empty($data['title']) ? sprintf('%s %s', $actionName, $modelName) : h($data['title']),
|
||||
'bodyHtml' => $this->element('genericElements/Form/formLayouts/formRaw', [
|
||||
'data' => $data,
|
||||
'formCreate' => $formCreate,
|
||||
'ajaxFlashMessage' => $ajaxFlashMessage,
|
||||
'fieldsString' => $fieldsString,
|
||||
|
@ -124,6 +125,7 @@
|
|||
]);
|
||||
} else if (!empty($raw)) {
|
||||
echo $this->element('genericElements/Form/formLayouts/formDefault', [
|
||||
'data' => $data,
|
||||
'actionName' => $actionName,
|
||||
'modelName' => $modelName,
|
||||
'submitButtonData' => $submitButtonData,
|
||||
|
@ -135,6 +137,7 @@
|
|||
]);
|
||||
} else {
|
||||
echo $this->element('genericElements/Form/formLayouts/formDefault', [
|
||||
'data' => $data,
|
||||
'actionName' => $actionName,
|
||||
'modelName' => $modelName,
|
||||
'submitButtonData' => $submitButtonData,
|
||||
|
|
|
@ -37,6 +37,9 @@ foreach ($metaTemplate->meta_template_fields as $metaTemplateField) {
|
|||
} else {
|
||||
$fieldData['field'] = sprintf('MetaTemplates.%s.meta_template_fields.%s.metaFields.%s.value', $metaField->meta_template_id, $metaField->meta_template_field_id, array_key_first($metaTemplateField->metaFields));
|
||||
}
|
||||
if ($metaTemplateField->type === 'boolean') {
|
||||
$fieldData['type'] = 'checkbox';
|
||||
}
|
||||
$this->Form->setTemplates($backupTemplates);
|
||||
$fieldsHtml .= $this->element(
|
||||
'genericElements/Form/fieldScaffold',
|
||||
|
@ -64,6 +67,9 @@ foreach ($metaTemplate->meta_template_fields as $metaTemplateField) {
|
|||
'field' => sprintf('MetaTemplates.%s.meta_template_fields.%s.metaFields.new.0', $metaTemplateField->meta_template_id, $metaTemplateField->id),
|
||||
'label' => $metaTemplateField->label,
|
||||
];
|
||||
if ($metaTemplateField->type === 'boolean') {
|
||||
$fieldData['type'] = 'checkbox';
|
||||
}
|
||||
$fieldsHtml .= $this->element(
|
||||
'genericElements/Form/fieldScaffold',
|
||||
[
|
||||
|
|
|
@ -27,4 +27,9 @@ if (!empty($field['url'])) {
|
|||
} else if (empty($field['raw'])) {
|
||||
$string = h($string);
|
||||
}
|
||||
foreach (['info', 'warning', 'danger'] as $message_type) {
|
||||
if (!empty($field[$message_type])) {
|
||||
$string .= sprintf(' (<span class="text-%s">%s</span>)', $message_type, $field[$message_type]);
|
||||
}
|
||||
}
|
||||
echo $string;
|
||||
|
|
|
@ -17,7 +17,10 @@ foreach($data['MetaTemplates'] as $metaTemplate) {
|
|||
foreach ($metaTemplateField->metaFields as $metaField) {
|
||||
$fields[] = [
|
||||
'key' => !$labelPrintedOnce ? $metaField->field : '',
|
||||
'raw' => $metaField->value
|
||||
'raw' => $metaField->value,
|
||||
'warning' => $metaField->warning ?? null,
|
||||
'info' => $metaField->info ?? null,
|
||||
'danger' => $metaField->danger ?? null
|
||||
];
|
||||
$labelPrintedOnce = true;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue