Merge branch 'develop' of github.com:cerebrate-project/cerebrate into develop

cli-modification-summary
Sami Mokaddem 2022-03-08 16:04:14 +01:00
commit f6900b0843
No known key found for this signature in database
GPG Key ID: 164C473F627A06FA
16 changed files with 224 additions and 40 deletions

View File

@ -84,7 +84,7 @@ class AuthKeysController extends AppController
'displayOnSuccess' => 'authkey_display',
'beforeSave' => function($data) use ($users) {
if (!in_array($data['user_id'], array_keys($users))) {
return false;
throw new MethodNotAllowedException(__('You are not authorised to do that.'));
}
return $data;
}

View File

@ -169,6 +169,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));
}
}
$data = $this->Table->patchEntity($data, $input, $patchEntityParams);
if (isset($params['beforeSave'])) {
$data = $params['beforeSave']($data);
@ -310,9 +316,18 @@ 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));
}
}
$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) {

View File

@ -17,7 +17,14 @@ class FloodProtectionComponent extends Component
public function initialize(array $config): void
{
$ip_source = Configure::check('security.logging.ip_source') ? Configure::read('security.logging.ip_source') : 'REMOTE_ADDR';
$this->remote_ip = $_SERVER[$ip_source];
if (!isset($_SERVER[$ip_source])) {
$ip_source = 'REMOTE_ADDR';
}
if (isset($_SERVER[$ip_source])) {
$this->remote_ip = $_SERVER[$ip_source];
} else {
$this->remote_ip = '127.0.0.1';
}
$temp = explode(PHP_EOL, $_SERVER[$ip_source]);
if (count($temp) > 1) {
$this->remote_ip = $temp[0];

View File

@ -37,10 +37,17 @@ class SharingGroupsController extends AppController
public function add()
{
$currentUser = $this->ACL->getUser();
$this->CRUD->add([
'override' => [
'user_id' => $this->ACL->getUser()['id']
]
],
'beforeSave' => function($data) use ($currentUser) {
if (!$currentUser['role']['perm_admin']) {
$data['organisation_id'] = $currentUser['organisation_id'];
}
return $data;
}
]);
$dropdownData = [
'organisation' => $this->getAvailableOrgForSg($this->ACL->getUser())

View File

@ -9,6 +9,7 @@ use \Cake\Database\Expression\QueryExpression;
use Cake\Http\Exception\UnauthorizedException;
use Cake\Http\Exception\MethodNotAllowedException;
use Cake\Core\Configure;
use Cake\Utility\Security;
class UsersController extends AppController
{
@ -44,15 +45,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'];
}
@ -62,6 +78,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;
}
@ -84,9 +115,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
@ -118,7 +147,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();
}
@ -136,7 +165,7 @@ class UsersController extends AppController
$params = [
'get' => [
'fields' => [
'id', 'individual_id', 'role_id', 'username', 'disabled'
'id', 'individual_id', 'role_id', 'disabled', 'username'
]
],
'removeEmpty' => [
@ -148,12 +177,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']) {
@ -166,6 +193,12 @@ class UsersController extends AppController
}
return $data;
};
$params['beforeSave'] = function ($data) use ($currentUser, $validRoles) {
if (!in_array($data['role_id'], array_keys($validRoles))) {
throw new MethodNotAllowedException(__('You cannot assign the chosen role to a user.'));
}
return $data;
};
}
}
$this->CRUD->edit($id, $params);
@ -311,7 +344,7 @@ 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'))) {
if (!Configure::check('security.registration.floodProtection') || Configure::read('security.registration.floodProtection')) {
$this->FloodProtection->check('register');
}
if ($this->request->is('post')) {

View File

@ -132,9 +132,9 @@ class MispConnector extends CommonConnectorTools
{
return $validator
->requirePresence('url')
->notEmpty('url', __('An URL must be provided'))
->notEmptyString('url', __('An URL must be provided'))
->requirePresence('authkey')
->notEmpty('authkey', __('An Authkey must be provided'))
->notEmptyString('authkey', __('An Authkey must be provided'))
->lengthBetween('authkey', [40, 40], __('The authkey must be 40 character long'))
->boolean('skip_ssl');
}

View File

@ -137,6 +137,30 @@ class AuthKeycloakBehavior extends Behavior
]
]
);
$logChange = [
'username' => $data['username'],
'individual_id' => $data['individual_id'],
'role_id' => $data['role_id']
];
if (!$response->isOk()) {
$logChange['error_code'] = $response->getStatusCode();
$logChange['error_body'] = $response->getStringBody();
$this->_table->auditLogs()->insert([
'request_action' => 'enrollUser',
'model' => 'User',
'model_id' => 0,
'model_title' => __('Failed Keycloak enrollment for user {0}', $data['username']),
'changed' => $logChange
]);
} else {
$this->_table->auditLogs()->insert([
'request_action' => 'enrollUser',
'model' => 'User',
'model_id' => 0,
'model_title' => __('Successful Keycloak enrollment for user {0}', $data['username']),
'changed' => $logChange
]);
}
return true;
}

