Compare commits

...

44 Commits
v1.25 ... main

Author SHA1 Message Date
iglocska 04322b24df
fix: [user editing] fixed for roles <= community admin, fixes #198 2024-12-06 07:25:02 +01:00
iglocska 4f1835dc4c
Merge branch 'develop' 2024-11-28 21:32:19 +01:00
iglocska 50b65021dc
Merge branch 'main' into develop 2024-11-28 21:32:09 +01:00
iglocska 55695fc09b
Merge branch 'main' of github.com:cerebrate-project/cerebrate 2024-11-28 21:32:00 +01:00
iglocska 0c320f7d9c
Merge branch 'develop' 2024-11-28 21:31:48 +01:00
iglocska ffd6c50294
Merge branch 'main' into develop 2024-11-28 21:31:35 +01:00
iglocska 9c54a4842f
chg: [version] bump 2024-11-28 21:30:14 +01:00
iglocska 1572681307
fix: [authkeys] better permission / listing handling
- allow group admins to manage api keys of their group
- when adding an authkey from the user view, don't list every user in the dropdown, focus on the selected user
2024-11-28 21:28:24 +01:00
iglocska 0ed3bef000
chg: [internal] authkey adding, more elegant solution 2024-11-28 20:44:51 +01:00
iglocska cfceaf0fb7
fix: [authkeys] don't barf if no valid roles exist 2024-11-28 20:42:27 +01:00
iglocska 04b640c8b6
fix: [diagnostics] allow for certain settings to be empty, fixes #176
- via the empty => true key
2024-11-28 18:00:13 +01:00
iglocska cce4115418
fix: [error handling] better error handling for bookmarks, fixes #188
- show why something failed
- actually fail if a field is missing for bookmarks
2024-11-28 17:47:43 +01:00
iglocska d799214a41
fix: [error] when deleting a role that had users attached to it was cryptic, fixes #180 2024-11-28 17:13:32 +01:00
iglocska 1c8bcc045e
fix: [security] Group admin ACL
- group admin can inject user into organisation not managed by themselves

- as reported by Jeroen Pinoy (@wachizungu)
2024-11-28 16:56:51 +01:00
iglocska da4bd943b7
fix: [typo] in the authkeyscontroller
- lead to users not being able to generate authkeys
2024-11-28 16:46:45 +01:00
iglocska 467ec29f54
Merge branch 'develop' of github.com:cerebrate-project/cerebrate into develop 2024-11-28 09:37:30 +01:00
iglocska 07f67fe9ea
fix: [cleanup] ACL component
- duplicate check removed
2024-11-28 09:37:06 +01:00
Andras Iklody 8e87dd8b28
Merge pull request #190 from Wachizungu/add-new-roles-to-default-admin-role
fix: default admin role doesn't have group admin and meta field edito…
2024-11-28 09:35:36 +01:00
iglocska 39e9fb4a76
Merge branch 'develop' of github.com:cerebrate-project/cerebrate into develop 2024-11-28 09:25:47 +01:00
iglocska 0131422ab8
fix: [security] Tightening of the role assignment permissions
- If a decoupled perm_admin role was configured on the system, this could be assigned by low privilege administrators, leading to privilege escalation
- This fix moves the responsibility of the check to the ACL component rather than the controller

- As reported by Jeroen Pinoy (@wachizungu)
2024-11-28 09:17:24 +01:00
Andras Iklody be290b6f10
Merge pull request #191 from Wachizungu/revert-debug-setting-value-type
fix: migrations not working on install due to debug mode not being bo…
2024-11-23 20:48:13 +01:00
Jeroen Pinoy 041028e04c
fix: migrations not working on install due to debug mode not being boolean 2024-11-23 20:27:00 +01:00
Jeroen Pinoy ed592c57c7
fix: default admin role doesn't have group admin and meta field editor permissions 2024-11-23 20:08:41 +01:00
Andras Iklody 8d953cd848
Merge pull request #182 from Wachizungu/fix-community-admin-role-add
fix: correctly set perm_community_admin value. fix #179
2024-11-23 09:54:57 +01:00
Andras Iklody 6fcf5d0b02
Merge pull request #183 from Wachizungu/fix-debug-setting-warnings
fix: use numeric values within range for debug level setting
2024-11-23 09:54:30 +01:00
Andras Iklody f7f9392cfe
Merge pull request #184 from Wachizungu/fix-proxy-diagnostics-severity-level
fix: Set proxy settings diagonistics severity level to info. fix #176
2024-11-23 09:54:13 +01:00
Andras Iklody b8ff31a906
Merge pull request #187 from Wachizungu/fix-security-allowed-bookmark-domains-read-config
fix: typo in read security allowed bookmark domains config
2024-11-23 09:53:57 +01:00
Andras Iklody 1702b84fe8
Merge pull request #189 from Wachizungu/chg-load-current-user-settings-view-in-case-id-unset
fix: users settings view throws internal server error when accessed w…
2024-11-23 09:53:24 +01:00
Jeroen Pinoy a1020bc42b
fix: users settings view throws internal server error when accessed without user id 2024-11-22 23:48:52 +01:00
Jeroen Pinoy 850d559cef
fix: typo in read security allowed bookmark domains config 2024-11-22 22:40:25 +01:00
iglocska 55cac2e2e6
new: [security] added functionality to tighten bookmark creation rules
- site admins can now limit the baseurls of the provided bookmark URLs to a list of values via the server settings
2024-11-22 12:48:37 +01:00
iglocska ab331dcfb9
fix: [crud] fixed the broken non ajax messages just introduced in the previous commit
- can't have my cake and eat it too
2024-11-22 12:47:16 +01:00
iglocska ac33e90f0c
fix: [message handling] of error messages
- correctly handle beforeSave / afterSave failures in ajax contexts

