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' => []
+ ]
+);