View File

@ -42,6 +42,7 @@ class AlignmentsTable extends AppTable
'type' => $type
];
$this->patchEntity($alignment, $data);
$this->save($alignment);
}
}
}

View File

@ -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
{
@ -55,7 +57,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();
@ -90,4 +94,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']);
}
}

View File

@ -8,6 +8,7 @@ require_once(APP . 'Model' . DS . 'Table' . DS . 'SettingProviders' . DS . 'Base
use App\Settings\SettingsProvider\BaseSettingsProvider;
use App\Settings\SettingsProvider\SettingValidator;
use Cake\Core\Configure;
class CerebrateSettingsProvider extends BaseSettingsProvider
{
@ -150,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',
@ -157,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',
@ -300,8 +314,10 @@ class CerebrateSettingsProvider extends BaseSettingsProvider
'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,
'description' => (Configure::check('security.logging.ip_source') && Configure::read('security.logging.ip_source') !== 'REMOTE_ADDR') ?
__('Enabling this setting will only allow 5 registrations / IP address every 15 minutes (rolling time-frame). WARNING: Be aware that you are not using REMOTE_ADDR (as configured via security.logging.ip_source) - this could lead to an attacker being able to spoof their IP and circumvent the flood protection. Only rely on the client IP if your reverse proxy in front of Cerebrate is properly setting this header.'):
__('Enabling this setting will only allow 5 registrations / IP address every 15 minutes (rolling time-frame).'),
'default' => true,
],
]
],
@ -371,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 . '.enabled')) {
$foundEnabledAuth = true;
}
}
}
return $foundEnabledAuth;
}
return true;
}
}

View File

