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');
|
$this->loadModel('Users');
|
||||||
$results = $this->fetchTable()->syncWithKeycloak();
|
$results = $this->fetchTable()->syncWithKeycloak();
|
||||||
$tableData = [
|
$tableData = [
|
||||||
['Changes to', 'Count']
|
['Modification type', 'Count', 'Affected users']
|
||||||
];
|
];
|
||||||
foreach ($results as $k => $v) {
|
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->out(__('Sync done. See the results below.'));
|
||||||
$io->helper('Table')->output($tableData);
|
$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' => [
|
'Individuals' => [
|
||||||
'add' => ['perm_admin'],
|
'add' => ['perm_admin'],
|
||||||
'delete' => ['perm_admin'],
|
'delete' => ['perm_admin'],
|
||||||
'edit' => ['perm_admin'],
|
'edit' => ['perm_admin', 'perm_org_admin'],
|
||||||
'filtering' => ['*'],
|
'filtering' => ['*'],
|
||||||
'index' => ['*'],
|
'index' => ['*'],
|
||||||
'tag' => ['perm_tagger'],
|
'tag' => ['perm_tagger'],
|
||||||
|
@ -169,6 +169,13 @@ class ACLComponent extends Component
|
||||||
'Pages' => [
|
'Pages' => [
|
||||||
'display' => ['*']
|
'display' => ['*']
|
||||||
],
|
],
|
||||||
|
'PermissionLimitations' => [
|
||||||
|
"index" => ['*'],
|
||||||
|
"add" => ['perm_admin'],
|
||||||
|
"view" => ['*'],
|
||||||
|
"edit" => ['perm_admin'],
|
||||||
|
"delete" => ['perm_admin']
|
||||||
|
],
|
||||||
'Roles' => [
|
'Roles' => [
|
||||||
'add' => ['perm_admin'],
|
'add' => ['perm_admin'],
|
||||||
'delete' => ['perm_admin'],
|
'delete' => ['perm_admin'],
|
||||||
|
|
|
@ -12,15 +12,19 @@ use Cake\Core\Configure;
|
||||||
use Cake\Core\Configure\Engine\PhpConfig;
|
use Cake\Core\Configure\Engine\PhpConfig;
|
||||||
use Cake\Utility\Inflector;
|
use Cake\Utility\Inflector;
|
||||||
use Cake\Routing\Router;
|
use Cake\Routing\Router;
|
||||||
|
use Cake\Collection\Collection;
|
||||||
|
|
||||||
class APIRearrangeComponent extends Component
|
class APIRearrangeComponent extends Component
|
||||||
{
|
{
|
||||||
public function rearrangeForAPI(object $data): object
|
public function rearrangeForAPI(object $data)
|
||||||
{
|
{
|
||||||
if (is_subclass_of($data, 'Iterator')) {
|
if (is_subclass_of($data, 'Iterator')) {
|
||||||
$data->each(function ($value, $key) {
|
$newData = [];
|
||||||
|
$data->each(function ($value, $key) use (&$newData) {
|
||||||
$value->rearrangeForAPI();
|
$value->rearrangeForAPI();
|
||||||
|
$newData[] = $value;
|
||||||
});
|
});
|
||||||
|
return new Collection($newData);
|
||||||
} else {
|
} else {
|
||||||
$data->rearrangeForAPI();
|
$data->rearrangeForAPI();
|
||||||
}
|
}
|
||||||
|
|
|
@ -76,6 +76,9 @@ class CRUDComponent extends Component
|
||||||
$query->order($options['order']);
|
$query->order($options['order']);
|
||||||
}
|
}
|
||||||
if ($this->Controller->ParamHandler->isRest()) {
|
if ($this->Controller->ParamHandler->isRest()) {
|
||||||
|
if ($this->metaFieldsSupported()) {
|
||||||
|
$query = $this->includeRequestedMetaFields($query);
|
||||||
|
}
|
||||||
$data = $query->all();
|
$data = $query->all();
|
||||||
if (isset($options['hidden'])) {
|
if (isset($options['hidden'])) {
|
||||||
$data->each(function($value, $key) use ($options) {
|
$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');
|
$this->Controller->restResponsePayload = $this->RestResponse->viewData($data, 'json');
|
||||||
} else {
|
} else {
|
||||||
if ($this->metaFieldsSupported()) {
|
if ($this->metaFieldsSupported()) {
|
||||||
|
@ -560,7 +569,14 @@ class CRUDComponent extends Component
|
||||||
$savedData = $this->Table->save($data);
|
$savedData = $this->Table->save($data);
|
||||||
if ($savedData !== false) {
|
if ($savedData !== false) {
|
||||||
if ($this->metaFieldsSupported() && !empty($metaFieldsToDelete)) {
|
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'])) {
|
if (isset($params['afterSave'])) {
|
||||||
$params['afterSave']($data);
|
$params['afterSave']($data);
|
||||||
|
@ -673,12 +689,15 @@ class CRUDComponent extends Component
|
||||||
if (!empty($pruneEmptyDisabled) && !$metaTemplate->enabled) {
|
if (!empty($pruneEmptyDisabled) && !$metaTemplate->enabled) {
|
||||||
unset($metaTemplates[$i]);
|
unset($metaTemplates[$i]);
|
||||||
}
|
}
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
$newestTemplate = $this->MetaTemplates->getNewestVersion($metaTemplate);
|
$newestTemplate = $this->MetaTemplates->getNewestVersion($metaTemplate);
|
||||||
if (!empty($newestTemplate) && !empty($metaTemplates[$i])) {
|
if (!empty($newestTemplate) && !empty($metaTemplates[$i])) {
|
||||||
$metaTemplates[$i]['hasNewerVersion'] = $newestTemplate;
|
$metaTemplates[$i]['hasNewerVersion'] = $newestTemplate;
|
||||||
}
|
}
|
||||||
|
$metaTemplates[$metaTemplate->id]['meta_template_fields'] = $metaTemplates[$metaTemplate->id]['meta_template_fields'];
|
||||||
}
|
}
|
||||||
|
$metaTemplates = $metaTemplates;
|
||||||
$data['MetaTemplates'] = $metaTemplates;
|
$data['MetaTemplates'] = $metaTemplates;
|
||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
|
@ -793,12 +812,6 @@ class CRUDComponent extends Component
|
||||||
if (empty($data)) {
|
if (empty($data)) {
|
||||||
throw new NotFoundException(__('Invalid {0}.', $this->ObjectAlias));
|
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('id', $data['id']);
|
||||||
$this->Controller->set('data', $data);
|
$this->Controller->set('data', $data);
|
||||||
$this->Controller->set('bulkEnabled', false);
|
$this->Controller->set('bulkEnabled', false);
|
||||||
|
|
|
@ -123,6 +123,11 @@ class Sidemenu {
|
||||||
'url' => '/auditLogs/index',
|
'url' => '/auditLogs/index',
|
||||||
'icon' => 'history',
|
'icon' => 'history',
|
||||||
],
|
],
|
||||||
|
'PermissionLimitations' => [
|
||||||
|
'label' => __('Permission Limitations'),
|
||||||
|
'url' => '/permissionLimitations/index',
|
||||||
|
'icon' => 'jedi',
|
||||||
|
],
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
'API' => [
|
'API' => [
|
||||||
|
|
|
@ -20,17 +20,30 @@ class IndividualsController extends AppController
|
||||||
|
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
|
$currentUser = $this->ACL->getUser();
|
||||||
|
$orgAdmin = !$currentUser['role']['perm_admin'] && $currentUser['role']['perm_org_admin'];
|
||||||
$this->CRUD->index([
|
$this->CRUD->index([
|
||||||
'filters' => $this->filterFields,
|
'filters' => $this->filterFields,
|
||||||
'quickFilters' => $this->quickFilterFields,
|
'quickFilters' => $this->quickFilterFields,
|
||||||
'quickFilterForMetaField' => ['enabled' => true, 'wildcard_search' => true],
|
'quickFilterForMetaField' => ['enabled' => true, 'wildcard_search' => true],
|
||||||
'contain' => $this->containFields,
|
'contain' => $this->containFields,
|
||||||
'statisticsFields' => $this->statisticsFields,
|
'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();
|
$responsePayload = $this->CRUD->getResponsePayload();
|
||||||
if (!empty($responsePayload)) {
|
if (!empty($responsePayload)) {
|
||||||
return $responsePayload;
|
return $responsePayload;
|
||||||
}
|
}
|
||||||
|
$editableIds = null;
|
||||||
|
if ($orgAdmin) {
|
||||||
|
$editableIds = $this->Individuals->getValidIndividualsToEdit($currentUser);
|
||||||
|
}
|
||||||
|
$this->set('editableIds', $editableIds);
|
||||||
$this->set('alignmentScope', 'individuals');
|
$this->set('alignmentScope', 'individuals');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,7 +72,29 @@ class IndividualsController extends AppController
|
||||||
|
|
||||||
public function edit($id)
|
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();
|
$responsePayload = $this->CRUD->getResponsePayload();
|
||||||
if (!empty($responsePayload)) {
|
if (!empty($responsePayload)) {
|
||||||
return $responsePayload;
|
return $responsePayload;
|
||||||
|
|
|
@ -99,6 +99,13 @@ class OrganisationsController extends AppController
|
||||||
|
|
||||||
public function edit($id)
|
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);
|
$this->CRUD->edit($id);
|
||||||
$responsePayload = $this->CRUD->getResponsePayload();
|
$responsePayload = $this->CRUD->getResponsePayload();
|
||||||
if (!empty($responsePayload)) {
|
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.'));
|
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;
|
return $data;
|
||||||
|
},
|
||||||
|
'afterSave' => function($data) {
|
||||||
|
if (Configure::read('keycloak.enabled')) {
|
||||||
|
$this->Users->enrollUserRouter($data);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
$responsePayload = $this->CRUD->getResponsePayload();
|
$responsePayload = $this->CRUD->getResponsePayload();
|
||||||
|
@ -136,7 +140,11 @@ class UsersController extends AppController
|
||||||
$id = $this->ACL->getUser()['id'];
|
$id = $this->ACL->getUser()['id'];
|
||||||
}
|
}
|
||||||
$this->CRUD->view($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();
|
$responsePayload = $this->CRUD->getResponsePayload();
|
||||||
if (!empty($responsePayload)) {
|
if (!empty($responsePayload)) {
|
||||||
|
@ -278,16 +286,21 @@ class UsersController extends AppController
|
||||||
'beforeSave' => function($data) use ($currentUser, $validRoles) {
|
'beforeSave' => function($data) use ($currentUser, $validRoles) {
|
||||||
if (!$currentUser['role']['perm_admin']) {
|
if (!$currentUser['role']['perm_admin']) {
|
||||||
if ($data['organisation_id'] !== $currentUser['organisation_id']) {
|
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))) {
|
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;
|
return $data;
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
$this->CRUD->delete($id);
|
$this->CRUD->delete($id, $params);
|
||||||
$responsePayload = $this->CRUD->getResponsePayload();
|
$responsePayload = $this->CRUD->getResponsePayload();
|
||||||
if (!empty($responsePayload)) {
|
if (!empty($responsePayload)) {
|
||||||
return $responsePayload;
|
return $responsePayload;
|
||||||
|
@ -354,6 +367,9 @@ class UsersController extends AppController
|
||||||
]);
|
]);
|
||||||
$this->Authentication->logout();
|
$this->Authentication->logout();
|
||||||
$this->Flash->success(__('Goodbye.'));
|
$this->Flash->success(__('Goodbye.'));
|
||||||
|
if (Configure::read('keycloak.enabled')) {
|
||||||
|
$this->redirect($this->Users->keyCloaklogout());
|
||||||
|
}
|
||||||
return $this->redirect(\Cake\Routing\Router::url('/users/login'));
|
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
|
// You can access the profile using $user->social_profile
|
||||||
|
|
||||||
$this->getTableLocator()->get('Users')->saveOrFail($user);
|
// $this->getTableLocator()->get('Users')->saveOrFail($user);
|
||||||
|
|
||||||
return $user;
|
return $user;
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,11 +18,9 @@ use Cake\Http\Exception\NotFoundException;
|
||||||
|
|
||||||
class AuthKeycloakBehavior extends Behavior
|
class AuthKeycloakBehavior extends Behavior
|
||||||
{
|
{
|
||||||
|
|
||||||
public function getUser(EntityInterface $profile, Session $session)
|
public function getUser(EntityInterface $profile, Session $session)
|
||||||
{
|
{
|
||||||
$userId = $session->read('Auth.User.id');
|
$userId = $session->read('Auth.User.id');
|
||||||
$userId = null;
|
|
||||||
if ($userId) {
|
if ($userId) {
|
||||||
return $this->_table->get($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
|
public function enrollUser($data): bool
|
||||||
{
|
{
|
||||||
$roleConditions = [
|
$roleConditions = [
|
||||||
'id' => $data['role_id']
|
'id' => $data['role_id']
|
||||||
];
|
];
|
||||||
if (!empty(Configure::read('keycloak.user_management.actions'))) {
|
|
||||||
$roleConditions['name'] = Configure::read('keycloak.default_role_name');
|
|
||||||
}
|
|
||||||
$user = [
|
$user = [
|
||||||
'username' => $data['username'],
|
'username' => $data['username'],
|
||||||
'disabled' => false,
|
'disabled' => false,
|
||||||
|
@ -110,12 +161,8 @@ class AuthKeycloakBehavior extends Behavior
|
||||||
)->first()
|
)->first()
|
||||||
];
|
];
|
||||||
$clientId = $this->getClientId();
|
$clientId = $this->getClientId();
|
||||||
$roles = $this->getAllRoles($clientId);
|
$newUserId = $this->createUser($user, $clientId);
|
||||||
$rolesParsed = [];
|
if (!$newUserId) {
|
||||||
foreach ($roles as $role) {
|
|
||||||
$rolesParsed[$role['name']] = $role['id'];
|
|
||||||
}
|
|
||||||
if (!$this->createUser($user, $clientId, $rolesParsed)) {
|
|
||||||
$logChange = [
|
$logChange = [
|
||||||
'username' => $user['username'],
|
'username' => $user['username'],
|
||||||
'individual_id' => $user['individual']['id'],
|
'individual_id' => $user['individual']['id'],
|
||||||
|
@ -141,6 +188,21 @@ class AuthKeycloakBehavior extends Behavior
|
||||||
'model_title' => __('Successful Keycloak enrollment for user {0}', $user['username']),
|
'model_title' => __('Successful Keycloak enrollment for user {0}', $user['username']),
|
||||||
'changed' => $logChange
|
'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;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -149,9 +211,9 @@ class AuthKeycloakBehavior extends Behavior
|
||||||
* handleUserUpdate
|
* handleUserUpdate
|
||||||
*
|
*
|
||||||
* @param \App\Model\Entity\User $user
|
* @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([
|
$user['individual'] = $this->_table->Individuals->find()->where([
|
||||||
'id' => $user['individual_id']
|
'id' => $user['individual_id']
|
||||||
|
@ -166,7 +228,19 @@ class AuthKeycloakBehavior extends Behavior
|
||||||
$users = [$user->toArray()];
|
$users = [$user->toArray()];
|
||||||
$clientId = $this->getClientId();
|
$clientId = $this->getClientId();
|
||||||
$changes = $this->syncUsers($users, $clientId);
|
$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()
|
private function getAdminAccessToken()
|
||||||
|
@ -208,9 +282,8 @@ class AuthKeycloakBehavior extends Behavior
|
||||||
|
|
||||||
public function syncWithKeycloak(): array
|
public function syncWithKeycloak(): array
|
||||||
{
|
{
|
||||||
|
$this->updateMappers();
|
||||||
$results = [];
|
$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(
|
$data['Users'] = $this->_table->find()->contain(['Individuals', 'Organisations', 'Roles'])->select(
|
||||||
[
|
[
|
||||||
'id',
|
'id',
|
||||||
|
@ -228,86 +301,15 @@ class AuthKeycloakBehavior extends Behavior
|
||||||
]
|
]
|
||||||
)->disableHydration()->toArray();
|
)->disableHydration()->toArray();
|
||||||
$clientId = $this->getClientId();
|
$clientId = $this->getClientId();
|
||||||
$results = [];
|
return $this->syncUsers($data['Users'], $clientId);
|
||||||
$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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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');
|
$response = $this->restApiRequest('%s/admin/realms/%s/users', [], 'get');
|
||||||
$keycloakUsers = json_decode($response->getStringBody(), true);
|
$keycloakUsers = json_decode($response->getStringBody(), true);
|
||||||
$keycloakUsersParsed = [];
|
$keycloakUsersParsed = [];
|
||||||
foreach ($keycloakUsers as $u) {
|
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']] = [
|
$keycloakUsersParsed[$u['username']] = [
|
||||||
'id' => $u['id'],
|
'id' => $u['id'],
|
||||||
'username' => $u['username'],
|
'username' => $u['username'],
|
||||||
|
@ -315,26 +317,28 @@ class AuthKeycloakBehavior extends Behavior
|
||||||
'firstName' => $u['firstName'],
|
'firstName' => $u['firstName'],
|
||||||
'lastName' => $u['lastName'],
|
'lastName' => $u['lastName'],
|
||||||
'email' => $u['email'],
|
'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) {
|
foreach ($users as &$user) {
|
||||||
$changed = false;
|
$changed = false;
|
||||||
if (empty($keycloakUsersParsed[$user['username']])) {
|
if (empty($keycloakUsersParsed[$user['username']])) {
|
||||||
if ($this->createUser($user, $clientId, $rolesParsed)) {
|
if ($this->createUser($user, $clientId)) {
|
||||||
$changes = true;
|
$changes['created'][] = $user['username'];
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if ($this->checkAndUpdateUser($keycloakUsersParsed[$user['username']], $user)) {
|
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;
|
return $changes;
|
||||||
|
@ -346,13 +350,23 @@ class AuthKeycloakBehavior extends Behavior
|
||||||
$keycloakUser['enabled'] == $user['disabled'] ||
|
$keycloakUser['enabled'] == $user['disabled'] ||
|
||||||
$keycloakUser['firstName'] !== $user['individual']['first_name'] ||
|
$keycloakUser['firstName'] !== $user['individual']['first_name'] ||
|
||||||
$keycloakUser['lastName'] !== $user['individual']['last_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 = [
|
$change = [
|
||||||
'enabled' => !$user['disabled'],
|
'enabled' => !$user['disabled'],
|
||||||
'firstName' => $user['individual']['first_name'],
|
'firstName' => $user['individual']['first_name'],
|
||||||
'lastName' => $user['individual']['last_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/' . $keycloakUser['id'], $change, 'put');
|
$response = $this->restApiRequest('%s/admin/realms/%s/users/' . $keycloakUser['id'], $change, 'put');
|
||||||
if (!$response->isOk()) {
|
if (!$response->isOk()) {
|
||||||
|
@ -373,14 +387,20 @@ class AuthKeycloakBehavior extends Behavior
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function createUser(array $user, string $clientId, array $rolesParsed): bool
|
private function createUser(array $user, string $clientId)
|
||||||
{
|
{
|
||||||
$newUser = [
|
$newUser = [
|
||||||
'username' => $user['username'],
|
'username' => $user['username'],
|
||||||
'enabled' => !$user['disabled'],
|
'enabled' => !$user['disabled'],
|
||||||
'firstName' => $user['individual']['first_name'],
|
'firstName' => $user['individual']['first_name'],
|
||||||
'lastName' => $user['individual']['last_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');
|
$response = $this->restApiRequest('%s/admin/realms/%s/users', $newUser, 'post');
|
||||||
if (!$response->isOk()) {
|
if (!$response->isOk()) {
|
||||||
|
@ -408,119 +428,78 @@ class AuthKeycloakBehavior extends Behavior
|
||||||
$users[0]['id'] = $users[0]['id'][0];
|
$users[0]['id'] = $users[0]['id'][0];
|
||||||
}
|
}
|
||||||
$user['id'] = $users[0]['id'];
|
$user['id'] = $users[0]['id'];
|
||||||
$this->assignRolesToUser($user, $rolesParsed, $clientId);
|
return $user['id'];
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function urlencodeEscapeForSprintf(string $input): string
|
private function urlencodeEscapeForSprintf(string $input): string
|
||||||
{
|
{
|
||||||
return str_replace('%', '%%', $input);
|
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
|
public function rearrangeTags(array $tags): array
|
||||||
{
|
{
|
||||||
foreach ($tags as &$tag) {
|
foreach ($tags as &$tag) {
|
||||||
unset($tag['_joinData']);
|
$tag = [
|
||||||
|
'id' => $tag['id'],
|
||||||
|
'name' => $tag['name'],
|
||||||
|
'colour' => $tag['colour']
|
||||||
|
];
|
||||||
}
|
}
|
||||||
return $tags;
|
return $tags;
|
||||||
}
|
}
|
||||||
|
@ -77,14 +81,47 @@ class AppModel extends Entity
|
||||||
{
|
{
|
||||||
$rearrangedAlignments = [];
|
$rearrangedAlignments = [];
|
||||||
$validAlignmentTypes = ['individual', 'organisation'];
|
$validAlignmentTypes = ['individual', 'organisation'];
|
||||||
|
$alignmentDataToKeep = [
|
||||||
|
'individual' => [
|
||||||
|
'id',
|
||||||
|
'email'
|
||||||
|
],
|
||||||
|
'organisation' => [
|
||||||
|
'id',
|
||||||
|
'uuid',
|
||||||
|
'name'
|
||||||
|
]
|
||||||
|
];
|
||||||
foreach ($alignments as $alignment) {
|
foreach ($alignments as $alignment) {
|
||||||
foreach ($validAlignmentTypes as $type) {
|
foreach (array_keys($alignmentDataToKeep) as $type) {
|
||||||
if (isset($alignment[$type])) {
|
if (isset($alignment[$type])) {
|
||||||
$alignment[$type]['type'] = $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;
|
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
|
class EncryptionKey extends AppModel
|
||||||
{
|
{
|
||||||
|
|
||||||
|
public function rearrangeForAPI(): void
|
||||||
|
{
|
||||||
|
$this->rearrangeSimplify(['organisation', 'individual']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,12 +33,28 @@ class Individual extends AppModel
|
||||||
{
|
{
|
||||||
$emails = [];
|
$emails = [];
|
||||||
if (!empty($this->meta_fields)) {
|
if (!empty($this->meta_fields)) {
|
||||||
foreach ($this->meta_fields as $metaField) {
|
foreach ($this->meta_fields as $metaField) {
|
||||||
if (str_contains($metaField->field, 'email')) {
|
if (!empty($metaField->field) && str_contains($metaField->field, 'email')) {
|
||||||
$emails[] = $metaField;
|
$emails[] = $metaField;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return $emails;
|
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
|
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'];
|
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);
|
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']);
|
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;
|
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' => [
|
'keycloak.screw' => [
|
||||||
'name' => 'Screw',
|
'name' => 'Screw',
|
||||||
'type' => 'string',
|
'type' => 'string',
|
||||||
|
@ -284,6 +263,14 @@ class CerebrateSettingsProvider extends BaseSettingsProvider
|
||||||
'description' => __('family_name mapped name in keycloak'),
|
'description' => __('family_name mapped name in keycloak'),
|
||||||
'dependsOn' => 'keycloak.enabled'
|
'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);
|
parent::initialize($config);
|
||||||
$this->addBehavior('Timestamp');
|
$this->addBehavior('Timestamp');
|
||||||
$this->addBehavior('UUID');
|
$this->addBehavior('UUID');
|
||||||
|
$this->addBehavior('MetaFields');
|
||||||
$this->addBehavior('AuditLog');
|
$this->addBehavior('AuditLog');
|
||||||
$this->addBehavior('NotifyAdmins', [
|
$this->addBehavior('NotifyAdmins', [
|
||||||
'fields' => ['role_id', 'individual_id', 'organisation_id', 'disabled', 'modified'],
|
'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)
|
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)
|
public function beforeSave(EventInterface $event, EntityInterface $entity, ArrayObject $options)
|
||||||
|
@ -69,9 +72,51 @@ class UsersTable extends AppTable
|
||||||
if (!$entity->isNew()) {
|
if (!$entity->isNew()) {
|
||||||
$success = $this->handleUserUpdateRouter($entity);
|
$success = $this->handleUserUpdateRouter($entity);
|
||||||
}
|
}
|
||||||
|
$permissionRestrictionCheck = $this->checkPermissionRestrictions($entity);
|
||||||
|
if ($permissionRestrictionCheck !== true) {
|
||||||
|
$entity->setErrors($permissionRestrictionCheck);
|
||||||
|
$event->stopPropagation();
|
||||||
|
$event->setResult(false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return $success;
|
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()
|
private function initAuthBehaviors()
|
||||||
{
|
{
|
||||||
if (!empty(Configure::read('keycloak'))) {
|
if (!empty(Configure::read('keycloak'))) {
|
||||||
|
@ -82,6 +127,7 @@ class UsersTable extends AppTable
|
||||||
public function validationDefault(Validator $validator): Validator
|
public function validationDefault(Validator $validator): Validator
|
||||||
{
|
{
|
||||||
$validator
|
$validator
|
||||||
|
->setStopOnFailure()
|
||||||
->requirePresence(['password'], 'create')
|
->requirePresence(['password'], 'create')
|
||||||
->add('password', [
|
->add('password', [
|
||||||
'password_complexity' => [
|
'password_complexity' => [
|
||||||
|
@ -202,10 +248,6 @@ class UsersTable extends AppTable
|
||||||
{
|
{
|
||||||
$role = $this->Roles->find()->where(['name' => $user['role']['name']])->first();
|
$role = $this->Roles->find()->where(['name' => $user['role']['name']])->first();
|
||||||
if (empty($role)) {
|
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)) {
|
if (empty($role)) {
|
||||||
throw new NotFoundException(__('Invalid role'));
|
throw new NotFoundException(__('Invalid role'));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{
|
{
|
||||||
"version": "1.6",
|
"version": "1.8",
|
||||||
"application": "Cerebrate"
|
"application": "Cerebrate"
|
||||||
}
|
}
|
||||||
|
|
|
@ -811,7 +811,10 @@ class BoostrapListTable extends BootstrapGeneric
|
||||||
private function genCell($field = [])
|
private function genCell($field = [])
|
||||||
{
|
{
|
||||||
if (isset($field['raw'])) {
|
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'])) {
|
} else if (isset($field['formatter'])) {
|
||||||
$cellContent = $field['formatter']($this->getValueFromObject($field), $this->item);
|
$cellContent = $field['formatter']($this->getValueFromObject($field), $this->item);
|
||||||
} else if (isset($field['type'])) {
|
} else if (isset($field['type'])) {
|
||||||
|
@ -822,6 +825,11 @@ class BoostrapListTable extends BootstrapGeneric
|
||||||
} else {
|
} else {
|
||||||
$cellContent = h($this->getValueFromObject($field));
|
$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', [
|
return $this->genNode('td', [
|
||||||
'class' => [
|
'class' => [
|
||||||
'col-8 col-sm-10',
|
'col-8 col-sm-10',
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
array(
|
array(
|
||||||
'field' => 'tag_list',
|
'field' => 'tag_list',
|
||||||
'type' => 'tags',
|
'type' => 'tags',
|
||||||
'requirements' => $this->request->getParam('action') === 'edit'
|
'requirements' => ($this->request->getParam('action') === 'edit' && $loggedUser['role']['perm_admin'])
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
'submit' => array(
|
'submit' => array(
|
||||||
|
|
|
@ -52,6 +52,12 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
||||||
'sort' => 'last_name',
|
'sort' => 'last_name',
|
||||||
'data_path' => 'last_name',
|
'data_path' => 'last_name',
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'name' => __('Associated User(s)'),
|
||||||
|
'sort' => 'user',
|
||||||
|
'data_path' => 'user',
|
||||||
|
'element' => 'user'
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'name' => __('Alignments'),
|
'name' => __('Alignments'),
|
||||||
'data_path' => 'alignments',
|
'data_path' => 'alignments',
|
||||||
|
@ -81,12 +87,25 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
||||||
[
|
[
|
||||||
'open_modal' => '/individuals/edit/[onclick_params_data_path]',
|
'open_modal' => '/individuals/edit/[onclick_params_data_path]',
|
||||||
'modal_params_data_path' => 'id',
|
'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]',
|
'open_modal' => '/individuals/delete/[onclick_params_data_path]',
|
||||||
'modal_params_data_path' => 'id',
|
'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'),
|
'placeholder' => __('Enter value to search'),
|
||||||
'data' => '',
|
'data' => '',
|
||||||
'searchKey' => 'value'
|
'searchKey' => 'value'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'type' => 'table_action',
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
<?php
|
<?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 = [
|
$fields = [
|
||||||
[
|
[
|
||||||
'key' => __('ID'),
|
'key' => __('ID'),
|
||||||
|
|
|
@ -112,6 +112,7 @@
|
||||||
echo $this->Bootstrap->modal([
|
echo $this->Bootstrap->modal([
|
||||||
'title' => empty($data['title']) ? sprintf('%s %s', $actionName, $modelName) : h($data['title']),
|
'title' => empty($data['title']) ? sprintf('%s %s', $actionName, $modelName) : h($data['title']),
|
||||||
'bodyHtml' => $this->element('genericElements/Form/formLayouts/formRaw', [
|
'bodyHtml' => $this->element('genericElements/Form/formLayouts/formRaw', [
|
||||||
|
'data' => $data,
|
||||||
'formCreate' => $formCreate,
|
'formCreate' => $formCreate,
|
||||||
'ajaxFlashMessage' => $ajaxFlashMessage,
|
'ajaxFlashMessage' => $ajaxFlashMessage,
|
||||||
'fieldsString' => $fieldsString,
|
'fieldsString' => $fieldsString,
|
||||||
|
@ -124,6 +125,7 @@
|
||||||
]);
|
]);
|
||||||
} else if (!empty($raw)) {
|
} else if (!empty($raw)) {
|
||||||
echo $this->element('genericElements/Form/formLayouts/formDefault', [
|
echo $this->element('genericElements/Form/formLayouts/formDefault', [
|
||||||
|
'data' => $data,
|
||||||
'actionName' => $actionName,
|
'actionName' => $actionName,
|
||||||
'modelName' => $modelName,
|
'modelName' => $modelName,
|
||||||
'submitButtonData' => $submitButtonData,
|
'submitButtonData' => $submitButtonData,
|
||||||
|
@ -135,6 +137,7 @@
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
echo $this->element('genericElements/Form/formLayouts/formDefault', [
|
echo $this->element('genericElements/Form/formLayouts/formDefault', [
|
||||||
|
'data' => $data,
|
||||||
'actionName' => $actionName,
|
'actionName' => $actionName,
|
||||||
'modelName' => $modelName,
|
'modelName' => $modelName,
|
||||||
'submitButtonData' => $submitButtonData,
|
'submitButtonData' => $submitButtonData,
|
||||||
|
|
|
@ -41,6 +41,9 @@ foreach ($metaTemplate->meta_template_fields as $metaTemplateField) {
|
||||||
} else {
|
} 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));
|
$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);
|
$this->Form->setTemplates($backupTemplates);
|
||||||
$fieldsHtml .= $this->element(
|
$fieldsHtml .= $this->element(
|
||||||
'genericElements/Form/fieldScaffold',
|
'genericElements/Form/fieldScaffold',
|
||||||
|
@ -72,6 +75,9 @@ foreach ($metaTemplate->meta_template_fields as $metaTemplateField) {
|
||||||
if ($metaTemplateField->formType === 'dropdown') {
|
if ($metaTemplateField->formType === 'dropdown') {
|
||||||
$fieldData = array_merge_recursive($fieldData, $metaTemplateField->formOptions);
|
$fieldData = array_merge_recursive($fieldData, $metaTemplateField->formOptions);
|
||||||
}
|
}
|
||||||
|
// if ($metaTemplateField->type === 'boolean') {
|
||||||
|
// $fieldData['type'] = 'checkbox';
|
||||||
|
// }
|
||||||
$fieldsHtml .= $this->element(
|
$fieldsHtml .= $this->element(
|
||||||
'genericElements/Form/fieldScaffold',
|
'genericElements/Form/fieldScaffold',
|
||||||
[
|
[
|
||||||
|
|
|
@ -1,11 +1,25 @@
|
||||||
<?php
|
<?php
|
||||||
if (!empty($row['user'])) {
|
if (!empty($row['user'])) {
|
||||||
$userId = $this->Hash->extract($row, 'user.id')[0];
|
if (isset($row['user']['id'])) {
|
||||||
$userName = $this->Hash->extract($row, 'user.username')[0];
|
$users = [$row['user']];
|
||||||
echo $this->Html->link(
|
} else {
|
||||||
h($userName),
|
$users = $row['user'];
|
||||||
['controller' => 'users', 'action' => 'view', $userId]
|
}
|
||||||
);
|
$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'])) {
|
} else if (empty($field['raw'])) {
|
||||||
$string = h($string);
|
$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;
|
echo $string;
|
||||||
|
|
|
@ -17,7 +17,10 @@ foreach($data['MetaTemplates'] as $metaTemplate) {
|
||||||
foreach ($metaTemplateField->metaFields as $metaField) {
|
foreach ($metaTemplateField->metaFields as $metaField) {
|
||||||
$fields[] = [
|
$fields[] = [
|
||||||
'key' => !$labelPrintedOnce ? $metaField->field : '',
|
'key' => !$labelPrintedOnce ? $metaField->field : '',
|
||||||
'raw' => $metaField->value
|
'raw' => $metaField->value,
|
||||||
|
'warning' => $metaField->warning ?? null,
|
||||||
|
'info' => $metaField->info ?? null,
|
||||||
|
'danger' => $metaField->danger ?? null
|
||||||
];
|
];
|
||||||
$labelPrintedOnce = true;
|
$labelPrintedOnce = true;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue