diff --git a/config/Migrations/20230907000003_OrgGrouping.php b/config/Migrations/20230907000003_OrgGrouping.php
new file mode 100644
index 0000000..764bf39
--- /dev/null
+++ b/config/Migrations/20230907000003_OrgGrouping.php
@@ -0,0 +1,123 @@
+hasTable('org_groups');
+ if (!$exists) {
+ $orgGroupsTable = $this->table('org_groups', [
+ 'signed' => false,
+ 'collation' => 'utf8mb4_unicode_ci'
+ ]);
+ $orgGroupsTable
+ ->addColumn('id', 'integer', [
+ 'autoIncrement' => true,
+ 'limit' => 10,
+ 'signed' => false,
+ ])
+ ->addPrimaryKey('id')
+ ->addColumn('uuid', 'string', [
+ 'null' => false,
+ 'limit' => 40,
+ 'collation' => 'ascii_general_ci',
+ 'encoding' => 'ascii',
+ ])
+ ->addColumn('name', 'string', [
+ 'null' => false,
+ 'limit' => 191,
+ 'collation' => 'utf8mb4_unicode_ci',
+ 'encoding' => 'utf8mb4',
+ ])
+ ->addColumn('description', 'text', [
+ 'default' => null,
+ 'null' => true
+ ])
+ ->addColumn('created', 'datetime', [
+ 'null' => false
+ ])
+ ->addColumn('modified', 'datetime', [
+ 'null' => false
+ ])
+ ->addIndex('name')
+ ->addIndex('uuid', ['unique' => true]);
+
+ $orgGroupsTable->create();
+ }
+
+
+ $exists = $this->hasTable('org_groups_organisations');
+ if (!$exists) {
+ $orgGroupsTable = $this->table('org_groups_organisations', [
+ 'signed' => false,
+ 'collation' => 'utf8mb4_unicode_ci'
+ ]);
+ $orgGroupsTable
+ ->addColumn('id', 'integer', [
+ 'autoIncrement' => true,
+ 'limit' => 10,
+ 'signed' => false,
+ ])
+ ->addPrimaryKey('id')
+ ->addColumn('org_group_id', 'integer', [
+ 'limit' => 10,
+ 'signed' => false,
+ ])
+ ->addIndex('org_group_id')
+ ->addColumn('organisation_id', 'integer', [
+ 'limit' => 10,
+ 'signed' => false,
+ ])
+ ->addIndex('organisation_id')
+ ->addIndex(['org_group_id', 'organisation_id'], ['unique' => true]);
+
+ $orgGroupsTable->create();
+ }
+
+
+ $exists = $this->hasTable('org_groups_admins');
+ if (!$exists) {
+ $orgGroupsTable = $this->table('org_groups_admins', [
+ 'signed' => false,
+ 'collation' => 'utf8mb4_unicode_ci'
+ ]);
+ $orgGroupsTable
+ ->addColumn('id', 'integer', [
+ 'autoIncrement' => true,
+ 'limit' => 10,
+ 'signed' => false,
+ ])
+ ->addPrimaryKey('id')
+ ->addColumn('org_group_id', 'integer', [
+ 'limit' => 10,
+ 'signed' => false,
+ ])
+ ->addIndex('org_group_id')
+ ->addColumn('user_id', 'integer', [
+ 'limit' => 10,
+ 'signed' => false,
+ ])
+ ->addIndex('user_id')
+ ->addIndex(['org_group_id', 'user_id'], ['unique' => true]);
+
+ $orgGroupsTable->create();
+ }
+ }
+}
diff --git a/config/Migrations/20230907000004_GroupAdminRole.php b/config/Migrations/20230907000004_GroupAdminRole.php
new file mode 100644
index 0000000..47b47bd
--- /dev/null
+++ b/config/Migrations/20230907000004_GroupAdminRole.php
@@ -0,0 +1,35 @@
+table('roles')->hasColumn('perm_group_admin');
+ if (!$exists) {
+ $this->table('roles')
+ ->addColumn('perm_group_admin', 'boolean', [
+ 'default' => 0,
+ 'null' => false,
+ ])
+ ->addIndex('perm_group_admin')
+ ->update();
+ }
+ }
+}
diff --git a/src/Controller/Component/ACLComponent.php b/src/Controller/Component/ACLComponent.php
index d403c28..dde8323 100644
--- a/src/Controller/Component/ACLComponent.php
+++ b/src/Controller/Component/ACLComponent.php
@@ -161,6 +161,23 @@ class ACLComponent extends Component
'MetaTemplateNameDirectory' => [
'index' => ['perm_admin'],
],
+ 'OrgGroups' => [
+ 'add' => ['perm_admin'],
+ 'delete' => ['perm_admin'],
+ 'edit' => ['perm_admin'],
+ 'index' => ['*'],
+ 'view' => ['*'],
+ 'filtering' => ['*'],
+ 'tag' => ['perm_admin'],
+ 'untag' => ['perm_admin'],
+ 'viewTags' => ['*'],
+ 'listAdmins' => ['*'],
+ 'listOrgs' => ['*'],
+ 'assignAdmin' => ['perm_admin'],
+ 'removeAdmin' => ['perm_admin'],
+ 'attachOrg' => ['perm_group_admin'],
+ 'detachOrg' => ['perm_group_admin']
+ ],
'Organisations' => [
'add' => ['perm_admin'],
'delete' => ['perm_admin'],
@@ -335,10 +352,19 @@ class ACLComponent extends Component
if (empty($user) || empty($currentUser)) {
return false;
}
+ if ($user['id'] === $currentUser['id']) {
+ return true;
+ }
if (!$currentUser['role']['perm_admin']) {
if ($user['role']['perm_admin']) {
return false; // org_admins cannot edit admins
}
+ if ($currentUser['role']['perm_group_admin']) {
+ $this->OrgGroup = TableRegistry::get('OrgGroup');
+ if ($this->OrgGroup->checkIfUserBelongsToGroupAdminsGroup($currentUser, $user)) {
+ return true;
+ }
+ }
if (!$currentUser['role']['perm_org_admin']) {
return false;
} else {
diff --git a/src/Controller/Component/CRUDComponent.php b/src/Controller/Component/CRUDComponent.php
index a1ecba1..47269ff 100644
--- a/src/Controller/Component/CRUDComponent.php
+++ b/src/Controller/Component/CRUDComponent.php
@@ -1711,4 +1711,132 @@ class CRUDComponent extends Component
}
return true;
}
+
+ public function linkObjects(string $functionName, int $id1, string $model1, string $model2, array $params = []): void
+ {
+ $this->Controller->loadModel($model1);
+ $this->Controller->loadModel($model2);
+ if ($this->request->is(['post', 'put'])) {
+ $input = $this->request->getData();
+ if (empty($input['id'])) {
+ throw new MethodNotAllowedException(__('No ID of target to attach defined.'));
+ }
+ $id2 = $input['id'];
+ $obj1 = $this->Table->get($id1);
+ $obj2 = $this->Table->$model2->get($id2);
+ $data = [
+ [
+ 'id' => $id1,
+ 'model' => $model1
+ ],
+ [
+ 'id' => $id2,
+ 'model' => $model2
+ ]
+ ];
+
+ try {
+ $savedData = $this->Table->$model2->link($obj1, [$obj2]);
+ } catch (Exception $e) {
+ $savedData = null;
+ }
+
+ if (!empty($savedData)) {
+ $message = __('{0} attached to {1}.', $model1, $model2);
+ if ($this->Controller->ParamHandler->isRest()) {
+ $this->Controller->restResponsePayload = $this->RestResponse->viewData($message, 'json');
+ } else if ($this->Controller->ParamHandler->isAjax()) {
+ if (!empty($params['displayOnSuccess'])) {
+ $displayOnSuccess = $this->renderViewInVariable($params['displayOnSuccess'], ['entity' => $data]);
+ $this->Controller->ajaxResponsePayload = $this->RestResponse->ajaxSuccessResponse($model1, 'index', $obj1, $message, ['displayOnSuccess' => $displayOnSuccess]);
+ } else {
+ $this->Controller->ajaxResponsePayload = $this->RestResponse->ajaxSuccessResponse($model1, 'index', $obj1, $message);
+ }
+ } else {
+ $this->Controller->Flash->success($message);
+ if (empty($params['redirect'])) {
+ $this->Controller->redirect(['action' => 'view', $id1]);
+ } else {
+ $this->Controller->redirect($params['redirect']);
+ }
+ }
+ } else {
+ $this->Controller->isFailResponse = true;
+ $message = __(
+ '{0} could not be attached to {1}.',
+ $model1,
+ $model2
+ );
+ if ($this->Controller->ParamHandler->isRest()) {
+ $this->Controller->restResponsePayload = $this->RestResponse->viewData($message, 'json');
+ } else if ($this->Controller->ParamHandler->isAjax()) {
+ $this->Controller->ajaxResponsePayload = $this->RestResponse->ajaxFailResponse($model1, $functionName, $data, $message, []);
+ } else {
+ $this->Controller->Flash->error($message);
+ }
+ }
+ }
+ }
+
+ public function unlinkObjects(string $functionName, int $id1, int $id2, string $model1, string $model2, array $params = []): void
+ {
+ $this->Controller->loadModel($model1);
+ $this->Controller->loadModel($model2);
+ $obj1 = $this->Table->get($id1);
+ $obj2 = $this->Table->$model2->get($id2);
+ $data = [
+ [
+ 'id' => $id1,
+ 'model' => $model1
+ ],
+ [
+ 'id' => $id2,
+ 'model' => $model2
+ ]
+ ];
+ $this->Controller->set('data', $data);
+ if ($this->request->is(['post', 'put'])) {
+ $input = $this->request->getData();
+ try {
+ $savedData = $this->Table->$model2->unlink($obj1, [$obj2]);
+ } catch (Exception $e) {
+ $savedData = null;
+ }
+
+ if (!empty($savedData)) {
+ $message = __('{0} detached from {1}.', $model1, $model2);
+ if ($this->Controller->ParamHandler->isRest()) {
+ $this->Controller->restResponsePayload = $this->RestResponse->viewData($message, 'json');
+ } else if ($this->Controller->ParamHandler->isAjax()) {
+ if (!empty($params['displayOnSuccess'])) {
+ $displayOnSuccess = $this->renderViewInVariable($params['displayOnSuccess'], ['entity' => $data]);
+ $this->Controller->ajaxResponsePayload = $this->RestResponse->ajaxSuccessResponse($model1, 'index', $obj1, $message, ['displayOnSuccess' => $displayOnSuccess]);
+ } else {
+ $this->Controller->ajaxResponsePayload = $this->RestResponse->ajaxSuccessResponse($model1, 'index', $obj1, $message);
+ }
+ } else {
+ $this->Controller->Flash->success($message);
+ if (empty($params['redirect'])) {
+ $this->Controller->redirect(['action' => 'view', $id1]);
+ } else {
+ $this->Controller->redirect($params['redirect']);
+ }
+ }
+ } else {
+ $this->Controller->isFailResponse = true;
+ $message = __(
+ '{0} could not be detached from {1}.',
+ $model1,
+ $model2
+ );
+ if ($this->Controller->ParamHandler->isRest()) {
+ $this->Controller->restResponsePayload = $this->RestResponse->viewData($message, 'json');
+ } else if ($this->Controller->ParamHandler->isAjax()) {
+ $this->Controller->ajaxResponsePayload = $this->RestResponse->ajaxFailResponse($model1, $functionName, $data, $message, []);
+ } else {
+ $this->Controller->Flash->error($message);
+ }
+ }
+ }
+ }
}
diff --git a/src/Controller/Component/Navigation/sidemenu.php b/src/Controller/Component/Navigation/sidemenu.php
index ec00e27..a701104 100644
--- a/src/Controller/Component/Navigation/sidemenu.php
+++ b/src/Controller/Component/Navigation/sidemenu.php
@@ -27,6 +27,11 @@ class Sidemenu {
'icon' => $this->iconTable['Organisations'],
'url' => '/organisations/index',
],
+ 'OrgGroups' => [
+ 'label' => __('Organisation Groups'),
+ 'icon' => $this->iconTable['OrgGroups'],
+ 'url' => '/orgGroups/index',
+ ],
'EncryptionKeys' => [
'label' => __('Encryption keys'),
'icon' => $this->iconTable['EncryptionKeys'],
diff --git a/src/Controller/Component/NavigationComponent.php b/src/Controller/Component/NavigationComponent.php
index 6eaa9ff..6dea2cf 100644
--- a/src/Controller/Component/NavigationComponent.php
+++ b/src/Controller/Component/NavigationComponent.php
@@ -23,6 +23,7 @@ class NavigationComponent extends Component
public $iconToTableMapping = [
'Individuals' => 'address-book',
'Organisations' => 'building',
+ 'OrgGroups' => 'city',
'EncryptionKeys' => 'key',
'SharingGroups' => 'user-friends',
'MailingLists' => 'mail-bulk',
diff --git a/src/Controller/OrgGroupsController.php b/src/Controller/OrgGroupsController.php
new file mode 100644
index 0000000..105a69e
--- /dev/null
+++ b/src/Controller/OrgGroupsController.php
@@ -0,0 +1,256 @@
+ true], 'uuid'];
+ public $filterFields = ['name', 'uuid'];
+ public $containFields = ['Organisations'];
+ public $statisticsFields = [];
+
+ public function index()
+ {
+ $additionalContainFields = [];
+
+ $this->CRUD->index([
+ 'filters' => $this->filterFields,
+ 'quickFilters' => $this->quickFilterFields,
+ 'quickFilterForMetaField' => ['enabled' => true, 'wildcard_search' => true],
+ 'contain' => $this->containFields,
+ 'statisticsFields' => $this->statisticsFields,
+ ]);
+ $responsePayload = $this->CRUD->getResponsePayload();
+ if (!empty($responsePayload)) {
+ return $responsePayload;
+ }
+ $this->set('metaGroup', 'ContactDB');
+ }
+
+ public function filtering()
+ {
+ $this->CRUD->filtering();
+ }
+
+ public function add()
+ {
+ $this->CRUD->add();
+ $responsePayload = $this->CRUD->getResponsePayload();
+ if (!empty($responsePayload)) {
+ return $responsePayload;
+ }
+ }
+
+ public function view($id)
+ {
+ $this->CRUD->view($id, ['contain' => ['Organisations']]);
+ $responsePayload = $this->CRUD->getResponsePayload();
+ if (!empty($responsePayload)) {
+ return $responsePayload;
+ }
+ $this->set('canEdit', $this->canEdit($id));
+ }
+
+ public function edit($id)
+ {
+ if (!$this->canEdit($id)) {
+ throw new MethodNotAllowedException(__('You cannot modify that group.'));
+ }
+ $this->CRUD->edit($id);
+ $responsePayload = $this->CRUD->getResponsePayload();
+ if (!empty($responsePayload)) {
+ return $responsePayload;
+ }
+ $this->set('metaGroup', 'ContactDB');
+ $this->render('add');
+ }
+
+ public function delete($id)
+ {
+ $this->CRUD->delete($id);
+ $responsePayload = $this->CRUD->getResponsePayload();
+ if (!empty($responsePayload)) {
+ return $responsePayload;
+ }
+ $this->set('metaGroup', 'ContactDB');
+ }
+
+ public function tag($id)
+ {
+ if (!$this->canEdit($id)) {
+ throw new MethodNotAllowedException(__('You cannot tag that organisation.'));
+ }
+ $this->CRUD->tag($id);
+ $responsePayload = $this->CRUD->getResponsePayload();
+ if (!empty($responsePayload)) {
+ return $responsePayload;
+ }
+ }
+
+ public function untag($id)
+ {
+ if (!$this->canEdit($id)) {
+ throw new MethodNotAllowedException(__('You cannot untag that organisation.'));
+ }
+ $this->CRUD->untag($id);
+ $responsePayload = $this->CRUD->getResponsePayload();
+ if (!empty($responsePayload)) {
+ return $responsePayload;
+ }
+ }
+
+ public function viewTags($id)
+ {
+ $this->CRUD->viewTags($id);
+ $responsePayload = $this->CRUD->getResponsePayload();
+ if (!empty($responsePayload)) {
+ return $responsePayload;
+ }
+ }
+
+ private function canEdit($groupId): bool
+ {
+ $currentUser = $this->ACL->getUser();
+ if ($currentUser['role']['perm_admin']) {
+ return true;
+ }
+ if ($currentUser['role']['perm_group_admin']) {
+ $orgGroup = $this->OrgGroups->get($groupId, ['contain' => 'Users']);
+ $found = false;
+ foreach ($orgGroup['users'] as $admin) {
+ if ($admin['id'] == $currentUser['id']) {
+ $found = true;
+ }
+ }
+ return $found;
+ }
+ return false;
+ }
+
+ // Listing should be available to all, it's purely informational
+ public function listAdmins($groupId)
+ {
+ if (empty($groupId)) {
+ throw new NotFoundException(__('Invalid {0}.', 'OrgGroup'));
+ }
+ $orgGroup = $this->OrgGroups->get($groupId, ['contain' => ['Users' => ['Individuals', 'Organisations']]]);
+ $this->set('data', $orgGroup['users']);
+ $this->set('canEdit', $this->ACL->getUser()['role']['perm_admin']);
+ $this->set('groupId', $groupId);
+ }
+
+ // Listing should be available to all, it's purely informational
+ public function listOrgs($groupId)
+ {
+ if (empty($groupId)) {
+ throw new NotFoundException(__('Invalid {0}.', 'OrgGroup'));
+ }
+ $orgGroup = $this->OrgGroups->get($groupId, ['contain' => 'Organisations']);
+ $this->set('data', $orgGroup['organisations']);
+ $this->set('canEdit', $this->canEdit($groupId));
+ $this->set('groupId', $groupId);
+ }
+
+ public function assignAdmin($groupId)
+ {
+ if (!$this->ACL->getUser()['role']['perm_admin']) {
+ throw new MethodNotAllowedException(__('You do not have permission to edit this group.'));
+ }
+ $this->CRUD->linkObjects(__FUNCTION__, $groupId, 'OrgGroups', 'Users', ['redirect' => '/orgGroups/listAdmins/' . $groupId]);
+ if ($this->request->is('post')) {
+ $responsePayload = $this->CRUD->getResponsePayload();
+ if (!empty($responsePayload)) {
+ return $responsePayload;
+ }
+ }
+ $orgGroup = $this->OrgGroups->get($groupId, ['contain' => 'Users']);
+ $this->loadModel('Users');
+ $this->loadModel('Roles');
+ $validRoles = $this->Roles->find('list')->disableHydration()->select(
+ ['id', 'name']
+ )->where(
+ ['OR' => ['perm_admin' => 1, 'perm_group_admin' => 1]]
+ )->toArray();
+ $admins = $this->Users->find('list')->disableHydration()->select(['id', 'username'])->where(['Users.role_id IN' => array_keys($validRoles)])->toArray();
+ asort($admins, SORT_STRING | SORT_FLAG_CASE);
+ if (!empty($orgGroup['users'])) {
+ foreach ($orgGroup['users'] as $admin) {
+ if (isset($admins[$admin['id']])) {
+ unset($admins[$admin['id']]);
+ }
+ }
+ }
+ $dropdownData = [
+ 'admins' => $admins
+ ];
+ $this->set(compact('dropdownData'));
+ }
+
+ public function removeAdmin($groupId, $adminId)
+ {
+ if (!$this->ACL->getUser()['role']['perm_admin']) {
+ throw new MethodNotAllowedException(__('You do not have permission to edit this group.'));
+ }
+ $this->CRUD->unlinkObjects(__FUNCTION__, $groupId, $adminId, 'OrgGroups', 'Users');
+ if ($this->request->is('post')) {
+ $responsePayload = $this->CRUD->getResponsePayload();
+ if (!empty($responsePayload)) {
+ return $responsePayload;
+ }
+ }
+ $this->viewBuilder()->setLayout('ajax');
+ $this->render('/genericTemplates/detach');
+ }
+
+ public function attachOrg($groupId)
+ {
+ if (!$this->OrgGroups->checkIfGroupAdmin($groupId, $this->ACL->getUser())) {
+ throw new MethodNotAllowedException(__('You do not have permission to edit this group.'));
+ }
+ $this->CRUD->linkObjects(__FUNCTION__, $groupId, 'OrgGroups', 'Organisations', ['redirect' => '/orgGroups/listOrgs/' . $groupId]);
+ if ($this->request->is('post')) {
+ $responsePayload = $this->CRUD->getResponsePayload();
+ if (!empty($responsePayload)) {
+ return $responsePayload;
+ }
+ }
+ $orgGroup = $this->OrgGroups->get($groupId, ['contain' => 'Organisations']);
+ $this->loadModel('Organisations');
+ $orgs = $this->Organisations->find('list')->disableHydration()->select(['id', 'name'])->toArray();
+ asort($orgs, SORT_STRING | SORT_FLAG_CASE);
+ foreach ($orgGroup['organisations'] as $organisation) {
+ if (isset($orgs[$organisation['id']])) {
+ unset($orgs[$organisation['id']]);
+ }
+ }
+ $dropdownData = [
+ 'orgs' => $orgs
+ ];
+ $this->set(compact('dropdownData'));
+ }
+
+ public function detachOrg($groupId, $orgId)
+ {
+ if (!$this->OrgGroups->checkIfGroupAdmin($groupId, $this->ACL->getUser())) {
+ throw new MethodNotAllowedException(__('You do not have permission to edit this group.'));
+ }
+ $this->CRUD->unlinkObjects(__FUNCTION__, $groupId, $orgId, 'OrgGroups', 'Organisations');
+ if ($this->request->is('post')) {
+ $responsePayload = $this->CRUD->getResponsePayload();
+ if (!empty($responsePayload)) {
+ return $responsePayload;
+ }
+ }
+ $this->viewBuilder()->setLayout('ajax');
+ $this->render('/genericTemplates/detach');
+ }
+}
diff --git a/src/Controller/OrganisationsController.php b/src/Controller/OrganisationsController.php
index 9739d04..9139033 100644
--- a/src/Controller/OrganisationsController.php
+++ b/src/Controller/OrganisationsController.php
@@ -15,7 +15,7 @@ class OrganisationsController extends AppController
public $quickFilterFields = [['name' => true], 'uuid', 'nationality', 'sector', 'type', 'url'];
public $filterFields = ['name', 'uuid', 'nationality', 'sector', 'type', 'url', 'Alignments.id', 'MetaFields.field', 'MetaFields.value', 'MetaFields.MetaTemplates.name'];
- public $containFields = ['Alignments' => 'Individuals'];
+ public $containFields = ['Alignments' => 'Individuals', 'OrgGroups'];
public $statisticsFields = ['nationality', 'sector'];
public function index()
@@ -105,7 +105,7 @@ class OrganisationsController extends AppController
public function view($id)
{
- $this->CRUD->view($id, ['contain' => ['Alignments' => 'Individuals']]);
+ $this->CRUD->view($id, ['contain' => ['Alignments' => 'Individuals', 'OrgGroups']]);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
diff --git a/src/Controller/UsersController.php b/src/Controller/UsersController.php
index 574d344..4661daf 100644
--- a/src/Controller/UsersController.php
+++ b/src/Controller/UsersController.php
@@ -13,7 +13,7 @@ class UsersController extends AppController
{
public $filterFields = ['Individuals.uuid', 'username', 'Individuals.email', 'Individuals.first_name', 'Individuals.last_name', 'Organisations.name', 'Organisation.nationality'];
public $quickFilterFields = ['Individuals.uuid', ['username' => true], ['Individuals.first_name' => true], ['Individuals.last_name' => true], 'Individuals.email'];
- public $containFields = ['Individuals', 'Roles', 'UserSettings', 'Organisations'];
+ public $containFields = ['Individuals', 'Roles', 'UserSettings', 'Organisations', 'OrgGroups'];
public function index()
{
@@ -177,7 +177,7 @@ class UsersController extends AppController
}
}
$this->CRUD->view($id, [
- 'contain' => ['Individuals' => ['Alignments' => 'Organisations'], 'Roles', 'Organisations'],
+ 'contain' => ['Individuals' => ['Alignments' => 'Organisations'], 'Roles', 'Organisations', 'OrgGroups'],
'afterFind' => function($data) use ($keycloakUsersParsed, $currentUser) {
if (empty($currentUser['role']['perm_admin']) && $currentUser['organisation_id'] != $data['organisation_id']) {
throw new NotFoundException(__('Invalid User.'));
@@ -329,6 +329,9 @@ class UsersController extends AppController
if (empty(Configure::read('user.allow-user-deletion'))) {
throw new MethodNotAllowedException(__('User deletion is disabled on this instance.'));
}
+ if (!$this->ACL->canEditUser($currentUser, $data)) {
+ throw new MethodNotAllowedException(__('You cannot edit the given user.'));
+ }
if (!$currentUser['role']['perm_admin']) {
if ($data['organisation_id'] !== $currentUser['organisation_id']) {
throw new MethodNotAllowedException(__('You do not have permission to delete the given user.'));
diff --git a/src/Model/Entity/OrgGroup.php b/src/Model/Entity/OrgGroup.php
new file mode 100644
index 0000000..eed2a19
--- /dev/null
+++ b/src/Model/Entity/OrgGroup.php
@@ -0,0 +1,26 @@
+ true,
+ 'id' => false,
+ 'created' => false
+ ];
+
+ protected $_accessibleOnNew = [
+ 'created' => true
+ ];
+
+ public function rearrangeForAPI(array $options = []): void
+ {
+ if (!empty($this->tags)) {
+ $this->tags = $this->rearrangeTags($this->tags);
+ }
+ }
+}
diff --git a/src/Model/Table/OrgGroupsTable.php b/src/Model/Table/OrgGroupsTable.php
new file mode 100644
index 0000000..47cd831
--- /dev/null
+++ b/src/Model/Table/OrgGroupsTable.php
@@ -0,0 +1,48 @@
+addBehavior('UUID');
+ $this->addBehavior('Timestamp');
+ $this->addBehavior('Tags.Tag');
+ $this->addBehavior('AuditLog');
+ $this->belongsToMany('Organisations', [
+ 'joinTable' => 'org_groups_organisations',
+ ]);
+ $this->belongsToMany('Users', [
+ 'joinTable' => 'org_groups_admins',
+ ]);
+ $this->setDisplayField('name');
+ }
+
+ public function validationDefault(Validator $validator): Validator
+ {
+ $validator
+ ->notEmptyString('name')
+ ->notEmptyString('uuid')
+ ->requirePresence(['name', 'uuid'], 'create');
+ return $validator;
+ }
+
+ public function checkIfGroupAdmin(int $groupId, mixed $user): bool
+ {
+ return true;
+ }
+
+ public function checkIfUserBelongsToGroupAdminsGroup(User $currentUser, User $userToCheck): bool
+ {
+ $managedGroups = $this->find('list')->where(['Users.id' => $currentUser['id']])->select(['id', 'uuid'])->disableHydration()->toArray();
+ return isset($managedGroups[$userToCheck['org_id']]);
+ }
+}
diff --git a/src/Model/Table/OrganisationsTable.php b/src/Model/Table/OrganisationsTable.php
index f56f36b..0a578ca 100644
--- a/src/Model/Table/OrganisationsTable.php
+++ b/src/Model/Table/OrganisationsTable.php
@@ -34,6 +34,9 @@ class OrganisationsTable extends AppTable
'conditions' => ['owner_model' => 'organisation']
]
);
+ $this->belongsToMany('OrgGroups', [
+ 'joinTable' => 'org_groups_organisations',
+ ]);
$this->addBehavior('MetaFields');
$this->setDisplayField('name');
}
diff --git a/src/Model/Table/UsersTable.php b/src/Model/Table/UsersTable.php
index cc2c0ba..e7bcb1f 100644
--- a/src/Model/Table/UsersTable.php
+++ b/src/Model/Table/UsersTable.php
@@ -51,6 +51,9 @@ class UsersTable extends AppTable
'strategy' => 'join'
]
);
+ $this->belongsToMany('OrgGroups', [
+ 'joinTable' => 'org_groups_admins',
+ ]);
$this->hasMany(
'UserSettings',
[
diff --git a/templates/OrgGroups/add.php b/templates/OrgGroups/add.php
new file mode 100644
index 0000000..5da45b9
--- /dev/null
+++ b/templates/OrgGroups/add.php
@@ -0,0 +1,25 @@
+element('genericElements/Form/genericForm', array(
+ 'data' => array(
+ 'description' => __('OrgGroups are an administrative concept, multiple organisations can belong to a grouping that allows common management by so called "GroupAdmins". This helps grouping organisations by sector, country or other commonalities into co-managed sub-communities.'),
+ 'model' => 'OrgGroup',
+ 'fields' => array(
+ array(
+ 'field' => 'name'
+ ),
+ array(
+ 'field' => 'uuid',
+ 'label' => 'UUID',
+ 'type' => 'uuid'
+ ),
+ array(
+ 'field' => 'description'
+ )
+ ),
+ 'submit' => array(
+ 'action' => $this->request->getParam('action')
+ )
+ )
+ ));
+?>
+
diff --git a/templates/OrgGroups/assign_admin.php b/templates/OrgGroups/assign_admin.php
new file mode 100644
index 0000000..4624561
--- /dev/null
+++ b/templates/OrgGroups/assign_admin.php
@@ -0,0 +1,20 @@
+element('genericElements/Form/genericForm', array(
+ 'data' => array(
+ 'description' => __('Assign a user to be an administrator of the group.'),
+ 'model' => null,
+ 'fields' => array(
+ array(
+ 'field' => 'id',
+ 'label' => __('User'),
+ 'type' => 'dropdown',
+ 'options' => $dropdownData['admins']
+ )
+ ),
+ 'submit' => array(
+ 'action' => $this->request->getParam('action')
+ )
+ )
+ ));
+?>
+
diff --git a/templates/OrgGroups/attach_org.php b/templates/OrgGroups/attach_org.php
new file mode 100644
index 0000000..f594f89
--- /dev/null
+++ b/templates/OrgGroups/attach_org.php
@@ -0,0 +1,20 @@
+element('genericElements/Form/genericForm', array(
+ 'data' => array(
+ 'description' => __('Assign an organisation to the group.'),
+ 'model' => null,
+ 'fields' => array(
+ array(
+ 'field' => 'id',
+ 'label' => __('Organisation'),
+ 'type' => 'dropdown',
+ 'options' => $dropdownData['orgs']
+ )
+ ),
+ 'submit' => array(
+ 'action' => $this->request->getParam('action')
+ )
+ )
+ ));
+?>
+
diff --git a/templates/OrgGroups/index.php b/templates/OrgGroups/index.php
new file mode 100644
index 0000000..577e238
--- /dev/null
+++ b/templates/OrgGroups/index.php
@@ -0,0 +1,86 @@
+element('genericElements/IndexTable/index_table', [
+ 'data' => [
+ 'data' => $data,
+ 'top_bar' => [
+ 'children' => [
+ [
+ 'type' => 'simple',
+ 'children' => [
+ 'data' => [
+ 'type' => 'simple',
+ 'text' => __('Add group'),
+ 'class' => 'btn btn-primary',
+ 'popover_url' => '/orgGroups/add'
+ ]
+ ]
+ ],
+ [
+ 'type' => 'search',
+ 'button' => __('Search'),
+ 'placeholder' => __('Enter value to search'),
+ 'data' => '',
+ 'searchKey' => 'value',
+ 'allowFilering' => true
+ ],
+ [
+ 'type' => 'table_action',
+ 'table_setting_id' => 'org_groups_index',
+ ]
+ ]
+ ],
+ 'fields' => [
+ [
+ 'name' => '#',
+ 'sort' => 'id',
+ 'class' => 'short',
+ 'data_path' => 'id',
+ ],
+ [
+ 'name' => __('Name'),
+ 'class' => 'short',
+ 'data_path' => 'name',
+ 'sort' => 'name',
+ ],
+ [
+ 'name' => __('UUID'),
+ 'sort' => 'uuid',
+ 'class' => 'short',
+ 'data_path' => 'uuid',
+ ],
+ [
+ 'name' => __('Description'),
+ 'data_path' => 'description',
+ 'sort' => 'description',
+ ],
+ [
+ 'name' => __('Tags'),
+ 'data_path' => 'tags',
+ 'element' => 'tags',
+ ],
+ ],
+ 'title' => __('Organisation Groups Index'),
+ 'description' => __('OrgGroups are an administrative concept, multiple organisations can belong to a grouping that allows common management by so called "GroupAdmins". This helps grouping organisations by sector, country or other commonalities into co-managed sub-communities.'),
+ 'actions' => [
+ [
+ 'url' => '/orgGroups/view',
+ 'url_params_data_paths' => ['id'],
+ 'icon' => 'eye'
+ ],
+ [
+ 'open_modal' => '/orgGroups/edit/[onclick_params_data_path]',
+ 'modal_params_data_path' => 'id',
+ 'icon' => 'edit',
+ 'requirement' => $loggedUser['role']['perm_admin']
+ ],
+ [
+ 'open_modal' => '/orgGroups/delete/[onclick_params_data_path]',
+ 'modal_params_data_path' => 'id',
+ 'icon' => 'trash',
+ 'requirement' => $loggedUser['role']['perm_admin']
+ ],
+ ]
+ ]
+]);
+echo '';
+?>
diff --git a/templates/OrgGroups/list_admins.php b/templates/OrgGroups/list_admins.php
new file mode 100644
index 0000000..8c1e848
--- /dev/null
+++ b/templates/OrgGroups/list_admins.php
@@ -0,0 +1,64 @@
+element('genericElements/IndexTable/index_table', [
+ 'data' => [
+ 'skip_pagination' => true,
+ 'data' => $data,
+ 'top_bar' => [
+ 'children' => [
+ [
+ 'type' => 'simple',
+ 'children' => [
+ 'data' => [
+ 'type' => 'simple',
+ 'text' => __('Add Group Administrator'),
+ 'class' => 'btn btn-primary',
+ 'popover_url' => '/orgGroups/assignAdmin/' . h($groupId),
+ 'reload_url' => '/orgGroups/listAdmins/' . h($groupId),
+ ],
+ ],
+ 'requirement' => $canEdit
+ ]
+ ]
+ ],
+ 'fields' => [
+ [
+ 'name' => '#',
+ 'sort' => 'id',
+ 'class' => 'short',
+ 'data_path' => 'id',
+ ],
+ [
+ 'name' => __('Username'),
+ 'class' => 'short',
+ 'data_path' => 'username',
+ 'sort' => 'username',
+ ],
+ [
+ 'name' => __('Email'),
+ 'class' => 'short',
+ 'data_path' => 'individual.email',
+ 'sort' => 'individual.email',
+ ],
+ [
+ 'name' => __('Organisation'),
+ 'sort' => 'organisation.name',
+ 'class' => 'short',
+ 'data_path' => 'organisation.name',
+ ]
+ ],
+ 'title' => null,
+ 'description' => null,
+ 'actions' => [
+ [
+ 'open_modal' => '/orgGroups/removeAdmin/' . h($groupId) . '/[onclick_params_data_path]',
+ 'reload_url' => '/orgGroups/listAdmins/' . h($groupId),
+ 'modal_params_data_path' => 'id',
+ 'icon' => 'unlink',
+ 'title' => __('Remove the administrator from the group'),
+ 'requirement' => $canEdit
+ ],
+ ]
+ ]
+]);
+echo '';
+?>
diff --git a/templates/OrgGroups/list_orgs.php b/templates/OrgGroups/list_orgs.php
new file mode 100644
index 0000000..f62e5a7
--- /dev/null
+++ b/templates/OrgGroups/list_orgs.php
@@ -0,0 +1,71 @@
+element('genericElements/IndexTable/index_table', [
+ 'data' => [
+ 'skip_pagination' => true,
+ 'data' => $data,
+ 'top_bar' => [
+ 'children' => [
+ [
+ 'type' => 'simple',
+ 'children' => [
+ 'data' => [
+ 'type' => 'simple',
+ 'text' => __('Add Organisation'),
+ 'class' => 'btn btn-primary',
+ 'popover_url' => '/orgGroups/attachOrg/' . h($groupId),
+ 'reload_url' => '/orgGroups/listOrgs/' . h($groupId),
+ 'requirement' => $canEdit
+ ]
+ ]
+ ],
+ [
+ 'type' => 'search',
+ 'button' => __('Search'),
+ 'placeholder' => __('Enter value to search'),
+ 'data' => '',
+ 'searchKey' => 'value',
+ 'allowFilering' => true
+ ]
+ ]
+ ],
+ 'fields' => [
+ [
+ 'name' => '#',
+ 'sort' => 'id',
+ 'class' => 'short',
+ 'data_path' => 'id',
+ ],
+ [
+ 'name' => __('Name'),
+ 'class' => 'short',
+ 'data_path' => 'name',
+ 'sort' => 'name',
+ ],
+ [
+ 'name' => __('UUID'),
+ 'sort' => 'uuid',
+ 'class' => 'short',
+ 'data_path' => 'uuid',
+ ]
+ ],
+ 'title' => null,
+ 'description' => null,
+ 'actions' => [
+ [
+ 'url' => '/organisations/view',
+ 'url_params_data_paths' => ['id'],
+ 'icon' => 'eye'
+ ],
+ [
+ 'open_modal' => '/orgGroups/detachOrg/' . h($groupId) . '/[onclick_params_data_path]',
+ 'reload_url' => '/orgGroups/listOrgs/' . h($groupId),
+ 'modal_params_data_path' => 'id',
+ 'icon' => 'unlink',
+ 'title' => __('Remove organisation from the group'),
+ 'requirement' => $canEdit
+ ],
+ ]
+ ]
+]);
+echo '';
+?>
diff --git a/templates/OrgGroups/view.php b/templates/OrgGroups/view.php
new file mode 100644
index 0000000..93b9dfd
--- /dev/null
+++ b/templates/OrgGroups/view.php
@@ -0,0 +1,44 @@
+element(
+ '/genericElements/SingleViews/single_view',
+ [
+ 'title' => __('Organisation Group View'),
+ 'data' => $entity,
+ 'fields' => [
+ [
+ 'key' => __('ID'),
+ 'path' => 'id'
+ ],
+ [
+ 'key' => __('Name'),
+ 'path' => 'name'
+ ],
+ [
+ 'key' => __('UUID'),
+ 'path' => 'uuid'
+ ],
+ [
+ 'key' => __('Description'),
+ 'path' => 'Description'
+ ],
+ [
+ 'key' => __('Tags'),
+ 'type' => 'tags',
+ 'editable' => $canEdit,
+ ]
+ ],
+ 'combinedFieldsView' => false,
+ 'children' => [
+ [
+ 'url' => '/orgGroups/listAdmins/{{0}}',
+ 'url_params' => ['id'],
+ 'title' => __('Administrators')
+ ],
+ [
+ 'url' => '/orgGroups/listOrgs/{{0}}',
+ 'url_params' => ['id'],
+ 'title' => __('Organisations')
+ ]
+ ]
+ ]
+);
diff --git a/templates/Organisations/index.php b/templates/Organisations/index.php
index 2347b04..d0bfb0c 100644
--- a/templates/Organisations/index.php
+++ b/templates/Organisations/index.php
@@ -59,6 +59,14 @@ echo $this->element('genericElements/IndexTable/index_table', [
'url' => '/individuals/index/?Organisations.id={{url_data}}',
'url_data_path' => 'id'
],
+ [
+ 'name' => __('Group memberships'),
+ 'data_path' => 'org_groups',
+ 'data_id_sub_path' => 'id',
+ 'data_value_sub_path' => 'name',
+ 'element' => 'link_list',
+ 'url_pattern' => '/orgGroups/view/{{data_id}}'
+ ],
[
'name' => __('URL'),
'sort' => 'url',
diff --git a/templates/Organisations/view.php b/templates/Organisations/view.php
index 0d3d2cd..a27339b 100644
--- a/templates/Organisations/view.php
+++ b/templates/Organisations/view.php
@@ -1,54 +1,67 @@
__('ID'),
+ 'path' => 'id'
+ ],
+ [
+ 'key' => __('Name'),
+ 'path' => 'name'
+ ],
+ [
+ 'key' => __('UUID'),
+ 'path' => 'uuid'
+ ],
+ [
+ 'key' => __('URL'),
+ 'path' => 'url'
+ ],
+ [
+ 'key' => __('Country'),
+ 'path' => 'nationality'
+ ],
+ [
+ 'key' => __('Sector'),
+ 'path' => 'sector'
+ ],
+ [
+ 'key' => __('Type'),
+ 'path' => 'type'
+ ],
+ [
+ 'key' => __('Contacts'),
+ 'path' => 'contacts'
+ ],
+ [
+ 'key' => __('Tags'),
+ 'type' => 'tags',
+ 'editable' => $canEdit,
+ ],
+ [
+ 'key' => __('Alignments'),
+ 'type' => 'alignment',
+ 'path' => '',
+ 'scope' => 'organisations'
+ ]
+];
+
+if (!empty($entity['org_groups'])) {
+ $fields[] = [
+ 'type' => 'link_list',
+ 'key' => __('Group memberships'),
+ 'path' => 'org_groups',
+ 'data_id_sub_path' => 'id',
+ 'data_value_sub_path' => 'name',
+ 'url_pattern' => '/orgGroups/view/{{data_id}}'
+ ];
+}
echo $this->element(
'/genericElements/SingleViews/single_view',
[
'title' => __('Organisation View'),
'data' => $entity,
- 'fields' => [
- [
- 'key' => __('ID'),
- 'path' => 'id'
- ],
- [
- 'key' => __('Name'),
- 'path' => 'name'
- ],
- [
- 'key' => __('UUID'),
- 'path' => 'uuid'
- ],
- [
- 'key' => __('URL'),
- 'path' => 'url'
- ],
- [
- 'key' => __('Country'),
- 'path' => 'nationality'
- ],
- [
- 'key' => __('Sector'),
- 'path' => 'sector'
- ],
- [
- 'key' => __('Type'),
- 'path' => 'type'
- ],
- [
- 'key' => __('Contacts'),
- 'path' => 'contacts'
- ],
- [
- 'key' => __('Tags'),
- 'type' => 'tags',
- 'editable' => $canEdit,
- ],
- [
- 'key' => __('Alignments'),
- 'type' => 'alignment',
- 'path' => '',
- 'scope' => 'organisations'
- ]
- ],
+ 'fields' => $fields,
'combinedFieldsView' => false,
'children' => []
]
diff --git a/templates/Roles/add.php b/templates/Roles/add.php
index c709865..f01e773 100644
--- a/templates/Roles/add.php
+++ b/templates/Roles/add.php
@@ -12,6 +12,11 @@
'type' => 'checkbox',
'label' => 'Full admin privilege'
],
+ [
+ 'field' => 'perm_group_admin',
+ 'type' => 'checkbox',
+ 'label' => 'Organisation Group admin privilege'
+ ],
[
'field' => 'perm_org_admin',
'type' => 'checkbox',
diff --git a/templates/Roles/index.php b/templates/Roles/index.php
index 02299e6..8e12ed9 100644
--- a/templates/Roles/index.php
+++ b/templates/Roles/index.php
@@ -50,6 +50,12 @@ echo $this->element('genericElements/IndexTable/index_table', [
'data_path' => 'perm_admin',
'element' => 'boolean'
],
+ [
+ 'name' => __('Group Admin'),
+ 'sort' => 'perm_group_admin',
+ 'data_path' => 'perm_group_admin',
+ 'element' => 'boolean'
+ ],
[
'name' => __('Org Admin'),
'sort' => 'perm_org_admin',
diff --git a/templates/Roles/view.php b/templates/Roles/view.php
index eac0175..01d0c8d 100644
--- a/templates/Roles/view.php
+++ b/templates/Roles/view.php
@@ -17,6 +17,11 @@ echo $this->element(
'path' => 'perm_admin',
'type' => 'boolean'
],
+ [
+ 'key' => __('Organisation Group admin permission'),
+ 'path' => 'perm_group_admin',
+ 'type' => 'boolean'
+ ],
[
'key' => __('Organisation admin permission'),
'path' => 'perm_org_admin',
diff --git a/templates/Users/index.php b/templates/Users/index.php
index 6fae052..3cf7ab5 100644
--- a/templates/Users/index.php
+++ b/templates/Users/index.php
@@ -63,6 +63,14 @@ echo $this->element('genericElements/IndexTable/index_table', [
'url' => '/organisations/view/{{0}}',
'url_vars' => ['organisation.id']
],
+ [
+ 'name' => __('Administered Groups'),
+ 'data_path' => 'org_groups',
+ 'data_id_sub_path' => 'id',
+ 'data_value_sub_path' => 'name',
+ 'element' => 'link_list',
+ 'url_pattern' => '/orgGroups/view/{{data_id}}'
+ ],
[
'name' => __('Email'),
'sort' => 'individual.email',
diff --git a/templates/Users/view.php b/templates/Users/view.php
index 5ffafda..cef8d41 100644
--- a/templates/Users/view.php
+++ b/templates/Users/view.php
@@ -55,6 +55,16 @@ $fields = [
'scope' => 'individuals'
]
];
+if (!empty($entity['org_groups'])) {
+ $fields[] = [
+ 'type' => 'link_list',
+ 'key' => __('Administered Groups'),
+ 'path' => 'org_groups',
+ 'data_id_sub_path' => 'id',
+ 'data_value_sub_path' => 'name',
+ 'url_pattern' => '/orgGroups/view/{{data_id}}'
+ ];
+}
if ($keycloakConfig['enabled']) {
$fields[] = [
'key' => __('Keycloak status'),
diff --git a/templates/element/genericElements/IndexTable/Fields/link_list.php b/templates/element/genericElements/IndexTable/Fields/link_list.php
new file mode 100644
index 0000000..e72fd55
--- /dev/null
+++ b/templates/element/genericElements/IndexTable/Fields/link_list.php
@@ -0,0 +1,15 @@
+Hash->extract($row, $field['data_path']);
+ $links = [];
+ foreach ($data as $object) {
+ $temp_id = h($this->Hash->extract($object, $field['data_id_sub_path'])[0]);
+ $temp_value = h($this->Hash->extract($object, $field['data_value_sub_path'])[0]);
+ $url = str_replace('{{data_id}}', $temp_id, $field['url_pattern']);
+ $links[] = sprintf(
+ '%s',
+ $url,
+ $temp_value
+ );
+ }
+ echo implode('
', $links);
+?>
diff --git a/templates/element/genericElements/ListTopBar/group_simple.php b/templates/element/genericElements/ListTopBar/group_simple.php
index a364d27..5ac3e51 100644
--- a/templates/element/genericElements/ListTopBar/group_simple.php
+++ b/templates/element/genericElements/ListTopBar/group_simple.php
@@ -2,7 +2,9 @@
if (!isset($data['requirement']) || $data['requirement']) {
$elements = '';
foreach ($data['children'] as $element) {
- $elements .= $this->element('/genericElements/ListTopBar/element_' . (empty($element['type']) ? 'simple' : h($element['type'])), array('data' => $element, 'tableRandomValue' => $tableRandomValue));
+ if (!isset($element['requirement']) || $element['requirement']) {
+ $elements .= $this->element('/genericElements/ListTopBar/element_' . (empty($element['type']) ? 'simple' : h($element['type'])), array('data' => $element, 'tableRandomValue' => $tableRandomValue));
+ }
}
echo sprintf(
'