- until now it was just silently failing giving cryptic messages to the user
2024-11-22 12:40:21 +01:00
Jeroen Pinoy b45cc8ae22
fix: Set proxy settings diagonistics severity level to info. fix #176 2024-11-16 15:27:10 +01:00
Jeroen Pinoy 54e4fca637
fix: use numeric values within range for debug level setting 2024-11-16 14:57:16 +01:00
Jeroen Pinoy 6813013379
fix: correctly set perm_community_admin value. fix #179 2024-11-16 11:23:04 +01:00
Andras Iklody 9b92f86627
Merge pull request #178 from Wachizungu/fix-typo
fix: fix typo in individuals description
2024-11-10 13:01:21 +01:00
Andras Iklody a63bb216dd
Merge pull request #177 from Wachizungu/fix-brood-add-description
fix: Correct description on add brood view
2024-11-10 13:01:03 +01:00
Jeroen Pinoy 7d08623ca8
fix: fix typo in individuals description 2024-11-10 12:16:38 +01:00
Jeroen Pinoy 9e69bc7239
fix: Correct description on add brood view 2024-11-10 12:11:22 +01:00
iglocska e8f5165b11
Merge branch 'develop' 2024-10-16 10:51:44 +02:00
iglocska 5d0b7715db
Merge branch 'develop' of github.com:cerebrate-project/cerebrate into develop 2024-10-16 10:51:26 +02:00
iglocska 2eb6b1ae77
fix: [pgp] key status check fixed for certain edge cases 2024-10-16 10:50:50 +02:00
Andras Iklody a05f28147f
Chg: Update VERSION.json
Urgency
2024-10-14 15:48:26 +02:00
20 changed files with 347 additions and 141 deletions

View File

