From b1f09dc97ee4dc270980cfa970b445b1eba7b833 Mon Sep 17 00:00:00 2001 From: iglocska Date: Wed, 9 Nov 2022 14:09:27 +0100 Subject: [PATCH] new: [permission limitations] subsystem added - add limitations for users with given meta fields - x number / org and y number / globally - add comments to the limitations - enforced on user creation/modification --- src/Controller/Component/ACLComponent.php | 7 ++ .../Component/Navigation/sidemenu.php | 5 + .../PermissionLimitationsController.php | 91 ++++++++++++++ src/Controller/UsersController.php | 11 +- src/Model/Entity/PermissionLimitation.php | 11 ++ .../Table/PermissionLimitationsTable.php | 119 ++++++++++++++++++ src/Model/Table/UsersTable.php | 47 ++++++- templates/PermissionLimitations/add.php | 38 ++++++ templates/PermissionLimitations/index.php | 79 ++++++++++++ templates/PermissionLimitations/view.php | 30 +++++ 10 files changed, 431 insertions(+), 7 deletions(-) create mode 100644 src/Controller/PermissionLimitationsController.php create mode 100644 src/Model/Entity/PermissionLimitation.php create mode 100644 src/Model/Table/PermissionLimitationsTable.php create mode 100644 templates/PermissionLimitations/add.php create mode 100644 templates/PermissionLimitations/index.php create mode 100644 templates/PermissionLimitations/view.php diff --git a/src/Controller/Component/ACLComponent.php b/src/Controller/Component/ACLComponent.php index 095c548..9c3dd4c 100644 --- a/src/Controller/Component/ACLComponent.php +++ b/src/Controller/Component/ACLComponent.php @@ -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'], diff --git a/src/Controller/Component/Navigation/sidemenu.php b/src/Controller/Component/Navigation/sidemenu.php index e9b9281..84fcf6c 100644 --- a/src/Controller/Component/Navigation/sidemenu.php +++ b/src/Controller/Component/Navigation/sidemenu.php @@ -123,6 +123,11 @@ class Sidemenu { 'url' => '/auditLogs/index', 'icon' => 'history', ], + 'PermissionLimitations' => [ + 'label' => __('Permission Limitations'), + 'url' => '/permissionLimitations/index', + 'icon' => 'jedi', + ], ] ], 'API' => [ diff --git a/src/Controller/PermissionLimitationsController.php b/src/Controller/PermissionLimitationsController.php new file mode 100644 index 0000000..b3d98f6 --- /dev/null +++ b/src/Controller/PermissionLimitationsController.php @@ -0,0 +1,91 @@ +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'); + } +} diff --git a/src/Controller/UsersController.php b/src/Controller/UsersController.php index 426f4a3..091382a 100644 --- a/src/Controller/UsersController.php +++ b/src/Controller/UsersController.php @@ -137,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)) { @@ -414,9 +418,4 @@ class UsersController extends AppController } $this->viewBuilder()->setLayout('login'); } - - public function test() - { - - } } diff --git a/src/Model/Entity/PermissionLimitation.php b/src/Model/Entity/PermissionLimitation.php new file mode 100644 index 0000000..17b0e4c --- /dev/null +++ b/src/Model/Entity/PermissionLimitation.php @@ -0,0 +1,11 @@ +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'] = ''; + $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( + ' : %s/%s', + $altText, + $icons[$scope], + $value['current'], + $value['limit'] + ); + } + } + } + } + } + } + return $data; + } +} diff --git a/src/Model/Table/UsersTable.php b/src/Model/Table/UsersTable.php index 2cfe8b2..647d32f 100644 --- a/src/Model/Table/UsersTable.php +++ b/src/Model/Table/UsersTable.php @@ -59,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) @@ -67,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'))) { @@ -80,6 +124,7 @@ class UsersTable extends AppTable public function validationDefault(Validator $validator): Validator { $validator + ->setStopOnFailure() ->requirePresence(['password'], 'create') ->add('password', [ 'password_complexity' => [ diff --git a/templates/PermissionLimitations/add.php b/templates/PermissionLimitations/add.php new file mode 100644 index 0000000..0dffc70 --- /dev/null +++ b/templates/PermissionLimitations/add.php @@ -0,0 +1,38 @@ +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') + ] + ] + ]); +?> + diff --git a/templates/PermissionLimitations/index.php b/templates/PermissionLimitations/index.php new file mode 100644 index 0000000..20dff1c --- /dev/null +++ b/templates/PermissionLimitations/index.php @@ -0,0 +1,79 @@ +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 ''; +?> diff --git a/templates/PermissionLimitations/view.php b/templates/PermissionLimitations/view.php new file mode 100644 index 0000000..0ec3852 --- /dev/null +++ b/templates/PermissionLimitations/view.php @@ -0,0 +1,30 @@ +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' => [] + ] +);