diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..a2c763a --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,52 @@ +name: test + +on: + push: + branches: [main, develop] + pull_request: + branches: [main, develop] + +jobs: + test: + runs-on: ${{ matrix.os }} + timeout-minutes: 5 + strategy: + fail-fast: false + matrix: + os: [ubuntu-20.04] + php: ["7.4"] + steps: + - uses: actions/checkout@v2 + + - name: Create config files + run: | + cp ./config/app_local.example.php ./config/app_local.php + cp ./config/config.example.json ./config/config.json + + - name: Setup MariaDB + uses: getong/mariadb-action@v1.1 + with: + host port: 3306 + container port: 3306 + mysql database: "cerebrate_test" + mysql user: "cerebrate" + mysql password: "cerebrate" + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: pdo, pdo_mysql, mysqli, simplexml + + - name: Install dependencies + env: + php_version: ${{ matrix.php }} + run: | + sudo apt-get -y update + sudo apt-get install -y --no-install-recommends curl git zip unzip libicu-dev libxml2-dev + composer install --no-progress --no-interaction + + - name: Run tests + run: composer test + env: + DEBUG: true diff --git a/INSTALL/INSTALL.md b/INSTALL/INSTALL.md index f4cef5f..8a9c901 100644 --- a/INSTALL/INSTALL.md +++ b/INSTALL/INSTALL.md @@ -1,7 +1,9 @@ ## Requirements An Ubuntu server (18.04/20.04 should both work fine) - though other linux installations should work too. + - apache2 (or nginx), mysql/mariadb, sqlite need to be installed and running +- php version 8+ is required - php extensions for intl, mysql, sqlite3, mbstring, xml need to be installed and running - php extention for curl (not required but makes composer run a little faster) - composer diff --git a/composer.json b/composer.json index f51bc6f..6d48735 100644 --- a/composer.json +++ b/composer.json @@ -55,7 +55,7 @@ "cs-fix": "phpcbf --colors --standard=vendor/cakephp/cakephp-codesniffer/CakePHP src/ tests/", "stan": "phpstan analyse src/", "test": [ - "sh ./tests/Helper/wiremock/start.sh", + "nohup sh ./tests/Helper/wiremock/start.sh >/dev/null 2>&1 &", "phpunit", "sh ./tests/Helper/wiremock/stop.sh" ] diff --git a/config/Migrations/20210311000000_InitialSchema.php b/config/Migrations/20210311000000_InitialSchema.php index d1e2ed1..30074d3 100644 --- a/config/Migrations/20210311000000_InitialSchema.php +++ b/config/Migrations/20210311000000_InitialSchema.php @@ -9,6 +9,10 @@ class InitialSchema extends AbstractMigration { public function change() { + $exists = $this->hasTable('broods'); + if ($exists) { + return true; + } $this->execute('SET unique_checks=0; SET foreign_key_checks=0;'); $this->execute("ALTER DATABASE CHARACTER SET 'utf8mb4';"); $this->execute("ALTER DATABASE COLLATE='utf8mb4_general_ci';"); diff --git a/config/Migrations/20220207000000_registration_flood_protection.php b/config/Migrations/20220207000000_registration_flood_protection.php new file mode 100644 index 0000000..d8d1cdf --- /dev/null +++ b/config/Migrations/20220207000000_registration_flood_protection.php @@ -0,0 +1,47 @@ +hasTable('flood_protections'); + if (!$exists) { + $table = $this->table('flood_protections', [ + 'signed' => false, + 'collation' => 'utf8mb4_unicode_ci', + ]); + $table + ->addColumn('remote_ip', 'string', [ + 'null' => false, + 'length' => 45, + ]) + ->addColumn('request_action', 'string', [ + 'null' => false, + 'length' => 191, + ]) + ->addColumn('expiration', 'integer', [ + 'null' => false, + 'signed' => false, + 'length' => 10, + ]) + ->addIndex('remote_ip') + ->addIndex('request_action') + ->addIndex('expiration'); + $table->create(); + } + } +} diff --git a/config/app_local.example.php b/config/app_local.example.php index 1ec0f4a..ed0c5ec 100644 --- a/config/app_local.example.php +++ b/config/app_local.example.php @@ -73,12 +73,10 @@ return [ * The test connection is used during the test suite. */ 'test' => [ - 'host' => 'localhost', - //'port' => 'non_standard_port_number', - 'username' => 'my_app', - 'password' => 'secret', - 'database' => 'test_myapp', - //'schema' => 'myapp', + 'host' => '127.0.0.1', + 'username' => 'cerebrate', + 'password' => 'cerebrate', + 'database' => 'cerebrate_test', ], ], diff --git a/src/Controller/AppController.php b/src/Controller/AppController.php index 9a3c8c6..dc5b81f 100644 --- a/src/Controller/AppController.php +++ b/src/Controller/AppController.php @@ -41,6 +41,7 @@ class AppController extends Controller public $restResponsePayload = null; public $user = null; public $breadcrumb = []; + public $request_ip = null; /** * Initialization hook method. @@ -86,6 +87,7 @@ class AppController extends Controller Configure::write('DebugKit.forceEnable', true); } $this->loadComponent('CustomPagination'); + $this->loadComponent('FloodProtection'); /* * Enable the following component for recommended CakePHP form protection settings. * see https://book.cakephp.org/4/en/controllers/components/form-protection.html @@ -135,7 +137,6 @@ class AppController extends Controller $this->ACL->checkAccess(); if (!$this->ParamHandler->isRest()) { - $this->set('breadcrumb', $this->Navigation->getBreadcrumb()); $this->set('ajax', $this->request->is('ajax')); $this->request->getParam('prefix'); $this->set('baseurl', Configure::read('App.fullBaseUrl')); @@ -149,6 +150,9 @@ class AppController extends Controller $this->set('metaGroup', !empty($this->isAdmin) ? 'Administration' : 'Cerebrate'); } } + if (mt_rand(1, 50) === 1) { + $this->FloodProtection->cleanup(); + } } public function beforeRender(EventInterface $event) diff --git a/src/Controller/Component/ACLComponent.php b/src/Controller/Component/ACLComponent.php index fb51f49..57180c2 100644 --- a/src/Controller/Component/ACLComponent.php +++ b/src/Controller/Component/ACLComponent.php @@ -188,12 +188,12 @@ class ACLComponent extends Component 'add' => ['*'], 'edit' => ['*'], 'delete' => ['*'], - 'getSettingByName' => ['*'], - 'setSetting' => ['*'], + 'getMySettingByName' => ['*'], + 'setMySetting' => ['*'], 'saveSetting' => ['*'], - 'getBookmarks' => ['*'], - 'saveBookmark' => ['*'], - 'deleteBookmark' => ['*'] + 'getMyBookmarks' => ['*'], + 'saveMyBookmark' => ['*'], + 'deleteMyBookmark' => ['*'] ], 'Api' => [ 'index' => ['*'] @@ -277,9 +277,32 @@ class ACLComponent extends Component $this->user = $user; } - public function getUser(): User + public function getUser(): ?User { - return $this->user; + if (!empty($this->user)) { + return $this->user; + } + return null; + } + + public function canEditUser(User $currentUser, User $user): bool + { + if (empty($user) || empty($currentUser)) { + return false; + } + if (!$currentUser['role']['perm_admin']) { + if ($user['role']['perm_admin']) { + return false; // org_admins cannot edit admins + } + if (!$currentUser['role']['perm_org_admin']) { + return false; + } else { + if ($currentUser['organisation_id'] !== $user['organisation_id']) { + return false; + } + } + } + return true; } /* diff --git a/src/Controller/Component/CRUDComponent.php b/src/Controller/Component/CRUDComponent.php index c532630..bb31f16 100644 --- a/src/Controller/Component/CRUDComponent.php +++ b/src/Controller/Component/CRUDComponent.php @@ -223,9 +223,6 @@ class CRUDComponent extends Component $metaTemplates = $this->getMetaTemplates(); $data = $this->attachMetaTemplatesIfNeeded($data, $metaTemplates->toArray()); } - if (!empty($params['fields'])) { - $this->Controller->set('fields', $params['fields']); - } if ($this->request->is('post')) { $patchEntityParams = [ 'associated' => [], @@ -291,6 +288,9 @@ class CRUDComponent extends Component } } } + if (!empty($params['fields'])) { + $this->Controller->set('fields', $params['fields']); + } $this->Controller->entity = $data; $this->Controller->set('entity', $data); } @@ -461,7 +461,10 @@ class CRUDComponent extends Component if (!empty($params['conditions'])) { $query->where($params['conditions']); } - $data = $query->first(); + $data = $data->first(); + if (isset($params['afterFind'])) { + $data = $params['afterFind']($data, $params); + } if (empty($data)) { throw new NotFoundException(__('Invalid {0}.', $this->ObjectAlias)); } @@ -469,9 +472,6 @@ class CRUDComponent extends Component $metaTemplates = $this->getMetaTemplates(); $data = $this->attachMetaTemplatesIfNeeded($data, $metaTemplates->toArray()); } - if (!empty($params['fields'])) { - $this->Controller->set('fields', $params['fields']); - } if ($this->request->is(['post', 'put'])) { $patchEntityParams = [ 'associated' => [] @@ -527,6 +527,9 @@ class CRUDComponent extends Component } } } + if (!empty($params['fields'])) { + $this->Controller->set('fields', $params['fields']); + } $this->Controller->entity = $data; $this->Controller->set('entity', $data); } @@ -658,11 +661,16 @@ class CRUDComponent extends Component } $data = $this->Table->get($id, $params); + if (empty($data)) { + throw new NotFoundException(__('Invalid {0}.', $this->ObjectAlias)); + } $data = $this->attachMetaTemplatesIfNeeded($data); - if (isset($params['afterFind'])) { $data = $params['afterFind']($data); } + if (empty($data)) { + throw new NotFoundException(__('Invalid {0}.', $this->ObjectAlias)); + } if ($this->Controller->ParamHandler->isRest()) { $this->Controller->restResponsePayload = $this->RestResponse->viewData($data, 'json'); } @@ -730,9 +738,13 @@ class CRUDComponent extends Component } $data = $query->first(); if (isset($params['beforeSave'])) { - $data = $params['beforeSave']($data); - 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)); + try { + $data = $params['beforeSave']($data); + 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)); + } + } catch (Exception $e) { + $data = false; } } if (!empty($data)) { @@ -1291,6 +1303,9 @@ class CRUDComponent extends Component } $data = $this->Table->get($id, $params); + if (isset($params['afterFind'])) { + $data = $params['afterFind']($data, $params); + } if ($this->request->is(['post', 'put'])) { if (isset($params['force_state'])) { $data->{$fieldName} = $params['force_state']; diff --git a/src/Controller/Component/FloodProtectionComponent.php b/src/Controller/Component/FloodProtectionComponent.php new file mode 100644 index 0000000..6fbd0ec --- /dev/null +++ b/src/Controller/Component/FloodProtectionComponent.php @@ -0,0 +1,58 @@ +remote_ip = $_SERVER[$ip_source]; + $temp = explode(PHP_EOL, $_SERVER[$ip_source]); + if (count($temp) > 1) { + $this->remote_ip = $temp[0]; + } + $this->FloodProtections = TableRegistry::getTableLocator()->get('FloodProtections'); + } + + public function check(string $action, int $limit = 5, int $expiration_time = 300): bool + { + $results = $this->FloodProtections->find('all')->where(['request_action' => $action, 'remote_ip' => $this->remote_ip, 'expiration' > time()])->toList(); + if (count($results) >= $limit) { + throw new TooManyRequestsException(__('Too many {0} requests have been issued ({1} requests allowed ever {2} seconds)', [$action, $limit, $expiration_time])); + } + return false; + } + + public function set(string $action, int $expiration_time = 300): bool + { + $entry = $this->FloodProtections->newEmptyEntity(); + $entry->expiration = time() + $expiration_time; + $entry->remote_ip = $this->remote_ip; + $entry->request_action = $action; + return (bool)$this->FloodProtections->save($entry); + + } + + public function checkAndSet(string $action, int $limit = 5, int $expiration_time = 300): bool + { + $result = $this->check($action, $limit, $expiration_time); + $this->set($action, $expiration_time); + return $result; + } + + public function cleanup(): void + { + $this->FloodProtections->deleteAll(['expiration <' => time()]); + } +} diff --git a/src/Controller/Component/Navigation/Users.php b/src/Controller/Component/Navigation/Users.php index 7e228db..21fb76b 100644 --- a/src/Controller/Component/Navigation/Users.php +++ b/src/Controller/Component/Navigation/Users.php @@ -1,7 +1,7 @@ bcf; $request = $this->request; $passedData = $this->request->getParam('pass'); - $this->bcf->addLink('Users', 'view', 'UserSettings', 'index', function ($config) use ($bcf, $request, $passedData) { + $currentUser = $this->currentUser; + $this->bcf->addLink('Users', 'view', 'UserSettings', 'index', function ($config) use ($bcf, $request, $passedData, $currentUser) { if (!empty($passedData[0])) { $user_id = $passedData[0]; $linkData = [ diff --git a/src/Controller/Component/Navigation/base.php b/src/Controller/Component/Navigation/base.php index 3900e69..30f904a 100644 --- a/src/Controller/Component/Navigation/base.php +++ b/src/Controller/Component/Navigation/base.php @@ -6,6 +6,7 @@ class BaseNavigation protected $bcf; protected $request; protected $viewVars; + public $currentUser; public function __construct($bcf, $request, $viewVars) { @@ -14,8 +15,13 @@ class BaseNavigation $this->viewVars = $viewVars; } + public function setCurrentUser($currentUser) + { + $this->currentUser = $currentUser; + } + public function addRoutes() {} public function addParents() {} public function addLinks() {} public function addActions() {} -} \ No newline at end of file +} diff --git a/src/Controller/Component/NavigationComponent.php b/src/Controller/Component/NavigationComponent.php index 5d3d1e5..9df48be 100644 --- a/src/Controller/Component/NavigationComponent.php +++ b/src/Controller/Component/NavigationComponent.php @@ -17,8 +17,9 @@ require_once(APP . 'Controller' . DS . 'Component' . DS . 'Navigation' . DS . 's class NavigationComponent extends Component { - private $user = null; + private $currentUser = null; public $breadcrumb = null; + public $fullBreadcrumb = null; public $iconToTableMapping = [ 'Individuals' => 'address-book', 'Organisations' => 'building', @@ -46,7 +47,6 @@ class NavigationComponent extends Component public function beforeRender($event) { $this->fullBreadcrumb = $this->genBreadcrumb(); - $this->breadcrumb = $this->getBreadcrumb(); } public function getSideMenu(): array @@ -57,7 +57,7 @@ class NavigationComponent extends Component return $sidemenu; } - + public function addUserBookmarks($sidemenu): array { $bookmarks = $this->getUserBookmarks(); @@ -82,7 +82,7 @@ class NavigationComponent extends Component }, $bookmarks); return $links; } - + public function getBreadcrumb(): array { $controller = $this->request->getParam('controller'); @@ -143,6 +143,7 @@ class NavigationComponent extends Component $reflection = new \ReflectionClass("BreadcrumbNavigation\\{$navigationClassname}Navigation"); $viewVars = $this->_registry->getController()->viewBuilder()->getVars(); $navigationClasses[$navigationClassname] = $reflection->newInstance($bcf, $request, $viewVars); + $navigationClasses[$navigationClassname]->setCurrentUser($this->currentUser); } return $navigationClasses; } @@ -288,7 +289,7 @@ class BreadcrumbFactory $this->addLink($controller, 'view', $controller, 'edit'); $this->addLink($controller, 'edit', $controller, 'view'); $this->addSelfLink($controller, 'edit'); - + $this->addAction($controller, 'view', $controller, 'add'); $this->addAction($controller, 'view', $controller, 'delete'); $this->addAction($controller, 'edit', $controller, 'add'); diff --git a/src/Controller/Component/ParamHandlerComponent.php b/src/Controller/Component/ParamHandlerComponent.php index ae59f73..bb004fa 100644 --- a/src/Controller/Component/ParamHandlerComponent.php +++ b/src/Controller/Component/ParamHandlerComponent.php @@ -48,7 +48,7 @@ class ParamHandlerComponent extends Component return $this->isRest; } if ($this->request->is('json')) { - if (!empty((string)$this->request->getBody()) && empty($this->request->getParsedBody())) { + if (!empty((string)$this->request->getBody()) && !is_array($this->request->getParsedBody())) { throw new MethodNotAllowedException('Invalid JSON input. Make sure that the JSON input is a correctly formatted JSON string. This request has been blocked to avoid an unfiltered request.'); } $this->isRest = true; diff --git a/src/Controller/Open/IndividualsController.php b/src/Controller/Open/IndividualsController.php index 28cd51d..79af257 100644 --- a/src/Controller/Open/IndividualsController.php +++ b/src/Controller/Open/IndividualsController.php @@ -11,13 +11,17 @@ use Cake\Http\Exception\NotFoundException; use Cake\Http\Exception\MethodNotAllowedException; use Cake\Http\Exception\ForbiddenException; use Cake\Event\EventInterface; +use Cake\Core\Configure; class IndividualsController extends AppController { public function beforeFilter(EventInterface $event) { parent::beforeFilter($event); - $this->Authentication->allowUnauthenticated(['index']); + $open = Configure::read('Cerebrate.open'); + if (!empty($open) && in_array('individuals', $open)) { + $this->Authentication->allowUnauthenticated(['index']); + } } public function index() diff --git a/src/Controller/Open/OrganisationsController.php b/src/Controller/Open/OrganisationsController.php index ad22f42..facda8a 100644 --- a/src/Controller/Open/OrganisationsController.php +++ b/src/Controller/Open/OrganisationsController.php @@ -10,13 +10,17 @@ use Cake\Http\Exception\NotFoundException; use Cake\Http\Exception\MethodNotAllowedException; use Cake\Http\Exception\ForbiddenException; use Cake\Event\EventInterface; +use Cake\Core\Configure; class OrganisationsController extends AppController { public function beforeFilter(EventInterface $event) { parent::beforeFilter($event); - $this->Authentication->allowUnauthenticated(['index']); + $open = Configure::read('Cerebrate.open'); + if (!empty($open) && in_array('organisations', $open)) { + $this->Authentication->allowUnauthenticated(['index']); + } } public function index() diff --git a/src/Controller/RolesController.php b/src/Controller/RolesController.php index 357eeeb..77daa5e 100644 --- a/src/Controller/RolesController.php +++ b/src/Controller/RolesController.php @@ -31,7 +31,15 @@ class RolesController extends AppController public function add() { - $this->CRUD->add(); + $rolesModel = $this->Roles; + $this->CRUD->add([ + 'afterSave' => function ($data) use ($rolesModel) { + if ($data['is_default']) { + $rolesModel->query()->update()->set(['is_default' => false])->where(['id !=' => $data->id])->execute(); + } + return true; + } + ]); $responsePayload = $this->CRUD->getResponsePayload(); if (!empty($responsePayload)) { return $responsePayload; @@ -51,7 +59,15 @@ class RolesController extends AppController public function edit($id) { - $this->CRUD->edit($id); + $rolesModel = $this->Roles; + $this->CRUD->edit($id, [ + 'afterSave' => function ($data) use ($rolesModel) { + if ($data['is_default']) { + $rolesModel->query()->update()->set(['is_default' => false])->where(['id !=' => $data->id])->execute(); + } + return true; + } + ]); $responsePayload = $this->CRUD->getResponsePayload(); if (!empty($responsePayload)) { return $responsePayload; diff --git a/src/Controller/SharingGroupsController.php b/src/Controller/SharingGroupsController.php index cf4c9b0..313833e 100644 --- a/src/Controller/SharingGroupsController.php +++ b/src/Controller/SharingGroupsController.php @@ -7,6 +7,7 @@ use Cake\Utility\Hash; use Cake\Utility\Text; use \Cake\Database\Expression\QueryExpression; use Cake\Error\Debugger; +use Cake\Http\Exception\NotFoundException; class SharingGroupsController extends AppController { @@ -52,8 +53,25 @@ class SharingGroupsController extends AppController public function view($id) { + $currentUser = $this->ACL->getUser(); $this->CRUD->view($id, [ - 'contain' => ['SharingGroupOrgs', 'Organisations', 'Users' => ['fields' => ['id', 'username']]] + 'contain' => ['SharingGroupOrgs', 'Organisations', 'Users' => ['fields' => ['id', 'username']]], + 'afterFind' => function($data) use ($currentUser) { + if (empty($currentUser['role']['perm_admin'])) { + $orgFround = false; + if (!empty($data['sharing_group_orgs'])) { + foreach ($data['sharing_group_orgs'] as $org) { + if ($org['id'] === $currentUser['organisation_id']) { + $orgFound = true; + } + } + } + if ($data['organisation_id'] !== $currentUser['organisation_id'] && !$orgFround) { + return null; + } + } + return $data; + } ]); $responsePayload = $this->CRUD->getResponsePayload(); if (!empty($responsePayload)) { @@ -68,6 +86,7 @@ class SharingGroupsController extends AppController if (empty($currentUser['role']['perm_admin'])) { $params['conditions'] = ['organisation_id' => $currentUser['organisation_id']]; } + $params['fields'] = ['name', 'releasability', 'description', 'active']; $this->CRUD->edit($id, $params); $responsePayload = $this->CRUD->getResponsePayload(); if (!empty($responsePayload)) { @@ -82,7 +101,11 @@ class SharingGroupsController extends AppController public function delete($id) { - $this->CRUD->delete($id); + $currentUser = $this->ACL->getUser(); + if (empty($currentUser['role']['perm_admin'])) { + $params['conditions'] = ['organisation_id' => $currentUser['organisation_id']]; + } + $this->CRUD->delete($id, $params); $responsePayload = $this->CRUD->getResponsePayload(); if (!empty($responsePayload)) { return $responsePayload; @@ -91,9 +114,18 @@ class SharingGroupsController extends AppController public function addOrg($id) { + $currentUser = $this->ACL->getUser(); $sharingGroup = $this->SharingGroups->get($id, [ 'contain' => 'SharingGroupOrgs' ]); + if (empty($currentUser['role']['perm_admin'])) { + if ($sharingGroup['organisation_id'] !== $currentUser['organisation_id']) { + $sharingGroup = null; + } + } + if (empty($sharingGroup)) { + throw new NotFoundException(__('Invalid SharingGroup.')); + } $conditions = []; $containedOrgIds = array_values(\Cake\Utility\Hash::extract($sharingGroup, 'sharing_group_orgs.{n}.id')); if (!empty($containedOrgIds)) { @@ -150,9 +182,18 @@ class SharingGroupsController extends AppController public function removeOrg($id, $org_id) { + $currentUser = $this->ACL->getUser(); $sharingGroup = $this->SharingGroups->get($id, [ 'contain' => 'SharingGroupOrgs' ]); + if (empty($currentUser['role']['perm_admin'])) { + if ($sharingGroup['organisation_id'] !== $currentUser['organisation_id']) { + $sharingGroup = null; + } + } + if (empty($sharingGroup)) { + throw new NotFoundException(__('Invalid SharingGroup.')); + } if ($this->request->is('post')) { $org = $this->SharingGroups->SharingGroupOrgs->get($org_id); $result = (bool)$this->SharingGroups->SharingGroupOrgs->unlink($sharingGroup, [$org]); diff --git a/src/Controller/UserSettingsController.php b/src/Controller/UserSettingsController.php index d28f6ca..7f9690a 100644 --- a/src/Controller/UserSettingsController.php +++ b/src/Controller/UserSettingsController.php @@ -36,9 +36,16 @@ class UserSettingsController extends AppController return $responsePayload; } if (!empty($this->request->getQuery('Users_id'))) { - $settingsForUser = $this->UserSettings->Users->find()->where([ + $conditions = [ 'id' => $this->request->getQuery('Users_id') - ])->first(); + ]; + if (empty($currentUser['role']['perm_admin'])) { + $conditions['organisation_id'] = $currentUser['organisation_id']; + } + $settingsForUser = $this->UserSettings->Users->find()->where($conditions)->first(); + if (empty($settingsForUser)) { + throw new NotFoundException(__('Invalid {0}.', __('user'))); + } $this->set('settingsForUser', $settingsForUser); } } @@ -57,7 +64,7 @@ class UserSettingsController extends AppController } } - public function add($user_id = false) + public function add($user_id=null) { $currentUser = $this->ACL->getUser(); $this->CRUD->add([ @@ -77,6 +84,8 @@ class UserSettingsController extends AppController if (empty($currentUser['role']['perm_admin'])) { $allUsers->where(['id' => $currentUser->id]); $user_id = $currentUser->id; + } else if (!is_null($user_id)) { + $allUsers->where(['id' => $user_id]); } $dropdownData = [ 'user' => $allUsers->all()->toArray(), @@ -124,7 +133,13 @@ class UserSettingsController extends AppController } } - public function getSettingByName($settingsName) + /** + * Get a setting by name for the currently logged-in user + * + * @param [type] $settingsName + * @return void + */ + public function getMySettingByName($settingsName) { $setting = $this->UserSettings->getSettingByName($this->ACL->getUser(), $settingsName); if (is_null($setting)) { @@ -140,7 +155,7 @@ class UserSettingsController extends AppController $this->render('view'); } - public function setSetting($settingsName = false) + public function setMySetting($settingsName = false) { if (!$this->request->is('get')) { $setting = $this->UserSettings->getSettingByName($this->ACL->getUser(), $settingsName); @@ -160,22 +175,23 @@ class UserSettingsController extends AppController $this->set('settingName', $settingsName); } - public function saveSetting() + public function saveSetting($user_id = false) { + $user = $this->getRequestedUserIfAllowed($user_id); if ($this->request->is('post')) { $data = $this->ParamHandler->harvestParams([ 'name', 'value' ]); - $setting = $this->UserSettings->getSettingByName($this->ACL->getUser(), $data['name']); + $setting = $this->UserSettings->getSettingByName($user, $data['name']); if (is_null($setting)) { // setting not found, create it - $result = $this->UserSettings->createSetting($this->ACL->getUser(), $data['name'], $data['value']); + $result = $this->UserSettings->createSetting($user, $data['name'], $data['value']); } else { - $result = $this->UserSettings->editSetting($this->ACL->getUser(), $data['name'], $data['value']); + $result = $this->UserSettings->editSetting($user, $data['name'], $data['value']); } $success = !empty($result); $message = $success ? __('Setting saved') : __('Could not save setting'); - $this->CRUD->setResponseForController('setSetting', $success, $message, $result); + $this->CRUD->setResponseForController('saveSetting', $success, $message, $result); $responsePayload = $this->CRUD->getResponsePayload(); if (!empty($responsePayload)) { return $responsePayload; @@ -183,7 +199,7 @@ class UserSettingsController extends AppController } } - public function getBookmarks($forSidebar = false) + public function getMyBookmarks($forSidebar = false) { $bookmarks = $this->UserSettings->getSettingByName($this->ACL->getUser(), $this->UserSettings->BOOKMARK_SETTING_NAME); $bookmarks = json_decode($bookmarks['value'], true); @@ -193,7 +209,7 @@ class UserSettingsController extends AppController $this->render('/element/UserSettings/saved-bookmarks'); } - public function saveBookmark() + public function saveMyBookmark() { if (!$this->request->is('get')) { $result = $this->UserSettings->saveBookmark($this->ACL->getUser(), $this->request->getData()); @@ -208,7 +224,7 @@ class UserSettingsController extends AppController $this->set('user_id', $this->ACL->getUser()->id); } - public function deleteBookmark() + public function deleteMyBookmark() { if (!$this->request->is('get')) { $result = $this->UserSettings->deleteBookmark($this->ACL->getUser(), $this->request->getData()); @@ -224,7 +240,7 @@ class UserSettingsController extends AppController } /** - * isLoggedUserAllowedToEdit + * isLoggedUserAllowedToEdit * * @param int|\App\Model\Entity\UserSetting $setting * @return boolean @@ -248,4 +264,26 @@ class UserSettingsController extends AppController } return $isAllowed; } + + /** + * Return the requested user if user permissions allow it. Otherwise, return the user currently logged-in + * + * @param bool|int $user_id + * @return void + */ + private function getRequestedUserIfAllowed($user_id = false) + { + $currentUser = $this->ACL->getUser(); + if (is_bool($user_id)) { + return $currentUser; + } + if (!empty($currentUser['role']['perm_admin'])) { + $user = $this->Users->get($user_id, [ + 'contain' => ['Roles', 'Individuals' => 'Organisations'] + ]); + } else { + $user = $currentUser; + } + return $user; + } } diff --git a/src/Controller/UsersController.php b/src/Controller/UsersController.php index 9ffb2fe..3d684a0 100644 --- a/src/Controller/UsersController.php +++ b/src/Controller/UsersController.php @@ -33,16 +33,34 @@ class UsersController extends AppController if (!empty($responsePayload)) { return $responsePayload; } + $this->set( + 'validRoles', + $this->Users->Roles->find('list')->select(['id', 'name'])->order(['name' => 'asc'])->where(['perm_admin' => 0])->all()->toArray() + ); $this->set('metaGroup', $this->isAdmin ? 'Administration' : 'Cerebrate'); } public function add() { $currentUser = $this->ACL->getUser(); + $validRoles = []; + if (!$currentUser['role']['perm_admin']) { + $validRoles = $this->Users->Roles->find('list')->select(['id', 'name'])->order(['name' => 'asc'])->where(['perm_admin' => 0])->all()->toArray(); + } else { + $validRoles = $this->Users->Roles->find('list')->order(['name' => 'asc'])->all()->toArray(); + } + $defaultRole = $this->Users->Roles->find()->select(['id'])->first()->toArray(); + $this->CRUD->add([ - 'beforeSave' => function($data) use ($currentUser) { + 'beforeSave' => function($data) use ($currentUser, $validRoles, $defaultRole) { + if (!isset($data['role_id']) && !empty($defaultRole)) { + $data['role_id'] = $defaultRole['id']; + } if (!$currentUser['role']['perm_admin']) { $data['organisation_id'] = $currentUser['organisation_id']; + if (!in_array($data['role_id'], array_keys($validRoles))) { + throw new MethodNotAllowedException(__('You do not have permission to assign that role.')); + } } $this->Users->enrollUserRouter($data); return $data; @@ -65,9 +83,7 @@ class UsersController extends AppController $org_conditions = ['id' => $currentUser['organisation_id']]; } $dropdownData = [ - 'role' => $this->Users->Roles->find('list', [ - 'sort' => ['name' => 'asc'] - ]), + 'role' => $validRoles, 'individual' => $this->Users->Individuals->find('list', [ 'sort' => ['email' => 'asc'] ]), @@ -77,12 +93,14 @@ class UsersController extends AppController ]) ]; $this->set(compact('dropdownData')); + $this->set('defaultRole', $defaultRole['id'] ?? null); $this->set('metaGroup', $this->isAdmin ? 'Administration' : 'Cerebrate'); } public function view($id = false) { - if (empty($id) || empty($this->ACL->getUser()['role']['perm_admin'])) { + $currentUser = $this->ACL->getUser(); + if (empty($id) || (empty($currentUser['role']['perm_org_admin']) && empty($currentUser['role']['perm_admin']))) { $id = $this->ACL->getUser()['id']; } $this->CRUD->view($id, [ @@ -98,6 +116,12 @@ class UsersController extends AppController public function edit($id = false) { $currentUser = $this->ACL->getUser(); + $validRoles = []; + if (!$currentUser['role']['perm_admin']) { + $validRoles = $this->Users->Roles->find('list')->select(['id', 'name'])->order(['name' => 'asc'])->where(['perm_admin' => 0])->all()->toArray(); + } else { + $validRoles = $this->Users->Roles->find('list')->order(['name' => 'asc'])->all()->toArray(); + } if (empty($id)) { $id = $currentUser['id']; } else { @@ -128,6 +152,21 @@ class UsersController extends AppController $params['fields'][] = 'role_id'; $params['fields'][] = 'organisation_id'; $params['fields'][] = 'disabled'; + } else if (!empty($this->ACL->getUser()['role']['perm_org_admin'])) { + $params['fields'][] = 'username'; + $params['fields'][] = 'role_id'; + $params['fields'][] = 'disabled'; + if (!$currentUser['role']['perm_admin']) { + $params['afterFind'] = function ($data, &$params) use ($currentUser, $validRoles) { + if (!in_array($data['role_id'], array_keys($validRoles))) { + throw new MethodNotAllowedException(__('You cannot edit the given privileged user.')); + } + if ($data['organisation_id'] !== $currentUser['organisation_id']) { + throw new MethodNotAllowedException(__('You cannot edit the given user.')); + } + return $data; + }; + } } $this->CRUD->edit($id, $params); $responsePayload = $this->CRUD->getResponsePayload(); @@ -135,9 +174,7 @@ class UsersController extends AppController return $responsePayload; } $dropdownData = [ - 'role' => $this->Users->Roles->find('list', [ - 'sort' => ['name' => 'asc'] - ]), + 'role' => $validRoles, 'individual' => $this->Users->Individuals->find('list', [ 'sort' => ['email' => 'asc'] ]), @@ -152,7 +189,19 @@ class UsersController extends AppController public function toggle($id, $fieldName = 'disabled') { - $this->CRUD->toggle($id, $fieldName); + $params = [ + 'contain' => 'Roles' + ]; + $currentUser = $this->ACL->getUser(); + if (!$currentUser['role']['perm_admin']) { + $params['afterFind'] = function ($user, &$params) use ($currentUser) { + if (!$this->ACL->canEditUser($currentUser, $user)) { + throw new MethodNotAllowedException(__('You cannot edit the given user.')); + } + return $user; + }; + } + $this->CRUD->toggle($id, $fieldName, $params); $responsePayload = $this->CRUD->getResponsePayload(); if (!empty($responsePayload)) { return $responsePayload; @@ -161,6 +210,24 @@ class UsersController extends AppController public function delete($id) { + $currentUser = $this->ACL->getUser(); + $validRoles = []; + if (!$currentUser['role']['perm_admin']) { + $validRoles = $this->Users->Roles->find('list')->order(['name' => 'asc'])->all()->toArray(); + } + $params = [ + 'beforeSave' => function($data) use ($currentUser, $validRoles) { + if (!$currentUser['role']['perm_admin']) { + if ($data['organisation_id'] !== $currentUser['organisation_id']) { + throw new MethodNotAllowedException(__('You do not have permission to remove the given user.')); + } + if (!in_array($data['role_id'], array_keys($validRoles))) { + throw new MethodNotAllowedException(__('You do not have permission to remove the given user.')); + } + } + return $data; + } + ]; $this->CRUD->delete($id); $responsePayload = $this->CRUD->getResponsePayload(); if (!empty($responsePayload)) { @@ -218,10 +285,21 @@ class UsersController extends AppController } } - public function settings() + public function settings($user_id=false) { - $this->set('user', $this->ACL->getUser()); - $all = $this->Users->UserSettings->getSettingsFromProviderForUser($this->ACL->getUser()['id'], true); + $editingAnotherUser = false; + $currentUser = $this->ACL->getUser(); + if (empty($currentUser['role']['perm_admin']) || $user_id == $currentUser->id) { + $user = $currentUser; + } else { + $user = $this->Users->get($user_id, [ + 'contain' => ['Roles', 'Individuals' => 'Organisations', 'Organisations', 'UserSettings'] + ]); + $editingAnotherUser = true; + } + $this->set('editingAnotherUser', $editingAnotherUser); + $this->set('user', $user); + $all = $this->Users->UserSettings->getSettingsFromProviderForUser($user->id, true); $this->set('settingsProvider', $all['settingsProvider']); $this->set('settings', $all['settings']); $this->set('settingsFlattened', $all['settingsFlattened']); @@ -233,6 +311,9 @@ class UsersController extends AppController if (empty(Configure::read('security.registration.self-registration'))) { throw new UnauthorizedException(__('User self-registration is not open.')); } + if (!empty(Configure::read('security.registration.floodProtection'))) { + $this->FloodProtection->check('register'); + } if ($this->request->is('post')) { $data = $this->request->getData(); $this->InboxProcessors = TableRegistry::getTableLocator()->get('InboxProcessors'); @@ -249,6 +330,9 @@ class UsersController extends AppController ], ]; $processorResult = $processor->create($data); + if (!empty(Configure::read('security.registration.floodProtection'))) { + $this->FloodProtection->set('register'); + } return $processor->genHTTPReply($this, $processorResult, ['controller' => 'Inbox', 'action' => 'index']); } $this->viewBuilder()->setLayout('login'); diff --git a/src/Http/Exception/TooManyRequestsException.php b/src/Http/Exception/TooManyRequestsException.php new file mode 100644 index 0000000..dae2c7f --- /dev/null +++ b/src/Http/Exception/TooManyRequestsException.php @@ -0,0 +1,43 @@ + [ + /* [ 'open_modal' => '/localTools/action/' . h($params['connection']['id']) . '/editUser?id={{0}}', 'modal_params_data_path' => ['User.id'], @@ -539,6 +540,7 @@ class MispConnector extends CommonConnectorTools 'icon' => 'trash', 'reload_url' => '/localTools/action/' . h($params['connection']['id']) . '/serversAction' ] + */ ], 'title' => false, 'description' => false, diff --git a/src/Model/Entity/EncryptionKey.php b/src/Model/Entity/EncryptionKey.php index 4fe40fa..c2f0f04 100644 --- a/src/Model/Entity/EncryptionKey.php +++ b/src/Model/Entity/EncryptionKey.php @@ -7,5 +7,4 @@ use Cake\ORM\Entity; class EncryptionKey extends AppModel { - } diff --git a/src/Model/Entity/Individual.php b/src/Model/Entity/Individual.php index 87398e2..cd1cc9d 100644 --- a/src/Model/Entity/Individual.php +++ b/src/Model/Entity/Individual.php @@ -11,10 +11,12 @@ class Individual extends AppModel '*' => true, 'id' => false, 'uuid' => false, + 'created' => false, ]; protected $_accessibleOnNew = [ 'uuid' => true, + 'created' => true, ]; protected $_virtual = ['full_name', 'alternate_emails']; diff --git a/src/Model/Entity/Organisation.php b/src/Model/Entity/Organisation.php index 6766963..a56d3c0 100644 --- a/src/Model/Entity/Organisation.php +++ b/src/Model/Entity/Organisation.php @@ -10,5 +10,10 @@ class Organisation extends AppModel protected $_accessible = [ '*' => true, 'id' => false, + 'created' => false + ]; + + protected $_accessibleOnNew = [ + 'created' => true ]; } diff --git a/src/Model/Entity/SharingGroup.php b/src/Model/Entity/SharingGroup.php index 34d4b01..e2d1366 100644 --- a/src/Model/Entity/SharingGroup.php +++ b/src/Model/Entity/SharingGroup.php @@ -13,11 +13,13 @@ class SharingGroup extends AppModel 'uuid' => false, 'organisation_id' => false, 'user_id' => false, + 'created' => false ]; protected $_accessibleOnNew = [ 'uuid' => true, 'organisation_id' => true, 'user_id' => true, + 'created' => true ]; } diff --git a/src/Model/Table/AppTable.php b/src/Model/Table/AppTable.php index f3443f6..55a4cda 100644 --- a/src/Model/Table/AppTable.php +++ b/src/Model/Table/AppTable.php @@ -185,4 +185,9 @@ class AppTable extends Table } } } + + public function isValidUrl($value, array $context): bool + { + return filter_var($value, FILTER_VALIDATE_URL); + } } diff --git a/src/Model/Table/BroodsTable.php b/src/Model/Table/BroodsTable.php index 9798260..0708c18 100644 --- a/src/Model/Table/BroodsTable.php +++ b/src/Model/Table/BroodsTable.php @@ -33,7 +33,11 @@ class BroodsTable extends AppTable ->requirePresence(['name', 'url', 'organisation_id'], 'create') ->notEmptyString('name') ->notEmptyString('url') - ->url('url', __('The provided value is not a valid URL')) + ->add('url', 'isValidUrl', [ + 'rule' => 'isValidUrl', + 'message' => __('The provided value is not a valid URL'), + 'provider' => 'table' + ]) ->naturalNumber('organisation_id', false); } diff --git a/src/Model/Table/FloodProtectionsTable.php b/src/Model/Table/FloodProtectionsTable.php new file mode 100644 index 0000000..045fc59 --- /dev/null +++ b/src/Model/Table/FloodProtectionsTable.php @@ -0,0 +1,21 @@ +setDisplayField('request_ip'); + } + + public function validationDefault(Validator $validator): Validator + { + return $validator; + } +} diff --git a/src/Model/Table/IndividualsTable.php b/src/Model/Table/IndividualsTable.php index 0fba61f..2e01282 100644 --- a/src/Model/Table/IndividualsTable.php +++ b/src/Model/Table/IndividualsTable.php @@ -70,6 +70,7 @@ class IndividualsTable extends AppTable $this->patchEntity($existingIndividual, $individual); $entityToSave = $existingIndividual; } + $entityToSave->setDirty('modified', false); $savedEntity = $this->save($entityToSave, ['associated' => false]); if (!$savedEntity) { return null; diff --git a/src/Model/Table/OrganisationsTable.php b/src/Model/Table/OrganisationsTable.php index b3fa792..a7f6d19 100644 --- a/src/Model/Table/OrganisationsTable.php +++ b/src/Model/Table/OrganisationsTable.php @@ -66,6 +66,7 @@ class OrganisationsTable extends AppTable $this->patchEntity($existingOrg, $org); $entityToSave = $existingOrg; } + $entityToSave->setDirty('modified', false); $savedEntity = $this->save($entityToSave, ['associated' => false]); if (!$savedEntity) { return null; diff --git a/src/Model/Table/SettingProviders/BaseSettingsProvider.php b/src/Model/Table/SettingProviders/BaseSettingsProvider.php index f9fae08..7bb6442 100644 --- a/src/Model/Table/SettingProviders/BaseSettingsProvider.php +++ b/src/Model/Table/SettingProviders/BaseSettingsProvider.php @@ -188,7 +188,7 @@ class BaseSettingsProvider * @param array $setting * @return mixed */ - public function evaluateFunctionForSetting($fun, $setting) + public function evaluateFunctionForSetting($fun, &$setting) { $functionResult = true; if (is_callable($fun)) { // Validate with anonymous function diff --git a/src/Model/Table/SettingProviders/CerebrateSettingsProvider.php b/src/Model/Table/SettingProviders/CerebrateSettingsProvider.php index fa80078..330e589 100644 --- a/src/Model/Table/SettingProviders/CerebrateSettingsProvider.php +++ b/src/Model/Table/SettingProviders/CerebrateSettingsProvider.php @@ -188,7 +188,11 @@ class CerebrateSettingsProvider extends BaseSettingsProvider 'severity' => 'info', 'default' => '', 'description' => __('The baseurl of the keycloak authentication endpoint, such as https://foo.bar/baz/auth.'), - 'dependsOn' => 'keycloak.enabled' + 'dependsOn' => 'keycloak.enabled', + 'beforeSave' => function (&$value, $setting, $validator) { + $value = rtrim($value, '/'); + return true; + } ], 'keycloak.authoritative' => [ 'name' => 'Authoritative', @@ -270,6 +274,21 @@ class CerebrateSettingsProvider extends BaseSettingsProvider ] ], 'Security' => [ + 'Logging' => [ + 'Logging' => [ + 'security.logging.ip_source' => [ + 'name' => __('Set IP source'), + 'type' => 'select', + 'description' => __('Select where the harvested IP should come from. This defaults to REMOTE_ADDR, but for instances behind a proxy HTTP_X_FORWARDED_FOR or HTTP_CLIENT_IP might make more sense.'), + 'default' => 'REMOTE_ADDR', + 'options' => [ + 'REMOTE_ADDR' => 'REMOTE_ADDR', + 'HTTP_X_FORWARDED_FOR' => 'HTTP_X_FORWARDED_FOR', + 'HTTP_CLIENT_IP' => __('HTTP_CLIENT_IP'), + ], + ], + ] + ], 'Registration' => [ 'Registration' => [ 'security.registration.self-registration' => [ @@ -278,6 +297,12 @@ class CerebrateSettingsProvider extends BaseSettingsProvider 'description' => __('Enable the self-registration feature where user can request account creation. Admin can view the request and accept it in the application inbox.'), 'default' => false, ], + 'security.registration.floodProtection' => [ + 'name' => __('Enable registration flood-protection'), + 'type' => 'boolean', + 'description' => __('Enabling this setting will only allow 5 registrations / IP address every 15 minutes (rolling time-frame).'), + 'default' => false, + ], ] ], 'Development' => [ diff --git a/src/Model/Table/SettingsTable.php b/src/Model/Table/SettingsTable.php index 7e9bcff..2eb941c 100644 --- a/src/Model/Table/SettingsTable.php +++ b/src/Model/Table/SettingsTable.php @@ -3,6 +3,7 @@ namespace App\Model\Table; use App\Model\Table\AppTable; use Cake\ORM\Table; +use Cake\Filesystem\File; use Cake\Core\Configure; use Cake\Error\Debugger; @@ -72,19 +73,29 @@ class SettingsTable extends AppTable } } } + $setting['value'] = $value ?? ''; + if (isset($setting['test'])) { + $validationResult = $this->SettingsProvider->evaluateFunctionForSetting($setting['test'], $setting); + if ($validationResult !== true) { + $errors[] = $validationResult; + $setting['errorMessage'] = $validationResult; + } + } if (empty($errors) && !empty($setting['beforeSave'])) { - $setting['value'] = $value ?? ''; $beforeSaveResult = $this->SettingsProvider->evaluateFunctionForSetting($setting['beforeSave'], $setting); if ($beforeSaveResult !== true) { $errors[] = $beforeSaveResult; + $setting['errorMessage'] = $validationResult; } } if (empty($errors)) { - $saveResult = $this->saveSettingOnDisk($name, $value); + $saveResult = $this->saveSettingOnDisk($name, $setting['value']); if ($saveResult) { if (!empty($setting['afterSave'])) { $this->SettingsProvider->evaluateFunctionForSetting($setting['afterSave'], $setting); } + } else { + $errors[] = __('Could not save settings on disk'); } } return $errors; @@ -129,8 +140,16 @@ class SettingsTable extends AppTable $settings = $this->readSettings(); $settings[$name] = $value; $settings = json_encode($settings, JSON_PRETTY_PRINT); - file_put_contents(CONFIG . 'config.json', $settings); - $this->loadSettings(); - return true; + $path = CONFIG . 'config.json'; + $file = new File($path); + if ($file->writable()) { + $success = file_put_contents($path, $settings); + if ($success) { + $this->loadSettings(); + } + } else { + $success = false; + } + return $success; } } diff --git a/src/Model/Table/SharingGroupsTable.php b/src/Model/Table/SharingGroupsTable.php index ff39220..e976e8c 100644 --- a/src/Model/Table/SharingGroupsTable.php +++ b/src/Model/Table/SharingGroupsTable.php @@ -66,6 +66,7 @@ class SharingGroupsTable extends AppTable $this->patchEntity($existingSG, $input); $entityToSave = $existingSG; } + $entityToSave->setDirty('modified', false); $savedEntity = $this->save($entityToSave, ['associated' => false]); if (!$savedEntity) { return null; diff --git a/src/Model/Table/UserSettingsTable.php b/src/Model/Table/UserSettingsTable.php index bdfe535..cc4b5db 100644 --- a/src/Model/Table/UserSettingsTable.php +++ b/src/Model/Table/UserSettingsTable.php @@ -135,4 +135,18 @@ class UserSettingsTable extends AppTable } return $result; } + + /** + * validURI - Ensure the provided URI can be safely put as a link + * + * @param String $uri + * @return bool if the URI is safe to be put as a link + */ + public function validURI(String $uri): bool + { + $parsed = parse_url($uri); + $isLocalPath = empty($parsed['scheme']) && empty($parsed['domain']) && !empty($parsed['path']); + $isValidURL = !empty($parsed['scheme']) && in_array($parsed['scheme'], ['http', 'https']) && filter_var($uri, FILTER_SANITIZE_URL); + return $isLocalPath || $isValidURL; + } } diff --git a/src/VERSION.json b/src/VERSION.json index 5ddd625..0bf0283 100644 --- a/src/VERSION.json +++ b/src/VERSION.json @@ -1,4 +1,4 @@ { - "version": "0.1", + "version": "1.4", "application": "Cerebrate" } diff --git a/templates/AuthKeys/index.php b/templates/AuthKeys/index.php index 070c3f3..1b5599a 100644 --- a/templates/AuthKeys/index.php +++ b/templates/AuthKeys/index.php @@ -10,7 +10,8 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'data' => [ 'type' => 'simple', 'text' => __('Add authentication key'), - 'popover_url' => '/authKeys/add' + 'popover_url' => '/authKeys/add', + 'reload_url' => $this->request->getRequestTarget() ] ] ], @@ -65,7 +66,8 @@ echo $this->element('genericElements/IndexTable/index_table', [ [ 'open_modal' => '/authKeys/delete/[onclick_params_data_path]', 'modal_params_data_path' => 'id', - 'icon' => 'trash' + 'icon' => 'trash', + 'reload_url' => $this->request->getRequestTarget() ] ] ] diff --git a/templates/Instance/home.php b/templates/Instance/home.php index a37c88c..f3c6604 100644 --- a/templates/Instance/home.php +++ b/templates/Instance/home.php @@ -1,5 +1,9 @@ user_settings_by_name['ui.bookmarks']['value']) ? json_decode($loggedUser->user_settings_by_name['ui.bookmarks']['value'], true) : []; +$this->userSettingsTable = TableRegistry::getTableLocator()->get('UserSettings'); ?>

