diff --git a/src/Command/SummaryCommand.php b/src/Command/SummaryCommand.php new file mode 100644 index 0000000..721d885 --- /dev/null +++ b/src/Command/SummaryCommand.php @@ -0,0 +1,231 @@ +setDescription('Create a summary for data associated to the passed nationality that has been modified.'); + $parser->addArgument('nationality', [ + 'short' => 'n', + 'help' => 'The organisation nationality.', + 'required' => false + ]); + $parser->addOption('days', [ + 'short' => 'd', + 'help' => 'The amount of days to look back in the logs', + 'default' => 7 + ]); + return $parser; + } + + public function execute(Arguments $args, ConsoleIo $io) + { + $this->__loadTables(); + $this->io = $io; + $nationality = $args->getArgument('nationality'); + $days = $args->getOption('days'); + if (!is_null($nationality)) { + $nationalities = [$nationality]; + } else { + $nationalities = $this->_fetchOrgNationalities(); + } + foreach ($nationalities as $nationality) { + $this->io->out(sprintf('Nationality: %s', $nationality)); + $this->_collectChangedForNationality($nationality, $days); + $this->io->out($io->nl(2)); + $this->io->hr(); + } + } + + protected function _collectChangedForNationality($nationality, $days) + { + $filename = sprintf('/tmp/%s.txt', $nationality); + $file_input = fopen($filename, 'w'); + $organisationIDsForNationality = $this->_fetchOrganisationsForNationality($nationality); + if (empty($organisationIDsForNationality)) { + $message = sprintf('No changes for organisations with nationality `%s`', $nationality); + fwrite($file_input, $message); + $this->io->warning($message); + return; + } + $userForOrg = $this->_fetchUserForOrg($organisationIDsForNationality); + $userID = Hash::extract($userForOrg, '{n}.id'); + $individualID = Hash::extract($userForOrg, '{n}.individual_id'); + + $message = 'Modified users:' . PHP_EOL; + fwrite($file_input, $message); + $this->io->out($message); + $logsUsers = $this->_fetchLogsForUsers($userID, $days); + $modifiedUsers = $this->_formatLogsForTable($logsUsers); + foreach ($modifiedUsers as $row) { + fputcsv($file_input, $row); + } + $this->io->helper('Table')->output($modifiedUsers); + + $message = PHP_EOL . 'Modified organisations:' . PHP_EOL; + fwrite($file_input, $message); + $this->io->out($message); + $logsOrgs = $this->_fetchLogsForOrgs($organisationIDsForNationality, $days); + $modifiedOrgs = $this->_formatLogsForTable($logsOrgs); + foreach ($modifiedOrgs as $row) { + fputcsv($file_input, $row); + } + $this->io->helper('Table')->output($modifiedOrgs); + + $message = PHP_EOL . 'Modified individuals:' . PHP_EOL; + fwrite($file_input, $message); + $this->io->out($message); + $logsIndividuals = $this->_fetchLogsForIndividuals($individualID, $days); + $modifiedIndividuals = $this->_formatLogsForTable($logsIndividuals); + foreach ($modifiedIndividuals as $row) { + fputcsv($file_input, $row); + } + $this->io->helper('Table')->output($modifiedIndividuals); + fclose($file_input); + } + + private function __loadTables() + { + $tables = ['Users', 'Organisations', 'Individuals', 'AuditLogs']; + foreach ($tables as $table) { + $this->loadModel($table); + } + } + + protected function _fetchOrganisationsForNationality(string $nationality): array + { + return array_keys($this->Organisations->find('list') + ->where([ + 'nationality' => $nationality, + ]) + ->all() + ->toArray()); + } + + protected function _fetchOrgNationalities(): array + { + return $this->Organisations->find() + ->where([ + 'nationality !=' => '', + ]) + ->all() + ->extract('nationality') + ->toList(); + } + + protected function _fetchUserForOrg(array $orgIDs = []): array + { + if (empty($orgIDs)) { + return []; + } + return $this->Users->find() + ->contain(['Individuals', 'Roles', 'UserSettings', 'Organisations']) + ->where([ + 'Organisations.id IN' => $orgIDs, + ]) + ->enableHydration(false) + ->all()->toList(); + } + + protected function _fetchLogsForUsers(array $userIDs = [], int $days=7): array + { + if (empty($userIDs)) { + return []; + } + return $this->_fetchLogs([ + 'contain' => ['Users'], + 'conditions' => [ + 'model' => 'Users', + 'request_action IN' => ['add', 'edit', 'delete'], + 'model_id IN' => $userIDs, + 'AuditLogs.created >=' => FrozenTime::now()->subDays($days), + ] + ]); + } + + protected function _fetchLogsForOrgs(array $orgIDs = [], int $days = 7): array + { + if (empty($orgIDs)) { + return []; + } + return $this->_fetchLogs([ + 'contain' => ['Users'], + 'conditions' => [ + 'model' => 'Organisations', + 'request_action IN' => ['add', 'edit', 'delete'], + 'model_id IN' => $orgIDs, + 'AuditLogs.created >=' => FrozenTime::now()->subDays($days), + ] + ]); + } + + protected function _fetchLogsForIndividuals(array $individualID = [], int $days = 7): array + { + if (empty($individualID)) { + return []; + } + return $this->_fetchLogs([ + 'contain' => ['Users'], + 'conditions' => [ + 'model' => 'Individuals', + 'request_action IN' => ['add', 'edit', 'delete'], + 'model_id IN' => $individualID, + 'AuditLogs.created >=' => FrozenTime::now()->subDays($days), + ] + ]); + } + + protected function _fetchLogs(array $options=[]): array + { + $logs = $this->AuditLogs->find() + ->contain($options['contain']) + ->where($options['conditions']) + ->enableHydration(false) + ->all()->toList(); + return array_map(function ($log) { + $log['changed'] = is_resource($log['changed']) ? stream_get_contents($log['changed']) : $log['changed']; + $log['changed'] = json_decode($log['changed']); + return $log; + }, $logs); + } + + protected function _formatLogsForTable($logEntries): array + { + $header = ['Model', 'Action', 'Editor user', 'Log ID', 'Datetime', 'Change']; + $data = [$header]; + foreach ($logEntries as $logEntry) { + $formatted = [ + $logEntry['model'], + $logEntry['request_action'], + sprintf('%s (%s)', $logEntry['user']['username'], $logEntry['user_id']), + $logEntry['id'], + $logEntry['created']->i18nFormat('yyyy-MM-dd HH:mm:ss'), + ]; + if ($logEntry['request_action'] == 'edit') { + $formatted[] = json_encode($logEntry['changed'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + } else { + $formatted[] = json_encode($logEntry['changed'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + } + $data[] = $formatted; + } + return $data; + } +} diff --git a/src/Controller/IndividualsController.php b/src/Controller/IndividualsController.php index d88d297..bd3c852 100644 --- a/src/Controller/IndividualsController.php +++ b/src/Controller/IndividualsController.php @@ -28,6 +28,12 @@ class IndividualsController extends AppController 'quickFilterForMetaField' => ['enabled' => true, 'wildcard_search' => true], 'contain' => $this->containFields, 'statisticsFields' => $this->statisticsFields, + 'afterFind' => function($data) use ($currentUser) { + if ($currentUser['role']['perm_admin']) { + $data['user'] = $this->Individuals->Users->find()->select(['id', 'username', 'Organisations.id', 'Organisations.name'])->contain('Organisations')->where(['individual_id' => $data['id']])->all()->toArray(); + } + return $data; + } ]); $responsePayload = $this->CRUD->getResponsePayload(); if (!empty($responsePayload)) { @@ -66,15 +72,29 @@ class IndividualsController extends AppController public function edit($id) { + $currentUser = $this->ACL->getUser(); + if (!$currentUser['role']['perm_admin']) { + $validIndividuals = $this->Individuals->getValidIndividualsToEdit($currentUser); + if (!in_array($id, $validIndividuals)) { + throw new MethodNotAllowedException(__('You cannot modify that individual.')); + } + } $currentUser = $this->ACL->getUser(); $validIndividualIds = []; - if ($currentUser['role']['perm_admin']) { + if (!$currentUser['role']['perm_admin']) { $validIndividualIds = $this->Individuals->getValidIndividualsToEdit($currentUser); - if (!isset($validIndividualIds[$id])) { + if (!in_array($id, $validIndividualIds)) { throw new NotFoundException(__('Invalid individual.')); } } - $this->CRUD->edit($id); + $this->CRUD->edit($id, [ + 'beforeSave' => function($data) use ($currentUser) { + if ($currentUser['role']['perm_admin'] && isset($data['uuid'])) { + unset($data['uuid']); + } + return $data; + } + ]); $responsePayload = $this->CRUD->getResponsePayload(); if (!empty($responsePayload)) { return $responsePayload; diff --git a/src/Model/Behavior/AuthKeycloakBehavior.php b/src/Model/Behavior/AuthKeycloakBehavior.php index 2104bda..0a485dd 100644 --- a/src/Model/Behavior/AuthKeycloakBehavior.php +++ b/src/Model/Behavior/AuthKeycloakBehavior.php @@ -145,9 +145,6 @@ class AuthKeycloakBehavior extends Behavior $roleConditions = [ 'id' => $data['role_id'] ]; - if (!empty(Configure::read('keycloak.user_management.actions'))) { - $roleConditions['name'] = Configure::read('keycloak.default_role_name'); - } $user = [ 'username' => $data['username'], 'disabled' => false, diff --git a/src/Model/Table/IndividualsTable.php b/src/Model/Table/IndividualsTable.php index ca5da26..c0b81ab 100644 --- a/src/Model/Table/IndividualsTable.php +++ b/src/Model/Table/IndividualsTable.php @@ -113,10 +113,12 @@ class IndividualsTable extends AppTable public function getValidIndividualsToEdit(object $currentUser): array { + $adminRoles = $this->Users->Roles->find('list')->select(['id'])->where(['perm_admin' => 1])->all()->toArray(); $validIndividualIds = $this->Users->find('list')->select(['individual_id'])->where( [ 'organisation_id' => $currentUser['organisation_id'], - 'disabled' => 0 + 'disabled' => 0, + 'role_id NOT IN' => array_keys($adminRoles) ] )->all()->toArray(); return array_keys($validIndividualIds); diff --git a/src/Model/Table/SettingProviders/CerebrateSettingsProvider.php b/src/Model/Table/SettingProviders/CerebrateSettingsProvider.php index 421f6b5..dfa2be4 100644 --- a/src/Model/Table/SettingProviders/CerebrateSettingsProvider.php +++ b/src/Model/Table/SettingProviders/CerebrateSettingsProvider.php @@ -208,27 +208,6 @@ class CerebrateSettingsProvider extends BaseSettingsProvider return true; } ], - 'keycloak.authoritative' => [ - 'name' => 'Authoritative', - 'type' => 'boolean', - 'severity' => 'info', - 'description' => __('Override local role and organisation settings based on the settings in KeyCloak'), - 'default' => false, - 'dependsOn' => 'keycloak.enabled' - ], - 'keycloak.default_role_name' => [ - 'name' => 'Default role', - 'type' => 'select', - 'severity' => 'info', - 'description' => __('Select the default role name to be used when creating users'), - 'options' => function ($settingsProviders) { - $roleTable = TableRegistry::getTableLocator()->get('Roles'); - $allRoleNames = $roleTable->find()->toArray(); - $allRoleNames = array_column($allRoleNames, 'name'); - return array_combine($allRoleNames, $allRoleNames); - }, - 'dependsOn' => 'keycloak.enabled' - ], 'keycloak.screw' => [ 'name' => 'Screw', 'type' => 'string', diff --git a/src/Model/Table/UsersTable.php b/src/Model/Table/UsersTable.php index 647d32f..a2ebfeb 100644 --- a/src/Model/Table/UsersTable.php +++ b/src/Model/Table/UsersTable.php @@ -245,10 +245,6 @@ class UsersTable extends AppTable { $role = $this->Roles->find()->where(['name' => $user['role']['name']])->first(); if (empty($role)) { - if (!empty(Configure::read('keycloak.default_role_name'))) { - $default_role_name = Configure::read('keycloak.default_role_name'); - $role = $this->Roles->find()->where(['name' => $default_role_name])->first(); - } if (empty($role)) { throw new NotFoundException(__('Invalid role')); } diff --git a/src/VERSION.json b/src/VERSION.json index 1fe70b7..a51d7e0 100644 --- a/src/VERSION.json +++ b/src/VERSION.json @@ -1,4 +1,4 @@ { - "version": "1.7", + "version": "1.8", "application": "Cerebrate" } diff --git a/templates/Individuals/index.php b/templates/Individuals/index.php index 2e39178..4a662aa 100644 --- a/templates/Individuals/index.php +++ b/templates/Individuals/index.php @@ -52,6 +52,12 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'sort' => 'last_name', 'data_path' => 'last_name', ], + [ + 'name' => __('Associated User(s)'), + 'sort' => 'user', + 'data_path' => 'user', + 'element' => 'user' + ], [ 'name' => __('Alignments'), 'data_path' => 'alignments', diff --git a/templates/Users/index.php b/templates/Users/index.php index dd287c8..826ae5e 100644 --- a/templates/Users/index.php +++ b/templates/Users/index.php @@ -21,6 +21,9 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'placeholder' => __('Enter value to search'), 'data' => '', 'searchKey' => 'value' + ], + [ + 'type' => 'table_action', ] ] ], diff --git a/templates/Users/view.php b/templates/Users/view.php index c9b3ea2..c6ca881 100644 --- a/templates/Users/view.php +++ b/templates/Users/view.php @@ -1,5 +1,7 @@ __('ID'), diff --git a/templates/element/genericElements/IndexTable/Fields/user.php b/templates/element/genericElements/IndexTable/Fields/user.php index 25504fc..8f6b82f 100644 --- a/templates/element/genericElements/IndexTable/Fields/user.php +++ b/templates/element/genericElements/IndexTable/Fields/user.php @@ -1,11 +1,25 @@ Hash->extract($row, 'user.id')[0]; - $userName = $this->Hash->extract($row, 'user.username')[0]; - echo $this->Html->link( - h($userName), - ['controller' => 'users', 'action' => 'view', $userId] - ); + if (isset($row['user']['id'])) { + $users = [$row['user']]; + } else { + $users = $row['user']; + } + $links = []; + foreach ($users as $user) { + $orgPrepend = ''; + if (!empty($user['organisation']['name']) && !empty($user['organisation']['id'])) { + $orgPrepend = '[' . $this->Html->link( + h($user['organisation']['name']), + ['controller' => 'organisations', 'action' => 'view', $user['organisation']['id']] + ) . '] '; + } + $links[] = $orgPrepend . $this->Html->link( + h($user['username']), + ['controller' => 'users', 'action' => 'view', $user['id']] + ); + } + echo implode('
', $links); } ?>