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/modificationcli-modification-summary
parent
9d2c152a4e
commit
b1f09dc97e
|
@ -169,6 +169,13 @@ class ACLComponent extends Component
|
||||||
'Pages' => [
|
'Pages' => [
|
||||||
'display' => ['*']
|
'display' => ['*']
|
||||||
],
|
],
|
||||||
|
'PermissionLimitations' => [
|
||||||
|
"index" => ['*'],
|
||||||
|
"add" => ['perm_admin'],
|
||||||
|
"view" => ['*'],
|
||||||
|
"edit" => ['perm_admin'],
|
||||||
|
"delete" => ['perm_admin']
|
||||||
|
],
|
||||||
'Roles' => [
|
'Roles' => [
|
||||||
'add' => ['perm_admin'],
|
'add' => ['perm_admin'],
|
||||||
'delete' => ['perm_admin'],
|
'delete' => ['perm_admin'],
|
||||||
|
|
|
@ -123,6 +123,11 @@ class Sidemenu {
|
||||||
'url' => '/auditLogs/index',
|
'url' => '/auditLogs/index',
|
||||||
'icon' => 'history',
|
'icon' => 'history',
|
||||||
],
|
],
|
||||||
|
'PermissionLimitations' => [
|
||||||
|
'label' => __('Permission Limitations'),
|
||||||
|
'url' => '/permissionLimitations/index',
|
||||||
|
'icon' => 'jedi',
|
||||||
|
],
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
'API' => [
|
'API' => [
|
||||||
|
|
|
@ -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');
|
||||||
|
}
|
||||||
|
}
|
|
@ -137,7 +137,11 @@ class UsersController extends AppController
|
||||||
$id = $this->ACL->getUser()['id'];
|
$id = $this->ACL->getUser()['id'];
|
||||||
}
|
}
|
||||||
$this->CRUD->view($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();
|
$responsePayload = $this->CRUD->getResponsePayload();
|
||||||
if (!empty($responsePayload)) {
|
if (!empty($responsePayload)) {
|
||||||
|
@ -414,9 +418,4 @@ class UsersController extends AppController
|
||||||
}
|
}
|
||||||
$this->viewBuilder()->setLayout('login');
|
$this->viewBuilder()->setLayout('login');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test()
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Model\Entity;
|
||||||
|
|
||||||
|
use App\Model\Entity\AppModel;
|
||||||
|
use Cake\ORM\Entity;
|
||||||
|
|
||||||
|
class PermissionLimitation extends AppModel
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -59,7 +59,9 @@ class UsersTable extends AppTable
|
||||||
|
|
||||||
public function beforeMarshal(EventInterface $event, ArrayObject $data, ArrayObject $options)
|
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)
|
public function beforeSave(EventInterface $event, EntityInterface $entity, ArrayObject $options)
|
||||||
|
@ -67,9 +69,51 @@ class UsersTable extends AppTable
|
||||||
if (!$entity->isNew()) {
|
if (!$entity->isNew()) {
|
||||||
$success = $this->handleUserUpdateRouter($entity);
|
$success = $this->handleUserUpdateRouter($entity);
|
||||||
}
|
}
|
||||||
|
$permissionRestrictionCheck = $this->checkPermissionRestrictions($entity);
|
||||||
|
if ($permissionRestrictionCheck !== true) {
|
||||||
|
$entity->setErrors($permissionRestrictionCheck);
|
||||||
|
$event->stopPropagation();
|
||||||
|
$event->setResult(false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return $success;
|
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()
|
private function initAuthBehaviors()
|
||||||
{
|
{
|
||||||
if (!empty(Configure::read('keycloak'))) {
|
if (!empty(Configure::read('keycloak'))) {
|
||||||
|
@ -80,6 +124,7 @@ class UsersTable extends AppTable
|
||||||
public function validationDefault(Validator $validator): Validator
|
public function validationDefault(Validator $validator): Validator
|
||||||
{
|
{
|
||||||
$validator
|
$validator
|
||||||
|
->setStopOnFailure()
|
||||||
->requirePresence(['password'], 'create')
|
->requirePresence(['password'], 'create')
|
||||||
->add('password', [
|
->add('password', [
|
||||||
'password_complexity' => [
|
'password_complexity' => [
|
||||||
|
|
|
@ -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' => []
|
||||||
|
]
|
||||||
|
);
|
Loading…
Reference in New Issue