@ -22,20 +22,29 @@ class AuthKeysController extends AppController
{
$currentUser = $this->ACL->getUser();
$conditions = [];
$userId = $this->request->getQuery('Users_id');
if (!empty($userId)) {
$conditions['AND']['Users.id'] = $userId;
}
if (empty($currentUser['role']['perm_community_admin'])) {
$conditions['Users.organisation_id'] = $currentUser['organisation_id'];
if (empty($currentUser['role']['perm_org_admin'])) {
$conditions['Users.id'] = $currentUser['id'];
}
}
$this->CRUD->index([
$indexOptions = [
'filters' => $this->filterFields,
'quickFilters' => $this->quickFilterFields,
'contain' => $this->containFields,
'exclude_fields' => ['authkey'],
'conditions' => $conditions,
'hidden' => []
]);
];
if (!empty($userId)) {
$indexOptions['action_query_strings'] = ['Users.id' => $userId];
}
$this->CRUD->index($indexOptions);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
@ -46,13 +55,7 @@ class AuthKeysController extends AppController
public function delete($id)
{
$currentUser = $this->ACL->getUser();
$conditions = [];
if (empty($currentUser['role']['perm_community_admin'])) {
$conditions['Users.organisation_id'] = $currentUser['organisation_id'];
if (empty($currentUser['role']['perm_org_admin'])) {
$conditions['Users.id'] = $currentUser['id'];
}
}
$conditions = $this->AuthKeys->buildUserConditions($currentUser);
$this->CRUD->delete($id, ['conditions' => $conditions, 'contain' => 'Users']);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
@ -67,21 +70,14 @@ class AuthKeysController extends AppController
$validUsers = [];
$userConditions = [];
$currentUser = $this->ACL->getUser();
if (empty($currentUser['role']['perm_community_admin'])) {
if (empty($currentUser['role']['perm_org_admin'])) {
$userConditions['id'] = $currentUser['id'];
} else {
$role_ids = $this->Users->Roles->find()->where(['perm_admin' => 0, 'perm_community_admin', 'perm_org_admin' => 0])->all()->extract('id')->toList();
$userConditions['organisation_id'] = $currentUser['organisation_id'];
$userConditions['OR'] = [
['role_id IN' => $role_ids],
['id' => $currentUser['id']],
];
}
}
$conditions = $this->AuthKeys->buildUserConditions($currentUser);
$userId = $this->request->getQuery('Users_id');
$users = $this->Users->find('list');
if (!empty($userConditions)) {
$users->where($userConditions);
if (!empty($conditions)) {
$users->where($conditions);
}
if (!empty($userId)) {
$users->where(['Users.id' => $userId]);
}
$users = $users->order(['username' => 'asc'])->all()->toArray();
$this->CRUD->add([

View File

@ -363,11 +363,19 @@ class ACLComponent extends Component
return true;
}
if ($user['role']['perm_community_admin']) {
return false; // org_admins cannot edit admins
$this->Roles = TableRegistry::get('Roles');
$validRoles = [];
if (!$currentUser['role']['perm_community_admin']) {
if ($currentUser['role']['perm_group_admin']) {
$validRoles = $this->Roles->find('list')->select(['id', 'name'])->order(['name' => 'asc'])->where(['perm_community_admin' => 0, 'perm_group_admin' => 0, 'perm_admin' => 0])->all()->toArray();
} else {
$validRoles = $this->Roles->find('list')->select(['id', 'name'])->order(['name' => 'asc'])->where(['perm_community_admin' => 0, 'perm_group_admin' => 0, 'perm_org_admin' => 0, 'perm_admin' => 0])->all()->toArray();
}
} else {
$validRoles = $this->Roles->find('list')->order(['name' => 'asc'])->all()->toArray();
}
if ($currentUser['role']['perm_org_admin'] && $user['role']['perm_group_admin']) {
return false; // org_admins cannot edit group_admin
if (!in_array($user['role_id'], array_keys($validRoles)) && $currentUser['id'] != $user['id']) {
return false;
}
if ($currentUser['role']['perm_group_admin']) {
$this->OrgGroups = TableRegistry::get('OrgGroups');
@ -378,9 +386,6 @@ class ACLComponent extends Component
if (!$currentUser['role']['perm_org_admin']) {
return false;
} else {
if ($currentUser['id'] == $user['id']) {
return true;
}
if ($currentUser['organisation_id'] === $user['organisation_id']) {
return true;
}

View File

@ -272,6 +272,10 @@ class CRUDComponent extends Component
$this->Controller->set('model', $this->Table);
$this->Controller->set('data', $data);
$this->Controller->set('embedInModal', $embedInModal);
if (!empty($options['action_query_strings'])) {
$this->Controller->set('action_query_strings', $options['action_query_strings']);
}
$this->Controller->set('skipTableToolbar', $skipTableToolbar);
}
}
@ -465,51 +469,73 @@ class CRUDComponent extends Component
}
}
$data = $this->Table->patchEntity($data, $input, $patchEntityParams);
$break = false;
if (isset($params['beforeSave'])) {
$data = $params['beforeSave']($data);
try {
$data = $params['beforeSave']($data);
} catch (\Exception $e) {
$message = $e->getMessage();
$break = true;
$this->__raiseErrorToUser(__('Could not save {0}.', $this->ObjectAlias), $message);
}
if ($data === false) {
throw new NotFoundException(__('Could not save {0} due to the input failing to meet expectations. Your input is bad and you should feel bad.', $this->ObjectAlias));
$break = true;
$this->__raiseErrorToUser(__('Could not save {0} due to the input failing to meet expectations. Your input is bad and you should feel bad.', $this->ObjectAlias));
}
}
$savedData = $this->Table->save($data);
if ($savedData !== false) {
if (isset($params['afterSave'])) {
$params['afterSave']($data);
}
$message = __('{0} added.', $this->ObjectAlias);
if ($this->Controller->ParamHandler->isRest()) {
$this->Controller->restResponsePayload = $this->RestResponse->viewData($savedData, 'json');
} else if ($this->Controller->ParamHandler->isAjax()) {
if (!empty($params['displayOnSuccess'])) {
$displayOnSuccess = $this->renderViewInVariable($params['displayOnSuccess'], ['entity' => $data]);
$this->Controller->ajaxResponsePayload = $this->RestResponse->ajaxSuccessResponse($this->ObjectAlias, 'add', $savedData, $message, ['displayOnSuccess' => $displayOnSuccess]);
} else {
$this->Controller->ajaxResponsePayload = $this->RestResponse->ajaxSuccessResponse($this->ObjectAlias, 'add', $savedData, $message);
if (!$break) {
$savedData = $this->Table->save($data);
if ($savedData !== false) {
if (isset($params['afterSave'])) {
try {
$data = $params['afterSave']($data);
} catch (\Exception $e) {
$message = $e->getMessage();
$break = true;
$this->__raiseErrorToUser(__('Saved {0}, but could not execute post-save actions.', $this->ObjectAlias), $message);
}
if ($data === false) {
$break = true;
$this->__raiseErrorToUser(__('Saved {0}, but the post-save actons failed.', $this->ObjectAlias));
}
}
if (!$break) {
$message = __('{0} added.', $this->ObjectAlias);
if ($this->Controller->ParamHandler->isRest()) {
$this->Controller->restResponsePayload = $this->RestResponse->viewData($savedData, 'json');
} else if ($this->Controller->ParamHandler->isAjax()) {
if (!empty($params['displayOnSuccess'])) {
$displayOnSuccess = $this->renderViewInVariable($params['displayOnSuccess'], ['entity' => $data]);
$this->Controller->ajaxResponsePayload = $this->RestResponse->ajaxSuccessResponse($this->ObjectAlias, 'add', $savedData, $message, ['displayOnSuccess' => $displayOnSuccess]);
} else {
$this->Controller->ajaxResponsePayload = $this->RestResponse->ajaxSuccessResponse($this->ObjectAlias, 'add', $savedData, $message);
}
} else {
$this->Controller->Flash->success($message);
if (empty($params['redirect'])) {
$this->Controller->redirect(['action' => 'view', $data->id]);
} else {
$this->Controller->redirect($params['redirect']);
}
}
}
} else {
$this->Controller->Flash->success($message);
if (empty($params['redirect'])) {
$this->Controller->redirect(['action' => 'view', $data->id]);
$this->Controller->isFailResponse = true;
$validationErrors = $data->getErrors();
$validationMessage = $this->prepareValidationMessage($validationErrors);
$message = __(
'{0} could not be added.{1}',
$this->ObjectAlias,
empty($validationMessage) ? '' : PHP_EOL . __('Reason: {0}', $validationMessage)
);
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($this->ObjectAlias, 'add', $data, $message, $validationErrors);
} else {
$this->Controller->redirect($params['redirect']);
$this->Controller->Flash->error($message);
}
}
} else {
$this->Controller->isFailResponse = true;
$validationErrors = $data->getErrors();
$validationMessage = $this->prepareValidationMessage($validationErrors);
$message = __(
'{0} could not be added.{1}',
$this->ObjectAlias,
empty($validationMessage) ? '' : PHP_EOL . __('Reason: {0}', $validationMessage)
);
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($this->ObjectAlias, 'add', $data, $message, $validationErrors);
} else {
$this->Controller->Flash->error($message);
}
}
}
if (!empty($params['fields'])) {
@ -723,6 +749,18 @@ class CRUDComponent extends Component
return $input;
}
private function __raiseErrorToUser($title, $message = null)
{
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($this->ObjectAlias, 'edit', [], $title, ['Custom checks' => ['Custom checks' => $message]]);
} else {
$this->Controller->isFailResponse = true;
$this->Controller->Flash->error($message);
}
}
public function edit(int $id, array $params = []): void
{
if (empty($id)) {
@ -800,54 +838,79 @@ class CRUDComponent extends Component
}
}
$data = $this->Table->patchEntity($data, $input, $patchEntityParams);
$break = false;
if (isset($params['beforeSave'])) {
$data = $params['beforeSave']($data);
try {
$data = $params['beforeSave']($data);
} catch (\Exception $e) {
$message = $e->getMessage();
$break = true;
$this->__raiseErrorToUser(__('Could not save {0}.', $this->ObjectAlias), $message);
}
if ($data === false) {
throw new NotFoundException(__('Could not save {0} due to the input failing to meet expectations. Your input is bad and you should feel bad.', $this->ObjectAlias));
$break = true;
$this->__raiseErrorToUser(__('Could not save {0} due to the input failing to meet expectations.', $this->ObjectAlias), __('Your input is bad and you should feel bad.'));
}
}
$savedData = $this->Table->save($data);
if ($savedData !== false) {
if ($metaFieldsEnabled && !empty($metaFieldsToDelete)) {
foreach ($metaFieldsToDelete as $k => $v) {
if ($v === null) {
unset($metaFieldsToDelete[$k]);
if (!$break) {
$savedData = $this->Table->save($data);
if ($savedData !== false) {
if ($metaFieldsEnabled && !empty($metaFieldsToDelete)) {
foreach ($metaFieldsToDelete as $k => $v) {
if ($v === null) {
unset($metaFieldsToDelete[$k]);
}
}
if (!empty($metaFieldsToDelete)) {
$this->Table->MetaFields->unlink($savedData, $metaFieldsToDelete);
}
}
if (!empty($metaFieldsToDelete)) {
$this->Table->MetaFields->unlink($savedData, $metaFieldsToDelete);
$break = false;
if (isset($params['afterSave'])) {
if (isset($params['afterSave'])) {
try {
$data = $params['afterSave']($data);
} catch (\Exception $e) {
$message = $e->getMessage();
$break = true;
$this->__raiseErrorToUser(__('Saved {0} `{1}`, but could not execute post-save actions.', $this->ObjectAlias, $savedData->{$this->Table->getDisplayField()}), $message);
}
if ($data === false) {
$break = true;
$this->__raiseErrorToUser(__('Saved {0} `{1}`, but the post-save actons failed.', $this->ObjectAlias, $savedData->{$this->Table->getDisplayField()}));
}
}
}
if (!$break) {
$message = __('{0} `{1}` updated.', $this->ObjectAlias, $savedData->{$this->Table->getDisplayField()});
if ($this->Controller->ParamHandler->isRest()) {
$this->Controller->restResponsePayload = $this->RestResponse->viewData($savedData, 'json');
} else if ($this->Controller->ParamHandler->isAjax()) {
$this->Controller->ajaxResponsePayload = $this->RestResponse->ajaxSuccessResponse($this->ObjectAlias, 'edit', $savedData, $message);
} else {
$this->Controller->Flash->success($message);
if (empty($params['redirect'])) {
$this->Controller->redirect(['action' => 'view', $id]);
} else {
$this->Controller->redirect($params['redirect']);
}
}
}
}
if (isset($params['afterSave'])) {
$params['afterSave']($data);
}
$message = __('{0} `{1}` updated.', $this->ObjectAlias, $savedData->{$this->Table->getDisplayField()});
if ($this->Controller->ParamHandler->isRest()) {
$this->Controller->restResponsePayload = $this->RestResponse->viewData($savedData, 'json');
} else if ($this->Controller->ParamHandler->isAjax()) {
$this->Controller->ajaxResponsePayload = $this->RestResponse->ajaxSuccessResponse($this->ObjectAlias, 'edit', $savedData, $message);
} else {
$this->Controller->Flash->success($message);
if (empty($params['redirect'])) {
$this->Controller->redirect(['action' => 'view', $id]);
$validationErrors = $data->getErrors();
$validationMessage = $this->prepareValidationError($data);
$message = __(
'{0} could not be modified.{1}',
$this->ObjectAlias,
empty($validationMessage) ? '' : PHP_EOL . __('Reason: {0}', $validationMessage)
);
if ($this->Controller->ParamHandler->isRest()) {
} else if ($this->Controller->ParamHandler->isAjax()) {
$this->Controller->ajaxResponsePayload = $this->RestResponse->ajaxFailResponse($this->ObjectAlias, 'edit', $data, $message, $validationErrors);
} else {
$this->Controller->redirect($params['redirect']);
$this->Controller->Flash->error($message);
}
}
} else {
$validationErrors = $data->getErrors();
$validationMessage = $this->prepareValidationError($data);
$message = __(
'{0} could not be modified.{1}',
$this->ObjectAlias,
empty($validationMessage) ? '' : PHP_EOL . __('Reason: {0}', $validationMessage)
);
if ($this->Controller->ParamHandler->isRest()) {
} else if ($this->Controller->ParamHandler->isAjax()) {
$this->Controller->ajaxResponsePayload = $this->RestResponse->ajaxFailResponse($this->ObjectAlias, 'edit', $data, $message, $validationErrors);
} else {
$this->Controller->Flash->error($message);
}
}
}
if (!empty($params['fields'])) {
@ -1106,6 +1169,7 @@ class CRUDComponent extends Component
throw new NotFoundException(__('Could not save {0} due to the input failing to meet expectations. Your input is bad and you should feel bad.', $this->ObjectAlias));
}
} catch (\Exception $e) {
$exceptionMessage = $e->getMessage();
$entity = false;
}
}
@ -1123,7 +1187,7 @@ class CRUDComponent extends Component
$isBulk,
__('{0} deleted.', $this->ObjectAlias),
__('All selected {0} have been deleted.', Inflector::pluralize($this->ObjectAlias)),
__('Could not delete {0}.', $this->ObjectAlias),
$exceptionMessage ?? __('Could not delete {0}.', $this->ObjectAlias),
__(
'{0} / {1} {2} have been deleted.',
$bulkSuccesses,
@ -1305,7 +1369,7 @@ class CRUDComponent extends Component
if (!empty($additionalData['redirect'])) { // If a redirection occurs, we need to make sure the flash message gets displayed
$this->Controller->Flash->error($message);
}
$this->Controller->ajaxResponsePayload = $this->RestResponse->ajaxFailResponse($this->ObjectAlias, $action, $data, $message, !is_null($errors) ? $errors : $data->getErrors());
$this->Controller->ajaxResponsePayload = $this->RestResponse->ajaxFailResponse($this->ObjectAlias, $action, $data, $message, $errors);
} else {
$this->Controller->Flash->error($message);
$this->Controller->redirect($this->Controller->referer());

View File

@ -440,14 +440,19 @@ class RestResponseComponent extends Component
return $this->viewData($response);
}
public function ajaxFailResponse($ObjectAlias, $action, $entity, $message, $errors = [])
public function ajaxFailResponse($ObjectAlias, $action, $entity = null, $message, $errors = [], $description = '')
{
$action = $this->__dissectAdminRouting($action);
$entity = is_array($entity) ? $entity : $entity->toArray();
if (empty($entity)) {
$entity = [];
} else {
$entity = is_array($entity) ? $entity : $entity->toArray();
}
$response = [
'success' => false,
'message' => $message,
'errors' => $errors,
'description' => $description,
'url' => !empty($entity['id']) ? $this->__generateURL($action, $ObjectAlias, $entity['id']) : ''
];
return $this->viewData($response);

View File

@ -78,7 +78,15 @@ class RolesController extends AppController
public function delete($id)
{
$this->CRUD->delete($id);
$this->CRUD->delete($id, [
'beforeSave' => function ($data) {
$userCount = $this->Roles->Users->find()->where(['role_id' => $data['id']])->count();
if ($userCount > 0) {
throw new ForbiddenException(__('You cannot delete a role that has users assigned to it.'));
}
return true;
}
]);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;

View File

@ -79,6 +79,10 @@ class UserSettingsController extends AppController
if (empty($currentUser['role']['perm_community_admin'])) {
$data['user_id'] = $currentUser->id;
}
$validationResult = $this->UserSettings->validateUserSetting($data, $currentUser);
if (!$validationResult !== true) {
throw new MethodNotAllowedException(__('You cannot create the given user setting. Reason: {0}', $validationResult));
}
return $data;
}
]);
@ -131,6 +135,10 @@ class UserSettingsController extends AppController
if ($data['user_id'] != $entity->user_id) {
throw new MethodNotAllowedException(__('You cannot assign the setting to a different user.'));
}
$validationResult = $this->UserSettings->validateUserSetting($data);
if ($validationResult !== true) {
throw new MethodNotAllowedException(__('Setting value: {0}', $validationResult));
}
return $data;
}
]);
@ -235,9 +243,10 @@ class UserSettingsController extends AppController
public function saveMyBookmark()
{
if (!$this->request->is('get')) {
$result = $this->UserSettings->saveBookmark($this->ACL->getUser(), $this->request->getData());
$errors = null;
$result = $this->UserSettings->saveBookmark($this->ACL->getUser(), $this->request->getData(), $errors);
$success = !empty($result);
$message = $success ? __('Bookmark saved') : __('Could not save bookmark');
$message = $success ? __('Bookmark saved') : ($errors ?? __('Could not save bookmark'));
$this->CRUD->setResponseForController('saveBookmark', $success, $message, $result);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {

View File

@ -86,10 +86,10 @@ class UsersController extends AppController
$individual_ids = [];
if (!$currentUser['role']['perm_community_admin']) {
if ($currentUser['role']['perm_group_admin']) {
$validRoles = $this->Users->Roles->find('list')->select(['id', 'name'])->order(['name' => 'asc'])->where(['perm_community_admin' => 0, 'perm_group_admin' => 0])->all()->toArray();
$validRoles = $this->Users->Roles->find('list')->select(['id', 'name'])->order(['name' => 'asc'])->where(['perm_community_admin' => 0, 'perm_group_admin' => 0, 'perm_admin' => 0])->all()->toArray();
$individual_ids = $this->Users->Individuals->find('aligned', ['organisation_id' => $currentUser['organisation_id']])->all()->extract('id')->toArray();
} else {
$validRoles = $this->Users->Roles->find('list')->select(['id', 'name'])->order(['name' => 'asc'])->where(['perm_community_admin' => 0, 'perm_group_admin' => 0, 'perm_org_admin' => 0])->all()->toArray();
$validRoles = $this->Users->Roles->find('list')->select(['id', 'name'])->order(['name' => 'asc'])->where(['perm_community_admin' => 0, 'perm_group_admin' => 0, 'perm_org_admin' => 0, 'perm_admin' => 0])->all()->toArray();
}
if (empty($individual_ids)) {
@ -247,10 +247,10 @@ class UsersController extends AppController
$validOrgIds = [];
if (!$currentUser['role']['perm_community_admin']) {
if ($currentUser['role']['perm_group_admin']) {
$validRoles = $this->Users->Roles->find('list')->select(['id', 'name'])->order(['name' => 'asc'])->where(['perm_community_admin' => 0, 'perm_group_admin' => 0])->all()->toArray();
$validRoles = $this->Users->Roles->find('list')->select(['id', 'name'])->order(['name' => 'asc'])->where(['perm_community_admin' => 0, 'perm_group_admin' => 0, 'perm_admin' => 0])->all()->toArray();
$validOrgIds = $this->Users->Organisations->OrgGroups->getGroupOrgIdsForUser($currentUser);
} else {
$validRoles = $this->Users->Roles->find('list')->select(['id', 'name'])->order(['name' => 'asc'])->where(['perm_community_admin' => 0, 'perm_group_admin' => 0, 'perm_org_admin' => 0])->all()->toArray();
$validRoles = $this->Users->Roles->find('list')->select(['id', 'name'])->order(['name' => 'asc'])->where(['perm_community_admin' => 0, 'perm_group_admin' => 0, 'perm_org_admin' => 0, 'perm_admin' => 0])->all()->toArray();
}
} else {
$validRoles = $this->Users->Roles->find('list')->order(['name' => 'asc'])->all()->toArray();
@ -320,10 +320,14 @@ class UsersController extends AppController
}
return $data;
};
$params['beforeSave'] = function ($data) use ($currentUser, $validRoles) {
if (!in_array($data['role_id'], array_keys($validRoles)) && $this->ACL->getUser()['id'] != $data['id']) {
$params['beforeSave'] = function ($data) use ($currentUser, $validRoles, $validOrgIds, $params) {
// only run these checks if the user CAN edit them and if the values are actually set in the request
if (in_array('role_id', $params['fields']) && isset($data['role_id']) && !in_array($data['role_id'], array_keys($validRoles)) && $this->ACL->getUser()['id'] != $data['id']) {
throw new MethodNotAllowedException(__('You cannot assign the chosen role to a user.'));
}
if (in_array('organisation_id', $params['fields']) && isset($data['organisation_id']) && !in_array($data['organisation_id'], $validOrgIds)) {
throw new MethodNotAllowedException(__('You cannot assign the chosen organisation to a user.'));
}
return $data;
};
}
@ -487,7 +491,7 @@ class UsersController extends AppController
{
$editingAnotherUser = false;
$currentUser = $this->ACL->getUser();
if ((empty($currentUser['role']['perm_community_admin']) && empty($currentUser['role']['perm_group_admin'])) || $user_id == $currentUser->id) {
if ((empty($currentUser['role']['perm_community_admin']) && empty($currentUser['role']['perm_group_admin'])) || empty($user_id) || $user_id == $currentUser->id) {
$user = $currentUser;
} else {
$user = $this->Users->get($user_id, [

View File

@ -107,7 +107,7 @@ class CryptGpgExtended extends \Crypt_GPG
$this->engine->reset();
$this->engine->setInput($input);
$this->engine->setOutput($output);
$this->engine->setOperation('--import', ['--import-options', 'show-only', '--with-colons']);
$this->engine->setOperation('--import', ['--import-options', 'show-only', '--with-colons', '--no-default-keyring --no-auto-check-trustdb --trust-model pgp']);
$this->engine->run();
$keys = [];

View File

@ -93,4 +93,36 @@ class AuthKeysTable extends AppTable
}
return [];
}
public function buildUserConditions($currentUser)
{
$conditions = [];
$validOrgs = $this->Users->getValidOrgsForUser($currentUser);
if (empty($currentUser['role']['perm_community_admin'])) {
$conditions['Users.organisation_id IN'] = $validOrgs;
if (empty($currentUser['role']['perm_group_admin'])) {
if (empty($currentUser['role']['perm_org_admin'])) {
$conditions['Users.id'] = $currentUser['id'];
} else {
$role_ids = $this->Users->Roles->find()->where(['perm_admin' => 0, 'perm_community_admin' => 0, 'perm_org_admin' => 0, 'perm_group_admin' => 0])->all()->extract('id')->toList();
$conditions['Users.organisation_id'] = $currentUser['organisation_id'];
$subConditions = [
['Users.id' => $currentUser['id']]
];
if (!empty($role_ids)) {
$subConditions[] = ['Users.role_id IN' => $role_ids];
}
$conditions['OR'] = $subConditions;
}
} else {
$conditions['Users.group_id'] = $currentUser['group_id'];
$role_ids = $this->Users->Roles->find()->where(['perm_admin' => 0, 'perm_community_admin' => 0, 'perm_group_admin' => 0])->all()->extract('id')->toList();
$conditions['OR'] = [
['Users.id' => $currentUser['id']],
['Users.role_id IN' => $role_ids]
];
}
}
return $conditions;
}
}

View File

@ -109,10 +109,10 @@ class EncryptionKeysTable extends AppTable
if (!$sortedKeys['valid']) {
$result[2] = 'The user\'s PGP key does not include a valid subkey that could be used for encryption.';
if ($sortedKeys['expired']) {
$result[2] .= ' ' . __n('Found %s subkey that have expired.', 'Found %s subkeys that have expired.', $sortedKeys['expired'], $sortedKeys['expired']);
$result[2] .= ' ' . __n(__('Found 1 subkey that has expired.'), __('Found {0} subkeys that have expired.', $sortedKeys['expired']), $sortedKeys['expired']);
}
if ($sortedKeys['noEncrypt']) {
$result[2] .= ' ' . __n('Found %s subkey that is sign only.', 'Found %s subkeys that are sign only.', $sortedKeys['noEncrypt'], $sortedKeys['noEncrypt']);
$result[2] .= ' ' . __n(__('Found 1 subkey that is sign only.'), __('Found {0} subkeys that are sign only.', $sortedKeys['noEncrypt']), $sortedKeys['noEncrypt']);
}
} else {
$result[0] = true;

View File

@ -163,18 +163,22 @@ class BaseSettingsProvider
$setting['error'] = false;
if (!$skipValidation) {
$validationResult = true;
if (!isset($setting['value'])) {
$validationResult = $this->settingValidator->testEmptyBecomesDefault(null, $setting);
} else if (isset($setting['test'])) {
$setting['value'] = $setting['value'] ?? '';
$validationResult = $this->evaluateFunctionForSetting($setting['test'], $setting);
}
if ($validationResult !== true) {
$setting['severity'] = $setting['severity'] ?? 'warning';
if (!in_array($setting['severity'], $this->severities)) {
$setting['severity'] = 'warning';
if (empty($setting['value']) && !empty($setting['empty'])) {
$validationResult = true;
} else {
if (!isset($setting['value'])) {
$validationResult = $this->settingValidator->testEmptyBecomesDefault(null, $setting);
} else if (isset($setting['test'])) {
$setting['value'] = $setting['value'] ?? '';
$validationResult = $this->evaluateFunctionForSetting($setting['test'], $setting);
}
if ($validationResult !== true) {
$setting['severity'] = $setting['severity'] ?? 'warning';
if (!in_array($setting['severity'], $this->severities)) {
$setting['severity'] = 'warning';
}
$setting['errorMessage'] = $validationResult;
}
$setting['errorMessage'] = $validationResult;
}
$setting['error'] = $validationResult !== true ? true : false;
}

View File

@ -136,15 +136,19 @@ class CerebrateSettingsProvider extends BaseSettingsProvider
'Proxy' => [
'Proxy.host' => [
'name' => __('Host'),
'severity' => 'info',
'type' => 'string',
'description' => __('The hostname of an HTTP proxy for outgoing sync requests. Leave empty to not use a proxy.'),
'test' => 'testHostname',
'empty' => true
],
'Proxy.port' => [
'name' => __('Port'),
'severity' => 'info',
'type' => 'integer',
'description' => __('The TCP port for the HTTP proxy.'),
'test' => 'testForRangeXY',
'empty' => true
],
'Proxy.user' => [
'name' => __('User'),
@ -152,6 +156,7 @@ class CerebrateSettingsProvider extends BaseSettingsProvider
'description' => __('The authentication username for the HTTP proxy.'),
'default' => 'admin',
'dependsOn' => 'proxy.host',
'empty' => true
],
'Proxy.password' => [
'name' => __('Password'),
@ -159,6 +164,7 @@ class CerebrateSettingsProvider extends BaseSettingsProvider
'description' => __('The authentication password for the HTTP proxy.'),
'default' => '',
'dependsOn' => 'proxy.host',
'empty' => true
],
],
],
@ -338,6 +344,17 @@ class CerebrateSettingsProvider extends BaseSettingsProvider
],
]
],
'Restrictions' => [
'Allowed bookmark domains' => [
'security.restrictions.allowed_bookmark_domains' => [
'name' => __('Allowed bookmark domains'),
'type' => 'string',
'severity' => 'info',
'description' => __('Comma separated list of allowed bookmark domains. Leave empty to allow all domains.'),
'default' => '',
],
],
],
'Development' => [
'Debugging' => [
'debug' => [
@ -350,7 +367,7 @@ class CerebrateSettingsProvider extends BaseSettingsProvider
true => __('Debug On'),
],
'test' => function ($value, $setting, $validator) {
$validator->range('value', [0, 2]);
$validator->range('value', [0, 1]);
return testValidator($value, $validator);
},
],

View File

@ -5,6 +5,7 @@ namespace App\Model\Table;
use App\Model\Table\AppTable;
use Cake\ORM\Table;
use Cake\Validation\Validator;
use Cake\Core\Configure;
require_once(APP . 'Model' . DS . 'Table' . DS . 'SettingProviders' . DS . 'UserSettingsProvider.php');
use App\Settings\SettingsProvider\UserSettingsProvider;
@ -94,14 +95,37 @@ class UserSettingsTable extends AppTable
return $savedData;
}
public function saveBookmark($user, $data)
public function saveBookmark($user, $data, &$message = null)
{
$setting = $this->getSettingByName($user, $this->BOOKMARK_SETTING_NAME);
$fieldsToCheck = [
'bookmark_label' => __('Label'),
'bookmark_name' => __('Name'),
'bookmark_url' => __('URL')
];
foreach ($fieldsToCheck as $field => $fieldName) {
if (empty($data[$field])) {
$message = __('Please fill in all fields, {0} missing.', $fieldName);
return null;
}
}
if (empty($data['bookmark_label']) || empty($data['bookmark_name']) || empty($data['bookmark_url'])) {
return null;
}
$bookmarkData = [
'label' => $data['bookmark_label'],
'name' => $data['bookmark_name'],
'url' => $data['bookmark_url'],
];
$restricted_domains = Configure::read('security.restrictions.allowed_bookmark_domains');
if (!empty($restricted_domains)) {
$restricted_domains = explode(',', $restricted_domains);
$parsed = parse_url($bookmarkData['url']);
if (!empty($parsed['host']) && !in_array($parsed['host'], $restricted_domains)) {
return null;
}
}
if (is_null($setting)) { // setting not found, create it
$bookmarksData = json_encode([$bookmarkData]);
$result = $this->createSetting($user, $this->BOOKMARK_SETTING_NAME, $bookmarksData);
@ -153,4 +177,40 @@ class UserSettingsTable extends AppTable
}
return $isLocalPath || $isValidURL;
}
public function validateUserSetting($data): bool|string
{
$errors = [];
$json_fields = ['ui.bookmarks'];
if (in_array($data['name'], $json_fields)) {
$decoded = json_decode($data['value'], true);
if (json_last_error() !== JSON_ERROR_NONE) {
return __('Invalid JSON data');
}
$value = $decoded;
} else {
$value = $data['value'];
}
if ($data['name'] === 'ui.bookmarks') {
if (array_values($value) !== $value) {
$value = [$value];
}
$restricted_domains = Configure::read('security.restrictions.allowed_bookmark_domains');
if (!empty($restricted_domains)) {
$restricted_domains = explode(',', $restricted_domains);
foreach ($restricted_domains as &$rd) {
$rd = trim($rd);
}
}
foreach ($value as $bookmark) {
if (!empty($restricted_domains)) {
$parsed = parse_url($bookmark['url']);
if (!in_array($parsed['host'], $restricted_domains)) {
return __('Invalid domain for bookmark. The only domains allowed are: {0}', implode(', ', $restricted_domains));
}
}
}
}
return true;
}
}

View File

@ -221,7 +221,9 @@ class UsersTable extends AppTable
'perm_admin' => 1,
'perm_community_admin' => 1,
'perm_org_admin' => 1,
'perm_sync' => 1
'perm_sync' => 1,
'perm_group_admin' => 1,
'perm_meta_field_editor' => 1
]);
$this->Roles->save($role);
$this->Organisations = TableRegistry::get('Organisations');

View File

@ -1,4 +1,4 @@
{
"version": "1.24",
"version": "1.26",
"application": "Cerebrate"
}

View File

@ -10,8 +10,8 @@ echo $this->element('genericElements/IndexTable/index_table', [
'data' => [
'type' => 'simple',
'text' => __('Add authentication key'),
'popover_url' => '/authKeys/add',
'reload_url' => $this->request->getRequestTarget()
'popover_url' => '/authKeys/add' . ($action_query_strings ? '?' . http_build_query($action_query_strings) : ''),
'reload_url' => $this->request->getRequestTarget(),
]
]
],

View File

@ -1,7 +1,7 @@
<?php
echo $this->element('genericElements/Form/genericForm', array(
'data' => array(
'description' => __('Individuals are natural persons. They are meant to describe the basic information about an individual that may or may not be a user of this community. Users in genral require an individual object to identify the person behind them - however, no user account is required to store information about an individual. Individuals can have affiliations to organisations and broods as well as cryptographic keys, using which their messages can be verified and which can be used to securely contact them.'),
'description' => __('Cerebrate can connect to other Cerebrate instances to exchange trust information and to instrument interconnectivity between connected local tools. Each such Cerebrate instance with its connected tools is considered to be a brood.'),
'model' => 'Organisations',
'fields' => array(
array(

View File

@ -1,7 +1,7 @@
<?php
echo $this->element('genericElements/Form/genericForm', array(
'data' => array(
'description' => __('Individuals are natural persons. They are meant to describe the basic information about an individual that may or may not be a user of this community. Users in genral require an individual object to identify the person behind them - however, no user account is required to store information about an individual. Individuals can have affiliations to organisations and broods as well as cryptographic keys, using which their messages can be verified and which can be used to securely contact them.'),
'description' => __('Individuals are natural persons. They are meant to describe the basic information about an individual that may or may not be a user of this community. Users in general require an individual object to identify the person behind them - however, no user account is required to store information about an individual. Individuals can have affiliations to organisations and broods as well as cryptographic keys, using which their messages can be verified and which can be used to securely contact them.'),
'model' => 'Organisations',
'fields' => array(
array(

View File

@ -13,7 +13,7 @@
'label' => 'Site admin privilege (instance management)'
],
[
'field' => 'perm_community',
'field' => 'perm_community_admin',
'type' => 'checkbox',
'label' => 'Community admin privilege (data admin)'
],

View File

@ -10,7 +10,7 @@ servers:
tags:
- name: Individuals
description: "Individuals are natural persons. They are meant to describe the basic information about an individual that may or may not be a user of this community. Users in genral require an individual object to identify the person behind them - however, no user account is required to store information about an individual. Individuals can have affiliations to organisations and broods as well as cryptographic keys, using which their messages can be verified and which can be used to securely contact them."
description: "Individuals are natural persons. They are meant to describe the basic information about an individual that may or may not be a user of this community. Users in general require an individual object to identify the person behind them - however, no user account is required to store information about an individual. Individuals can have affiliations to organisations and broods as well as cryptographic keys, using which their messages can be verified and which can be used to securely contact them."
- name: Users
description: "Users enrolled in this Cerebrate instance."
- name: Organisations