@@ -9,18 +13,24 @@ $bookmarks = !empty($loggedUser->user_settings_by_name['ui.bookmarks']['value'])

- + - +

diff --git a/templates/SharingGroups/add.php b/templates/SharingGroups/add.php index 5b3e413..73eae9d 100644 --- a/templates/SharingGroups/add.php +++ b/templates/SharingGroups/add.php @@ -11,7 +11,8 @@ 'field' => 'organisation_id', 'type' => 'dropdown', 'label' => __('Owner organisation'), - 'options' => $dropdownData['organisation'] + 'options' => $dropdownData['organisation'], + 'default' => $loggedUser['organisation_id'] ], array( 'field' => 'releasability', diff --git a/templates/UserSettings/add.php b/templates/UserSettings/add.php index 38a14c8..a691afc 100644 --- a/templates/UserSettings/add.php +++ b/templates/UserSettings/add.php @@ -9,8 +9,7 @@ 'type' => 'dropdown', 'label' => __('User'), 'options' => $dropdownData['user'], - 'value' => !empty($user_id) ? $user_id : '', - 'disabled' => !empty($user_id), + 'value' => !is_null($user_id) ? $user_id : '', ], [ 'field' => 'name', diff --git a/templates/UserSettings/delete_bookmark.php b/templates/UserSettings/delete_my_bookmark.php similarity index 100% rename from templates/UserSettings/delete_bookmark.php rename to templates/UserSettings/delete_my_bookmark.php diff --git a/templates/UserSettings/save_bookmark.php b/templates/UserSettings/save_my_bookmark.php similarity index 100% rename from templates/UserSettings/save_bookmark.php rename to templates/UserSettings/save_my_bookmark.php diff --git a/templates/UserSettings/set_setting.php b/templates/UserSettings/set_my_setting.php similarity index 100% rename from templates/UserSettings/set_setting.php rename to templates/UserSettings/set_my_setting.php diff --git a/templates/Users/add.php b/templates/Users/add.php index a3c90a8..d3fcb7f 100644 --- a/templates/Users/add.php +++ b/templates/Users/add.php @@ -18,7 +18,8 @@ 'field' => 'organisation_id', 'type' => 'dropdown', 'label' => __('Associated organisation'), - 'options' => $dropdownData['organisation'] + 'options' => $dropdownData['organisation'], + 'default' => $loggedUser['organisation_id'] ], [ 'field' => 'password', @@ -39,7 +40,8 @@ 'field' => 'role_id', 'type' => 'dropdown', 'label' => __('Role'), - 'options' => $dropdownData['role'] + 'options' => $dropdownData['role'], + 'default' => $defaultRole ?? null ], [ 'field' => 'disabled', diff --git a/templates/Users/index.php b/templates/Users/index.php index 9caa7de..dd287c8 100644 --- a/templates/Users/index.php +++ b/templates/Users/index.php @@ -102,12 +102,48 @@ echo $this->element('genericElements/IndexTable/index_table', [ [ 'open_modal' => '/users/edit/[onclick_params_data_path]', 'modal_params_data_path' => 'id', - 'icon' => 'edit' + 'icon' => 'edit', + 'complex_requirement' => [ + 'options' => [ + 'datapath' => [ + 'role_id' => 'role_id' + ] + ], + 'function' => function ($row, $options) use ($loggedUser, $validRoles) { + if (empty($loggedUser['role']['perm_admin'])) { + if (empty($loggedUser['role']['perm_org_admin'])) { + return false; + } + if (!isset($validRoles[$options['datapath']['role_id']])) { + return false; + } + } + return true; + } + ] ], [ 'open_modal' => '/users/delete/[onclick_params_data_path]', 'modal_params_data_path' => 'id', - 'icon' => 'trash' + 'icon' => 'trash', + 'complex_requirement' => [ + 'options' => [ + 'datapath' => [ + 'role_id' => 'role_id' + ] + ], + 'function' => function ($row, $options) use ($loggedUser, $validRoles) { + if (empty($loggedUser['role']['perm_admin'])) { + if (empty($loggedUser['role']['perm_org_admin'])) { + return false; + } + if (!isset($validRoles[$options['datapath']['role_id']])) { + return false; + } + } + return true; + } + ] ], ] ] diff --git a/templates/Users/login.php b/templates/Users/login.php index 1f62282..8f2e3b2 100644 --- a/templates/Users/login.php +++ b/templates/Users/login.php @@ -30,7 +30,7 @@ use Cake\Core\Configure; echo ''; } - if (!empty(Configure::read('keycloak'))) { + if (!empty(Configure::read('keycloak.enabled'))) { echo sprintf('

%s
', __('Or')); echo $this->Form->create(null, [ 'url' => Cake\Routing\Router::url([ diff --git a/templates/Users/settings.php b/templates/Users/settings.php index da014ca..cd3a920 100644 --- a/templates/Users/settings.php +++ b/templates/Users/settings.php @@ -10,10 +10,12 @@ foreach ($settingsProvider as $settingTitle => $settingContent) { ]); } -$navLinks[] = __('Bookmarks'); -$tabContents[] = $this->element('UserSettings/saved-bookmarks', [ - 'bookmarks' => !empty($user->user_settings_by_name['ui.bookmarks']['value']) ? json_decode($user->user_settings_by_name['ui.bookmarks']['value'], true) : [] -]); +if (empty($editingAnotherUser)) { + $navLinks[] = __('Bookmarks'); + $tabContents[] = $this->element('UserSettings/saved-bookmarks', [ + 'bookmarks' => !empty($user->user_settings_by_name['ui.bookmarks']['value']) ? json_decode($user->user_settings_by_name['ui.bookmarks']['value'], true) : [] + ]); +} $tabsOptions = [ 'vertical' => true, @@ -29,11 +31,15 @@ $tabsOptions = [ ]; $tabs = $this->Bootstrap->tabs($tabsOptions); echo $this->Html->script('settings'); +$saveUrl = '/userSettings/saveSetting'; +if(!empty($editingAnotherUser)) { + $saveUrl .= '/' . h($user->id); +} ?>

@@ -43,7 +49,17 @@ echo $this->Html->script('settings'); username) ?> individual->full_name) ?> -
+ + Bootstrap->alert([ + 'text' => __('Currently editing the account settings of another user.'), + 'variant' => 'warning', + 'dismissible' => false + ]) + ?> + +
+
diff --git a/templates/element/Settings/category.php b/templates/element/Settings/category.php index 03e17e4..317dac0 100644 --- a/templates/element/Settings/category.php +++ b/templates/element/Settings/category.php @@ -57,6 +57,6 @@ $mainPanelHeight = 'calc(100vh - 42px - 1rem - 56px - 38px - 1rem)';
- + %s

', __('No settings available for this category')) ?>
\ No newline at end of file diff --git a/templates/element/Settings/field.php b/templates/element/Settings/field.php index 34bdde8..d52bc14 100644 --- a/templates/element/Settings/field.php +++ b/templates/element/Settings/field.php @@ -13,11 +13,11 @@ (!empty($setting['error']) ? $appView->get('variantFromSeverity')[$setting['severity']] : ''), ], ($setting['type'] == 'textarea' ? '' : 'type') => ($setting['type'] == 'textarea' ? '' : 'text'), - 'id' => $settingId, - 'data-setting-name' => $settingName, - 'value' => isset($setting['value']) ? $setting['value'] : "", - 'placeholder' => $setting['default'] ?? '', - 'aria-describedby' => "{$settingId}Help" + 'id' => h($settingId), + 'data-setting-name' => h($settingName), + 'value' => isset($setting['value']) ? h($setting['value']) : "", + 'placeholder' => empty($setting['default']) ? '' : h($setting['default']), + 'aria-describedby' => h("{$settingId}Help") ] ); })($settingName, $setting, $this); @@ -28,13 +28,13 @@ return $this->Bootstrap->switch([ 'label' => h($setting['description']), 'checked' => !empty($setting['value']), - 'id' => $settingId, + 'id' => h($settingId), 'class' => [ (!empty($setting['error']) ? 'is-invalid' : ''), (!empty($setting['error']) ? $appView->get('variantFromSeverity')[$setting['severity']] : ''), ], 'attrs' => [ - 'data-setting-name' => $settingName + 'data-setting-name' => h($settingName) ] ]); })($settingName, $setting, $this); @@ -53,16 +53,16 @@ 'type' => 'number', 'min' => '0', 'step' => 1, - 'id' => $settingId, - 'data-setting-name' => $settingName, - 'aria-describedby' => "{$settingId}Help" + 'id' => h($settingId), + 'data-setting-name' => h($settingName), + 'aria-describedby' => h("{$settingId}Help") ]); })($settingName, $setting, $this); } elseif ($setting['type'] == 'select' || $setting['type'] == 'multi-select') { $input = (function ($settingName, $setting, $appView) { $settingId = str_replace('.', '_', $settingName); - $setting['value'] = $setting['value'] ?? ''; + $setting['value'] = empty($setting['value']) ? '' : h($setting['value']); if ($setting['type'] == 'multi-select') { if (!is_array($setting['value'])) { $firstChar = substr($setting['value'], 0, 1); @@ -77,7 +77,7 @@ foreach ($setting['options'] as $key => $value) { $optionParam = [ 'class' => [], - 'value' => $key, + 'value' => h($key), ]; if ($setting['type'] == 'multi-select') { if (in_array($key, $setting['value'])) { @@ -100,10 +100,10 @@ (!empty($setting['error']) ? $appView->get('variantFromSeverity')[$setting['severity']] : ''), ], ($setting['type'] == 'multi-select' ? 'multiple' : '') => ($setting['type'] == 'multi-select' ? 'multiple' : ''), - 'id' => $settingId, - 'data-setting-name' => $settingName, - 'placeholder' => $setting['default'] ?? '', - 'aria-describedby' => "{$settingId}Help" + 'id' => h($settingId), + 'data-setting-name' => h($settingName), + 'placeholder' => empty($setting['default']) ? '' : h($setting['default']), + 'aria-describedby' => h("{$settingId}Help") ], $options); })($settingName, $setting, $this); } diff --git a/templates/element/genericElements/Form/Fields/dropdownField.php b/templates/element/genericElements/Form/Fields/dropdownField.php index a00f0e9..26e8e88 100644 --- a/templates/element/genericElements/Form/Fields/dropdownField.php +++ b/templates/element/genericElements/Form/Fields/dropdownField.php @@ -5,7 +5,8 @@ 'value' => $fieldData['value'] ?? null, 'multiple' => $fieldData['multiple'] ?? false, 'disabled' => $fieldData['disabled'] ?? false, - 'class' => ($fieldData['class'] ?? '') . ' formDropdown form-select' + 'class' => ($fieldData['class'] ?? '') . ' formDropdown form-select', + 'default' => ($fieldData['default'] ?? null) ]; if (!empty($fieldData['label'])) { $controlParams['label'] = $fieldData['label']; diff --git a/templates/element/genericElements/Form/formLayouts/formDefault.php b/templates/element/genericElements/Form/formLayouts/formDefault.php index 7953082..56a439a 100644 --- a/templates/element/genericElements/Form/formLayouts/formDefault.php +++ b/templates/element/genericElements/Form/formLayouts/formDefault.php @@ -6,7 +6,7 @@
- +
diff --git a/templates/element/genericElements/Form/formLayouts/formRaw.php b/templates/element/genericElements/Form/formLayouts/formRaw.php index 371cb0a..1e445f7 100644 --- a/templates/element/genericElements/Form/formLayouts/formRaw.php +++ b/templates/element/genericElements/Form/formLayouts/formRaw.php @@ -1,6 +1,6 @@
- +
diff --git a/templates/element/genericElements/IndexTable/Fields/org.php b/templates/element/genericElements/IndexTable/Fields/org.php index 3f255e8..89f50dc 100644 --- a/templates/element/genericElements/IndexTable/Fields/org.php +++ b/templates/element/genericElements/IndexTable/Fields/org.php @@ -18,13 +18,13 @@ if ($field['fields']['allow_picture'] && !empty($org['id'])) { echo sprintf( '%s', - $baseurl . 'organisations/view/' . h($org['id']), + $baseurl . '/organisations/view/' . h($org['id']), h($org['name']) ); //echo $this->OrgImg->getOrgImg(array('name' => $org['name'], 'id' => $org['id'], 'size' => 24)); } else { echo sprintf( - '%s', + '%s', $baseurl, empty($org['id']) ? h($org['uuid']) : h($org['id']), h($org['name']) diff --git a/templates/element/genericElements/ListTopBar/element_simple.php b/templates/element/genericElements/ListTopBar/element_simple.php index a14976f..5573254 100644 --- a/templates/element/genericElements/ListTopBar/element_simple.php +++ b/templates/element/genericElements/ListTopBar/element_simple.php @@ -1,8 +1,10 @@