@ -1,4 +1,12 @@
<?php
use Cake\Core\Configure;
$passwordRequired = false;
if ($this->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',

View File

@ -12,26 +12,29 @@ use Cake\Core\Configure;
'style' => ['filter: drop-shadow(4px 4px 4px #924da666);']
])
);
echo sprintf('<h4 class="text-uppercase fw-light mb-3">%s</h4>', __('Sign In'));
$template = [
'inputContainer' => '<div class="form-floating input {{type}}{{required}}">{{content}}</div>',
'formGroup' => '{{input}}{{label}}',
'submitContainer' => '<div class="submit d-grid">{{content}}</div>',
];
$this->Form->setTemplates($template);
echo $this->Form->create(null, ['url' => ['controller' => 'users', 'action' => 'login']]);
echo $this->Form->control('username', ['label' => 'Username', 'class' => 'form-control mb-2', 'placeholder' => __('Username')]);
echo $this->Form->control('password', ['type' => 'password', 'label' => 'Password', 'class' => 'form-control mb-3', 'placeholder' => __('Password')]);
echo $this->Form->control(__('Login'), ['type' => 'submit', 'class' => 'btn btn-primary']);
echo $this->Form->end();
if (!empty(Configure::read('security.registration.self-registration'))) {
echo '<div class="text-end">';
echo sprintf('<span class="text-secondary ms-auto" style="font-size: 0.8rem">%s <a href="/users/register" class="text-decoration-none link-primary fw-bold">%s</a></span>', __('Doesn\'t have an account?'), __('Sign up'));
echo '</div>';
if (!Configure::check('password_auth.enabled') || Configure::read('password_auth.enabled')) {
echo sprintf('<h4 class="text-uppercase fw-light mb-3">%s</h4>', __('Sign In'));
echo $this->Form->create(null, ['url' => ['controller' => 'users', 'action' => 'login']]);
echo $this->Form->control('username', ['label' => 'Username', 'class' => 'form-control mb-2', 'placeholder' => __('Username')]);
echo $this->Form->control('password', ['type' => 'password', 'label' => 'Password', 'class' => 'form-control mb-3', 'placeholder' => __('Password')]);
echo $this->Form->control(__('Login'), ['type' => 'submit', 'class' => 'btn btn-primary']);
echo $this->Form->end();
if (!empty(Configure::read('security.registration.self-registration'))) {
echo '<div class="text-end">';
echo sprintf('<span class="text-secondary ms-auto" style="font-size: 0.8rem">%s <a href="/users/register" class="text-decoration-none link-primary fw-bold">%s</a></span>', __('Don\'t have an account?'), __('Sign up'));
echo '</div>';
}
if (!empty(Configure::read('keycloak.enabled'))) {
echo sprintf('<div class="d-flex align-items-center my-2"><hr class="d-inline-block flex-grow-1"/><span class="mx-3 fw-light">%s</span><hr class="d-inline-block flex-grow-1"/></div>', __('Or'));
}
}
if (!empty(Configure::read('keycloak.enabled'))) {
echo sprintf('<div class="d-flex align-items-center my-2"><hr class="d-inline-block flex-grow-1"/><span class="mx-3 fw-light">%s</span><hr class="d-inline-block flex-grow-1"/></div>', __('Or'));
echo $this->Form->create(null, [
'url' => Cake\Routing\Router::url([
'prefix' => false,

View File

@ -17,8 +17,11 @@ echo $this->element(
'path' => 'username'
],
[
'type' => 'generic',
'key' => __('Email'),
'path' => 'individual.email'
'path' => 'individual.email',
'url' => '/individuals/view/{{0}}',
'url_vars' => 'individual_id'
],
[
'type' => 'generic',

View File

@ -98,7 +98,7 @@
);
}
$reload_url = !empty($action['reload_url']) ? $action['reload_url'] : $this->Url->build(['action' => 'index']);
$action['onclick'] = sprintf('UI.submissionModalForIndex(\'%s\', \'%s\', \'%s\')', $modal_url, $reload_url, $tableRandomValue);
$action['onclick'] = sprintf('UI.submissionModalForIndex(\'%s\', \'%s\', \'%s\')', h($modal_url), h($reload_url), h($tableRandomValue));
}
echo sprintf(
'<a href="%s" title="%s" aria-label="%s" %s %s class="btn btn-sm btn-outline-dark table-link-action"><i class="%s"></i></a> ',

View File

@ -18,7 +18,11 @@ $form = $this->element('genericElements/Form/genericForm', [
]);
$formHTML = sprintf('<div class="d-none">%s</div>', $form);
$bodyMessage = !empty($deletionText) ? __($deletionText) : __('Are you sure you want to delete {0} #{1}?', h(Cake\Utility\Inflector::singularize($this->request->getParam('controller'))), h($id));
if (!empty($id)) {
$bodyMessage = !empty($deletionText) ? __($deletionText) : __('Are you sure you want to delete {0} #{1}?', h(Cake\Utility\Inflector::singularize($this->request->getParam('controller'))), h($id));
} else {
$bodyMessage = !empty($deletionText) ? __($deletionText) : __('Are you sure you want to delete the given {0}?', h(Cake\Utility\Inflector::singularize($this->request->getParam('controller'))));
}
$bodyHTML = sprintf('%s%s', $formHTML, $bodyMessage);
echo $this->Bootstrap->modal([

View File

@ -65,8 +65,7 @@ class AddAuthKeyApiTest extends TestCase
]
);
$this->assertResponseCode(404);
$this->addWarning('Should return 405 Method Not Allowed instead of 404 Not Found');
$this->assertResponseCode(405);
$this->assertDbRecordNotExists('AuthKeys', ['uuid' => $uuid]);
}
}