diff --git a/src/Controller/Component/CRUDComponent.php b/src/Controller/Component/CRUDComponent.php index 4b364a9..99fa39d 100644 --- a/src/Controller/Component/CRUDComponent.php +++ b/src/Controller/Component/CRUDComponent.php @@ -249,6 +249,12 @@ class CRUDComponent extends Component if (!empty($params['fields'])) { $patchEntityParams['fields'] = $params['fields']; } + if (isset($params['beforeMarshal'])) { + $input = $params['beforeMarshal']($input); + if ($input === false) { + throw new NotFoundException(__('Could not save {0} due to the marshaling failing. Your input is bad and you should feel bad.', $this->ObjectAlias)); + } + } if ($this->metaFieldsSupported()) { $massagedData = $this->massageMetaFields($data, $input, $metaTemplates); unset($input['MetaTemplates']); // Avoid MetaTemplates to be overriden when patching entity @@ -500,6 +506,12 @@ class CRUDComponent extends Component if (!empty($params['fields'])) { $patchEntityParams['fields'] = $params['fields']; } + if (isset($params['beforeMarshal'])) { + $input = $params['beforeMarshal']($input); + if ($input === false) { + throw new NotFoundException(__('Could not save {0} due to the marshaling failing. Your input is bad and you should feel bad.', $this->ObjectAlias)); + } + } if ($this->metaFieldsSupported()) { $massagedData = $this->massageMetaFields($data, $input, $metaTemplates); unset($input['MetaTemplates']); // Avoid MetaTemplates to be overriden when patching entity @@ -509,6 +521,9 @@ class CRUDComponent extends Component $data = $this->Table->patchEntity($data, $input, $patchEntityParams); 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)); + } } $savedData = $this->Table->save($data); if ($savedData !== false) { diff --git a/src/Controller/UsersController.php b/src/Controller/UsersController.php index 8c98b4f..763c044 100644 --- a/src/Controller/UsersController.php +++ b/src/Controller/UsersController.php @@ -6,6 +6,7 @@ use Cake\ORM\TableRegistry; use Cake\Http\Exception\UnauthorizedException; use Cake\Http\Exception\MethodNotAllowedException; use Cake\Core\Configure; +use Cake\Utility\Security; class UsersController extends AppController { @@ -41,15 +42,30 @@ class UsersController extends AppController { $currentUser = $this->ACL->getUser(); $validRoles = []; + $individuals_params = [ + 'sort' => ['email' => 'asc'] + ]; + $individual_ids = []; if (!$currentUser['role']['perm_admin']) { - $validRoles = $this->Users->Roles->find('list')->select(['id', 'name'])->order(['name' => 'asc'])->where(['perm_admin' => 0])->all()->toArray(); + $validRoles = $this->Users->Roles->find('list')->select(['id', 'name'])->order(['name' => 'asc'])->where(['perm_admin' => 0, 'perm_org_admin' => 0])->all()->toArray(); + $individual_ids = $this->Users->Individuals->find('aligned', ['organisation_id' => $currentUser['organisation_id']])->all()->extract('id')->toArray(); + if (empty($individual_ids)) { + $individual_ids = [-1]; + } + $individuals_params['conditions'] = ['id IN' => $individual_ids]; } else { $validRoles = $this->Users->Roles->find('list')->order(['name' => 'asc'])->all()->toArray(); } $defaultRole = $this->Users->Roles->find()->select(['id'])->first()->toArray(); - + $individuals = $this->Users->Individuals->find('list', $individuals_params)->toArray(); $this->CRUD->add([ - 'beforeSave' => function($data) use ($currentUser, $validRoles, $defaultRole) { + 'beforeMarshal' => function($data) { + if (empty($data['password'])) { + $data['password'] = Security::randomString(20); + } + return $data; + }, + 'beforeSave' => function($data) use ($currentUser, $validRoles, $defaultRole, $individual_ids) { if (!isset($data['role_id']) && !empty($defaultRole)) { $data['role_id'] = $defaultRole['id']; } @@ -59,6 +75,21 @@ class UsersController extends AppController throw new MethodNotAllowedException(__('You do not have permission to assign that role.')); } } + if ((!isset($data['individual_id']) || $data['individual_id'] === 'new') && !empty($data['individual'])) { + $existingOrg = $this->Users->Organisations->find('all')->where(['id' => $data['organisation_id']])->select(['uuid'])->first(); + if (empty($existingOrg)) { + throw new MethodNotAllowedException(__('No valid organisation found. Either encode the organisation separately or select a valid one.')); + } + $data['individual']['alignments'][] = ['type' => 'Member', 'organisation' => ['uuid' => $existingOrg['uuid']]]; + $data['individual_id'] = $this->Users->Individuals->captureIndividual($data['individual']); + } else if (!$currentUser['role']['perm_admin'] && isset($data['individual_id'])) { + if (!in_array($data['individual_id'], $individual_ids)) { + throw new MethodNotAllowedException(__('The selected individual is not aligned with your organisation. Creating a user for them is not permitted.')); + } + } + if (empty($data['individual_id'])) { + throw new MethodNotAllowedException(__('No valid individual found. Either supply it in the request or set the individual_id to a valid value.')); + } $this->Users->enrollUserRouter($data); return $data; } @@ -81,9 +112,7 @@ class UsersController extends AppController } $dropdownData = [ 'role' => $validRoles, - 'individual' => $this->Users->Individuals->find('list', [ - 'sort' => ['email' => 'asc'] - ]), + 'individual' => $individuals, 'organisation' => $this->Users->Organisations->find('list', [ 'sort' => ['name' => 'asc'], 'conditions' => $org_conditions @@ -115,7 +144,7 @@ class UsersController extends AppController $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(); + $validRoles = $this->Users->Roles->find('list')->select(['id', 'name'])->order(['name' => 'asc'])->where(['perm_admin' => 0, 'perm_org_admin' => 0])->all()->toArray(); } else { $validRoles = $this->Users->Roles->find('list')->order(['name' => 'asc'])->all()->toArray(); } @@ -133,7 +162,7 @@ class UsersController extends AppController $params = [ 'get' => [ 'fields' => [ - 'id', 'individual_id', 'role_id', 'username', 'disabled' + 'id', 'individual_id', 'role_id', 'disabled', 'username' ] ], 'removeEmpty' => [ @@ -145,12 +174,10 @@ class UsersController extends AppController ]; if (!empty($this->ACL->getUser()['role']['perm_admin'])) { $params['fields'][] = 'individual_id'; - $params['fields'][] = 'username'; $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']) { diff --git a/src/Model/Table/AlignmentsTable.php b/src/Model/Table/AlignmentsTable.php index c05ad90..3cbf5c5 100644 --- a/src/Model/Table/AlignmentsTable.php +++ b/src/Model/Table/AlignmentsTable.php @@ -42,6 +42,7 @@ class AlignmentsTable extends AppTable 'type' => $type ]; $this->patchEntity($alignment, $data); + $this->save($alignment); } } } diff --git a/src/Model/Table/IndividualsTable.php b/src/Model/Table/IndividualsTable.php index 2e01282..6ee0fd7 100644 --- a/src/Model/Table/IndividualsTable.php +++ b/src/Model/Table/IndividualsTable.php @@ -5,6 +5,8 @@ namespace App\Model\Table; use App\Model\Table\AppTable; use Cake\ORM\Table; use Cake\Validation\Validator; +use Cake\ORM\Query; + class IndividualsTable extends AppTable { @@ -59,7 +61,9 @@ class IndividualsTable extends AppTable 'uuid' => $individual['uuid'] ])->first(); } else { - return null; + $existingIndividual = $this->find()->where([ + 'email' => $individual['email'] + ])->first(); } if (empty($existingIndividual)) { $entityToSave = $this->newEmptyEntity(); @@ -94,4 +98,16 @@ class IndividualsTable extends AppTable } } } + + public function findAligned(Query $query, array $options) + { + $query = $query->select(['Individuals.id']); + if (empty($options['organisation_id'])) { + $query->leftJoinWith('Alignments')->where(['Alignments.organisation_id IS' => null]); + } else { + $query->innerJoinWith('Alignments') + ->where(['Alignments.organisation_id IN' => $options['organisation_id']]); + } + return $query->group(['Individuals.id', 'Individuals.uuid']); + } } diff --git a/src/Model/Table/SettingProviders/CerebrateSettingsProvider.php b/src/Model/Table/SettingProviders/CerebrateSettingsProvider.php index 908d9aa..5ed9f3b 100644 --- a/src/Model/Table/SettingProviders/CerebrateSettingsProvider.php +++ b/src/Model/Table/SettingProviders/CerebrateSettingsProvider.php @@ -151,6 +151,17 @@ class CerebrateSettingsProvider extends BaseSettingsProvider ], 'Authentication' => [ 'Providers' => [ + 'PasswordAuth' => [ + 'password_auth.enabled' => [ + 'name' => 'Disable password authentication', + 'type' => 'boolean', + 'severity' => 'warning', + 'description' => __('Enable username/password authentication.'), + 'default' => true, + 'test' => 'testEnabledAuth', + 'authentication_type' => 'password_auth' + ], + ], 'KeyCloak' => [ 'keycloak.enabled' => [ 'name' => 'Enabled', @@ -158,6 +169,8 @@ class CerebrateSettingsProvider extends BaseSettingsProvider 'severity' => 'warning', 'description' => __('Enable keycloak authentication'), 'default' => false, + 'test' => 'testEnabledAuth', + 'authentication_type' => 'keycloak' ], 'keycloak.provider.applicationId' => [ 'name' => 'Client ID', @@ -374,4 +387,24 @@ class CerebrateSettingValidator extends SettingValidator } return true; } + + public function testEnabledAuth($value, &$setting) + { + $providers = [ + 'password_auth', + 'keycloak' + ]; + if (!$value) { + $foundEnabledAuth = __('Cannot make change - this would disable every possible authentication method.'); + foreach ($providers as $provider) { + if ($provider !== $setting['authentication_type']) { + if (Configure::read($provider . '.enable')) { + $foundEnabledAuth = true; + } + } + } + return $foundEnabledAuth; + } + return true; + } } diff --git a/templates/Users/add.php b/templates/Users/add.php index d3fcb7f..1f68ae5 100644 --- a/templates/Users/add.php +++ b/templates/Users/add.php @@ -1,4 +1,12 @@ request->getParam('action') === 'add') { + $dropdownData['individual'] = ['new' => __('New individual')] + $dropdownData['individual']; + if (!Configure::check('password_auth.enabled') || Configure::read('password_auth.enabled')) { + $passwordRequired = 'required'; + } + } echo $this->element('genericElements/Form/genericForm', [ 'data' => [ 'description' => __('Roles define global rules for a set of users, including first and foremost access controls to certain functionalities.'), @@ -10,6 +18,32 @@ 'label' => __('Associated individual'), 'options' => $dropdownData['individual'] ], + [ + 'field' => 'individual.email', + 'stateDependence' => [ + 'source' => '#individual_id-field', + 'option' => 'new' + ], + 'required' => false + ], + [ + 'field' => 'individual.first_name', + 'label' => 'First name', + 'stateDependence' => [ + 'source' => '#individual_id-field', + 'option' => 'new' + ], + 'required' => false + ], + [ + 'field' => 'individual.last_name', + 'label' => 'Last name', + 'stateDependence' => [ + 'source' => '#individual_id-field', + 'option' => 'new' + ], + 'required' => false + ], [ 'field' => 'username', 'autocomplete' => 'off' @@ -25,16 +59,18 @@ 'field' => 'password', 'label' => __('Password'), 'type' => 'password', - 'required' => $this->request->getParam('action') === 'add' ? 'required' : false, + 'required' => $passwordRequired, 'autocomplete' => 'new-password', - 'value' => '' + 'value' => '', + 'requirements' => (bool)$passwordRequired ], [ 'field' => 'confirm_password', 'label' => __('Confirm Password'), 'type' => 'password', - 'required' => $this->request->getParam('action') === 'add' ? 'required' : false, - 'autocomplete' => 'off' + 'required' => $passwordRequired, + 'autocomplete' => 'off', + 'requirements' => (bool)$passwordRequired ], [ 'field' => 'role_id', diff --git a/templates/Users/login.php b/templates/Users/login.php index 8f2e3b2..7317a43 100644 --- a/templates/Users/login.php +++ b/templates/Users/login.php @@ -12,26 +12,29 @@ use Cake\Core\Configure; 'style' => ['filter: drop-shadow(4px 4px 4px #924da666);'] ]) ); - echo sprintf('