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
cli-modification-summary
iglocska 2022-11-09 14:09:27 +01:00
parent 9d2c152a4e
commit b1f09dc97e
No known key found for this signature in database
GPG Key ID: BEA224F1FEF113AC
10 changed files with 431 additions and 7 deletions

View File

@ -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'],

View File

@ -123,6 +123,11 @@ class Sidemenu {
'url' => '/auditLogs/index',
'icon' => 'history',
],
'PermissionLimitations' => [
'label' => __('Permission Limitations'),
'url' => '/permissionLimitations/index',
'icon' => 'jedi',
],
]
],
'API' => [

View File

@ -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');
}
}

View File

@ -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()
{
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace App\Model\Entity;
use App\Model\Entity\AppModel;
use Cake\ORM\Entity;
class PermissionLimitation extends AppModel
{
}

View File

@ -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;
}
}

View File

@ -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' => [

View File

@ -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>

View File

@ -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>';
?>

View File

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