Merge branch 'develop' of github.com:cerebrate-project/cerebrate into feature-metafield-dropdown
commit
b53f2681b4
|
@ -0,0 +1,160 @@
|
|||
# Installing Cerebrate on RedHat Enterprise Linux (RHEL 8)
|
||||
>This installation instructions assume SELinux is enabled, and in Enforcing mode.
|
||||
>and that you want to keep it that way :)
|
||||
>You need to be root when running these commands.
|
||||
|
||||
## Prerequisites
|
||||
>Install needed packages:
|
||||
```Shell
|
||||
dnf install @httpd mariadb-server git @php unzip sqlite vim wget php-intl php-ldap php-mysqlnd php-pdo php-zip
|
||||
```
|
||||
## Install composer
|
||||
>Instructions taken from https://getcomposer.org/download/
|
||||
```PHP
|
||||
cd /root
|
||||
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
|
||||
php -r "if (hash_file('sha384', 'composer-setup.php') === '55ce33d7678c5a611085589f1f3ddf8b3c52d662cd01d4ba75c0ee0459970c2200a51f492d557530c71c15d8dba01eae') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
|
||||
php composer-setup.php --install-dir=/usr/bin --filename=composer
|
||||
php -r "unlink('composer-setup.php');"
|
||||
```
|
||||
|
||||
## Prepare MySQL for cerebrate
|
||||
>Enable and start mysql database. Select a secure password for root user, delete test user and database.
|
||||
|
||||
```Shell
|
||||
systemctl start mariadb
|
||||
systemctl enable mariadb
|
||||
mysql_secure_installation
|
||||
```
|
||||
### Create a new database, user and password for cerebrate
|
||||
```Shell
|
||||
mysql -u root -p
|
||||
```
|
||||
```SQL
|
||||
CREATE DATABASE cerebrate;
|
||||
CREATE USER 'cerebrate'@'localhost' IDENTIFIED BY 'CHANGE_ME_PASSWORD';
|
||||
GRANT USAGE ON *.* to cerebrate@localhost;
|
||||
GRANT ALL PRIVILEGES ON cerebrate.* to cerebrate@localhost;
|
||||
FLUSH PRIVILEGES;
|
||||
QUIT;
|
||||
```
|
||||
## Allow ports through the firewall
|
||||
```Shell
|
||||
firewall-cmd --zone=public --add-service=http --permanent
|
||||
firewall-cmd --zone=public --add-port=8001/tcp --permanent
|
||||
```
|
||||
> reload firewall and show applied firewall rules
|
||||
```Shell
|
||||
firewall-cmd --reload
|
||||
firewall-cmd --zone public --list-all
|
||||
```
|
||||
|
||||
## Main Cerebrate Installation
|
||||
>Steps to install Cerebrate on RHEL
|
||||
|
||||
### Clone this repository
|
||||
```Shell
|
||||
mkdir /var/www/cerebrate
|
||||
git clone https://github.com/cerebrate-project/cerebrate.git /var/www/cerebrate
|
||||
```
|
||||
|
||||
### Run composer
|
||||
```Shell
|
||||
mkdir -p /var/www/.composer
|
||||
chown -R apache.apache /var/www/.composer
|
||||
chown -R apache.apache /var/www/cerebrate
|
||||
cd /var/www/cerebrate
|
||||
composer install
|
||||
```
|
||||
>you will see a prompt: \
|
||||
>`Do you trust "cakephp/plugin-installer" to execurte code and wish to enable it now? (writes "allow-plugins" to composer.json) [y,n,d,?]` \
|
||||
>*repond with* `y` \
|
||||
>`Do you trust "dealerdirect/phpcodesniffer-composer-installer" to execute code and wish to enable it now? (writes "allow-plugins" to composer.json) [y,n,d,?]` \
|
||||
>*repond with* `y`
|
||||
|
||||
### Create your local configuration and set the db credentials
|
||||
```Shell
|
||||
cp -a /var/www/cerebrate/config/app_local.example.php /var/www/cerebrate/config/app_local.php
|
||||
cp -a /var/www/cerebrate/config/config.example.json /var/www/cerebrate/config/config.json
|
||||
```
|
||||
|
||||
### Modify the Datasource -> default array's in file `app_local.php`
|
||||
>Simply modify the `Datasources` section, to reflect your values for: username, password, and database
|
||||
>fields, as configured in the above [#create-a-new-database-user-and-password-for-cerebrate](<#create-a-new-database-user-and-password-for-cerebrate>)
|
||||
```Shell
|
||||
vim /var/www/cerebrate/config/app_local.php
|
||||
```
|
||||
```PHP
|
||||
'Datasources' => [
|
||||
'default' => [
|
||||
'host' => 'localhost',
|
||||
'username' => 'cerebrate',
|
||||
'password' => 'CHANGE_ME_PASSWORD',
|
||||
'database' => 'cerebrate',
|
||||
...
|
||||
```
|
||||
|
||||
### Run the database schema migrations
|
||||
```Shell
|
||||
usermod -s /bin/bash apache
|
||||
|
||||
chown -R apache.apache /var/www/.composer
|
||||
chown -R apache.apache /var/www/cerebrate
|
||||
|
||||
su apache <<'EOFi'
|
||||
/var/www/cerebrate/bin/cake migrations migrate
|
||||
/var/www/cerebrate/bin/cake migrations migrate -p tags
|
||||
/var/www/cerebrate/bin/cake migrations migrate -p ADmad/SocialAuth
|
||||
EOFi
|
||||
|
||||
usermod -s /sbin/nologin apache
|
||||
```
|
||||
|
||||
|
||||
### Clean cakephp caches
|
||||
```Shell
|
||||
rm /var/www/cerebrate/tmp/cache/models/*
|
||||
rm /var/www/cerebrate/tmp/cache/persistent/*
|
||||
```
|
||||
|
||||
### copy the Apache httpd template to the default apache configuration folder
|
||||
> in our case we used apache to serve this website, NGINX could also be used.
|
||||
```Shell
|
||||
cp -v /var/www/cerebrate/INSTALL/cerebrate_apache_dev.conf /etc/httpd/conf.d/.
|
||||
mkdir /var/log/apache2
|
||||
chown apache.root -R /var/log/apache2
|
||||
restorecon -Rv /etc/httpd/conf.d/*
|
||||
restorecon -Rv /var/log/*
|
||||
```
|
||||
### Make changes to the apache httpd site configuration file
|
||||
>Edit the file `/etc/httpd/conf.d/cerebrate_apache_dev.conf` change the two references of port 8000 to 8001
|
||||
```Shell
|
||||
vi /etc/httpd/conf.d/cerebrate_apache_dev.conf
|
||||
```
|
||||
### Make changes to SELinux
|
||||
>From the SELinux Manual page [services with non standard ports](<https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/using_selinux/configuring-selinux-for-applications-and-services-with-non-standard-configurations_using-selinux>)
|
||||
>We need SELinux to allow httpd to connect to our custom port 8001/tcp
|
||||
```SELinux Policy
|
||||
semanage port -a -t http_port_t -p tcp 8001
|
||||
```
|
||||
>Change SELinux context for folder /var/www/cerebrate
|
||||
```SELinux Policy
|
||||
semanage fcontext -a -t httpd_sys_content_t "/var/www/cerebrate(/.*)?"
|
||||
restorecon -Rv /var/www/cerebrate/
|
||||
chown apache.apache /var/www/cerebrate
|
||||
```
|
||||
|
||||
## Apply changes/restart Apache httpd
|
||||
>Look out for any errors during restart.
|
||||
```
|
||||
systemctl enable httpd
|
||||
systemctl restart httpd
|
||||
```
|
||||
|
||||
## Point your browser to: http://localhost:8001
|
||||
> If everything worked, you should be able to log in using the default credentials below:
|
||||
|
||||
```
|
||||
Username: admin
|
||||
Password: Password1234
|
||||
```
|
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Migrations\AbstractMigration;
|
||||
|
||||
final class PermissionRestrictions extends AbstractMigration
|
||||
{
|
||||
/**
|
||||
* Change Method.
|
||||
*
|
||||
* Write your reversible migrations using this method.
|
||||
*
|
||||
* More information on writing migrations is available here:
|
||||
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
|
||||
*
|
||||
* Remember to call "create()" or "update()" and NOT "save()" when working
|
||||
* with the Table class.
|
||||
*/
|
||||
public function change(): void
|
||||
{
|
||||
$exists = $this->hasTable('permission_limitations');
|
||||
if (!$exists) {
|
||||
$table = $this->table('permission_limitations', [
|
||||
'signed' => false,
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
]);
|
||||
$table
|
||||
->addColumn('scope', 'string', [
|
||||
'null' => false,
|
||||
'length' => 20,
|
||||
'collation' => 'ascii_general_ci'
|
||||
])
|
||||
->addColumn('permission', 'string', [
|
||||
'null' => false,
|
||||
'length' => 40,
|
||||
'collation' => 'utf8mb4_unicode_ci'
|
||||
])
|
||||
->addColumn('max_occurrence', 'integer', [
|
||||
'null' => false,
|
||||
'signed' => false
|
||||
])
|
||||
->addColumn('comment', 'blob', [])
|
||||
->addIndex('scope')
|
||||
->addIndex('permission');
|
||||
$table->create();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Migrations\AbstractMigration;
|
||||
use Phinx\Db\Adapter\MysqlAdapter;
|
||||
|
||||
final class SupportForMegalomaniacPgpKeys extends AbstractMigration
|
||||
{
|
||||
/**
|
||||
* Change Method.
|
||||
*
|
||||
* Write your reversible migrations using this method.
|
||||
*
|
||||
* More information on writing migrations is available here:
|
||||
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
|
||||
*
|
||||
* Remember to call "create()" or "update()" and NOT "save()" when working
|
||||
* with the Table class.
|
||||
*/
|
||||
public function change(): void
|
||||
{
|
||||
$encryption_keys = $this->table('encryption_keys');
|
||||
$encryption_keys->changeColumn('encryption_key', 'text', ['limit' => MysqlAdapter::TEXT_MEDIUM])->save();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Migrations\AbstractMigration;
|
||||
use Phinx\Db\Adapter\MysqlAdapter;
|
||||
|
||||
final class SupportForMegalomaniacPgpKeysRound2 extends AbstractMigration
|
||||
{
|
||||
/**
|
||||
* Change Method.
|
||||
*
|
||||
* Write your reversible migrations using this method.
|
||||
*
|
||||
* More information on writing migrations is available here:
|
||||
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
|
||||
*
|
||||
* Remember to call "create()" or "update()" and NOT "save()" when working
|
||||
* with the Table class.
|
||||
*/
|
||||
public function change(): void
|
||||
{
|
||||
$encryption_keys = $this->table('audit_logs');
|
||||
$encryption_keys->changeColumn('changed', 'blob', ['limit' => MysqlAdapter::BLOB_MEDIUM])->save();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"name": "CSIRT network permissions",
|
||||
"namespace": "csn",
|
||||
"description": "Template of additional tooling permissions for the CSIRT network",
|
||||
"version": 1,
|
||||
"scope": "user",
|
||||
"uuid": "447ded8b-314b-41c7-a913-4ce32535b28d",
|
||||
"source": "CSIRT network",
|
||||
"metaFields": [
|
||||
{
|
||||
"field": "perm_mattermost",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"field": "perm_bigbluebutton",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"field": "perm_sharepoint",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"field": "perm_misp",
|
||||
"type": "boolean"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -16,10 +16,10 @@ class KeycloakSyncCommand extends Command
|
|||
$this->loadModel('Users');
|
||||
$results = $this->fetchTable()->syncWithKeycloak();
|
||||
$tableData = [
|
||||
['Changes to', 'Count']
|
||||
['Modification type', 'Count', 'Affected users']
|
||||
];
|
||||
foreach ($results as $k => $v) {
|
||||
$tableData[] = [$k, '<text-right>' . $v . '</text-right>'];
|
||||
$tableData[] = [$k, '<text-right>' . count($v) . '</text-right>', '<text-right>' . implode(', ', $v) . '</text-right>'];
|
||||
}
|
||||
$io->out(__('Sync done. See the results below.'));
|
||||
$io->helper('Table')->output($tableData);
|
||||
|
|
|
@ -0,0 +1,231 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use Cake\Console\Command;
|
||||
use Cake\Console\Arguments;
|
||||
use Cake\Console\ConsoleIo;
|
||||
use Cake\Console\ConsoleOptionParser;
|
||||
use Cake\Filesystem\File;
|
||||
use Cake\Utility\Hash;
|
||||
use Cake\Utility\Text;
|
||||
use Cake\Validation\Validator;
|
||||
use Cake\Http\Client;
|
||||
use Cake\I18n\FrozenTime;
|
||||
|
||||
class SummaryCommand extends Command
|
||||
{
|
||||
protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
|
||||
{
|
||||
$parser->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;
|
||||
}
|
||||
}
|
|
@ -87,7 +87,7 @@ class ACLComponent extends Component
|
|||
'Individuals' => [
|
||||
'add' => ['perm_admin'],
|
||||
'delete' => ['perm_admin'],
|
||||
'edit' => ['perm_admin'],
|
||||
'edit' => ['perm_admin', 'perm_org_admin'],
|
||||
'filtering' => ['*'],
|
||||
'index' => ['*'],
|
||||
'tag' => ['perm_tagger'],
|
||||
|
@ -169,6 +169,13 @@ class ACLComponent extends Component
|
|||
'Pages' => [
|
||||
'display' => ['*']
|
||||
],
|
||||
'PermissionLimitations' => [
|
||||
"index" => ['*'],
|
||||
"add" => ['perm_admin'],
|
||||
"view" => ['*'],
|
||||
"edit" => ['perm_admin'],
|
||||
"delete" => ['perm_admin']
|
||||
],
|
||||
'Roles' => [
|
||||
'add' => ['perm_admin'],
|
||||
'delete' => ['perm_admin'],
|
||||
|
|
|
@ -12,15 +12,19 @@ use Cake\Core\Configure;
|
|||
use Cake\Core\Configure\Engine\PhpConfig;
|
||||
use Cake\Utility\Inflector;
|
||||
use Cake\Routing\Router;
|
||||
use Cake\Collection\Collection;
|
||||
|
||||
class APIRearrangeComponent extends Component
|
||||
{
|
||||
public function rearrangeForAPI(object $data): object
|
||||
public function rearrangeForAPI(object $data)
|
||||
{
|
||||
if (is_subclass_of($data, 'Iterator')) {
|
||||
$data->each(function ($value, $key) {
|
||||
$newData = [];
|
||||
$data->each(function ($value, $key) use (&$newData) {
|
||||
$value->rearrangeForAPI();
|
||||
$newData[] = $value;
|
||||
});
|
||||
return new Collection($newData);
|
||||
} else {
|
||||
$data->rearrangeForAPI();
|
||||
}
|
||||
|
|
|
@ -76,6 +76,9 @@ class CRUDComponent extends Component
|
|||
$query->order($options['order']);
|
||||
}
|
||||
if ($this->Controller->ParamHandler->isRest()) {
|
||||
if ($this->metaFieldsSupported()) {
|
||||
$query = $this->includeRequestedMetaFields($query);
|
||||
}
|
||||
$data = $query->all();
|
||||
if (isset($options['hidden'])) {
|
||||
$data->each(function($value, $key) use ($options) {
|
||||
|
@ -101,6 +104,12 @@ class CRUDComponent extends Component
|
|||
});
|
||||
}
|
||||
}
|
||||
if ($this->metaFieldsSupported()) {
|
||||
$metaTemplates = $this->getMetaTemplates()->toArray();
|
||||
$data = $data->map(function($value, $key) use ($metaTemplates) {
|
||||
return $this->attachMetaTemplatesIfNeeded($value, $metaTemplates);
|
||||
});
|
||||
}
|
||||
$this->Controller->restResponsePayload = $this->RestResponse->viewData($data, 'json');
|
||||
} else {
|
||||
if ($this->metaFieldsSupported()) {
|
||||
|
@ -560,7 +569,14 @@ class CRUDComponent extends Component
|
|||
$savedData = $this->Table->save($data);
|
||||
if ($savedData !== false) {
|
||||
if ($this->metaFieldsSupported() && !empty($metaFieldsToDelete)) {
|
||||
$this->Table->MetaFields->unlink($savedData, $metaFieldsToDelete);
|
||||
foreach ($metaFieldsToDelete as $k => $v) {
|
||||
if ($v === null) {
|
||||
unset($metaFieldsToDelete[$k]);
|
||||
}
|
||||
}
|
||||
if (!empty($metaFieldsToDelete)) {
|
||||
$this->Table->MetaFields->unlink($savedData, $metaFieldsToDelete);
|
||||
}
|
||||
}
|
||||
if (isset($params['afterSave'])) {
|
||||
$params['afterSave']($data);
|
||||
|
@ -673,12 +689,15 @@ class CRUDComponent extends Component
|
|||
if (!empty($pruneEmptyDisabled) && !$metaTemplate->enabled) {
|
||||
unset($metaTemplates[$i]);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
$newestTemplate = $this->MetaTemplates->getNewestVersion($metaTemplate);
|
||||
if (!empty($newestTemplate) && !empty($metaTemplates[$i])) {
|
||||
$metaTemplates[$i]['hasNewerVersion'] = $newestTemplate;
|
||||
}
|
||||
$metaTemplates[$metaTemplate->id]['meta_template_fields'] = $metaTemplates[$metaTemplate->id]['meta_template_fields'];
|
||||
}
|
||||
$metaTemplates = $metaTemplates;
|
||||
$data['MetaTemplates'] = $metaTemplates;
|
||||
return $data;
|
||||
}
|
||||
|
@ -793,12 +812,6 @@ class CRUDComponent extends Component
|
|||
if (empty($data)) {
|
||||
throw new NotFoundException(__('Invalid {0}.', $this->ObjectAlias));
|
||||
}
|
||||
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));
|
||||
}
|
||||
}
|
||||
$this->Controller->set('id', $data['id']);
|
||||
$this->Controller->set('data', $data);
|
||||
$this->Controller->set('bulkEnabled', false);
|
||||
|
|
|
@ -123,6 +123,11 @@ class Sidemenu {
|
|||
'url' => '/auditLogs/index',
|
||||
'icon' => 'history',
|
||||
],
|
||||
'PermissionLimitations' => [
|
||||
'label' => __('Permission Limitations'),
|
||||
'url' => '/permissionLimitations/index',
|
||||
'icon' => 'jedi',
|
||||
],
|
||||
]
|
||||
],
|
||||
'API' => [
|
||||
|
|
|
@ -20,17 +20,30 @@ class IndividualsController extends AppController
|
|||
|
||||
public function index()
|
||||
{
|
||||
$currentUser = $this->ACL->getUser();
|
||||
$orgAdmin = !$currentUser['role']['perm_admin'] && $currentUser['role']['perm_org_admin'];
|
||||
$this->CRUD->index([
|
||||
'filters' => $this->filterFields,
|
||||
'quickFilters' => $this->quickFilterFields,
|
||||
'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)) {
|
||||
return $responsePayload;
|
||||
}
|
||||
$editableIds = null;
|
||||
if ($orgAdmin) {
|
||||
$editableIds = $this->Individuals->getValidIndividualsToEdit($currentUser);
|
||||
}
|
||||
$this->set('editableIds', $editableIds);
|
||||
$this->set('alignmentScope', 'individuals');
|
||||
}
|
||||
|
||||
|
@ -59,7 +72,29 @@ class IndividualsController extends AppController
|
|||
|
||||
public function edit($id)
|
||||
{
|
||||
$this->CRUD->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']) {
|
||||
$validIndividualIds = $this->Individuals->getValidIndividualsToEdit($currentUser);
|
||||
if (!in_array($id, $validIndividualIds)) {
|
||||
throw new NotFoundException(__('Invalid individual.'));
|
||||
}
|
||||
}
|
||||
$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;
|
||||
|
|
|
@ -99,6 +99,13 @@ class OrganisationsController extends AppController
|
|||
|
||||
public function edit($id)
|
||||
{
|
||||
$currentUser = $this->ACL->getUser();
|
||||
if (
|
||||
!($currentUser['organisation']['id'] == $id && $currentUser['role']['perm_org_admin']) &&
|
||||
!$currentUser['role']['perm_admin']
|
||||
) {
|
||||
throw new MethodNotAllowedException(__('You cannot modify that organisation.'));
|
||||
}
|
||||
$this->CRUD->edit($id);
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
if (!empty($responsePayload)) {
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Controller\AppController;
|
||||
use Cake\Utility\Hash;
|
||||
use Cake\Utility\Text;
|
||||
use \Cake\Database\Expression\QueryExpression;
|
||||
use Cake\Http\Exception\NotFoundException;
|
||||
use Cake\Http\Exception\MethodNotAllowedException;
|
||||
use Cake\Http\Exception\ForbiddenException;
|
||||
|
||||
class PermissionLimitationsController extends AppController
|
||||
{
|
||||
public $filterFields = ['scope', 'permission'];
|
||||
public $quickFilterFields = ['name'];
|
||||
public $containFields = [];
|
||||
|
||||
public function index()
|
||||
{
|
||||
$this->CRUD->index([
|
||||
'filters' => $this->filterFields,
|
||||
'quickFilters' => $this->quickFilterFields,
|
||||
'afterFind' => function($data) {
|
||||
$data['comment'] = is_resource($data['comment']) ? stream_get_contents($data['comment']) : $data['comment'];
|
||||
return $data;
|
||||
}
|
||||
]);
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
if (!empty($responsePayload)) {
|
||||
return $responsePayload;
|
||||
}
|
||||
$this->set('metaGroup', 'PermissionLimitations');
|
||||
}
|
||||
|
||||
public function add()
|
||||
{
|
||||
$this->CRUD->add([
|
||||
'afterFind' => function($data) {
|
||||
$data['comment'] = is_resource($data['comment']) ? stream_get_contents($data['comment']) : $data['comment'];
|
||||
return $data;
|
||||
}
|
||||
]);
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
if (!empty($responsePayload)) {
|
||||
return $responsePayload;
|
||||
}
|
||||
$this->set('metaGroup', 'PermissionLimitations');
|
||||
}
|
||||
|
||||
public function view($id)
|
||||
{
|
||||
$this->CRUD->view($id, [
|
||||
'afterFind' => function($data) {
|
||||
$data['comment'] = is_resource($data['comment']) ? stream_get_contents($data['comment']) : $data['comment'];
|
||||
return $data;
|
||||
}
|
||||
]);
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
if (!empty($responsePayload)) {
|
||||
return $responsePayload;
|
||||
}
|
||||
$this->set('metaGroup', 'PermissionLimitations');
|
||||
}
|
||||
|
||||
public function edit($id)
|
||||
{
|
||||
$this->CRUD->edit($id, [
|
||||
'afterFind' => function($data) {
|
||||
$data['comment'] = is_resource($data['comment']) ? stream_get_contents($data['comment']) : $data['comment'];
|
||||
return $data;
|
||||
}
|
||||
]);
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
if (!empty($responsePayload)) {
|
||||
return $responsePayload;
|
||||
}
|
||||
$this->set('metaGroup', 'PermissionLimitations');
|
||||
$this->render('add');
|
||||
}
|
||||
|
||||
public function delete($id)
|
||||
{
|
||||
$this->CRUD->delete($id);
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
if (!empty($responsePayload)) {
|
||||
return $responsePayload;
|
||||
}
|
||||
$this->set('metaGroup', 'PermissionLimitations');
|
||||
}
|
||||
}
|
|
@ -96,8 +96,12 @@ class UsersController extends AppController
|
|||
throw new MethodNotAllowedException(__('Invalid individual selected - when KeyCloak is enabled, only one user account may be assigned to an individual.'));
|
||||
}
|
||||
}
|
||||
$this->Users->enrollUserRouter($data);
|
||||
return $data;
|
||||
},
|
||||
'afterSave' => function($data) {
|
||||
if (Configure::read('keycloak.enabled')) {
|
||||
$this->Users->enrollUserRouter($data);
|
||||
}
|
||||
}
|
||||
]);
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
|
@ -136,7 +140,11 @@ class UsersController extends AppController
|
|||
$id = $this->ACL->getUser()['id'];
|
||||
}
|
||||
$this->CRUD->view($id, [
|
||||
'contain' => ['Individuals' => ['Alignments' => 'Organisations'], 'Roles', 'Organisations']
|
||||
'contain' => ['Individuals' => ['Alignments' => 'Organisations'], 'Roles', 'Organisations'],
|
||||
'afterFind' => function($data) {
|
||||
$data = $this->fetchTable('PermissionLimitations')->attachLimitations($data);
|
||||
return $data;
|
||||
}
|
||||
]);
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
if (!empty($responsePayload)) {
|
||||
|
@ -278,16 +286,21 @@ class UsersController extends AppController
|
|||
'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.'));
|
||||
throw new MethodNotAllowedException(__('You do not have permission to delete 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.'));
|
||||
throw new MethodNotAllowedException(__('You do not have permission to delete the given user.'));
|
||||
}
|
||||
}
|
||||
if (Configure::read('keycloak.enabled')) {
|
||||
if (!$this->Users->deleteUser($data)) {
|
||||
throw new MethodNotAllowedException(__('Could not delete the user from KeyCloak. Please try again later, or consider disabling the user instead.'));
|
||||
}
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
];
|
||||
$this->CRUD->delete($id);
|
||||
$this->CRUD->delete($id, $params);
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
if (!empty($responsePayload)) {
|
||||
return $responsePayload;
|
||||
|
@ -354,6 +367,9 @@ class UsersController extends AppController
|
|||
]);
|
||||
$this->Authentication->logout();
|
||||
$this->Flash->success(__('Goodbye.'));
|
||||
if (Configure::read('keycloak.enabled')) {
|
||||
$this->redirect($this->Users->keyCloaklogout());
|
||||
}
|
||||
return $this->redirect(\Cake\Routing\Router::url('/users/login'));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@ class SocialAuthListener implements EventListenerInterface
|
|||
|
||||
// You can access the profile using $user->social_profile
|
||||
|
||||
$this->getTableLocator()->get('Users')->saveOrFail($user);
|
||||
// $this->getTableLocator()->get('Users')->saveOrFail($user);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
|
|
@ -18,11 +18,9 @@ use Cake\Http\Exception\NotFoundException;
|
|||
|
||||
class AuthKeycloakBehavior extends Behavior
|
||||
{
|
||||
|
||||
public function getUser(EntityInterface $profile, Session $session)
|
||||
{
|
||||
$userId = $session->read('Auth.User.id');
|
||||
$userId = null;
|
||||
if ($userId) {
|
||||
return $this->_table->get($userId);
|
||||
}
|
||||
|
@ -86,14 +84,67 @@ class AuthKeycloakBehavior extends Behavior
|
|||
);
|
||||
}
|
||||
|
||||
public function getUserIdByUsername(string $username)
|
||||
{
|
||||
$response = $this->restApiRequest(
|
||||
'%s/admin/realms/%s/users/?username=' . urlencode($username),
|
||||
[],
|
||||
'GET'
|
||||
);
|
||||
if (!$response->isOk()) {
|
||||
$responseBody = json_decode($response->getStringBody(), true);
|
||||
$this->_table->auditLogs()->insert([
|
||||
'request_action' => 'keycloakGetUser',
|
||||
'model' => 'User',
|
||||
'model_id' => 0,
|
||||
'model_title' => __('Failed to fetch user ({0}) from keycloak', $username),
|
||||
'changed' => ['error' => empty($responseBody['errorMessage']) ? 'Unknown error.' : $responseBody['errorMessage']]
|
||||
]);
|
||||
}
|
||||
$responseBody = json_decode($response->getStringBody(), true);
|
||||
if (empty($responseBody[0]['id'])) {
|
||||
return false;
|
||||
}
|
||||
return $responseBody[0]['id'];
|
||||
}
|
||||
|
||||
public function deleteUser($data): bool
|
||||
{
|
||||
$userId = $this->getUserIdByUsername($data['username']);
|
||||
if ($userId === false) {
|
||||
$this->_table->auditLogs()->insert([
|
||||
'request_action' => 'keycloakUserDeletion',
|
||||
'model' => 'User',
|
||||
'model_id' => 0,
|
||||
'model_title' => __('User {0} not found in keycloak, deleting the user locally.', $data['username']),
|
||||
'changed' => []
|
||||
]);
|
||||
return true;
|
||||
}
|
||||
$response = $this->restApiRequest(
|
||||
'%s/admin/realms/%s/users/' . urlencode($userId),
|
||||
[],
|
||||
'delete'
|
||||
);
|
||||
if (!$response->isOk()) {
|
||||
$responseBody = json_decode($response->getStringBody(), true);
|
||||
$this->_table->auditLogs()->insert([
|
||||
'request_action' => 'keycloakUserDeletion',
|
||||
'model' => 'User',
|
||||
'model_id' => 0,
|
||||
'model_title' => __('Failed to delete user {0} ({1}) in keycloak', $data['username'], $userId),
|
||||
'changed' => ['error' => empty($responseBody['errorMessage']) ? 'Unknown error.' : $responseBody['errorMessage']]
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function enrollUser($data): bool
|
||||
{
|
||||
$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,
|
||||
|
@ -110,12 +161,8 @@ class AuthKeycloakBehavior extends Behavior
|
|||
)->first()
|
||||
];
|
||||
$clientId = $this->getClientId();
|
||||
$roles = $this->getAllRoles($clientId);
|
||||
$rolesParsed = [];
|
||||
foreach ($roles as $role) {
|
||||
$rolesParsed[$role['name']] = $role['id'];
|
||||
}
|
||||
if (!$this->createUser($user, $clientId, $rolesParsed)) {
|
||||
$newUserId = $this->createUser($user, $clientId);
|
||||
if (!$newUserId) {
|
||||
$logChange = [
|
||||
'username' => $user['username'],
|
||||
'individual_id' => $user['individual']['id'],
|
||||
|
@ -141,6 +188,21 @@ class AuthKeycloakBehavior extends Behavior
|
|||
'model_title' => __('Successful Keycloak enrollment for user {0}', $user['username']),
|
||||
'changed' => $logChange
|
||||
]);
|
||||
$response = $this->restApiRequest(
|
||||
'%s/admin/realms/%s/users/' . urlencode($newUserId) . '/execute-actions-email',
|
||||
['UPDATE_PASSWORD'],
|
||||
'put'
|
||||
);
|
||||
if (!$response->isOk()) {
|
||||
$responseBody = json_decode($response->getStringBody(), true);
|
||||
$this->_table->auditLogs()->insert([
|
||||
'request_action' => 'keycloakWelcomeEmail',
|
||||
'model' => 'User',
|
||||
'model_id' => 0,
|
||||
'model_title' => __('Failed to send welcome mail to user ({0}) in keycloak', $user['username']),
|
||||
'changed' => ['error' => empty($responseBody['errorMessage']) ? 'Unknown error.' : $responseBody['errorMessage']]
|
||||
]);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
@ -149,9 +211,9 @@ class AuthKeycloakBehavior extends Behavior
|
|||
* handleUserUpdate
|
||||
*
|
||||
* @param \App\Model\Entity\User $user
|
||||
* @return boolean If the update was a success
|
||||
* @return array Containing changes if successful
|
||||
*/
|
||||
public function handleUserUpdate(\App\Model\Entity\User $user): bool
|
||||
public function handleUserUpdate(\App\Model\Entity\User $user): array
|
||||
{
|
||||
$user['individual'] = $this->_table->Individuals->find()->where([
|
||||
'id' => $user['individual_id']
|
||||
|
@ -166,7 +228,19 @@ class AuthKeycloakBehavior extends Behavior
|
|||
$users = [$user->toArray()];
|
||||
$clientId = $this->getClientId();
|
||||
$changes = $this->syncUsers($users, $clientId);
|
||||
return !empty($changes);
|
||||
return $changes;
|
||||
}
|
||||
|
||||
public function keyCloaklogout(): string
|
||||
{
|
||||
$keycloakConfig = Configure::read('keycloak');
|
||||
$logoutUrl = sprintf(
|
||||
'%s/realms/%s/protocol/openid-connect/logout?redirect_uri=%s',
|
||||
$keycloakConfig['provider']['baseUrl'],
|
||||
$keycloakConfig['provider']['realm'],
|
||||
urlencode(Configure::read('App.fullBaseUrl'))
|
||||
);
|
||||
return $logoutUrl;
|
||||
}
|
||||
|
||||
private function getAdminAccessToken()
|
||||
|
@ -208,9 +282,8 @@ class AuthKeycloakBehavior extends Behavior
|
|||
|
||||
public function syncWithKeycloak(): array
|
||||
{
|
||||
$this->updateMappers();
|
||||
$results = [];
|
||||
$data['Roles'] = $this->_table->Roles->find()->disableHydration()->toArray();
|
||||
$data['Organisations'] = $this->_table->Organisations->find()->disableHydration()->toArray();
|
||||
$data['Users'] = $this->_table->find()->contain(['Individuals', 'Organisations', 'Roles'])->select(
|
||||
[
|
||||
'id',
|
||||
|
@ -228,86 +301,15 @@ class AuthKeycloakBehavior extends Behavior
|
|||
]
|
||||
)->disableHydration()->toArray();
|
||||
$clientId = $this->getClientId();
|
||||
$results = [];
|
||||
$results['roles'] = $this->syncRoles(Hash::extract($data['Roles'], '{n}.name'), $clientId, 'Role');
|
||||
$results['organisations'] = $this->syncRoles(Hash::extract($data['Organisations'], '{n}.name'), $clientId, 'Organisation');
|
||||
$results['users'] = $this->syncUsers($data['Users'], $clientId);
|
||||
return $results;
|
||||
return $this->syncUsers($data['Users'], $clientId);
|
||||
}
|
||||
|
||||
private function syncRoles(array $roles, string $clientId, string $scope = 'Role'): int
|
||||
private function syncUsers(array $users, $clientId): array
|
||||
{
|
||||
$keycloakRoles = $this->getAllRoles($clientId);
|
||||
$keycloakRolesParsed = Hash::extract($keycloakRoles, '{n}.name');
|
||||
$scopeString = $scope . ':';
|
||||
$modified = 0;
|
||||
foreach ($roles as $role) {
|
||||
if (!in_array($scopeString . $role, $keycloakRolesParsed)) {
|
||||
$roleToPush = [
|
||||
'name' => $scopeString . $role,
|
||||
'clientRole' => true
|
||||
];
|
||||
$url = '%s/admin/realms/%s/clients/' . $clientId . '/roles';
|
||||
$response = $this->restApiRequest($url, $roleToPush, 'post');
|
||||
if (!$response->isOk()) {
|
||||
$this->_table->auditLogs()->insert([
|
||||
'request_action' => 'keycloakCreateRole',
|
||||
'model' => 'User',
|
||||
'model_id' => 0,
|
||||
'model_title' => __('Failed to create role ({0}) in keycloak', $scopeString . $role),
|
||||
'changed' => [
|
||||
'code' => $response->getStatusCode(),
|
||||
'error_body' => $response->getStringBody()
|
||||
]
|
||||
]);
|
||||
}
|
||||
$modified += 1;
|
||||
}
|
||||
$keycloakRolesParsed = array_diff($keycloakRolesParsed, [$scopeString . $role]);
|
||||
}
|
||||
foreach ($keycloakRolesParsed as $roleToRemove) {
|
||||
if (substr($roleToRemove, 0, strlen($scopeString)) === $scopeString) {
|
||||
$url = '%s/admin/realms/%s/clients/' . $clientId . '/roles/' . $roleToRemove;
|
||||
$response = $this->restApiRequest($url, [], 'delete');
|
||||
if (!$response->isOk()) {
|
||||
$this->_table->auditLogs()->insert([
|
||||
'request_action' => 'keycloakRemoveRole',
|
||||
'model' => 'User',
|
||||
'model_id' => 0,
|
||||
'model_title' => __('Failed to remove role ({0}) in keycloak', $roleToRemove),
|
||||
'changed' => [
|
||||
'code' => $response->getStatusCode(),
|
||||
'error_body' => $response->getStringBody()
|
||||
]
|
||||
]);
|
||||
}
|
||||
$modified += 1;
|
||||
}
|
||||
}
|
||||
return $modified;
|
||||
}
|
||||
|
||||
private function getAllRoles(string $clientId): array
|
||||
{
|
||||
$response = $this->restApiRequest('%s/admin/realms/%s/clients/' . $clientId . '/roles', [], 'get');
|
||||
return json_decode($response->getStringBody(), true);
|
||||
}
|
||||
|
||||
private function syncUsers(array $users, $clientId, $roles = null): int
|
||||
{
|
||||
if ($roles === null) {
|
||||
$roles = $this->getAllRoles($clientId);
|
||||
}
|
||||
$rolesParsed = [];
|
||||
foreach ($roles as $role) {
|
||||
$rolesParsed[$role['name']] = $role['id'];
|
||||
}
|
||||
$response = $this->restApiRequest('%s/admin/realms/%s/users', [], 'get');
|
||||
$keycloakUsers = json_decode($response->getStringBody(), true);
|
||||
$keycloakUsersParsed = [];
|
||||
foreach ($keycloakUsers as $u) {
|
||||
$response = $this->restApiRequest('%s/admin/realms/%s/users/' . $u['id'] . '/role-mappings/clients/' . $clientId, [], 'get');
|
||||
$roleMappings = json_decode($response->getStringBody(), true);
|
||||
$keycloakUsersParsed[$u['username']] = [
|
||||
'id' => $u['id'],
|
||||
'username' => $u['username'],
|
||||
|
@ -315,26 +317,28 @@ class AuthKeycloakBehavior extends Behavior
|
|||
'firstName' => $u['firstName'],
|
||||
'lastName' => $u['lastName'],
|
||||
'email' => $u['email'],
|
||||
'roles' => $roleMappings
|
||||
'attributes' => [
|
||||
'role_name' => $u['attributes']['role_name'][0] ?? '',
|
||||
'role_uuid' => $u['attributes']['role_uuid'][0] ?? '',
|
||||
'org_uuid' => $u['attributes']['org_uuid'][0] ?? '',
|
||||
'org_name' => $u['attributes']['org_name'][0] ?? ''
|
||||
]
|
||||
];
|
||||
}
|
||||
$changes = 0;
|
||||
$changes = [
|
||||
'created' => [],
|
||||
'modified' => [],
|
||||
];
|
||||
foreach ($users as &$user) {
|
||||
$changed = false;
|
||||
if (empty($keycloakUsersParsed[$user['username']])) {
|
||||
if ($this->createUser($user, $clientId, $rolesParsed)) {
|
||||
$changes = true;
|
||||
if ($this->createUser($user, $clientId)) {
|
||||
$changes['created'][] = $user['username'];
|
||||
}
|
||||
} else {
|
||||
if ($this->checkAndUpdateUser($keycloakUsersParsed[$user['username']], $user)) {
|
||||
$changes = true;
|
||||
$changes['modified'][] = $user['username'];
|
||||
}
|
||||
if ($this->checkAndUpdateUserRoles($keycloakUsersParsed[$user['username']], $user, $clientId, $rolesParsed)) {
|
||||
$changes = true;
|
||||
}
|
||||
}
|
||||
if ($changed) {
|
||||
$changes += 1;
|
||||
}
|
||||
}
|
||||
return $changes;
|
||||
|
@ -346,13 +350,23 @@ class AuthKeycloakBehavior extends Behavior
|
|||
$keycloakUser['enabled'] == $user['disabled'] ||
|
||||
$keycloakUser['firstName'] !== $user['individual']['first_name'] ||
|
||||
$keycloakUser['lastName'] !== $user['individual']['last_name'] ||
|
||||
$keycloakUser['email'] !== $user['individual']['email']
|
||||
$keycloakUser['email'] !== $user['individual']['email'] ||
|
||||
(empty($keycloakUser['attributes']['role_name']) || $keycloakUser['attributes']['role_name'] !== $user['role']['name']) ||
|
||||
(empty($keycloakUser['attributes']['role_uuid']) || $keycloakUser['attributes']['role_uuid'] !== $user['role']['uuid']) ||
|
||||
(empty($keycloakUser['attributes']['org_name']) || $keycloakUser['attributes']['org_name'] !== $user['organisation']['name']) ||
|
||||
(empty($keycloakUser['attributes']['org_uuid']) || $keycloakUser['attributes']['org_uuid'] !== $user['organisation']['uuid'])
|
||||
) {
|
||||
$change = [
|
||||
'enabled' => !$user['disabled'],
|
||||
'firstName' => $user['individual']['first_name'],
|
||||
'lastName' => $user['individual']['last_name'],
|
||||
'email' => $user['individual']['email'],
|
||||
'attributes' => [
|
||||
'role_name' => $user['role']['name'],
|
||||
'role_uuid' => $user['role']['uuid'],
|
||||
'org_name' => $user['organisation']['name'],
|
||||
'org_uuid' => $user['organisation']['uuid']
|
||||
]
|
||||
];
|
||||
$response = $this->restApiRequest('%s/admin/realms/%s/users/' . $keycloakUser['id'], $change, 'put');
|
||||
if (!$response->isOk()) {
|
||||
|
@ -373,14 +387,20 @@ class AuthKeycloakBehavior extends Behavior
|
|||
return false;
|
||||
}
|
||||
|
||||
private function createUser(array $user, string $clientId, array $rolesParsed): bool
|
||||
private function createUser(array $user, string $clientId)
|
||||
{
|
||||
$newUser = [
|
||||
'username' => $user['username'],
|
||||
'enabled' => !$user['disabled'],
|
||||
'firstName' => $user['individual']['first_name'],
|
||||
'lastName' => $user['individual']['last_name'],
|
||||
'email' => $user['individual']['email']
|
||||
'email' => $user['individual']['email'],
|
||||
'attributes' => [
|
||||
'role_name' => $user['role']['name'],
|
||||
'role_uuid' => $user['role']['uuid'],
|
||||
'org_name' => $user['organisation']['name'],
|
||||
'org_uuid' => $user['organisation']['uuid']
|
||||
]
|
||||
];
|
||||
$response = $this->restApiRequest('%s/admin/realms/%s/users', $newUser, 'post');
|
||||
if (!$response->isOk()) {
|
||||
|
@ -408,119 +428,78 @@ class AuthKeycloakBehavior extends Behavior
|
|||
$users[0]['id'] = $users[0]['id'][0];
|
||||
}
|
||||
$user['id'] = $users[0]['id'];
|
||||
$this->assignRolesToUser($user, $rolesParsed, $clientId);
|
||||
return true;
|
||||
}
|
||||
|
||||
private function assignRolesToUser(array $user, array $rolesParsed, string $clientId): bool
|
||||
{
|
||||
$roles = [
|
||||
[
|
||||
'id' => $rolesParsed['Role:' . $user['role']['name']],
|
||||
'name' => 'Role:' . $user['role']['name'],
|
||||
'clientRole' => true,
|
||||
'containerId' => $clientId
|
||||
],
|
||||
[
|
||||
'id' => $rolesParsed['Organisation:' . $user['organisation']['name']],
|
||||
'name' => 'Organisation:' . $user['organisation']['name'],
|
||||
'clientRole' => true,
|
||||
'containerId' => $clientId
|
||||
]
|
||||
];
|
||||
$response = $this->restApiRequest('%s/admin/realms/%s/users/' . $user['id'] . '/role-mappings/clients/' . $clientId, $roles, 'post');
|
||||
if (!$response->isOk()) {
|
||||
$this->_table->auditLogs()->insert([
|
||||
'request_action' => 'keycloakAssignRoles',
|
||||
'model' => 'User',
|
||||
'model_id' => 0,
|
||||
'model_title' => __('Failed to create assign role ({0}) in keycloak to user {1}', $user['role']['name'], $user['username']),
|
||||
'changed' => [
|
||||
'code' => $response->getStatusCode(),
|
||||
'error_body' => $response->getStringBody()
|
||||
]
|
||||
]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private function checkAndUpdateUserRoles(array $keycloakUser, array $user, string $clientId, array $rolesParsed): bool
|
||||
{
|
||||
$assignedRoles = $this->restApiRequest('%s/admin/realms/%s/users/' . $keycloakUser['id'] . '/role-mappings/clients/' . $clientId, [], 'get');
|
||||
$assignedRoles = json_decode($assignedRoles->getStringBody(), true);
|
||||
$keycloakUserRoles = Hash::extract($assignedRoles, '{n}.name');
|
||||
$assignedRolesParsed = [];
|
||||
foreach ($assignedRoles as $k => $v) {
|
||||
$assignedRolesParsed[$v['name']] = $v;
|
||||
}
|
||||
$userRoles = [
|
||||
'Organisation:' . $user['organisation']['name'] => [
|
||||
'id' => $rolesParsed['Organisation:' . $user['organisation']['name']],
|
||||
'name' => 'Organisation:' . $user['organisation']['name'],
|
||||
'clientRole' => true,
|
||||
'containerId' => $clientId
|
||||
],
|
||||
'Role:' . $user['role']['name'] => [
|
||||
'id' => $rolesParsed['Role:' . $user['role']['name']],
|
||||
'name' => 'Role:' . $user['role']['name'],
|
||||
'clientRole' => true,
|
||||
'containerId' => $clientId
|
||||
]
|
||||
];
|
||||
$toAdd = array_diff(array_keys($userRoles), $keycloakUserRoles);
|
||||
$toRemove = array_diff($keycloakUserRoles, array_keys($userRoles));
|
||||
$changed = false;
|
||||
foreach ($toRemove as $k => $role) {
|
||||
if (substr($role, 0, strlen('Organisation:')) !== 'Organisation:' && substr($role, 0, strlen('Role:')) !== 'Role:') {
|
||||
unset($toRemove[$k]);
|
||||
} else {
|
||||
$toRemove[$k] = $assignedRolesParsed[$role];
|
||||
}
|
||||
}
|
||||
if (!empty($toRemove)) {
|
||||
$toRemove = array_values($toRemove);
|
||||
$response = $this->restApiRequest('%s/admin/realms/%s/users/' . $keycloakUser['id'] . '/role-mappings/clients/' . $clientId, $toRemove, 'delete');
|
||||
if (!$response->isOk()) {
|
||||
$this->_table->auditLogs()->insert([
|
||||
'request_action' => 'keycloakDetachRole',
|
||||
'model' => 'User',
|
||||
'model_id' => 0,
|
||||
'model_title' => __('Failed to detach role ({0}) in keycloak from user {1}', $user['role']['name'], $user['username']),
|
||||
'changed' => [
|
||||
'code' => $response->getStatusCode(),
|
||||
'error_body' => $response->getStringBody()
|
||||
]
|
||||
]);
|
||||
} else {
|
||||
$changed = true;
|
||||
}
|
||||
}
|
||||
foreach ($toAdd as $k => $name) {
|
||||
$toAdd[$k] = $userRoles[$name];
|
||||
}
|
||||
if (!empty($toAdd)) {
|
||||
$toAdd = array_values($toAdd);
|
||||
$response = $this->restApiRequest('%s/admin/realms/%s/users/' . $keycloakUser['id'] . '/role-mappings/clients/' . $clientId, $toAdd, 'post');
|
||||
if (!$response->isOk()) {
|
||||
$this->_table->auditLogs()->insert([
|
||||
'request_action' => 'keycloakAttachRoles',
|
||||
'model' => 'User',
|
||||
'model_id' => 0,
|
||||
'model_title' => __('Failed to attach role ({0}) in keycloak to user {1}', $user['role']['name'], $user['username']),
|
||||
'changed' => [
|
||||
'code' => $response->getStatusCode(),
|
||||
'error_body' => $response->getStringBody()
|
||||
]
|
||||
]);
|
||||
} else {
|
||||
$changed = true;
|
||||
}
|
||||
}
|
||||
return $changed;
|
||||
return $user['id'];
|
||||
}
|
||||
|
||||
private function urlencodeEscapeForSprintf(string $input): string
|
||||
{
|
||||
return str_replace('%', '%%', $input);
|
||||
}
|
||||
|
||||
public function updateMappers(): bool
|
||||
{
|
||||
$clientId = $this->getClientId();
|
||||
$response = $this->restApiRequest('%s/admin/realms/%s/clients/' . $clientId . '/protocol-mappers/models?protocolMapper=oidc-usermodel-attribute-mapper', [], 'get');
|
||||
if ($response->isOk()) {
|
||||
$mappers = json_decode($response->getStringBody(), true);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
$enabledMappers = [];
|
||||
$defaultMappers = [
|
||||
'org_name' => 0,
|
||||
'org_uuid' => 0,
|
||||
'role_name' => 0,
|
||||
'role_uuid' => 0
|
||||
];
|
||||
$mappersToEnable = explode(',', Configure::read('keycloak.user_meta_mapping'));
|
||||
foreach ($mappers as $mapper) {
|
||||
if ($mapper['protocolMapper'] !== 'oidc-usermodel-attribute-mapper') {
|
||||
continue;
|
||||
}
|
||||
if (in_array($mapper['name'], array_keys($defaultMappers))) {
|
||||
$defaultMappers[$mapper['name']] = 1;
|
||||
continue;
|
||||
}
|
||||
$enabledMappers[$mapper['name']] = $mapper;
|
||||
}
|
||||
$payload = [];
|
||||
foreach ($mappersToEnable as $mapperToEnable) {
|
||||
$payload[] = [
|
||||
'protocol' => 'openid-connect',
|
||||
'name' => $mapperToEnable,
|
||||
'protocolMapper' => 'oidc-usermodel-attribute-mapper',
|
||||
'config' => [
|
||||
'id.token.claim' => true,
|
||||
'access.token.claim' => true,
|
||||
'userinfo.token.claim' => true,
|
||||
'user.attribute' => $mapperToEnable,
|
||||
'claim.name' => $mapperToEnable
|
||||
]
|
||||
];
|
||||
}
|
||||
foreach ($defaultMappers as $defaultMapper => $enabled) {
|
||||
if (!$enabled) {
|
||||
$payload[] = [
|
||||
'protocol' => 'openid-connect',
|
||||
'name' => $defaultMapper,
|
||||
'protocolMapper' => 'oidc-usermodel-attribute-mapper',
|
||||
'config' => [
|
||||
'id.token.claim' => true,
|
||||
'access.token.claim' => true,
|
||||
'userinfo.token.claim' => true,
|
||||
'user.attribute' => $defaultMapper,
|
||||
'claim.name' => $defaultMapper
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
if (!empty($payload)) {
|
||||
$response = $this->restApiRequest('%s/admin/realms/%s/clients/' . $clientId . '/protocol-mappers/add-models', $payload, 'post');
|
||||
if (!$response->isOk()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -68,7 +68,11 @@ class AppModel extends Entity
|
|||
public function rearrangeTags(array $tags): array
|
||||
{
|
||||
foreach ($tags as &$tag) {
|
||||
unset($tag['_joinData']);
|
||||
$tag = [
|
||||
'id' => $tag['id'],
|
||||
'name' => $tag['name'],
|
||||
'colour' => $tag['colour']
|
||||
];
|
||||
}
|
||||
return $tags;
|
||||
}
|
||||
|
@ -77,14 +81,47 @@ class AppModel extends Entity
|
|||
{
|
||||
$rearrangedAlignments = [];
|
||||
$validAlignmentTypes = ['individual', 'organisation'];
|
||||
$alignmentDataToKeep = [
|
||||
'individual' => [
|
||||
'id',
|
||||
'email'
|
||||
],
|
||||
'organisation' => [
|
||||
'id',
|
||||
'uuid',
|
||||
'name'
|
||||
]
|
||||
];
|
||||
foreach ($alignments as $alignment) {
|
||||
foreach ($validAlignmentTypes as $type) {
|
||||
foreach (array_keys($alignmentDataToKeep) as $type) {
|
||||
if (isset($alignment[$type])) {
|
||||
$alignment[$type]['type'] = $alignment['type'];
|
||||
$rearrangedAlignments[$type][] = $alignment[$type];
|
||||
$temp = [];
|
||||
foreach ($alignmentDataToKeep[$type] as $field) {
|
||||
$temp[$field] = $alignment[$type][$field];
|
||||
}
|
||||
$rearrangedAlignments[$type][] = $temp;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $rearrangedAlignments;
|
||||
}
|
||||
|
||||
public function rearrangeSimplify(array $typesToRearrange): void
|
||||
{
|
||||
if (in_array('organisation', $typesToRearrange) && isset($this->organisation)) {
|
||||
$this->organisation = [
|
||||
'id' => $this->organisation['id'],
|
||||
'name' => $this->organisation['name'],
|
||||
'uuid' => $this->organisation['uuid']
|
||||
];
|
||||
}
|
||||
if (in_array('individual', $typesToRearrange) && isset($this->individual)) {
|
||||
$this->individual = [
|
||||
'id' => $this->individual['id'],
|
||||
'email' => $this->individual['email'],
|
||||
'uuid' => $this->individual['uuid']
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,4 +7,9 @@ use Cake\ORM\Entity;
|
|||
|
||||
class EncryptionKey extends AppModel
|
||||
{
|
||||
|
||||
public function rearrangeForAPI(): void
|
||||
{
|
||||
$this->rearrangeSimplify(['organisation', 'individual']);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,12 +33,28 @@ class Individual extends AppModel
|
|||
{
|
||||
$emails = [];
|
||||
if (!empty($this->meta_fields)) {
|
||||
foreach ($this->meta_fields as $metaField) {
|
||||
if (str_contains($metaField->field, 'email')) {
|
||||
$emails[] = $metaField;
|
||||
}
|
||||
}
|
||||
foreach ($this->meta_fields as $metaField) {
|
||||
if (!empty($metaField->field) && str_contains($metaField->field, 'email')) {
|
||||
$emails[] = $metaField;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $emails;
|
||||
}
|
||||
|
||||
public function rearrangeForAPI(): void
|
||||
{
|
||||
if (!empty($this->tags)) {
|
||||
$this->tags = $this->rearrangeTags($this->tags);
|
||||
}
|
||||
if (!empty($this->alignments)) {
|
||||
$this->alignments = $this->rearrangeAlignments($this->alignments);
|
||||
}
|
||||
if (!empty($this->meta_fields)) {
|
||||
$this->rearrangeMetaFields();
|
||||
}
|
||||
if (!empty($this->MetaTemplates)) {
|
||||
unset($this->MetaTemplates);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
namespace App\Model\Entity;
|
||||
|
||||
use App\Model\Entity\AppModel;
|
||||
use Cake\ORM\Entity;
|
||||
|
||||
class PermissionLimitation extends AppModel
|
||||
{
|
||||
|
||||
}
|
|
@ -11,7 +11,7 @@ use App\Settings\SettingsProvider\UserSettingsProvider;
|
|||
|
||||
class User extends AppModel
|
||||
{
|
||||
protected $_hidden = ['password', 'confirm_password'];
|
||||
protected $_hidden = ['password', 'confirm_password', 'user_settings_by_name', 'user_settings_by_name_with_fallback', 'SettingsProvider', 'user_settings'];
|
||||
|
||||
protected $_virtual = ['user_settings_by_name', 'user_settings_by_name_with_fallback'];
|
||||
|
||||
|
@ -48,4 +48,39 @@ class User extends AppModel
|
|||
return (new DefaultPasswordHasher())->hash($password);
|
||||
}
|
||||
}
|
||||
|
||||
public function rearrangeForAPI(): void
|
||||
{
|
||||
if (!empty($this->tags)) {
|
||||
$this->tags = $this->rearrangeTags($this->tags);
|
||||
}
|
||||
if (!empty($this->meta_fields)) {
|
||||
$this->rearrangeMetaFields();
|
||||
}
|
||||
if (!empty($this->MetaTemplates)) {
|
||||
unset($this->MetaTemplates);
|
||||
}
|
||||
if (!empty($this->user_settings_by_name)) {
|
||||
$this->rearrangeUserSettings();
|
||||
}
|
||||
$this->rearrangeSimplify(['organisation', 'individual']);
|
||||
}
|
||||
|
||||
private function rearrangeUserSettings()
|
||||
{
|
||||
$settings = [];
|
||||
if (isset($this->user_settings_by_name)) {
|
||||
foreach ($this->user_settings_by_name as $setting => $data) {
|
||||
$settings[$setting] = $data['value'];
|
||||
}
|
||||
}
|
||||
if (isset($this->user_settings_by_name_with_fallback)) {
|
||||
foreach ($this->user_settings_by_name_with_fallback as $setting => $data) {
|
||||
if (!isset($settings[$setting])) {
|
||||
$settings[$setting] = $data['value'];
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->settings = $settings;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -110,4 +110,17 @@ class IndividualsTable extends AppTable
|
|||
}
|
||||
return $query->group(['Individuals.id', 'Individuals.uuid']);
|
||||
}
|
||||
|
||||
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,
|
||||
'role_id NOT IN' => array_keys($adminRoles)
|
||||
]
|
||||
)->all()->toArray();
|
||||
return array_keys($validIndividualIds);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,119 @@
|
|||
<?php
|
||||
|
||||
namespace App\Model\Table;
|
||||
|
||||
use App\Model\Table\AppTable;
|
||||
use Cake\ORM\Table;
|
||||
use Cake\Validation\Validator;
|
||||
use Cake\Error\Debugger;
|
||||
use Cake\ORM\TableRegistry;
|
||||
|
||||
class PermissionLimitationsTable extends AppTable
|
||||
{
|
||||
public function initialize(array $config): void
|
||||
{
|
||||
parent::initialize($config);
|
||||
$this->addBehavior('AuditLog');
|
||||
$this->setDisplayField('permission');
|
||||
}
|
||||
|
||||
public function validationDefault(Validator $validator): Validator
|
||||
{
|
||||
$validator
|
||||
->notEmptyString('permission')
|
||||
->notEmptyString('scope')
|
||||
->requirePresence(['permission', 'scope', 'max_occurrence'], 'create');
|
||||
return $validator;
|
||||
}
|
||||
|
||||
public function getListOfLimitations(\App\Model\Entity\User $data)
|
||||
{
|
||||
$Users = TableRegistry::getTableLocator()->get('Users');
|
||||
$ownOrgUserIds = $Users->find('list', [
|
||||
'keyField' => 'id',
|
||||
'valueField' => 'id',
|
||||
'conditions' => [
|
||||
'organisation_id' => $data['organisation_id']
|
||||
]
|
||||
])->all()->toList();
|
||||
$MetaFields = TableRegistry::getTableLocator()->get('MetaFields');
|
||||
$raw = $this->find()->select(['scope', 'permission', 'max_occurrence'])->disableHydration()->toArray();
|
||||
$limitations = [];
|
||||
foreach ($raw as $entry) {
|
||||
$limitations[$entry['permission']][$entry['scope']] = [
|
||||
'limit' => $entry['max_occurrence']
|
||||
];
|
||||
}
|
||||
foreach ($limitations as $field => $data) {
|
||||
if (isset($data['global'])) {
|
||||
$limitations[$field]['global']['current'] = $MetaFields->find('all', [
|
||||
'conditions' => [
|
||||
'scope' => 'user',
|
||||
'field' => $field
|
||||
]
|
||||
])->count();
|
||||
}
|
||||
if (isset($data['global'])) {
|
||||
$limitations[$field]['organisation']['current'] = $MetaFields->find('all', [
|
||||
'conditions' => [
|
||||
'scope' => 'user',
|
||||
'field' => $field,
|
||||
'parent_id IN' => array_values($ownOrgUserIds)
|
||||
]
|
||||
])->count();
|
||||
}
|
||||
}
|
||||
return $limitations;
|
||||
}
|
||||
|
||||
public function attachLimitations(\App\Model\Entity\User $data)
|
||||
{
|
||||
$permissionLimitations = $this->getListOfLimitations($data);
|
||||
$icons = [
|
||||
'global' => 'globe',
|
||||
'organisation' => 'sitemap'
|
||||
|
||||
];
|
||||
if (!empty($data['MetaTemplates'])) {
|
||||
foreach ($data['MetaTemplates'] as &$metaTemplate) {
|
||||
foreach ($metaTemplate['meta_template_fields'] as &$meta_template_field) {
|
||||
$boolean = $meta_template_field['type'] === 'boolean';
|
||||
foreach ($meta_template_field['metaFields'] as &$metaField) {
|
||||
if ($boolean) {
|
||||
$metaField['value'] = '<i class="fas fa-' . ((bool)$metaField['value'] ? 'check' : 'times') . '"></i>';
|
||||
$metaField['no_escaping'] = true;
|
||||
}
|
||||
if (isset($permissionLimitations[$metaField['field']])) {
|
||||
foreach ($permissionLimitations[$metaField['field']] as $scope => $value) {
|
||||
$messageType = 'warning';
|
||||
if ($value['limit'] > $value['current']) {
|
||||
$messageType = 'info';
|
||||
}
|
||||
if ($value['limit'] < $value['current']) {
|
||||
$messageType = 'danger';
|
||||
}
|
||||
if (empty($metaField[$messageType])) {
|
||||
$metaField[$messageType] = '';
|
||||
}
|
||||
$altText = __(
|
||||
'There is a limitation enforced on the number of users with this permission {0}. Currently {1} slot(s) are used up of a maximum of {2} slot(s).',
|
||||
$scope === 'global' ? __('instance wide') : __('for your organisation'),
|
||||
$value['current'],
|
||||
$value['limit']
|
||||
);
|
||||
$metaField[$messageType] .= sprintf(
|
||||
' <span title="%s"><span class="text-dark"><i class="fas fa-%s"></i>: </span>%s/%s</span>',
|
||||
$altText,
|
||||
$icons[$scope],
|
||||
$value['current'],
|
||||
$value['limit']
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
}
|
|
@ -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',
|
||||
|
@ -284,6 +263,14 @@ class CerebrateSettingsProvider extends BaseSettingsProvider
|
|||
'description' => __('family_name mapped name in keycloak'),
|
||||
'dependsOn' => 'keycloak.enabled'
|
||||
],
|
||||
'keycloak.user_meta_mapping' => [
|
||||
'name' => 'User Meta-field attribute mapping',
|
||||
'type' => 'string',
|
||||
'severity' => 'info',
|
||||
'default' => '',
|
||||
'description' => __('List of user metafields to push to keycloak as attributes. When using multiple templates, the attribute names have to be unique. Expects a comma separated list.'),
|
||||
'dependsOn' => 'keycloak.enabled'
|
||||
]
|
||||
]
|
||||
]
|
||||
],
|
||||
|
|
|
@ -23,6 +23,7 @@ class UsersTable extends AppTable
|
|||
parent::initialize($config);
|
||||
$this->addBehavior('Timestamp');
|
||||
$this->addBehavior('UUID');
|
||||
$this->addBehavior('MetaFields');
|
||||
$this->addBehavior('AuditLog');
|
||||
$this->addBehavior('NotifyAdmins', [
|
||||
'fields' => ['role_id', 'individual_id', 'organisation_id', 'disabled', 'modified'],
|
||||
|
@ -61,7 +62,9 @@ class UsersTable extends AppTable
|
|||
|
||||
public function beforeMarshal(EventInterface $event, ArrayObject $data, ArrayObject $options)
|
||||
{
|
||||
$data['username'] = trim(mb_strtolower($data['username']));
|
||||
if (isset($data['username'])) {
|
||||
$data['username'] = trim(mb_strtolower($data['username']));
|
||||
}
|
||||
}
|
||||
|
||||
public function beforeSave(EventInterface $event, EntityInterface $entity, ArrayObject $options)
|
||||
|
@ -69,9 +72,51 @@ class UsersTable extends AppTable
|
|||
if (!$entity->isNew()) {
|
||||
$success = $this->handleUserUpdateRouter($entity);
|
||||
}
|
||||
$permissionRestrictionCheck = $this->checkPermissionRestrictions($entity);
|
||||
if ($permissionRestrictionCheck !== true) {
|
||||
$entity->setErrors($permissionRestrictionCheck);
|
||||
$event->stopPropagation();
|
||||
$event->setResult(false);
|
||||
return false;
|
||||
}
|
||||
return $success;
|
||||
}
|
||||
|
||||
private function checkPermissionRestrictions(EntityInterface $entity)
|
||||
{
|
||||
if (!isset($this->PermissionLimitations)) {
|
||||
$this->PermissionLimitations = TableRegistry::get('PermissionLimitations');
|
||||
}
|
||||
$new = $entity->isNew();
|
||||
$permissions = $this->PermissionLimitations->getListOfLimitations($entity);
|
||||
foreach ($permissions as $permission_name => $permission) {
|
||||
foreach ($permission as $scope => $permission_data) {
|
||||
if (!empty($entity['meta_fields'])) {
|
||||
$enabled = false;
|
||||
foreach ($entity['meta_fields'] as $metaField) {
|
||||
if ($metaField['field'] === $permission_name) {
|
||||
$enabled = true;
|
||||
}
|
||||
}
|
||||
if (!$enabled) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
$valueToCompareTo = $permission_data['current'] + ($new ? 1 : 0);
|
||||
if ($valueToCompareTo > $permission_data['limit']) {
|
||||
return [
|
||||
$permission_name =>
|
||||
__(
|
||||
'{0} limit exceeded.',
|
||||
$scope
|
||||
)
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private function initAuthBehaviors()
|
||||
{
|
||||
if (!empty(Configure::read('keycloak'))) {
|
||||
|
@ -82,6 +127,7 @@ class UsersTable extends AppTable
|
|||
public function validationDefault(Validator $validator): Validator
|
||||
{
|
||||
$validator
|
||||
->setStopOnFailure()
|
||||
->requirePresence(['password'], 'create')
|
||||
->add('password', [
|
||||
'password_complexity' => [
|
||||
|
@ -202,10 +248,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'));
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
"version": "1.6",
|
||||
"version": "1.8",
|
||||
"application": "Cerebrate"
|
||||
}
|
||||
|
|
|
@ -811,7 +811,10 @@ class BoostrapListTable extends BootstrapGeneric
|
|||
private function genCell($field = [])
|
||||
{
|
||||
if (isset($field['raw'])) {
|
||||
$cellContent = h($field['raw']);
|
||||
$cellContent = $field['raw'];
|
||||
if (empty($field['no_escaping'])) {
|
||||
$field['raw'] = h($field['raw']);
|
||||
}
|
||||
} else if (isset($field['formatter'])) {
|
||||
$cellContent = $field['formatter']($this->getValueFromObject($field), $this->item);
|
||||
} else if (isset($field['type'])) {
|
||||
|
@ -822,6 +825,11 @@ class BoostrapListTable extends BootstrapGeneric
|
|||
} else {
|
||||
$cellContent = h($this->getValueFromObject($field));
|
||||
}
|
||||
foreach (['info', 'warning', 'danger'] as $message_type) {
|
||||
if (!empty($field[$message_type])) {
|
||||
$cellContent .= sprintf(' <span class="text-%s">%s</span>', $message_type, $field[$message_type]);
|
||||
}
|
||||
}
|
||||
return $this->genNode('td', [
|
||||
'class' => [
|
||||
'col-8 col-sm-10',
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
array(
|
||||
'field' => 'tag_list',
|
||||
'type' => 'tags',
|
||||
'requirements' => $this->request->getParam('action') === 'edit'
|
||||
'requirements' => ($this->request->getParam('action') === 'edit' && $loggedUser['role']['perm_admin'])
|
||||
),
|
||||
),
|
||||
'submit' => array(
|
||||
|
|
|
@ -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',
|
||||
|
@ -81,12 +87,25 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
[
|
||||
'open_modal' => '/individuals/edit/[onclick_params_data_path]',
|
||||
'modal_params_data_path' => 'id',
|
||||
'icon' => 'edit'
|
||||
'icon' => 'edit',
|
||||
'complex_requirement' => [
|
||||
'function' => function ($row, $options) use ($loggedUser, $editableIds) {
|
||||
if ($loggedUser['role']['perm_admin'] || ($editableIds && in_array($row['id'], $editableIds))) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
]
|
||||
],
|
||||
[
|
||||
'open_modal' => '/individuals/delete/[onclick_params_data_path]',
|
||||
'modal_params_data_path' => 'id',
|
||||
'icon' => 'trash'
|
||||
'icon' => 'trash',
|
||||
'complex_requirement' => [
|
||||
'function' => function ($row, $options) use ($loggedUser) {
|
||||
return (bool)$loggedUser['role']['perm_admin'];
|
||||
}
|
||||
]
|
||||
],
|
||||
]
|
||||
]
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
echo $this->element('genericElements/Form/genericForm', [
|
||||
'data' => [
|
||||
'description' => __(
|
||||
'Add a limitation of how many users can have the given permission. The scope applies the limitation globally or for a given organisation.
|
||||
Permissions can be valid role permissions or any user meta field.
|
||||
An example: perm_misp global limit 500, organisation limit 10 would ensure that there are a maximum of 500 MISP admitted users on the instance, limiting the number of users to 10 / org.'
|
||||
),
|
||||
'model' => 'PermissionLimitation',
|
||||
'fields' => [
|
||||
[
|
||||
'field' => 'scope',
|
||||
'type' => 'dropdown',
|
||||
'label' => 'Scope',
|
||||
'options' => [
|
||||
'global' => 'global',
|
||||
'organisation' => 'organisation'
|
||||
]
|
||||
],
|
||||
[
|
||||
'field' => 'permission'
|
||||
],
|
||||
[
|
||||
'field' => 'max_occurrence',
|
||||
'label' => 'Limit'
|
||||
],
|
||||
[
|
||||
'field' => 'comment',
|
||||
'label' => 'Comment'
|
||||
]
|
||||
],
|
||||
'submit' => [
|
||||
'action' => $this->request->getParam('action')
|
||||
]
|
||||
]
|
||||
]);
|
||||
?>
|
||||
</div>
|
|
@ -0,0 +1,79 @@
|
|||
<?php
|
||||
echo $this->element('genericElements/IndexTable/index_table', [
|
||||
'data' => [
|
||||
'data' => $data,
|
||||
'top_bar' => [
|
||||
'children' => [
|
||||
[
|
||||
'type' => 'simple',
|
||||
'children' => [
|
||||
'data' => [
|
||||
'type' => 'simple',
|
||||
'text' => __('Add permission limitation'),
|
||||
'class' => 'btn btn-primary',
|
||||
'popover_url' => '/PermissionLimitations/add'
|
||||
]
|
||||
]
|
||||
],
|
||||
[
|
||||
'type' => 'search',
|
||||
'button' => __('Search'),
|
||||
'placeholder' => __('Enter value to search'),
|
||||
'data' => '',
|
||||
'searchKey' => 'value'
|
||||
]
|
||||
]
|
||||
],
|
||||
'fields' => [
|
||||
[
|
||||
'name' => '#',
|
||||
'sort' => 'id',
|
||||
'data_path' => 'id',
|
||||
],
|
||||
[
|
||||
'name' => __('Scope'),
|
||||
'sort' => 'scope',
|
||||
'data_path' => 'scope',
|
||||
],
|
||||
[
|
||||
'name' => __('Permission'),
|
||||
'sort' => 'permission',
|
||||
'data_path' => 'permission'
|
||||
],
|
||||
[
|
||||
'name' => __('Limit'),
|
||||
'sort' => 'max_occurrence',
|
||||
'data_path' => 'max_occurrence'
|
||||
],
|
||||
[
|
||||
'name' => __('Comment'),
|
||||
'sort' => 'comment',
|
||||
'data_path' => 'comment'
|
||||
]
|
||||
],
|
||||
'title' => __('Permission Limitations Index'),
|
||||
'description' => __('A list of configurable user roles. Create or modify user access roles based on the settings below.'),
|
||||
'pull' => 'right',
|
||||
'actions' => [
|
||||
[
|
||||
'url' => '/permissionLimitations/view',
|
||||
'url_params_data_paths' => ['id'],
|
||||
'icon' => 'eye'
|
||||
],
|
||||
[
|
||||
'open_modal' => '/permissionLimitations/edit/[onclick_params_data_path]',
|
||||
'modal_params_data_path' => 'id',
|
||||
'icon' => 'edit',
|
||||
'requirement' => !empty($loggedUser['role']['perm_admin'])
|
||||
],
|
||||
[
|
||||
'open_modal' => '/permissionLimitations/delete/[onclick_params_data_path]',
|
||||
'modal_params_data_path' => 'id',
|
||||
'icon' => 'trash',
|
||||
'requirement' => !empty($loggedUser['role']['perm_admin'])
|
||||
],
|
||||
]
|
||||
]
|
||||
]);
|
||||
echo '</div>';
|
||||
?>
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
echo $this->element(
|
||||
'/genericElements/SingleViews/single_view',
|
||||
[
|
||||
'data' => $entity,
|
||||
'fields' => [
|
||||
[
|
||||
'key' => __('ID'),
|
||||
'path' => 'id'
|
||||
],
|
||||
[
|
||||
'key' => __('Scope'),
|
||||
'path' => 'scope'
|
||||
],
|
||||
[
|
||||
'key' => __('Permission'),
|
||||
'path' => 'permission'
|
||||
],
|
||||
[
|
||||
'key' => __('Limit'),
|
||||
'path' => 'limit'
|
||||
],
|
||||
[
|
||||
'key' => __('Comment'),
|
||||
'path' => 'comment'
|
||||
]
|
||||
],
|
||||
'children' => []
|
||||
]
|
||||
);
|
|
@ -21,6 +21,9 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
'placeholder' => __('Enter value to search'),
|
||||
'data' => '',
|
||||
'searchKey' => 'value'
|
||||
],
|
||||
[
|
||||
'type' => 'table_action',
|
||||
]
|
||||
]
|
||||
],
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
<?php
|
||||
$kcurl = $keycloakConfig['provider']['baseUrl'] . '/realms/' . $keycloakConfig['provider']['realm'] . '/account/#/security/signingin';
|
||||
if (!empty($keycloakConfig['enabled'])) {
|
||||
$kcurl = $keycloakConfig['provider']['baseUrl'] . '/realms/' . $keycloakConfig['provider']['realm'] . '/account/#/security/signingin';
|
||||
}
|
||||
$fields = [
|
||||
[
|
||||
'key' => __('ID'),
|
||||
|
|
|
@ -112,6 +112,7 @@
|
|||
echo $this->Bootstrap->modal([
|
||||
'title' => empty($data['title']) ? sprintf('%s %s', $actionName, $modelName) : h($data['title']),
|
||||
'bodyHtml' => $this->element('genericElements/Form/formLayouts/formRaw', [
|
||||
'data' => $data,
|
||||
'formCreate' => $formCreate,
|
||||
'ajaxFlashMessage' => $ajaxFlashMessage,
|
||||
'fieldsString' => $fieldsString,
|
||||
|
@ -124,6 +125,7 @@
|
|||
]);
|
||||
} else if (!empty($raw)) {
|
||||
echo $this->element('genericElements/Form/formLayouts/formDefault', [
|
||||
'data' => $data,
|
||||
'actionName' => $actionName,
|
||||
'modelName' => $modelName,
|
||||
'submitButtonData' => $submitButtonData,
|
||||
|
@ -135,6 +137,7 @@
|
|||
]);
|
||||
} else {
|
||||
echo $this->element('genericElements/Form/formLayouts/formDefault', [
|
||||
'data' => $data,
|
||||
'actionName' => $actionName,
|
||||
'modelName' => $modelName,
|
||||
'submitButtonData' => $submitButtonData,
|
||||
|
|
|
@ -41,6 +41,9 @@ foreach ($metaTemplate->meta_template_fields as $metaTemplateField) {
|
|||
} else {
|
||||
$fieldData['field'] = sprintf('MetaTemplates.%s.meta_template_fields.%s.metaFields.%s.value', $metaField->meta_template_id, $metaField->meta_template_field_id, array_key_first($metaTemplateField->metaFields));
|
||||
}
|
||||
if ($metaTemplateField->type === 'boolean') {
|
||||
$fieldData['type'] = 'checkbox';
|
||||
}
|
||||
$this->Form->setTemplates($backupTemplates);
|
||||
$fieldsHtml .= $this->element(
|
||||
'genericElements/Form/fieldScaffold',
|
||||
|
@ -72,6 +75,9 @@ foreach ($metaTemplate->meta_template_fields as $metaTemplateField) {
|
|||
if ($metaTemplateField->formType === 'dropdown') {
|
||||
$fieldData = array_merge_recursive($fieldData, $metaTemplateField->formOptions);
|
||||
}
|
||||
// if ($metaTemplateField->type === 'boolean') {
|
||||
// $fieldData['type'] = 'checkbox';
|
||||
// }
|
||||
$fieldsHtml .= $this->element(
|
||||
'genericElements/Form/fieldScaffold',
|
||||
[
|
||||
|
|
|
@ -1,11 +1,25 @@
|
|||
<?php
|
||||
if (!empty($row['user'])) {
|
||||
$userId = $this->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('<br />', $links);
|
||||
}
|
||||
|
||||
?>
|
||||
|
|
|
@ -27,4 +27,9 @@ if (!empty($field['url'])) {
|
|||
} else if (empty($field['raw'])) {
|
||||
$string = h($string);
|
||||
}
|
||||
foreach (['info', 'warning', 'danger'] as $message_type) {
|
||||
if (!empty($field[$message_type])) {
|
||||
$string .= sprintf(' (<span class="text-%s">%s</span>)', $message_type, $field[$message_type]);
|
||||
}
|
||||
}
|
||||
echo $string;
|
||||
|
|
|
@ -17,7 +17,10 @@ foreach($data['MetaTemplates'] as $metaTemplate) {
|
|||
foreach ($metaTemplateField->metaFields as $metaField) {
|
||||
$fields[] = [
|
||||
'key' => !$labelPrintedOnce ? $metaField->field : '',
|
||||
'raw' => $metaField->value
|
||||
'raw' => $metaField->value,
|
||||
'warning' => $metaField->warning ?? null,
|
||||
'info' => $metaField->info ?? null,
|
||||
'danger' => $metaField->danger ?? null
|
||||
];
|
||||
$labelPrintedOnce = true;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue