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

pull/93/head
Sami Mokaddem 2022-01-20 09:00:45 +01:00
commit 324ac1ce40
No known key found for this signature in database
GPG Key ID: 164C473F627A06FA
177 changed files with 13566 additions and 4849 deletions

52
.github/workflows/docker-publish.yml vendored Normal file
View File

@ -0,0 +1,52 @@
name: Docker
on:
push:
branches: [ main ]
tags: [ 'v*.*' ]
pull_request:
branches: [ main ]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@v1
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v3
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# https://github.com/docker/build-push-action
- name: Build and push Docker image
uses: docker/build-push-action@v2
with:
file: docker/Dockerfile
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
"COMPOSER_VERSION=2.1.5"
"PHP_VERSION=7.4"
"DEBIAN_RELEASE=buster"

4
.gitignore vendored
View File

@ -7,3 +7,7 @@ vendor
webroot/theme/node_modules
webroot/scss/*.css
.vscode
docker/run/
.phpunit.result.cache
config.json
phpunit.xml

View File

@ -1,8 +1,9 @@
## Requirements
An Ubuntu server (18.04/20.04 should both work fine) - though other linux installations should work too.
- apache2, mysql/mariadb, sqlite need to be installed and running
- apache2 (or nginx), mysql/mariadb, sqlite need to be installed and running
- php extensions for intl, mysql, sqlite3, mbstring, xml need to be installed and running
- php extention for curl (not required but makes composer run a little faster)
- composer
## Network requirements
@ -17,8 +18,16 @@ Cerebrate communicates via HTTPS so in order to be able to connect to other cere
## Cerebrate installation instructions
It should be sufficient to issue the following command to install the dependencies:
- for apache
```bash
sudo apt install apache2 mariadb-server git composer php-intl php-mbstring php-dom php-xml unzip php-ldap php-sqlite3 sqlite libapache2-mod-php php-mysql
sudo apt install apache2 mariadb-server git composer php-intl php-mbstring php-dom php-xml unzip php-ldap php-sqlite3 php-curl sqlite libapache2-mod-php php-mysql
```
- for nginx
```bash
sudo apt install nginx mariadb-server git composer php-intl php-mbstring php-dom php-xml unzip php-ldap php-sqlite3 sqlite php-fpm php-curl php-mysql
```
Clone this repository (for example into /var/www/cerebrate)
@ -32,12 +41,19 @@ sudo -u www-data git clone https://github.com/cerebrate-project/cerebrate.git /v
Run composer
```bash
sudo mkdir -p /var/www/.composer
sudo chown www-data:www-data /var/www/.composer
cd /var/www/cerebrate
sudo -u www-data composer install
sudo -H -u www-data composer install
```
Create a database for cerebrate
With a fresh install of Ubuntu sudo to the (system) root user before logging in as the mysql root
```Bash
sudo -i mysql -u root
```
From SQL shell:
```mysql
mysql
@ -46,6 +62,7 @@ CREATE USER 'cerebrate'@'localhost' IDENTIFIED BY 'YOUR_PASSWORD';
GRANT USAGE ON *.* to cerebrate@localhost;
GRANT ALL PRIVILEGES ON cerebrate.* to cerebrate@localhost;
FLUSH PRIVILEGES;
QUIT;
```
Or from Bash:
@ -57,12 +74,6 @@ sudo mysql -e "GRANT ALL PRIVILEGES ON cerebrate.* to cerebrate@localhost;"
sudo mysql -e "FLUSH PRIVILEGES;"
```
Load the default table structure into the database
```bash
sudo mysql -u cerebrate -p cerebrate < /var/www/cerebrate/INSTALL/mysql.sql
```
create your local configuration and set the db credentials
```bash
@ -71,7 +82,7 @@ sudo -u www-data cp -a /var/www/cerebrate/config/config.example.json /var/www/ce
sudo -u www-data vim /var/www/cerebrate/config/app_local.php
```
mod_rewrite needs to be enabled:
mod_rewrite needs to be enabled if __using apache__:
```bash
sudo a2enmod rewrite
@ -91,9 +102,9 @@ This would be, when following the steps above:
Run the database schema migrations
```bash
/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
sudo -u www-data /var/www/cerebrate/bin/cake migrations migrate
sudo -u www-data /var/www/cerebrate/bin/cake migrations migrate -p tags
sudo -u www-data /var/www/cerebrate/bin/cake migrations migrate -p ADmad/SocialAuth
```
Clean cakephp caches
@ -104,16 +115,31 @@ sudo rm /var/www/cerebrate/tmp/cache/persistent/*
Create an apache config file for cerebrate / ssh key and point the document root to /var/www/cerebrate/webroot and you're good to go
For development installs the following can be done:
For development installs the following can be done for either apache or nginx:
```bash
# Apache
# This configuration is purely meant for local installations for development / testing
# Using HTTP on an unhardened apache is by no means meant to be used in any production environment
sudo cp /var/www/cerebrate/INSTALL/cerebrate_dev.conf /etc/apache2/sites-available/
sudo ln -s /etc/apache2/sites-available/cerebrate_dev.conf /etc/apache2/sites-enabled/
sudo cp /var/www/cerebrate/INSTALL/cerebrate_apache_dev.conf /etc/apache2/sites-available/
sudo ln -s /etc/apache2/sites-available/cerebrate_apache_dev.conf /etc/apache2/sites-enabled/
sudo service apache2 restart
```
OR
```bash
# NGINX
# This configuration is purely meant for local installations for development / testing
# Using HTTP on an unhardened apache is by no means meant to be used in any production environment
sudo cp /var/www/cerebrate/INSTALL/cerebrate_nginx.conf /etc/nginx/sites-available/
sudo ln -s /etc/nginx/sites-available/cerebrate_nginx.conf /etc/nginx/sites-enabled/
sudo systemctl disable apache2 # may be required if apache is using port
sudo service nginx restart
sudo systemctl enable nginx
```
Now you can point your browser to: http://localhost:8000
To log in use the default credentials below:

View File

View File

@ -0,0 +1,37 @@
## Cerebrate Nginx Web Server Configuration
server {
listen 8000;
# listen 443 ssl;
root /var/www/cerebrate/webroot;
error_log /var/log/nginx/cerebrate_error.log;
access_log /var/log/nginx/cerebrate_access.log;
# Add index.php to the list if you are using PHP
index index.html index.htm index.nginx-debian.html index.php;
server_name _;
# Configure Crypto Keys/Certificates/DH
# If enabling this setting change port above, should also set the server name
# ssl_certificate /path/to/ssl/cert;
# ssl_certificate_key /path/to/ssl/cert;
# enable HSTS
# add_header Strict-Transport-Security "max-age=15768000; includeSubdomains";
# add_header X-Frame-Options SAMEORIGIN;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
}
}

View File

@ -1,402 +0,0 @@
-- MySQL dump 10.16 Distrib 10.1.44-MariaDB, for debian-linux-gnu (x86_64)
--
-- Host: localhost Database: cerebrate
-- ------------------------------------------------------
-- Server version 10.1.44-MariaDB-0ubuntu0.18.04.1
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
--
-- Table structure for table `alignment_tags`
--
DROP TABLE IF EXISTS `alignment_tags`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `alignment_tags` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`alignment_id` int(10) unsigned NOT NULL,
`tag_id` int(10) unsigned NOT NULL,
PRIMARY KEY (`id`),
KEY `alignment_id` (`alignment_id`),
KEY `tag_id` (`tag_id`),
CONSTRAINT `alignment_tags_ibfk_1` FOREIGN KEY (`alignment_id`) REFERENCES `alignments` (`id`),
CONSTRAINT `alignment_tags_ibfk_10` FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`),
CONSTRAINT `alignment_tags_ibfk_11` FOREIGN KEY (`alignment_id`) REFERENCES `alignments` (`id`),
CONSTRAINT `alignment_tags_ibfk_12` FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`),
CONSTRAINT `alignment_tags_ibfk_2` FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`),
CONSTRAINT `alignment_tags_ibfk_3` FOREIGN KEY (`alignment_id`) REFERENCES `alignments` (`id`),
CONSTRAINT `alignment_tags_ibfk_4` FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`),
CONSTRAINT `alignment_tags_ibfk_5` FOREIGN KEY (`alignment_id`) REFERENCES `alignments` (`id`),
CONSTRAINT `alignment_tags_ibfk_6` FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`),
CONSTRAINT `alignment_tags_ibfk_7` FOREIGN KEY (`alignment_id`) REFERENCES `alignments` (`id`),
CONSTRAINT `alignment_tags_ibfk_8` FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`),
CONSTRAINT `alignment_tags_ibfk_9` FOREIGN KEY (`alignment_id`) REFERENCES `alignments` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `alignments`
--
DROP TABLE IF EXISTS `alignments`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `alignments` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`individual_id` int(10) unsigned NOT NULL,
`organisation_id` int(10) unsigned NOT NULL,
`type` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT 'member',
PRIMARY KEY (`id`),
KEY `individual_id` (`individual_id`),
KEY `organisation_id` (`organisation_id`),
CONSTRAINT `alignments_ibfk_1` FOREIGN KEY (`individual_id`) REFERENCES `individuals` (`id`),
CONSTRAINT `alignments_ibfk_2` FOREIGN KEY (`organisation_id`) REFERENCES `organisations` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `auth_keys`
--
DROP TABLE IF EXISTS `auth_keys`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `auth_keys` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`uuid` varchar(40) COLLATE utf8mb4_unicode_ci NOT NULL,
`authkey` varchar(72) CHARACTER SET ascii DEFAULT NULL,
`authkey_start` varchar(4) CHARACTER SET ascii DEFAULT NULL,
`authkey_end` varchar(4) CHARACTER SET ascii DEFAULT NULL,
`created` int(10) unsigned NOT NULL,
`expiration` int(10) unsigned NOT NULL,
`user_id` int(10) unsigned NOT NULL,
`comment` text COLLATE utf8mb4_unicode_ci,
PRIMARY KEY (`id`),
KEY `authkey_start` (`authkey_start`),
KEY `authkey_end` (`authkey_end`),
KEY `created` (`created`),
KEY `expiration` (`expiration`),
KEY `user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `broods`
--
DROP TABLE IF EXISTS `broods`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `broods` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`uuid` varchar(40) CHARACTER SET ascii DEFAULT NULL,
`name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,
`url` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,
`description` text COLLATE utf8mb4_unicode_ci,
`organisation_id` int(10) unsigned NOT NULL,
`trusted` tinyint(1) DEFAULT NULL,
`pull` tinyint(1) DEFAULT NULL,
`skip_proxy` tinyint(1) DEFAULT NULL,
`authkey` varchar(40) CHARACTER SET ascii DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `uuid` (`uuid`),
KEY `name` (`name`),
KEY `url` (`url`),
KEY `authkey` (`authkey`),
KEY `organisation_id` (`organisation_id`),
FOREIGN KEY (`organisation_id`) REFERENCES `organisations` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `encryption_keys`
--
DROP TABLE IF EXISTS `encryption_keys`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `encryption_keys` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`uuid` varchar(40) CHARACTER SET ascii DEFAULT NULL,
`type` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,
`encryption_key` text COLLATE utf8mb4_unicode_ci,
`revoked` tinyint(1) DEFAULT NULL,
`expires` int(10) unsigned DEFAULT NULL,
`owner_id` int(10) unsigned DEFAULT NULL,
`owner_type` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL,
PRIMARY KEY (`id`),
KEY `uuid` (`uuid`),
KEY `type` (`type`),
KEY `expires` (`expires`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `individual_encryption_keys`
--
DROP TABLE IF EXISTS `individual_encryption_keys`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `individual_encryption_keys` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`individual_id` int(10) unsigned NOT NULL,
`encryption_key_id` int(10) unsigned NOT NULL,
PRIMARY KEY (`id`),
KEY `individual_id` (`individual_id`),
KEY `encryption_key_id` (`encryption_key_id`),
CONSTRAINT `individual_encryption_keys_ibfk_1` FOREIGN KEY (`individual_id`) REFERENCES `individuals` (`id`),
CONSTRAINT `individual_encryption_keys_ibfk_2` FOREIGN KEY (`encryption_key_id`) REFERENCES `encryption_keys` (`id`),
CONSTRAINT `individual_encryption_keys_ibfk_3` FOREIGN KEY (`individual_id`) REFERENCES `individuals` (`id`),
CONSTRAINT `individual_encryption_keys_ibfk_4` FOREIGN KEY (`encryption_key_id`) REFERENCES `encryption_keys` (`id`),
CONSTRAINT `individual_encryption_keys_ibfk_5` FOREIGN KEY (`individual_id`) REFERENCES `individuals` (`id`),
CONSTRAINT `individual_encryption_keys_ibfk_6` FOREIGN KEY (`encryption_key_id`) REFERENCES `encryption_keys` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `individuals`
--
DROP TABLE IF EXISTS `individuals`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `individuals` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`uuid` varchar(40) CHARACTER SET ascii DEFAULT NULL,
`email` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,
`first_name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,
`last_name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,
`position` text COLLATE utf8mb4_unicode_ci,
PRIMARY KEY (`id`),
KEY `uuid` (`uuid`),
KEY `email` (`email`),
KEY `first_name` (`first_name`),
KEY `last_name` (`last_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `local_tools`
--
DROP TABLE IF EXISTS `local_tools`;
CREATE TABLE `local_tools` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,
`connector` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,
`settings` text COLLATE utf8mb4_unicode_ci,
`exposed` tinyint(1) NOT NULL,
`description` text COLLATE utf8mb4_unicode_ci,
PRIMARY KEY (`id`),
KEY `name` (`name`),
KEY `connector` (`connector`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
-- Table structure for table `organisation_encryption_keys`
--
DROP TABLE IF EXISTS `organisation_encryption_keys`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `organisation_encryption_keys` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`organisation_id` int(10) unsigned NOT NULL,
`encryption_key_id` int(10) unsigned NOT NULL,
PRIMARY KEY (`id`),
KEY `organisation_id` (`organisation_id`),
KEY `encryption_key_id` (`encryption_key_id`),
CONSTRAINT `organisation_encryption_keys_ibfk_1` FOREIGN KEY (`organisation_id`) REFERENCES `organisations` (`id`),
CONSTRAINT `organisation_encryption_keys_ibfk_2` FOREIGN KEY (`encryption_key_id`) REFERENCES `encryption_keys` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `organisations`
--
DROP TABLE IF EXISTS `organisations`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `organisations` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`uuid` varchar(40) CHARACTER SET ascii DEFAULT NULL,
`name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,
`url` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`nationality` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`sector` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`type` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`contacts` text COLLATE utf8mb4_unicode_ci,
PRIMARY KEY (`id`),
KEY `uuid` (`uuid`),
KEY `name` (`name`),
KEY `url` (`url`),
KEY `nationality` (`nationality`),
KEY `sector` (`sector`),
KEY `type` (`type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `roles`
--
DROP TABLE IF EXISTS `roles`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `roles` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`uuid` varchar(40) CHARACTER SET ascii DEFAULT NULL,
`name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,
`is_default` tinyint(1) DEFAULT NULL,
`perm_admin` tinyint(1) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `name` (`name`),
KEY `uuid` (`uuid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `tags`
--
DROP TABLE IF EXISTS `tags`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `tags` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,
`description` text COLLATE utf8mb4_unicode_ci,
`colour` varchar(6) CHARACTER SET ascii NOT NULL,
PRIMARY KEY (`id`),
KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `users`
--
DROP TABLE IF EXISTS `users`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `users` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`uuid` varchar(40) CHARACTER SET ascii DEFAULT NULL,
`username` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`password` varchar(191) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`role_id` int(11) unsigned NOT NULL,
`individual_id` int(11) unsigned NOT NULL,
`disabled` tinyint(1) DEFAULT '0',
PRIMARY KEY (`id`),
KEY `uuid` (`uuid`),
KEY `role_id` (`role_id`),
KEY `individual_id` (`individual_id`),
CONSTRAINT `users_ibfk_1` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`),
CONSTRAINT `users_ibfk_2` FOREIGN KEY (`individual_id`) REFERENCES `individuals` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
CREATE TABLE `sharing_groups` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`uuid` varchar(40) CHARACTER SET ascii DEFAULT NULL,
`name` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,
`releasability` text DEFAULT NULL,
`description` text DEFAULT NULL,
`organisation_id` int(10) unsigned NOT NULL,
`user_id` int(10) unsigned NOT NULL,
`active` tinyint(1) DEFAULT '1',
`local` tinyint(1) DEFAULT '1',
PRIMARY KEY (`id`),
KEY `uuid` (`uuid`),
KEY `user_id` (`user_id`),
KEY `organisation_id` (`organisation_id`),
KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE `sgo` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`sharing_group_id` int(10) unsigned NOT NULL,
`organisation_id` int(10) unsigned NOT NULL,
`deleted` tinyint(1) DEFAULT 0,
PRIMARY KEY (`id`),
KEY `sharing_group_id` (`sharing_group_id`),
KEY `organisation_id` (`organisation_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE `meta_fields` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`scope` varchar(191) NOT NULL,
`parent_id` int(10) unsigned NOT NULL,
`field` varchar(191) NOT NULL,
`value` varchar(191) NOT NULL,
`uuid` varchar(40) CHARACTER SET ascii DEFAULT NULL,
`meta_template_id` int(10) unsigned NOT NULL,
`meta_template_field_id` int(10) unsigned NOT NULL,
`is_default` tinyint(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
KEY `scope` (`scope`),
KEY `uuid` (`uuid`),
KEY `parent_id` (`parent_id`),
KEY `field` (`field`),
KEY `value` (`value`),
KEY `meta_template_id` (`meta_template_id`),
KEY `meta_template_field_id` (`meta_template_field_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE `meta_templates` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`scope` varchar(191) NOT NULL,
`name` varchar(191) NOT NULL,
`namespace` varchar(191) NOT NULL,
`description` text,
`version` varchar(191) NOT NULL,
`uuid` varchar(40) CHARACTER SET ascii,
`source` varchar(191),
`enabled` tinyint(1) DEFAULT 0,
`is_default` tinyint(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
KEY `scope` (`scope`),
KEY `source` (`source`),
KEY `name` (`name`),
KEY `namespace` (`namespace`),
KEY `version` (`version`),
KEY `uuid` (`uuid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE `meta_template_fields` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`field` varchar(191) NOT NULL,
`type` varchar(191) NOT NULL,
`meta_template_id` int(10) unsigned NOT NULL,
`regex` text,
`multiple` tinyint(1) DEFAULT 0,
`enabled` tinyint(1) DEFAULT 0,
PRIMARY KEY (`id`),
CONSTRAINT `meta_template_id` FOREIGN KEY (`meta_template_id`) REFERENCES `meta_templates` (`id`),
KEY `field` (`field`),
KEY `type` (`type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-- Dump completed on 2020-06-22 14:30:02

View File

@ -1,24 +1,31 @@
# cerebrate
The Cerebrate Sync Platform core software. Cerebrate is an open-source platform meant to act as a trusted contact information provider and interconnection orchestrator for other security tools.
Cerebrate is an [open-source platform](https://github.com/cerebrate-project) meant to act as a trusted contact information provider and interconnection orchestrator for other security tools (such as [MISP](https://www.misp-project.org/)).
It is currently being built under the MeliCERTes v2 project and is heavily work in progress.
# Features
# Current features
- Advanced repository to manage individuals and organisations;
- Key store for public encryption and signing cryptographic keys (e.g. PGP);
- Distributed synchronisation model where multiple Cerebrate instances can be interconnected amongst organisations and/or departments;
- Management of individuals and their affiliations to each organisations;
- Advanced API and CLI to integrate with existing tools (e.g. importing existing directory information);
- Dynamic model for creating new organisational structures;
- Support existing organisational structures such as [FIRST.org](https://www.first.org/) directory, EU [CSIRTs network](https://csirtsnetwork.eu/);
- Local tooling interconnection to easily connect existing tools with their native protocols;
- Repository of organisations and individuals
- Maintain signing and encryption keys
- Maintain affiliations between organisations and individuals
Cerebrate is developed in the scope of the MeliCERTes v2 project.
## Screenshots
![Dashboard](https://www.cerebrate-project.org/assets/images/screenshots/Screenshot%20from%202021-10-19%2016-31-56.png)
List of individuals along with their affiliations
![List of individuals](/documentation/images/individuals.png)
![List of individuals](https://www.cerebrate-project.org/assets/images/screenshots/Screenshot%20from%202021-10-19%2016-32-35.png)
Adding organisations
![Adding an organisation](/documentation/images/add_org.png)
![Adding an organisation](https://www.cerebrate-project.org/assets/images/screenshots/Screenshot%20from%202021-10-19%2016-33-04.png)
Everything is available via the API, here an example of a search query for all international organisations in the DB.
@ -28,6 +35,10 @@ Managing public keys and assigning them to users both for communication and vali
![Encryption key management](/documentation/images/add_encryption_key.png)
Dynamic model for creating new organisation structre
![Meta Field Templates](https://www.cerebrate-project.org/assets/images/screenshots/Screenshot%20from%202021-10-19%2016-38-21.png)
# Requirements and installation
The platform is built on CakePHP 4 along with Bootstrap 4 and shares parts of the code-base with [MISP](https://www.github.com/MISP).
@ -45,6 +56,7 @@ For installation via docker, refer to the [cerebrate-docker](https://github.com/
~~~~
The software is released under the AGPLv3.
Copyright (C) 2019, 2020 Andras Iklody
Copyright (C) 2019, 2021 Andras Iklody
Copyright (C) 2020-2021 Sami Mokaddem
Copyright (C) CIRCL - Computer Incident Response Center Luxembourg
~~~~

View File

@ -9,7 +9,7 @@
"admad/cakephp-social-auth": "^1.1",
"cakephp/authentication": "^2.0",
"cakephp/authorization": "^2.0",
"cakephp/cakephp": "^4.0",
"cakephp/cakephp": "^4.3",
"cakephp/migrations": "^3.0",
"cakephp/plugin-installer": "^1.2",
"erusev/parsedown": "^1.7",
@ -19,9 +19,12 @@
"cakephp/bake": "^2.0.3",
"cakephp/cakephp-codesniffer": "~4.0.0",
"cakephp/debug_kit": "^4.0",
"fzaninotto/faker": "^1.9",
"josegonzalez/dotenv": "^3.2",
"league/openapi-psr7-validator": "^0.16.4",
"phpunit/phpunit": "^8.5",
"psy/psysh": "@stable"
"psy/psysh": "@stable",
"wiremock-php/wiremock-php": "^2.32"
},
"suggest": {
"markstory/asset_compress": "An asset compression plugin which provides file concatenation and a flexible filter system for preprocessing and minification.",
@ -44,7 +47,6 @@
"scripts": {
"post-install-cmd": "App\\Console\\Installer::postInstall",
"post-create-project-cmd": "App\\Console\\Installer::postInstall",
"post-autoload-dump": "Cake\\Composer\\Installer\\PluginInstaller::postAutoloadDump",
"check": [
"@test",
"@cs-check"
@ -52,7 +54,11 @@
"cs-check": "phpcs --colors -p --standard=vendor/cakephp/cakephp-codesniffer/CakePHP src/ tests/",
"cs-fix": "phpcbf --colors --standard=vendor/cakephp/cakephp-codesniffer/CakePHP src/ tests/",
"stan": "phpstan analyse src/",
"test": "phpunit --colors=always"
"test": [
"sh ./tests/Helper/wiremock/start.sh",
"phpunit",
"sh ./tests/Helper/wiremock/stop.sh"
]
},
"prefer-stable": true,
"config": {

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,96 @@
<?php
declare(strict_types=1);
use Migrations\AbstractMigration;
final class AuditLogs 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 $autoId = false; // turn off automatic `id` column create. We want it to be `int(10) unsigned`
public function change(): void
{
$exists = $this->hasTable('audit_logs');
if (!$exists) {
$table = $this->table('audit_logs', [
'signed' => false,
'collation' => 'utf8mb4_unicode_ci'
]);
$table
->addColumn('id', 'integer', [
'autoIncrement' => true,
'limit' => 10,
'signed' => false,
])
->addPrimaryKey('id')
->addColumn('user_id', 'integer', [
'default' => null,
'null' => true,
'signed' => false,
'length' => 10
])
->addColumn('authkey_id', 'integer', [
'default' => null,
'null' => true,
'signed' => false,
'length' => 10
])
->addColumn('request_ip', 'varbinary', [
'default' => null,
'null' => true,
'length' => 16
])
->addColumn('request_type', 'boolean', [
'null' => false
])
->addColumn('request_id', 'integer', [
'default' => null,
'null' => true,
'signed' => false,
'length' => 10
])
->addColumn('request_action', 'string', [
'null' => false,
'length' => 20
])
->addColumn('model', 'string', [
'null' => false,
'length' => 80
])
->addColumn('model_id', 'integer', [
'default' => null,
'null' => true,
'signed' => false,
'length' => 10
])
->addColumn('model_title', 'text', [
'default' => null,
'null' => true
])
->addColumn('change', 'blob', [
])
->addColumn('created', 'datetime', [
'default' => null,
'null' => false,
])
->addIndex('user_id')
->addIndex('request_ip')
->addIndex('model')
->addIndex('model_id')
->addIndex('request_action')
->addIndex('created');
$table->create();
}
}
}

View File

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
use Migrations\AbstractMigration;
final class UserOrg 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->table('users')->hasColumn('organisation_id');
if (!$exists) {
$this->table('users')
->addColumn('organisation_id', 'integer', [
'default' => null,
'null' => true,
'signed' => false,
'length' => 10
])
->addIndex('organisation_id')
->update();
}
$q1 = $this->getQueryBuilder();
$org_id = $q1->select(['min(id)'])->from('organisations')->execute()->fetchAll()[0][0];
if (!empty($org_id)) {
$q2 = $this->getQueryBuilder();
$q2->update('users')
->set('organisation_id', $org_id)
->where(['organisation_id IS NULL'])
->execute();
}
}
}

View File

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
use Migrations\AbstractMigration;
final class AuditChanged 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->table('audit_logs')->hasColumn('change');
if ($exists) {
$this->table('audit_logs')
->renameColumn('change', 'changed')
->update();
}
}
}

View File

@ -5,6 +5,16 @@
* Note: It is not recommended to commit files with credentials such as app_local.php
* into source code version control.
*/
// set the baseurl here if you want to set it manually
$baseurl = env('CEREBRATE_BASEURL', false);
// Do not modify the this block
$temp = parse_url($baseurl);
$base = empty($temp['path']) ? false : $temp['path'];
// end of block
return [
/*
* Debug Level:
@ -89,4 +99,8 @@ return [
'url' => env('EMAIL_TRANSPORT_DEFAULT_URL', null),
],
],
'Cerebrate' => [
'open' => [],
'dark' => 0
]
];

1
debian/install vendored
View File

@ -7,4 +7,3 @@ webroot /usr/share/php-cerebrate
config /usr/share/php-cerebrate
debian/cerebrate.local.conf /etc/apache2/sites-available/
debian/config.php /etc/cerebrate/
INSTALL/mysql.sql => /usr/share/dbconfig-common/data/php-cerebrate/install/mysql

79
docker/Dockerfile Normal file
View File

@ -0,0 +1,79 @@
ARG COMPOSER_VERSION
ARG PHP_VERSION
ARG DEBIAN_RELEASE
FROM php:${PHP_VERSION}-apache-${DEBIAN_RELEASE}
# we need some extra libs to be installed in the runtime
RUN apt-get update && \
apt-get install -y --no-install-recommends curl git zip unzip && \
apt-get install -y --no-install-recommends libicu-dev libxml2-dev && \
docker-php-ext-install intl pdo pdo_mysql mysqli simplexml && \
apt-get remove -y --purge libicu-dev libxml2-dev && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
COPY composer.json composer.json
# install composer as root
ARG COMPOSER_VERSION
RUN curl -sL https://getcomposer.org/installer | \
php -- --install-dir=/usr/bin/ --filename=composer --version=${COMPOSER_VERSION}
# switch back to unprivileged user for composer install
USER www-data
RUN composer install \
--no-interaction \
--no-plugins \
--no-scripts \
--prefer-dist
# web server configuration
USER root
# allow .htaccess overrides and push them
RUN a2enmod rewrite
RUN sed -i -r '/DocumentRoot/a \\t<Directory /var/www/html/>\n\t\tAllowOverride all\n\t</Directory>' /etc/apache2/sites-available/000-default.conf
COPY --chown=www-data docker/etc/DocumentRoot.htaccess /var/www/html/.htaccess
COPY --chown=www-data docker/etc/webroot.htaccess /var/www/html/webroot/.htaccess
# passing environment variables through apache
RUN a2enmod env
RUN echo 'PassEnv CEREBRATE_DB_HOST' >> /etc/apache2/conf-enabled/environment.conf
RUN echo 'PassEnv CEREBRATE_DB_NAME' >> /etc/apache2/conf-enabled/environment.conf
RUN echo 'PassEnv CEREBRATE_DB_PASSWORD' >> /etc/apache2/conf-enabled/environment.conf
RUN echo 'PassEnv CEREBRATE_DB_PORT' >> /etc/apache2/conf-enabled/environment.conf
RUN echo 'PassEnv CEREBRATE_DB_SCHEMA' >> /etc/apache2/conf-enabled/environment.conf
RUN echo 'PassEnv CEREBRATE_DB_USERNAME' >> /etc/apache2/conf-enabled/environment.conf
RUN echo 'PassEnv CEREBRATE_EMAIL_HOST' >> /etc/apache2/conf-enabled/environment.conf
RUN echo 'PassEnv CEREBRATE_EMAIL_PASSWORD' >> /etc/apache2/conf-enabled/environment.conf
RUN echo 'PassEnv CEREBRATE_EMAIL_PORT' >> /etc/apache2/conf-enabled/environment.conf
RUN echo 'PassEnv CEREBRATE_EMAIL_TLS' >> /etc/apache2/conf-enabled/environment.conf
RUN echo 'PassEnv CEREBRATE_EMAIL_USERNAME' >> /etc/apache2/conf-enabled/environment.conf
RUN echo 'PassEnv CEREBRATE_SECURITY_SALT' >> /etc/apache2/conf-enabled/environment.conf
# entrypoint
COPY docker/entrypoint.sh /entrypoint.sh
RUN chmod 755 /entrypoint.sh
# copy actual codebase
COPY --chown=www-data . /var/www/html
# last checks with unprivileged user
USER www-data
# CakePHP seems to not handle very well externally installed components
# this will chown/chmod/symlink all in place for its own good
RUN composer install --no-interaction
# app config override making use of environment variables
COPY --chown=www-data docker/etc/app_local.php /var/www/html/config/app_local.php
# version 1.0 addition requires a config/config.json file
# can still be overriden by a docker volume
RUN cp -a /var/www/html/config/config.example.json /var/www/html/config/config.json
# also can be overridin by a docker volume
RUN mkdir -p /var/www/html/logs
ENTRYPOINT [ "/entrypoint.sh" ]

30
docker/README.md Normal file
View File

@ -0,0 +1,30 @@
# Actual data and volumes
The actual database will be located in `./run/database` exposed with the
following volume `- ./run/database:/var/lib/mysql`
Application logs (CakePHP / Cerebrate) will be stored in `./run/logs`,
volume `- ./run/logs:/var/www/html/logs`
You're free to change those parameters if you're using Swarm, Kubernetes or
your favorite config management tool to deploy this stack
# Building yourself
You can create the following Makefile in basedir of this repository
and issue `make image`
```
COMPOSER_VERSION?=2.1.5
PHP_VERSION?=7.4
DEBIAN_RELEASE?=buster
IMAGE_NAME?=cerebrate:latest
image:
docker build -t $(IMAGE_NAME) \
-f docker/Dockerfile \
--build-arg COMPOSER_VERSION=$(COMPOSER_VERSION) \
--build-arg PHP_VERSION=$(PHP_VERSION) \
--build-arg DEBIAN_RELEASE=$(DEBIAN_RELEASE) \
.
```

29
docker/docker-compose.yml Normal file
View File

@ -0,0 +1,29 @@
version: "3"
services:
database:
image: mariadb:10.6
restart: always
volumes:
- ./run/database:/var/lib/mysql
environment:
MARIADB_RANDOM_ROOT_PASSWORD: "yes"
MYSQL_DATABASE: "cerebrate"
MYSQL_USER: "cerebrate"
MYSQL_PASSWORD: "etarberec"
www:
image: ghcr.io/cerebrate-project/cerebrate:main
ports:
- "8080:80"
volumes:
- ./run/logs:/var/www/html/logs
- ./wait-for-it.sh:/usr/local/bin/wait-for-it.sh:ro
entrypoint: wait-for-it.sh -t 0 -h database -p 3306 -- /entrypoint.sh
environment:
DEBUG: "true"
CEREBRATE_DB_USERNAME: "cerebrate"
CEREBRATE_DB_PASSWORD: "etarberec"
CEREBRATE_DB_NAME: "cerebrate"
CEREBRATE_DB_HOST: database
CEREBRATE_SECURITY_SALT: supersecret
depends_on:
- database

28
docker/entrypoint.sh Executable file
View File

@ -0,0 +1,28 @@
#!/bin/sh
set -e
run_all_migrations() {
./bin/cake migrations migrate
./bin/cake migrations migrate -p tags
./bin/cake migrations migrate -p ADmad/SocialAuth
}
delete_model_cache() {
echo >&2 "Deleting cakephp cache..."
rm -rf ./tmp/cache/models/*
rm -rf ./tmp/cache/persistent/*
}
delete_model_cache
# waiting for DB to come up
for try in 1 2 3 4 5 6; do
echo >&2 "migration - attempt $try"
run_all_migrations && break || true
sleep 5
[ "$try" = "6" ] && exit 1
done
exec /usr/local/bin/apache2-foreground "$@"

View File

@ -0,0 +1,3 @@
RewriteEngine on
RewriteRule ^$ webroot/ [L]
RewriteRule (.*) webroot/$1 [L]

44
docker/etc/app_local.php Normal file
View File

@ -0,0 +1,44 @@
<?php
$db = [
'username' => env('CEREBRATE_DB_USERNAME', 'cerebrate'),
'password' => env('CEREBRATE_DB_PASSWORD', ''),
'host' => env('CEREBRATE_DB_HOST', 'localhost'),
'database' => env('CEREBRATE_DB_NAME', 'cerebrate'),
];
// non-default port can be set on demand - otherwise the DB driver will choose the default
if (!empty(env('CEREBRATE_DB_PORT'))) {
$db['port'] = env('CEREBRATE_DB_PORT');
}
// If not using the default 'public' schema with the PostgreSQL driver set it here.
if (!empty(env('CEREBRATE_DB_SCHEMA'))) {
$db['schema'] = env('CEREBRATE_DB_SCHEMA');
}
return [
'debug' => filter_var(env('DEBUG', false), FILTER_VALIDATE_BOOLEAN),
'Security' => [
'salt' => env('CEREBRATE_SECURITY_SALT'),
],
'Datasources' => [
'default' => $db,
],
'EmailTransport' => [
'default' => [
// host could be ssl://smtp.gmail.com then set port to 465
'host' => env('CEREBRATE_EMAIL_HOST', 'localhost'),
'port' => env('CEREBRATE_EMAIL_PORT', 25),
'username' => env('CEREBRATE_EMAIL_USERNAME', null),
'password' => env('CEREBRATE_EMAIL_PASSWORD', null),
'tls' => env('CEREBRATE_EMAIL_TLS', null)
],
],
'Cerebrate' => [
'open' => [],
'dark' => 0
]
];

View File

@ -0,0 +1,3 @@
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [L]

186
docker/wait-for-it.sh Executable file
View File

@ -0,0 +1,186 @@
#!/usr/bin/env bash
# Use this script to test if a given TCP host/port are available
# The MIT License (MIT)
# Copyright (c) 2016 Giles Hall
# Source: https://github.com/vishnubob/wait-for-it
WAITFORIT_cmdname=${0##*/}
echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi }
usage()
{
cat << USAGE >&2
Usage:
$WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args]
-h HOST | --host=HOST Host or IP under test
-p PORT | --port=PORT TCP port under test
Alternatively, you specify the host and port as host:port
-s | --strict Only execute subcommand if the test succeeds
-q | --quiet Don't output any status messages
-t TIMEOUT | --timeout=TIMEOUT
Timeout in seconds, zero for no timeout
-- COMMAND ARGS Execute command with args after the test finishes
USAGE
exit 1
}
wait_for()
{
if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then
echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT"
else
echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout"
fi
WAITFORIT_start_ts=$(date +%s)
while :
do
if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then
nc -z $WAITFORIT_HOST $WAITFORIT_PORT
WAITFORIT_result=$?
else
(echo -n > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1
WAITFORIT_result=$?
fi
if [[ $WAITFORIT_result -eq 0 ]]; then
WAITFORIT_end_ts=$(date +%s)
echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds"
break
fi
sleep 1
done
return $WAITFORIT_result
}
wait_for_wrapper()
{
# In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692
if [[ $WAITFORIT_QUIET -eq 1 ]]; then
timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &
else
timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &
fi
WAITFORIT_PID=$!
trap "kill -INT -$WAITFORIT_PID" INT
wait $WAITFORIT_PID
WAITFORIT_RESULT=$?
if [[ $WAITFORIT_RESULT -ne 0 ]]; then
echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT"
fi
return $WAITFORIT_RESULT
}
# process arguments
while [[ $# -gt 0 ]]
do
case "$1" in
*:* )
WAITFORIT_hostport=(${1//:/ })
WAITFORIT_HOST=${WAITFORIT_hostport[0]}
WAITFORIT_PORT=${WAITFORIT_hostport[1]}
shift 1
;;
--child)
WAITFORIT_CHILD=1
shift 1
;;
-q | --quiet)
WAITFORIT_QUIET=1
shift 1
;;
-s | --strict)
WAITFORIT_STRICT=1
shift 1
;;
-h)
WAITFORIT_HOST="$2"
if [[ $WAITFORIT_HOST == "" ]]; then break; fi
shift 2
;;
--host=*)
WAITFORIT_HOST="${1#*=}"
shift 1
;;
-p)
WAITFORIT_PORT="$2"
if [[ $WAITFORIT_PORT == "" ]]; then break; fi
shift 2
;;
--port=*)
WAITFORIT_PORT="${1#*=}"
shift 1
;;
-t)
WAITFORIT_TIMEOUT="$2"
if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi
shift 2
;;
--timeout=*)
WAITFORIT_TIMEOUT="${1#*=}"
shift 1
;;
--)
shift
WAITFORIT_CLI=("$@")
break
;;
--help)
usage
;;
*)
echoerr "Unknown argument: $1"
usage
;;
esac
done
if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then
echoerr "Error: you need to provide a host and port to test."
usage
fi
WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15}
WAITFORIT_STRICT=${WAITFORIT_STRICT:-0}
WAITFORIT_CHILD=${WAITFORIT_CHILD:-0}
WAITFORIT_QUIET=${WAITFORIT_QUIET:-0}
# Check to see if timeout is from busybox?
WAITFORIT_TIMEOUT_PATH=$(type -p timeout)
WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH)
WAITFORIT_BUSYTIMEFLAG=""
if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then
WAITFORIT_ISBUSY=1
# Check if busybox timeout uses -t flag
# (recent Alpine versions don't support -t anymore)
if timeout &>/dev/stdout | grep -q -e '-t '; then
WAITFORIT_BUSYTIMEFLAG="-t"
fi
else
WAITFORIT_ISBUSY=0
fi
if [[ $WAITFORIT_CHILD -gt 0 ]]; then
wait_for
WAITFORIT_RESULT=$?
exit $WAITFORIT_RESULT
else
if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then
wait_for_wrapper
WAITFORIT_RESULT=$?
else
wait_for
WAITFORIT_RESULT=$?
fi
fi
if [[ $WAITFORIT_CLI != "" ]]; then
if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then
echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess"
exit $WAITFORIT_RESULT
fi
exec "${WAITFORIT_CLI[@]}"
else
exit $WAITFORIT_RESULT
fi

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,17 @@
# Prerequisites based on usecases
This document list the requirements that have to be met in order to perform the desired usecase.
## Connect a local tool to cerebrate
- **Networking**: The *cerebrate* application must be able to contact the local tool service. That means the address and the port of the local tool must be reachable by *cerebrate*.
- **User permissions**: Depends on the actions performed by Cerebrate on the local tool.
- Example: For a standard MISP configuration, a simple user with the `user` role is enough for Cerebrate to pass the health check.
## Conect two cerebrate instances together
- **Networking**: The two *cerebrate* applications must be able to contact each others. That means the address and the port of both tools must be reachable by the other one.
- **User permissions**: No specific role or set of permission is required. Any user role can be used.
## Connect two local tools through cerebrate
- **Networking**: The two *cerebrate* applications must be able to contact each others. That means the address and the port of both tools must be reachable by the other one. This also applies to both the local tools.
- **User permissions**: Depends on the actions performed by Cerebrate on the local tool.
- Example: For a standard MISP configuration, in order to have two instance connected, the API key used by *cebrate* to orchestrate the inter-connection must belong to a user having the `site-admin` permission flag. This is essential as only the `site-admin` permission allows to create synchronisation links between MISP instances.

1
logs/.gitkeep Normal file
View File

@ -0,0 +1 @@

View File

@ -1,13 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
colors="true"
processIsolation="false"
stopOnFailure="false"
bootstrap="tests/bootstrap.php"
>
<phpunit colors="true" processIsolation="false" stopOnFailure="false" bootstrap="tests/bootstrap.php">
<php>
<ini name="memory_limit" value="-1"/>
<ini name="apc.enable_cli" value="1"/>
<ini name="memory_limit" value="-1" />
<ini name="apc.enable_cli" value="1" />
<env name="WIREMOCK_HOST" value="localhost" />
<env name="WIREMOCK_PORT" value="8080" />
<env name="OPENAPI_SPEC" value="webroot/docs/openapi.yaml" />
<env name="SKIP_DB_MIGRATIONS" value="0" />
</php>
<!-- Add any additional test suites you want to run here -->
@ -15,17 +14,17 @@
<testsuite name="app">
<directory>tests/TestCase/</directory>
</testsuite>
<!-- Add plugin test suites here. -->
<testsuite name="controller">
<directory>./tests/TestCase/Controller</directory>
</testsuite>
<testsuite name="api">
<directory>./tests/TestCase/Api</directory>
</testsuite>
</testsuites>
<!-- Setup a listener for fixtures -->
<listeners>
<listener class="Cake\TestSuite\Fixture\FixtureInjector">
<arguments>
<object class="Cake\TestSuite\Fixture\FixtureManager"/>
</arguments>
</listener>
</listeners>
<extensions>
<extension class="\Cake\TestSuite\Fixture\PHPUnitExtension" />
</extensions>
<!-- Ignore vendor tests in code coverage reports -->
<filter>

View File

@ -10,22 +10,22 @@ class TagSystem extends AbstractMigration
$tags = $this->table('tags_tags');
$tags->addColumn('namespace', 'string', [
'default' => null,
'limit' => 255,
'limit' => 191,
'null' => true,
])
->addColumn('predicate', 'string', [
'default' => null,
'limit' => 255,
'limit' => 191,
'null' => true,
])
->addColumn('value', 'string', [
'default' => null,
'limit' => 255,
'limit' => 191,
'null' => true,
])
->addColumn('name', 'string', [
'default' => null,
'limit' => 255,
'limit' => 191,
'null' => false,
])
->addColumn('colour', 'string', [
@ -66,7 +66,7 @@ class TagSystem extends AbstractMigration
])
->addColumn('fk_model', 'string', [
'default' => null,
'limit' => 255,
'limit' => 191,
'null' => false,
'comment' => 'The model name of the entity being tagged'
])

View File

@ -114,7 +114,6 @@ class TagBehavior extends Behavior
$property = $this->getConfig('tagsAssoc.propertyName');
$options['accessibleFields'][$property] = true;
$options['associated']['Tags']['accessibleFields']['id'] = true;
if (isset($data['tags'])) {
if (!empty($data['tags'])) {
$data[$property] = $this->normalizeTags($data['tags']);
@ -131,7 +130,6 @@ class TagBehavior extends Behavior
if (!$tag->isNew()) {
continue;
}
$existingTag = $this->getExistingTag($tag->name);
if (!$existingTag) {
continue;
@ -176,15 +174,14 @@ class TagBehavior extends Behavior
$result[] = array_merge($common, ['id' => $existingTag->id]);
continue;
}
$result[] = array_merge(
$common,
[
'name' => $tagIdentifier,
'colour' => '#924da6'
]
);
}
return $result;
}

View File

@ -118,6 +118,7 @@ class Application extends BaseApplication implements AuthenticationServiceProvid
'collectionFactory' => null,
'logErrors' => true,
]));
\SocialConnect\JWX\JWT::$screw = Configure::check('keycloak.screw') ? Configure::read('keycloak.screw') : 0;
}
$middlewareQueue->add(new AuthenticationMiddleware($this))
->add(new BodyParserMiddleware());

View File

@ -165,7 +165,7 @@ class ImporterCommand extends Command
'valueField' => 'id'
])->where(['meta_template_id' => $metaTemplate->id])->toArray();
} else {
$this->io->error("Unkown template for UUID {$config['metaTemplateUUID']}");
$this->io->error("Unknown template for UUID {$config['metaTemplateUUID']}");
die(1);
}
}
@ -196,7 +196,7 @@ class ImporterCommand extends Command
$metaEntity->meta_template_field_id = $metaTemplateFieldsMapping[$fieldName];
} else {
$hasErrors = true;
$this->io->error("Field $fieldName is unkown for template {$metaTemplate->name}");
$this->io->error("Field $fieldName is unknown for template {$metaTemplate->name}");
break;
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Controller;
use App\Controller\AppController;
class ApiController extends AppController
{
/**
* Controller action for displaying built-in Redoc UI
*
* @return \Cake\Http\Response|null|void Renders view
*/
public function index()
{
$url = '/docs/openapi.yaml';
$this->set('url', $url);
}
}

View File

@ -54,7 +54,6 @@ class AppController extends Controller
public function initialize(): void
{
parent::initialize();
$this->loadComponent('RequestHandler');
$this->loadComponent('Flash');
$this->loadComponent('RestResponse');
@ -102,7 +101,7 @@ class AppController extends Controller
$this->ACL->setPublicInterfaces();
if (!empty($this->request->getAttribute('identity'))) {
$user = $this->Users->get($this->request->getAttribute('identity')->getIdentifier(), [
'contain' => ['Roles', 'Individuals' => 'Organisations', 'UserSettings']
'contain' => ['Roles', 'Individuals' => 'Organisations', 'UserSettings', 'Organisations']
]);
if (!empty($user['disabled'])) {
$this->Authentication->logout();
@ -111,9 +110,13 @@ class AppController extends Controller
}
unset($user['password']);
$this->ACL->setUser($user);
$this->request->getSession()->write('authUser', $user);
$this->isAdmin = $user['role']['perm_admin'];
$this->set('menu', $this->ACL->getMenu());
$this->set('loggedUser', $this->ACL->getUser());
if (!$this->ParamHandler->isRest()) {
$this->set('menu', $this->ACL->getMenu());
$this->set('loggedUser', $this->ACL->getUser());
$this->set('roleAccess', $this->ACL->getRoleAccess(false, false));
}
} else if ($this->ParamHandler->isRest()) {
throw new MethodNotAllowedException(__('Invalid user credentials.'));
}
@ -128,17 +131,20 @@ class AppController extends Controller
}
$this->ACL->checkAccess();
$this->set('ajax', $this->request->is('ajax'));
$this->request->getParam('prefix');
$this->set('baseurl', Configure::read('App.fullBaseUrl'));
if (!empty($user) && !empty($user->user_settings_by_name['ui.bsTheme']['value'])) {
$this->set('bsTheme', $user->user_settings_by_name['ui.bsTheme']['value']);
} else {
$this->set('bsTheme', Configure::check('ui.bsTheme') ? Configure::read('ui.bsTheme') : 'default');
}
if (!$this->ParamHandler->isRest()) {
$this->set('breadcrumb', $this->Navigation->getBreadcrumb());
$this->set('ajax', $this->request->is('ajax'));
$this->request->getParam('prefix');
$this->set('baseurl', Configure::read('App.fullBaseUrl'));
if (!empty($user) && !empty($user->user_settings_by_name['ui.bsTheme']['value'])) {
$this->set('bsTheme', $user->user_settings_by_name['ui.bsTheme']['value']);
} else {
$this->set('bsTheme', Configure::check('ui.bsTheme') ? Configure::read('ui.bsTheme') : 'default');
}
if ($this->modelClass == 'Tags.Tags') {
$this->set('metaGroup', !empty($this->isAdmin) ? 'Administration' : 'Cerebrate');
if ($this->modelClass == 'Tags.Tags') {
$this->set('metaGroup', !empty($this->isAdmin) ? 'Administration' : 'Cerebrate');
}
}
}
@ -151,13 +157,30 @@ class AppController extends Controller
{
if (!empty($_SERVER['HTTP_AUTHORIZATION']) && strlen($_SERVER['HTTP_AUTHORIZATION'])) {
$this->loadModel('AuthKeys');
$logModel = $this->Users->auditLogs();
$authKey = $this->AuthKeys->checkKey($_SERVER['HTTP_AUTHORIZATION']);
if (!empty($authKey)) {
$this->loadModel('Users');
$user = $this->Users->get($authKey['user_id']);
$logModel->insert([
'request_action' => 'login',
'model' => 'Users',
'model_id' => $user['id'],
'model_title' => $user['username'],
'changed' => []
]);
if (!empty($user)) {
$this->Authentication->setIdentity($user);
}
} else {
$user = $logModel->userInfo();
$logModel->insert([
'request_action' => 'login',
'model' => 'Users',
'model_id' => $user['id'],
'model_title' => $user['name'],
'changed' => []
]);
}
}
}
@ -172,4 +195,9 @@ class AppController extends Controller
{
return $this->RestResponse->viewData($this->ACL->findMissingFunctionNames());
}
public function getRoleAccess()
{
return $this->RestResponse->viewData($this->ACL->getRoleAccess(false, false));
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\Controller;
use App\Controller\AppController;
use Cake\Utility\Hash;
use Cake\Utility\Text;
use Cake\ORM\TableRegistry;
use \Cake\Database\Expression\QueryExpression;
use Cake\Http\Exception\UnauthorizedException;
use Cake\Core\Configure;
class AuditLogsController extends AppController
{
public $filterFields = ['model_id', 'model', 'request_action', 'user_id', 'title'];
public $quickFilterFields = ['model', 'request_action', 'title'];
public $containFields = ['Users'];
public function index()
{
$this->CRUD->index([
'contain' => $this->containFields,
'filters' => $this->filterFields,
'quickFilters' => $this->quickFilterFields,
'afterFind' => function($data) {
$data['request_ip'] = inet_ntop(stream_get_contents($data['request_ip']));
$data['changed'] = stream_get_contents($data['changed']);
return $data;
}
]);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
}
$this->set('metaGroup', 'Administration');
}
}

View File

@ -16,15 +16,25 @@ class AuthKeysController extends AppController
{
public $filterFields = ['Users.username', 'authkey', 'comment', 'Users.id'];
public $quickFilterFields = ['authkey', ['comment' => true]];
public $containFields = ['Users'];
public $containFields = ['Users' => ['fields' => ['id', 'username']]];
public function index()
{
$currentUser = $this->ACL->getUser();
$conditions = [];
if (empty($currentUser['role']['perm_admin'])) {
$conditions['Users.organisation_id'] = $currentUser['organisation_id'];
if (empty($currentUser['role']['perm_org_admin'])) {
$conditions['Users.id'] = $currentUser['id'];
}
}
$this->CRUD->index([
'filters' => $this->filterFields,
'quickFilters' => $this->quickFilterFields,
'contain' => $this->containFields,
'exclude_fields' => ['authkey']
'exclude_fields' => ['authkey'],
'conditions' => $conditions,
'hidden' => []
]);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
@ -35,7 +45,15 @@ class AuthKeysController extends AppController
public function delete($id)
{
$this->CRUD->delete($id);
$currentUser = $this->ACL->getUser();
$conditions = [];
if (empty($currentUser['role']['perm_admin'])) {
$conditions['Users.organisation_id'] = $currentUser['organisation_id'];
if (empty($currentUser['role']['perm_org_admin'])) {
$conditions['Users.id'] = $currentUser['id'];
}
}
$this->CRUD->delete($id, ['conditions' => $conditions, 'contain' => 'Users']);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
@ -46,8 +64,30 @@ class AuthKeysController extends AppController
public function add()
{
$this->set('metaGroup', $this->isAdmin ? 'Administration' : 'Cerebrate');
$validUsers = [];
$userConditions = [];
$currentUser = $this->ACL->getUser();
if (empty($currentUser['role']['perm_admin'])) {
if (empty($currentUser['role']['perm_org_admin'])) {
$userConditions['id'] = $currentUser['id'];
} else {
$role_ids = $this->Users->Roles->find()->where(['perm_admin' => 0])->all()->extract('id')->toList();
$userConditions['role_id IN'] = $role_ids;
}
}
$users = $this->Users->find('list');
if (!empty($userConditions)) {
$users->where($userConditions);
}
$users = $users->order(['username' => 'asc'])->all()->toArray();
$this->CRUD->add([
'displayOnSuccess' => 'authkey_display'
'displayOnSuccess' => 'authkey_display',
'beforeSave' => function($data) use ($users) {
if (!in_array($data['user_id'], array_keys($users))) {
return false;
}
return $data;
}
]);
$responsePayload = $this->CRUD->getResponsePayload([
'displayOnSuccess' => 'authkey_display'
@ -57,9 +97,7 @@ class AuthKeysController extends AppController
}
$this->loadModel('Users');
$dropdownData = [
'user' => $this->Users->find('list', [
'sort' => ['username' => 'asc']
])
'user' => $users
];
$this->set(compact('dropdownData'));
}

View File

@ -37,6 +37,7 @@ class ACLComponent extends Component
'*' => [
'checkPermission' => ['*'],
'generateUUID' => ['*'],
'getRoleAccess' => ['*'],
'queryACL' => ['perm_admin']
],
'Alignments' => [
@ -45,6 +46,9 @@ class ACLComponent extends Component
'index' => ['*'],
'view' => ['*']
],
'AuditLogs' => [
'index' => ['perm_admin']
],
'AuthKeys' => [
'add' => ['*'],
'delete' => ['*'],
@ -64,6 +68,7 @@ class ACLComponent extends Component
'view' => ['perm_admin']
],
'EncryptionKeys' => [
'view' => ['*'],
'add' => ['*'],
'edit' => ['*'],
'delete' => ['*'],
@ -82,22 +87,30 @@ class ACLComponent extends Component
'add' => ['perm_admin'],
'delete' => ['perm_admin'],
'edit' => ['perm_admin'],
'filtering' => ['*'],
'index' => ['*'],
'view' => ['*']
'tag' => ['perm_tagger'],
'untag' => ['perm_tagger'],
'view' => ['*'],
'viewTags' => ['*']
],
'Instance' => [
'home' => ['*'],
'migrate' => ['perm_admin'],
'migrationIndex' => ['perm_admin'],
'rollback' => ['perm_admin'],
'saveSetting' => ['perm_admin'],
'searchAll' => ['*'],
'settings' => ['perm_admin'],
'status' => ['*']
],
'LocalTools' => [
'action' => ['perm_admin'],
'add' => ['perm_admin'],
'batchAction' => ['perm_admin'],
'broodTools' => ['perm_admin'],
'connectionRequest' => ['perm_admin'],
'connectLocal' => ['perm_admin'],
// 'connectLocal' => ['perm_admin'],
'delete' => ['perm_admin'],
'edit' => ['perm_admin'],
'exposedTools' => ['OR' => ['perm_admin', 'perm_sync']],
@ -123,7 +136,10 @@ class ACLComponent extends Component
'edit' => ['perm_admin'],
'filtering' => ['*'],
'index' => ['*'],
'view' => ['*']
'tag' => ['perm_tagger'],
'untag' => ['perm_tagger'],
'view' => ['*'],
'viewTags' => ['*']
],
'Outbox' => [
'createEntry' => ['perm_admin'],
@ -145,25 +161,42 @@ class ACLComponent extends Component
'view' => ['*']
],
'SharingGroups' => [
'add' => ['perm_admin'],
'addOrg' => ['perm_admin'],
'delete' => ['perm_admin'],
'edit' => ['perm_admin'],
'add' => ['perm_org_admin'],
'addOrg' => ['perm_org_admin'],
'delete' => ['perm_org_admin'],
'edit' => ['perm_org_admin'],
'index' => ['*'],
'listOrgs' => ['*'],
'removeOrg' => ['perm_admin'],
'removeOrg' => ['perm_org_admin'],
'view' => ['*']
],
'Users' => [
'add' => ['perm_admin'],
'delete' => ['perm_admin'],
'add' => ['perm_org_admin'],
'delete' => ['perm_org_admin'],
'edit' => ['*'],
'index' => ['perm_admin'],
'index' => ['perm_org_admin'],
'login' => ['*'],
'logout' => ['*'],
'register' => ['*'],
'toggle' => ['perm_admin'],
'settings' => ['*'],
'toggle' => ['perm_org_admin'],
'view' => ['*']
],
'UserSettings' => [
'index' => ['*'],
'view' => ['*'],
'add' => ['*'],
'edit' => ['*'],
'delete' => ['*'],
'getSettingByName' => ['*'],
'setSetting' => ['*'],
'saveSetting' => ['*'],
'getBookmarks' => ['*'],
'saveBookmark' => ['*'],
'deleteBookmark' => ['*']
],
'Api' => [
'index' => ['*']
]
);
@ -267,9 +300,19 @@ class ACLComponent extends Component
return true;
}
//$this->__checkLoggedActions($user, $controller, $action);
if (isset($this->aclList['*'][$action])) {
if ($this->evaluateAccessLeaf('*', $action)) {
return true;
}
}
if (!isset($this->aclList[$controller])) {
return $this->__error(404, __('Invalid controller.'), $soft);
}
return $this->evaluateAccessLeaf($controller, $action);
}
private function evaluateAccessLeaf(string $controller, string $action): bool
{
if (isset($this->aclList[$controller][$action]) && !empty($this->aclList[$controller][$action])) {
if (in_array('*', $this->aclList[$controller][$action])) {
return true;
@ -290,6 +333,12 @@ class ACLComponent extends Component
if ($allConditionsMet) {
return true;
}
} else {
foreach ($this->aclList[$controller][$action] as $permission) {
if ($this->user['role'][$permission]) {
return true;
}
}
}
}
return false;
@ -391,13 +440,18 @@ class ACLComponent extends Component
return $missing;
}
public function getRoleAccess($role = false, $url_mode = true)
{
return $this->__checkRoleAccess($role, $url_mode);
}
public function printRoleAccess($content = false)
{
$results = [];
$this->Role = TableRegistry::get('Role');
$this->Role = TableRegistry::get('Roles');
$conditions = [];
if (is_numeric($content)) {
$conditions = array('Role.id' => $content);
$conditions = array('id' => $content);
}
$roles = $this->Role->find('all', array(
'recursive' => -1,
@ -413,44 +467,54 @@ class ACLComponent extends Component
return $results;
}
private function __checkRoleAccess($role)
private function __formatControllerAction(array $results, string $controller, string $action, $url_mode = true): array
{
$result = [];
foreach ($this->__aclList as $controller => $actions) {
$controllerNames = Inflector::variable($controller) == Inflector::underscore($controller) ? array(Inflector::variable($controller)) : array(Inflector::variable($controller), Inflector::underscore($controller));
foreach ($controllerNames as $controllerName) {
foreach ($actions as $action => $permissions) {
if ($role['perm_site_admin']) {
$result[] = DS . $controllerName . DS . $action;
} elseif (in_array('*', $permissions)) {
$result[] = DS . $controllerName . DS . $action . DS . '*';
} elseif (isset($permissions['OR'])) {
$access = false;
foreach ($permissions['OR'] as $permission) {
if ($role[$permission]) {
$access = true;
}
if ($url_mode) {
$results[] = DS . $controller . DS . $action . DS . '*';
} else {
$results[$controller][] = $action;
}
return $results;
}
private function __checkRoleAccess($role = false, $url_mode = true)
{
$results = [];
if ($role === false) {
$role = $this->getUser()['role'];
}
foreach ($this->aclList as $controller => $actions) {
foreach ($actions as $action => $permissions) {
if ($role['perm_admin']) {
$results = $this->__formatControllerAction($results, $controller, $action, $url_mode);
} elseif (in_array('*', $permissions)) {
$results = $this->__formatControllerAction($results, $controller, $action, $url_mode);
} elseif (isset($permissions['OR'])) {
$access = false;
foreach ($permissions['OR'] as $permission) {
if ($role[$permission]) {
$access = true;
}
if ($access) {
$result[] = DS . $controllerName . DS . $action . DS . '*';
}
} elseif (isset($permissions['AND'])) {
$access = true;
foreach ($permissions['AND'] as $permission) {
if ($role[$permission]) {
$access = false;
}
}
if ($access) {
$result[] = DS . $controllerName . DS . $action . DS . '*';
}
} elseif (isset($permissions[0]) && $role[$permissions[0]]) {
$result[] = DS . $controllerName . DS . $action . DS . '*';
}
if ($access) {
$results = $this->__formatControllerAction($results, $controller, $action, $url_mode);
}
} elseif (isset($permissions['AND'])) {
$access = true;
foreach ($permissions['AND'] as $permission) {
if ($role[$permission]) {
$access = false;
}
}
if ($access) {
$results = $this->__formatControllerAction($results, $controller, $action, $url_mode);
}
} elseif (isset($permissions[0]) && $role[$permissions[0]]) {
$results = $this->__formatControllerAction($results, $controller, $action, $url_mode);
}
}
}
return $result;
return $results;
}
public function getMenu()

View File

@ -56,6 +56,9 @@ class CRUDComponent extends Component
}
$query = $this->setFilters($params, $query, $options);
$query = $this->setQuickFilters($params, $query, $options);
if (!empty($options['conditions'])) {
$query->where($options['conditions']);
}
if (!empty($options['contain'])) {
$query->contain($options['contain']);
}
@ -67,11 +70,25 @@ class CRUDComponent extends Component
}
if ($this->Controller->ParamHandler->isRest()) {
$data = $query->all();
if (isset($options['hidden'])) {
$data->each(function($value, $key) use ($options) {
$hidden = is_array($options['hidden']) ? $options['hidden'] : [$options['hidden']];
$value->setHidden($hidden);
return $value;
});
}
if (isset($options['afterFind'])) {
$function = $options['afterFind'];
if (is_callable($options['afterFind'])) {
$data = $options['afterFind']($data);
$function = $options['afterFind'];
$data->each(function($value, $key) use ($function) {
return $function($value);
});
} else {
$data = $this->Table->{$options['afterFind']}($data);
$t = $this->Table;
$data->each(function($value, $key) use ($t, $function) {
return $t->$function($value);
});
}
}
$this->Controller->restResponsePayload = $this->RestResponse->viewData($data, 'json');
@ -82,10 +99,17 @@ class CRUDComponent extends Component
$this->Controller->loadComponent('Paginator');
$data = $this->Controller->Paginator->paginate($query);
if (isset($options['afterFind'])) {
$function = $options['afterFind'];
if (is_callable($options['afterFind'])) {
$data = $options['afterFind']($data);
$function = $options['afterFind'];
$data->each(function($value, $key) use ($function) {
return $function($value);
});
} else {
$data = $this->Table->{$options['afterFind']}($data);
$t = $this->Table;
$data->each(function($value, $key) use ($t, $function) {
return $t->$function($value);
});
}
}
$this->setFilteringContext($options['contextFilters'] ?? [], $params);
@ -220,6 +244,9 @@ class CRUDComponent extends Component
$data = $this->Table->patchEntity($data, $input, $patchEntityParams);
if (isset($params['beforeSave'])) {
$data = $params['beforeSave']($data);
if ($data === false) {
throw new NotFoundException(__('Could not save {0} due to the input failing to meet expectations. Your input is bad and you should feel bad.', $this->ObjectAlias));
}
}
$savedData = $this->Table->save($data);
if ($savedData !== false) {
@ -417,18 +444,25 @@ class CRUDComponent extends Component
$params['contain'][] = 'Tags';
$this->setAllTags();
}
$getParam = isset($params['get']) ? $params['get'] : $params;
$queryParam = isset($params['get']) ? $params['get'] : $params;
if ($this->metaFieldsSupported()) {
if (empty($getParam['contain'])) {
$getParam['contain'] = [];
if (empty( $queryParam['contain'])) {
$queryParam['contain'] = [];
}
if (is_array($getParam['contain'])) {
$getParam['contain'][] = 'MetaFields';
if (is_array( $queryParam['contain'])) {
$queryParam['contain'][] = 'MetaFields';
} else {
$getParam['contain'] = [$getParam['contain'], 'MetaFields'];
$queryParam['contain'] = [ $queryParam['contain'], 'MetaFields'];
}
}
$data = $this->Table->get($id, $getParam);
$query = $this->Table->find()->where(['id' => $id]);
if (!empty($params['conditions'])) {
$query->where($params['conditions']);
}
$data = $query->first();
if (empty($data)) {
throw new NotFoundException(__('Invalid {0}.', $this->ObjectAlias));
}
if ($this->metaFieldsSupported()) {
$metaTemplates = $this->getMetaTemplates();
$data = $this->attachMetaTemplatesIfNeeded($data, $metaTemplates->toArray());
@ -653,11 +687,27 @@ class CRUDComponent extends Component
return $data;
}
public function delete($id = false): void
public function delete($id=false, $params=[]): void
{
if ($this->request->is('get')) {
if (!empty($id)) {
$data = $this->Table->get($id);
if(!empty($id)) {
$query = $this->Table->find()->where([$this->Table->getAlias() . '.id' => $id]);
if (!empty($params['conditions'])) {
$query->where($params['conditions']);
}
if (!empty($params['contain'])) {
$query->contain($params['contain']);
}
$data = $query->first();
if (empty($data)) {
throw new NotFoundException(__('Invalid {0}.', $this->ObjectAlias));
}
if (isset($params['beforeSave'])) {
$data = $params['beforeSave']($data);
if ($data === false) {
throw new NotFoundException(__('Could not save {0} due to the input failing to meet expectations. Your input is bad and you should feel bad.', $this->ObjectAlias));
}
}
$this->Controller->set('id', $data['id']);
$this->Controller->set('data', $data);
$this->Controller->set('bulkEnabled', false);
@ -669,8 +719,26 @@ class CRUDComponent extends Component
$isBulk = count($ids) > 1;
$bulkSuccesses = 0;
foreach ($ids as $id) {
$data = $this->Table->get($id);
$success = $this->Table->delete($data);
$query = $this->Table->find()->where([$this->Table->getAlias() . '.id' => $id]);
if (!empty($params['conditions'])) {
$query->where($params['conditions']);
}
if (!empty($params['contain'])) {
$query->contain($params['contain']);
}
$data = $query->first();
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));
}
}
if (!empty($data)) {
$success = $this->Table->delete($data);
$success = true;
} else {
$success = false;
}
if ($success) {
$bulkSuccesses++;
}

View File

@ -50,7 +50,12 @@ class Sidemenu {
'label' => __('Broods'),
'icon' => $this->iconTable['Broods'],
'url' => '/broods/index',
]
],
'API' => [
'label' => __('API'),
'icon' => $this->iconTable['API'],
'url' => '/api/index',
],
],
__('Administration') => [
'Roles' => [
@ -118,6 +123,11 @@ class Sidemenu {
'url' => '/instance/migrationIndex',
'icon' => 'database',
],
'AuditLogs' => [
'label' => __('Audit Logs'),
'url' => '/auditLogs/index',
'icon' => 'history',
],
]
],
],

View File

@ -35,6 +35,7 @@ class NavigationComponent extends Component
'LocalTools' => 'tools',
'Instance' => 'server',
'Tags' => 'tags',
'API' => 'code',
];
public function initialize(array $config): void

View File

@ -4,6 +4,7 @@ namespace App\Controller\Component;
use Cake\Controller\Component;
use Cake\Core\Configure;
use Cake\Http\Exception\MethodNotAllowedException;
class ParamHandlerComponent extends Component
{
@ -47,7 +48,7 @@ class ParamHandlerComponent extends Component
return $this->isRest;
}
if ($this->request->is('json')) {
if (!empty($this->request->input()) && empty($this->request->input('json_decode'))) {
if (!empty((string)$this->request->getBody()) && empty($this->request->getParsedBody())) {
throw new MethodNotAllowedException('Invalid JSON input. Make sure that the JSON input is a correctly formatted JSON string. This request has been blocked to avoid an unfiltered request.');
}
$this->isRest = true;

View File

@ -718,11 +718,11 @@ class RestResponseComponent extends Component
'operators' => array('equal'),
'help' => __('A valid x509 certificate ')
),
'change' => array(
'changed' => array(
'input' => 'text',
'type' => 'string',
'operators' => array('equal'),
'help' => __('The text contained in the change field')
'help' => __('The text contained in the changed field')
),
'change_pw' => array(
'input' => 'radio',

View File

@ -14,7 +14,7 @@ use Cake\Error\Debugger;
class EncryptionKeysController extends AppController
{
public $filterFields = ['owner_model', 'organisation_id', 'individual_id', 'encryption_key'];
public $filterFields = ['owner_model', 'owner_id', 'encryption_key'];
public $quickFilterFields = ['encryption_key'];
public $containFields = ['Individuals', 'Organisations'];
public $statisticsFields = ['type'];
@ -41,7 +41,15 @@ class EncryptionKeysController extends AppController
public function delete($id)
{
$this->CRUD->delete($id);
$orgConditions = [];
$individualConditions = [];
$dropdownData = [];
$currentUser = $this->ACL->getUser();
$params = [];
if (empty($currentUser['role']['perm_admin'])) {
$params = $this->buildBeforeSave($params, $currentUser, $orgConditions, $individualConditions, $dropdownData);
}
$this->CRUD->delete($id, $params);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
@ -49,35 +57,92 @@ class EncryptionKeysController extends AppController
$this->set('metaGroup', 'ContactDB');
}
public function add()
private function buildBeforeSave(array $params, $currentUser, array &$orgConditions, array &$individualConditions, array &$dropdownData): array
{
$this->CRUD->add(['redirect' => $this->referer()]);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
if (empty($currentUser['role']['perm_admin'])) {
$orgConditions = [
'id' => $currentUser['organisation_id']
];
if (empty($currentUser['role']['perm_org_admin'])) {
$individualConditions = [
'id' => $currentUser['individual_id']
];
} else {
$this->loadModel('Alignments');
$individualConditions = ['id IN' => $this->Alignments->find('list', [
'keyField' => 'id',
'valueField' => 'individual_id',
'conditions' => ['organisation_id' => $currentUser['organisation_id']]
])->toArray()];
}
$params['beforeSave'] = function($entity) use($currentUser) {
if ($entity['owner_model'] === 'organisation') {
if ($entity['owner_id'] !== $currentUser['organisation_id']) {
throw new MethodNotAllowedException(__('Selected organisation cannot be linked by the current user.'));
}
} else {
if ($currentUser['role']['perm_org_admin']) {
$this->loadModel('Alignments');
$validIndividuals = $this->Alignments->find('list', [
'keyField' => 'individual_id',
'valueField' => 'id',
'conditions' => ['organisation_id' => $currentUser['organisation_id']]
])->toArray();
if (!isset($validIndividuals[$entity['owner_id']])) {
throw new MethodNotAllowedException(__('Selected individual cannot be linked by the current user.'));
}
} else {
if ($entity['owner_id'] !== $currentUser['id']) {
throw new MethodNotAllowedException(__('Selected individual cannot be linked by the current user.'));
}
}
}
return $entity;
};
}
$this->loadModel('Organisations');
$this->loadModel('Individuals');
$dropdownData = [
'organisation' => $this->Organisations->find('list', [
'sort' => ['name' => 'asc']
]),
'individual' => $this->Individuals->find('list', [
'sort' => ['email' => 'asc']
])
'organisation' => $this->Organisations->find('list')->order(['name' => 'asc'])->where($orgConditions)->all()->toArray(),
'individual' => $this->Individuals->find('list')->order(['email' => 'asc'])->where($individualConditions)->all()->toArray()
];
return $params;
}
public function add()
{
$orgConditions = [];
$individualConditions = [];
$dropdownData = [];
$currentUser = $this->ACL->getUser();
$params = [
'redirect' => $this->referer()
];
$params = $this->buildBeforeSave($params, $currentUser, $orgConditions, $individualConditions, $dropdownData);
$this->CRUD->add($params);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
}
$this->set(compact('dropdownData'));
$this->set('metaGroup', 'ContactDB');
}
public function edit($id = false)
{
$orgConditions = [];
$individualConditions = [];
$dropdownData = [];
$currentUser = $this->ACL->getUser();
$params = [
'fields' => [
'type', 'encryption_key', 'revoked'
],
'redirect' => $this->referer()
];
if (empty($currentUser['role']['perm_admin'])) {
$params = $this->buildBeforeSave($params, $currentUser, $orgConditions, $individualConditions, $dropdownData);
}
$this->CRUD->edit($id, $params);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
@ -87,4 +152,16 @@ class EncryptionKeysController extends AppController
$this->set('metaGroup', 'ContactDB');
$this->render('add');
}
public function view($id = false)
{
$this->CRUD->view($id, [
'contain' => ['Individuals', 'Organisations']
]);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
}
$this->set('metaGroup', 'ContactDB');
}
}

View File

@ -69,6 +69,12 @@ class InstanceController extends AppController
usort($status, function($a, $b) {
return strcmp($b['id'], $a['id']);
});
if ($this->ParamHandler->isRest()) {
return $this->RestResponse->viewData([
'status' => $status,
'updateAvailables' => $migrationStatus['updateAvailables'],
], 'json');
}
$this->set('status', $status);
$this->set('updateAvailables', $migrationStatus['updateAvailables']);
}
@ -139,6 +145,14 @@ class InstanceController extends AppController
{
$this->Settings = $this->getTableLocator()->get('Settings');
$all = $this->Settings->getSettings(true);
if ($this->ParamHandler->isRest()) {
return $this->RestResponse->viewData([
'settingsProvider' => $all['settingsProvider'],
'settings' => $all['settings'],
'settingsFlattened' => $all['settingsFlattened'],
'notices' => $all['notices'],
], 'json');
}
$this->set('settingsProvider', $all['settingsProvider']);
$this->set('settings', $all['settings']);
$this->set('settingsFlattened', $all['settingsFlattened']);

View File

@ -340,6 +340,7 @@ class LocalToolsController extends AppController
}
}
/*
public function connectLocal($local_tool_id)
{
$params = [
@ -355,10 +356,8 @@ class LocalToolsController extends AppController
$params['target_tool_id'] = $postParams['target_tool_id'];
$result = $this->LocalTools->encodeLocalConnection($params);
// Send message to remote inbox
debug($result);
} else {
$target_tools = $this->LocalTools->findConnectable($local_tool);
debug($target_tools);
if (empty($target_tools)) {
throw new NotFoundException(__('No tools found to connect.'));
}
@ -369,4 +368,5 @@ class LocalToolsController extends AppController
]);
}
}
*/
}

View File

@ -6,6 +6,7 @@ use App\Controller\AppController;
use Cake\Utility\Hash;
use Cake\Utility\Text;
use \Cake\Database\Expression\QueryExpression;
use Cake\Http\Exception\BadRequestException;
use Cake\Http\Exception\NotFoundException;
use Cake\Http\Exception\MethodNotAllowedException;
use Cake\Http\Exception\ForbiddenException;

View File

@ -16,10 +16,16 @@ class SharingGroupsController extends AppController
public function index()
{
$currentUser = $this->ACL->getUser();
$conditions = [];
if (empty($currentUser['role']['perm_admin'])) {
$conditions['SharingGroups.organisation_id'] = $currentUser['organisation_id'];
}
$this->CRUD->index([
'contain' => $this->containFields,
'filters' => $this->filterFields,
'quickFilters' => $this->quickFilterFields
'quickFilters' => $this->quickFilterFields,
'conditions' => $conditions
]);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
@ -57,7 +63,12 @@ class SharingGroupsController extends AppController
public function edit($id = false)
{
$this->CRUD->edit($id);
$params = [];
$currentUser = $this->ACL->getUser();
if (empty($currentUser['role']['perm_admin'])) {
$params['conditions'] = ['organisation_id' => $currentUser['organisation_id']];
}
$this->CRUD->edit($id, $params);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
@ -201,11 +212,11 @@ class SharingGroupsController extends AppController
$organisations = [];
if (!empty($user['role']['perm_admin'])) {
$organisations = $this->SharingGroups->Organisations->find('list')->order(['name' => 'ASC'])->toArray();
} else if (!empty($user['individual']['organisations'])) {
} else {
$organisations = $this->SharingGroups->Organisations->find('list', [
'sort' => ['name' => 'asc'],
'conditions' => [
'id IN' => array_values(\Cake\Utility\Hash::extract($user, 'individual.organisations.{n}.id'))
'id' => $user['organisation_id']
]
]);
}

View File

@ -9,6 +9,8 @@ use \Cake\Database\Expression\QueryExpression;
use Cake\Http\Exception\NotFoundException;
use Cake\Http\Exception\MethodNotAllowedException;
use Cake\Http\Exception\ForbiddenException;
use Cake\Http\Exception\UnauthorizedException;
class UserSettingsController extends AppController
{
@ -19,8 +21,12 @@ class UserSettingsController extends AppController
public function index()
{
$conditions = [];
$currentUser = $this->ACL->getUser();
if (empty($currentUser['role']['perm_admin'])) {
$conditions['user_id'] = $currentUser->id;
}
$this->CRUD->index([
'conditions' => [],
'conditions' => $conditions,
'contain' => $this->containFields,
'filters' => $this->filterFields,
'quickFilters' => $this->quickFilterFields,
@ -39,6 +45,9 @@ class UserSettingsController extends AppController
public function view($id)
{
if (!$this->isLoggedUserAllowedToEdit($id)) {
throw new NotFoundException(__('Invalid {0}.', 'user setting'));
}
$this->CRUD->view($id, [
'contain' => ['Users']
]);
@ -50,10 +59,13 @@ class UserSettingsController extends AppController
public function add($user_id = false)
{
$currentUser = $this->ACL->getUser();
$this->CRUD->add([
'redirect' => ['action' => 'index', $user_id],
'beforeSave' => function($data) use ($user_id) {
$data['user_id'] = $user_id;
'beforeSave' => function ($data) use ($currentUser) {
if (empty($currentUser['role']['perm_admin'])) {
$data['user_id'] = $currentUser->id;
}
return $data;
}
]);
@ -61,10 +73,13 @@ class UserSettingsController extends AppController
if (!empty($responsePayload)) {
return $responsePayload;
}
$allUsers = $this->UserSettings->Users->find('list', ['keyField' => 'id', 'valueField' => 'username'])->order(['username' => 'ASC']);
if (empty($currentUser['role']['perm_admin'])) {
$allUsers->where(['id' => $currentUser->id]);
$user_id = $currentUser->id;
}
$dropdownData = [
'user' => $this->UserSettings->Users->find('list', [
'sort' => ['username' => 'asc']
]),
'user' => $allUsers->all()->toArray(),
];
$this->set(compact('dropdownData'));
$this->set('user_id', $user_id);
@ -75,6 +90,11 @@ class UserSettingsController extends AppController
$entity = $this->UserSettings->find()->where([
'id' => $id
])->first();
if (!$this->isLoggedUserAllowedToEdit($entity)) {
throw new NotFoundException(__('Invalid {0}.', 'user setting'));
}
$entity = $this->CRUD->edit($id, [
'redirect' => ['action' => 'index', $entity->user_id]
]);
@ -94,6 +114,9 @@ class UserSettingsController extends AppController
public function delete($id)
{
if (!$this->isLoggedUserAllowedToEdit($id)) {
throw new NotFoundException(__('Invalid {0}.', 'user setting'));
}
$this->CRUD->delete($id);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
@ -160,7 +183,7 @@ class UserSettingsController extends AppController
}
}
public function getBookmarks($forSidebar=false)
public function getBookmarks($forSidebar = false)
{
$bookmarks = $this->UserSettings->getSettingByName($this->ACL->getUser(), $this->UserSettings->BOOKMARK_SETTING_NAME);
$bookmarks = json_decode($bookmarks['value'], true);
@ -200,4 +223,29 @@ class UserSettingsController extends AppController
$this->set('user_id', $this->ACL->getUser()->id);
}
/**
* isLoggedUserAllowedToEdit
*
* @param int|\App\Model\Entity\UserSetting $setting
* @return boolean
*/
private function isLoggedUserAllowedToEdit($setting): bool
{
$currentUser = $this->ACL->getUser();
$isAllowed = false;
if (!empty($currentUser['role']['perm_admin'])) {
$isAllowed = true;
} else {
if (is_numeric($setting)) {
$setting = $this->UserSettings->find()->where([
'id' => $setting
])->first();
if (empty($setting)) {
return false;
}
}
$isAllowed = $setting->user_id == $currentUser->id;
}
return $isAllowed;
}
}

View File

@ -7,20 +7,27 @@ use Cake\Utility\Text;
use Cake\ORM\TableRegistry;
use \Cake\Database\Expression\QueryExpression;
use Cake\Http\Exception\UnauthorizedException;
use Cake\Http\Exception\MethodNotAllowedException;
use Cake\Core\Configure;
class UsersController extends AppController
{
public $filterFields = ['Individuals.uuid', 'username', 'Individuals.email', 'Individuals.first_name', 'Individuals.last_name'];
public $filterFields = ['Individuals.uuid', 'username', 'Individuals.email', 'Individuals.first_name', 'Individuals.last_name', 'Organisations.name'];
public $quickFilterFields = ['Individuals.uuid', ['username' => true], ['Individuals.first_name' => true], ['Individuals.last_name' => true], 'Individuals.email'];
public $containFields = ['Individuals', 'Roles', 'UserSettings'];
public $containFields = ['Individuals', 'Roles', 'UserSettings', 'Organisations'];
public function index()
{
$currentUser = $this->ACL->getUser();
$conditions = [];
if (empty($currentUser['role']['perm_admin'])) {
$conditions['organisation_id'] = $currentUser['organisation_id'];
}
$this->CRUD->index([
'contain' => $this->containFields,
'filters' => $this->filterFields,
'quickFilters' => $this->quickFilterFields,
'conditions' => $conditions
]);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
@ -31,8 +38,12 @@ class UsersController extends AppController
public function add()
{
$currentUser = $this->ACL->getUser();
$this->CRUD->add([
'beforeSave' => function($data) {
'beforeSave' => function($data) use ($currentUser) {
if (!$currentUser['role']['perm_admin']) {
$data['organisation_id'] = $currentUser['organisation_id'];
}
$this->Users->enrollUserRouter($data);
return $data;
}
@ -41,12 +52,28 @@ class UsersController extends AppController
if (!empty($responsePayload)) {
return $responsePayload;
}
/*
$alignments = $this->Users->Individuals->Alignments->find('list', [
//'keyField' => 'id',
'valueField' => 'organisation_id',
'groupField' => 'individual_id'
])->toArray();
$alignments = array_map(function($value) { return array_values($value); }, $alignments);
*/
$org_conditions = [];
if (empty($currentUser['role']['perm_admin'])) {
$org_conditions = ['id' => $currentUser['organisation_id']];
}
$dropdownData = [
'role' => $this->Users->Roles->find('list', [
'sort' => ['name' => 'asc']
]),
'individual' => $this->Users->Individuals->find('list', [
'sort' => ['email' => 'asc']
]),
'organisation' => $this->Users->Organisations->find('list', [
'sort' => ['name' => 'asc'],
'conditions' => $org_conditions
])
];
$this->set(compact('dropdownData'));
@ -59,7 +86,7 @@ class UsersController extends AppController
$id = $this->ACL->getUser()['id'];
}
$this->CRUD->view($id, [
'contain' => ['Individuals' => ['Alignments' => 'Organisations'], 'Roles']
'contain' => ['Individuals' => ['Alignments' => 'Organisations'], 'Roles', 'Organisations']
]);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
@ -70,9 +97,18 @@ class UsersController extends AppController
public function edit($id = false)
{
if (empty($id) || empty($this->ACL->getUser()['role']['perm_admin'])) {
$id = $this->ACL->getUser()['id'];
$currentUser = $this->ACL->getUser();
if (empty($id)) {
$id = $currentUser['id'];
} else {
$id = intval($id);
if ((empty($currentUser['role']['perm_org_admin']) && empty($currentUser['role']['perm_admin']))) {
if ($id !== $currentUser['id']) {
throw new MethodNotAllowedException(__('You are not authorised to edit that user.'));
}
}
}
$params = [
'get' => [
'fields' => [
@ -83,11 +119,15 @@ class UsersController extends AppController
'password'
],
'fields' => [
'id', 'individual_id', 'username', 'disabled', 'password', 'confirm_password'
'password', 'confirm_password'
]
];
if (!empty($this->ACL->getUser()['role']['perm_admin'])) {
$params['fields'][] = 'individual_id';
$params['fields'][] = 'username';
$params['fields'][] = 'role_id';
$params['fields'][] = 'organisation_id';
$params['fields'][] = 'disabled';
}
$this->CRUD->edit($id, $params);
$responsePayload = $this->CRUD->getResponsePayload();
@ -100,6 +140,9 @@ class UsersController extends AppController
]),
'individual' => $this->Users->Individuals->find('list', [
'sort' => ['email' => 'asc']
]),
'organisation' => $this->Users->Organisations->find('list', [
'sort' => ['name' => 'asc']
])
];
$this->set(compact('dropdownData'));
@ -130,11 +173,27 @@ class UsersController extends AppController
{
$result = $this->Authentication->getResult();
// If the user is logged in send them away.
$logModel = $this->Users->auditLogs();
if ($result->isValid()) {
$user = $logModel->userInfo();
$logModel->insert([
'request_action' => 'login',
'model' => 'Users',
'model_id' => $user['id'],
'model_title' => $user['name'],
'changed' => []
]);
$target = $this->Authentication->getLoginRedirect() ?? '/instance/home';
return $this->redirect($target);
}
if ($this->request->is('post') && !$result->isValid()) {
$logModel->insert([
'request_action' => 'login_fail',
'model' => 'Users',
'model_id' => 0,
'model_title' => 'unknown_user',
'changed' => []
]);
$this->Flash->error(__('Invalid username or password'));
}
$this->viewBuilder()->setLayout('login');
@ -144,6 +203,15 @@ class UsersController extends AppController
{
$result = $this->Authentication->getResult();
if ($result->isValid()) {
$logModel = $this->Users->auditLogs();
$user = $logModel->userInfo();
$logModel->insert([
'request_action' => 'logout',
'model' => 'Users',
'model_id' => $user['id'],
'model_title' => $user['name'],
'changed' => []
]);
$this->Authentication->logout();
$this->Flash->success(__('Goodbye.'));
return $this->redirect(\Cake\Routing\Router::url('/users/login'));
@ -162,7 +230,7 @@ class UsersController extends AppController
public function register()
{
if (empty(Configure::read('Cerebrate')['security.registration.self-registration'])) {
if (empty(Configure::read('security.registration.self-registration'))) {
throw new UnauthorizedException(__('User self-registration is not open.'));
}
if ($this->request->is('post')) {

View File

@ -2,6 +2,8 @@
namespace CommonConnectorTools;
use Cake\ORM\Locator\LocatorAwareTrait;
use Cake\Log\Log;
use Cake\Log\Engine\FileLog;
class CommonConnectorTools
{
@ -20,6 +22,38 @@ class CommonConnectorTools
const STATE_CANCELLED = 'Request cancelled';
const STATE_DECLINED = 'Request declined by remote';
public function __construct()
{
if (empty(Log::getConfig("LocalToolDebug{$this->connectorName}"))) {
Log::setConfig("LocalToolDebug{$this->connectorName}", [
'className' => FileLog::class,
'path' => LOGS,
'file' => "{$this->connectorName}-debug",
'scopes' => [$this->connectorName],
'levels' => ['notice', 'info', 'debug'],
]);
}
if (empty(Log::getConfig("LocalToolError{$this->connectorName}"))) {
Log::setConfig("LocalToolError{$this->connectorName}", [
'className' => FileLog::class,
'path' => LOGS,
'file' => "{$this->connectorName}-error",
'scopes' => [$this->connectorName],
'levels' => ['warning', 'error', 'critical', 'alert', 'emergency'],
]);
}
}
protected function logDebug($message)
{
Log::debug($message, [$this->connectorName]);
}
protected function logError($message, $scope=[])
{
Log::error($message, [$this->connectorName]);
}
public function addExposedFunction(string $functionName): void
{
$this->exposedFunctions[] = $functionName;

View File

@ -122,6 +122,11 @@ class MispConnector extends CommonConnectorTools
'type' => 'boolean'
],
];
public $settingsPlaceholder = [
'url' => 'https://your.misp.intance',
'authkey' => '',
'skip_ssl' => '0',
];
public function addSettingValidatorRules($validator)
{
@ -183,6 +188,7 @@ class MispConnector extends CommonConnectorTools
$settings = json_decode($connection->settings, true);
$http = $this->genHTTPClient($connection, $options);
$url = sprintf('%s%s', $settings['url'], $relativeURL);
$this->logDebug(sprintf('%s %s %s', __('Posting data') . PHP_EOL, "POST {$url}" . PHP_EOL, json_encode($data)));
return $http->post($url, $data, $options);
}
@ -234,14 +240,18 @@ class MispConnector extends CommonConnectorTools
if (!empty($params['softError'])) {
return $response;
}
throw new NotFoundException(__('Could not retrieve the requested resource.'));
$errorMsg = __('Could not post to the requested resource for `{0}`. Remote returned:', $url) . PHP_EOL . $response->getStringBody();
$this->logError($errorMsg);
throw new NotFoundException($errorMsg);
}
}
private function postData(string $url, array $params): Response
{
if (empty($params['connection'])) {
throw new NotFoundException(__('No connection object received.'));
$errorMsg = __('No connection object received.');
$this->logError($errorMsg);
throw new NotFoundException($errorMsg);
}
$url = $this->urlAppendParams($url, $params);
if (!is_string($params['body'])) {
@ -251,7 +261,9 @@ class MispConnector extends CommonConnectorTools
if ($response->isOk()) {
return $response;
} else {
throw new NotFoundException(__('Could not post to the requested resource. Remote returned:') . PHP_EOL . $response->getStringBody());
$errorMsg = __('Could not post to the requested resource for `{0}`. Remote returned:', $url) . PHP_EOL . $response->getStringBody();
$this->logError($errorMsg);
throw new NotFoundException($errorMsg);
}
}
@ -309,54 +321,55 @@ class MispConnector extends CommonConnectorTools
'additionalUrlParams' => $urlParams
]
]
]
],
'fields' => [
[
'name' => 'Setting',
'sort' => 'setting',
'data_path' => 'setting',
],
'fields' => [
[
'name' => 'Setting',
'sort' => 'setting',
'data_path' => 'setting',
[
'name' => 'Criticality',
'sort' => 'level',
'data_path' => 'level',
'arrayData' => [
0 => 'Critical',
1 => 'Recommended',
2 => 'Optional'
],
[
'name' => 'Criticality',
'sort' => 'level',
'data_path' => 'level',
'arrayData' => [
0 => 'Critical',
1 => 'Recommended',
2 => 'Optional'
],
'element' => 'array_lookup_field'
],
[
'name' => __('Value'),
'sort' => 'value',
'data_path' => 'value',
'options' => 'options'
],
[
'name' => __('Type'),
'sort' => 'type',
'data_path' => 'type',
],
[
'name' => __('Error message'),
'sort' => 'errorMessage',
'data_path' => 'errorMessage',
]
'element' => 'array_lookup_field'
],
'title' => false,
'description' => false,
'pull' => 'right',
'actions' => [
[
'open_modal' => '/localTools/action/' . h($params['connection']['id']) . '/modifySettingAction?setting={{0}}',
'modal_params_data_path' => ['setting'],
'icon' => 'download',
'reload_url' => '/localTools/action/' . h($params['connection']['id']) . '/ServerSettingsAction'
]
[
'name' => __('Value'),
'sort' => 'value',
'data_path' => 'value',
'options' => 'options'
],
[
'name' => __('Type'),
'sort' => 'type',
'data_path' => 'type',
],
[
'name' => __('Error message'),
'sort' => 'errorMessage',
'data_path' => 'errorMessage',
]
],
'title' => false,
'description' => false,
'pull' => 'right',
'actions' => [
[
'open_modal' => '/localTools/action/' . h($params['connection']['id']) . '/modifySettingAction?setting={{0}}',
'modal_params_data_path' => ['setting'],
'icon' => 'download',
'reload_url' => '/localTools/action/' . h($params['connection']['id']) . '/ServerSettingsAction'
]
]
];
]
];
if (!empty($params['quickFilter'])) {
$needle = strtolower($params['quickFilter']);
foreach ($finalSettings['data']['data'] as $k => $v) {

View File

@ -46,6 +46,22 @@ class SkeletonConnector extends CommonConnectorTools
'redirect' => 'serverSettingsAction'
]
];
public $settings = [
'url' => [
'type' => 'text'
],
'authkey' => [
'type' => 'text'
],
'skip_ssl' => [
'type' => 'boolean'
],
];
public $settingsPlaceholder = [
'url' => 'https://your.url',
'authkey' => '',
'skip_ssl' => '0',
];
public function health(Object $connection): array
{

View File

@ -0,0 +1,213 @@
<?php
namespace App\Model\Behavior;
use ArrayObject;
use Cake\Datasource\EntityInterface;
use Cake\Event\EventInterface;
use Cake\ORM\Behavior;
use Cake\ORM\Entity;
use Cake\ORM\Query;
use Cake\Utility\Text;
use Cake\Utility\Security;
use \Cake\Http\Session;
use Cake\Core\Configure;
use Cake\ORM\TableRegistry;
use App\Model\Table\AuditLogTable;
class AuditLogBehavior extends Behavior
{
/** @var array */
private $config;
/** @var array|null */
private $old;
/** @var AuditLog|null */
private $AuditLogs;
// Hash is faster that in_array
private $skipFields = [
'id' => true,
'lastpushedid' => true,
'timestamp' => true,
'revision' => true,
'modified' => true,
'date_modified' => true, // User
'current_login' => true, // User
'last_login' => true, // User
'newsread' => true, // User
'proposal_email_lock' => true, // Event
];
public function initialize(array $config): void
{
$this->config = $config;
}
public function beforeSave(EventInterface $event, EntityInterface $entity, ArrayObject $options)
{
$fields = $entity->extract($entity->getVisible(), true);
$skipFields = $this->skipFields;
$fieldsToFetch = array_filter($fields, function($key) use ($skipFields) {
return strpos($key, '_') !== 0 && !isset($skipFields[$key]);
}, ARRAY_FILTER_USE_KEY);
// Do not fetch old version when just few fields will be fetched
$fieldToFetch = [];
if (!empty($options['fieldList'])) {
foreach ($options['fieldList'] as $field) {
if (!isset($this->skipFields[$field])) {
$fieldToFetch[] = $field;
}
}
if (empty($fieldToFetch)) {
$this->old = null;
return true;
}
}
if ($entity->id) {
$this->old = $this->_table->find()->where(['id' => $entity->id])->contain($fieldToFetch)->first();
} else {
$this->old = null;
}
return true;
}
public function afterSave(EventInterface $event, EntityInterface $entity, ArrayObject $options)
{
if ($entity->id) {
$id = $entity->id;
} else {
$id = null;
}
if ($entity->isNew()) {
$action = $entity->getConstant('ACTION_ADD');
} else {
$action = $entity->getConstant('ACTION_EDIT');
if (isset($entity['deleted'])) {
if ($entity['deleted']) {
$action = $entity->getConstant('ACTION_SOFT_DELETE');
} else if (!$entity['deleted'] && $this->old['deleted']) {
$action = $entity->getConstant('ACTION_UNDELETE');
}
}
}
$changedFields = $this->changedFields($entity, isset($options['fieldList']) ? $options['fieldList'] : null);
if (empty($changedFields)) {
return;
}
$modelTitleField = $this->_table->getDisplayField();
if (is_callable($modelTitleField)) {
$modelTitle = $modelTitleField($entity, isset($this->old) ? $this->old : []);
} else if (isset($entity[$modelTitleField])) {
$modelTitle = $entity[$modelTitleField];
} else if ($this->old[$modelTitleField]) {
$modelTitle = $this->old[$modelTitleField];
}
$this->auditLogs()->insert([
'request_action' => $action,
'model' => $entity->getSource(),
'model_id' => $id,
'model_title' => $modelTitle,
'changed' => $changedFields
]);
}
public function beforeDelete(EventInterface $event, EntityInterface $entity, ArrayObject $options)
{
$this->old = $this->_table->find()->where(['id' => $entity->id])->first();
return true;
}
public function afterDelete(EventInterface $event, EntityInterface $entity, ArrayObject $options)
{
$modelTitleField = $this->_table->getDisplayField();
if (is_callable($modelTitleField)) {
$modelTitle = $modelTitleField($entity, []);
} else if (isset($entity[$modelTitleField])) {
$modelTitle = $entity[$modelTitleField];
}
$this->auditLogs()->insert([
'request_action' => $entity->getConstant('ACTION_DELETE'),
'model' => $entity->getSource(),
'model_id' => $this->old->id,
'model_title' => $modelTitle,
'changed' => $this->changedFields($entity)
]);
}
/**
* @param Model $model
* @param array|null $fieldsToSave
* @return array
*/
private function changedFields(EntityInterface $entity, $fieldsToSave = null)
{
$dbFields = $this->_table->getSchema()->typeMap();
$changedFields = [];
foreach ($entity->extract($entity->getVisible()) as $key => $value) {
if (isset($this->skipFields[$key])) {
continue;
}
if (!isset($dbFields[$key])) {
continue;
}
if ($fieldsToSave && !in_array($key, $fieldsToSave, true)) {
continue;
}
if (isset($entity[$key]) && isset($this->old[$key])) {
$old = $this->old[$key];
} else {
$old = null;
}
// Normalize
if (is_bool($old)) {
$old = $old ? 1 : 0;
}
if (is_bool($value)) {
$value = $value ? 1 : 0;
}
$dbType = $dbFields[$key];
if ($dbType === 'integer' || $dbType === 'tinyinteger' || $dbType === 'biginteger' || $dbType === 'boolean') {
$value = (int)$value;
if ($old !== null) {
$old = (int)$old;
}
}
if ($value == $old) {
continue;
}
if ($key === 'password' || $key === 'authkey') {
$value = '*****';
if ($old !== null) {
$old = $value;
}
}
if ($old === null) {
$changedFields[$key] = $value;
} else {
$changedFields[$key] = [$old, $value];
}
}
return $changedFields;
}
/**
* @return AuditLogs
*/
public function auditLogs()
{
if ($this->AuditLogs === null) {
$this->AuditLogs = TableRegistry::getTableLocator()->get('AuditLogs');
}
return $this->AuditLogs;
}
public function log()
{
}
}

View File

@ -98,7 +98,7 @@ class AuthKeycloakBehavior extends Behavior
{
$individual = $this->_table->Individuals->find()->where(
['id' => $data['individual_id']]
)->contain(['Organisations'])->first();
)->first();
$roleConditions = [
'id' => $data['role_id']
];
@ -106,10 +106,9 @@ class AuthKeycloakBehavior extends Behavior
$roleConditions['name'] = Configure::read('keycloak.default_role_name');
}
$role = $this->_table->Roles->find()->where($roleConditions)->first();
$orgs = [];
foreach ($individual['organisations'] as $org) {
$orgs[] = $org['uuid'];
}
$org = $this->_table->Organisations->find()->where([
['id' => $data['organisation_id']]
])->first();
$token = $this->getAdminAccessToken();
$keyCloakUser = [
'firstName' => $individual['first_name'],
@ -118,7 +117,7 @@ class AuthKeycloakBehavior extends Behavior
'email' => $individual['email'],
'attributes' => [
'role_name' => empty($role['name']) ? Configure::read('keycloak.default_role_name') : $role['name'],
'org_uuid' => empty($orgs[0]) ? '' : $orgs[0]
'org_uuid' => $org['uuid']
]
];
$keycloakConfig = Configure::read('keycloak');

View File

@ -6,6 +6,28 @@ use Cake\ORM\Entity;
class AppModel extends Entity
{
const BROTLI_HEADER = "\xce\xb2\xcf\x81";
const BROTLI_MIN_LENGTH = 200;
const ACTION_ADD = 'add',
ACTION_EDIT = 'edit',
ACTION_SOFT_DELETE = 'soft_delete',
ACTION_DELETE = 'delete',
ACTION_UNDELETE = 'undelete',
ACTION_TAG = 'tag',
ACTION_TAG_LOCAL = 'tag_local',
ACTION_REMOVE_TAG = 'remove_tag',
ACTION_REMOVE_TAG_LOCAL = 'remove_local_tag',
ACTION_LOGIN = 'login',
ACTION_LOGIN_FAIL = 'login_fail',
ACTION_LOGOUT = 'logout';
public function getConstant($name)
{
return constant('self::' . $name);
}
public function getAccessibleFieldForNew(): array
{
return $this->_accessibleOnNew ?? [];

View File

@ -0,0 +1,68 @@
<?php
namespace App\Model\Entity;
use App\Model\Entity\AppModel;
use Cake\ORM\Entity;
use Cake\Core\Configure;
class AuditLog extends AppModel
{
private $compressionEnabled = false;
public function __construct(array $properties = [], array $options = [])
{
$this->compressionEnabled = Configure::read('Cerebrate.log_compress') && function_exists('brotli_compress');
parent::__construct($properties, $options);
}
protected function _getTitle(): String
{
return $this->generateUserFriendlyTitle($this);
}
/**
* @param string $change
* @return array|string
* @throws JsonException
*/
private function decodeChange($change)
{
if (substr($change, 0, 4) === self::BROTLI_HEADER) {
if (function_exists('brotli_uncompress')) {
$change = brotli_uncompress(substr($change, 4));
if ($change === false) {
return 'Compressed';
}
} else {
return 'Compressed';
}
}
return json_decode($change, true);
}
/**
* @param array $auditLog
* @return string
*/
public function generateUserFriendlyTitle($auditLog)
{
if (in_array($auditLog['request_action'], [self::ACTION_TAG, self::ACTION_TAG_LOCAL, self::ACTION_REMOVE_TAG, self::ACTION_REMOVE_TAG_LOCAL], true)) {
$attached = ($auditLog['request_action'] === self::ACTION_TAG || $auditLog['request_action'] === self::ACTION_TAG_LOCAL);
$local = ($auditLog['request_action'] === self::ACTION_TAG_LOCAL || $auditLog['request_action'] === self::ACTION_REMOVE_TAG_LOCAL) ? __('local') : __('global');
if ($attached) {
return __('Attached %s tag "%s" to %s #%s', $local, $auditLog['model_title'], strtolower($auditLog['model']), $auditLog['model_id']);
} else {
return __('Detached %s tag "%s" from %s #%s', $local, $auditLog['model_title'], strtolower($auditLog['model']), $auditLog['model_id']);
}
}
$title = "{$auditLog['model']} #{$auditLog['model_id']}";
if (isset($auditLog['model_title']) && $auditLog['model_title']) {
$title .= ": {$auditLog['model_title']}";
}
return $title;
}
}

View File

@ -10,10 +10,5 @@ class Organisation extends AppModel
protected $_accessible = [
'*' => true,
'id' => false,
'uuid' => false,
];
protected $_accessibleOnNew = [
'uuid' => true,
];
}

View File

@ -0,0 +1,11 @@
<?php
namespace App\Model\Entity;
use App\Model\Entity\AppModel;
use Cake\ORM\Entity;
class Outbox extends AppModel
{
}

View File

@ -12,6 +12,7 @@ class AlignmentsTable extends AppTable
{
parent::initialize($config);
$this->belongsTo('Individuals');
$this->addBehavior('AuditLog');
$this->belongsTo('Organisations');
$this->addBehavior('Timestamp');
}

View File

@ -107,8 +107,9 @@ class AppTable extends Table
$statistics['amount'] = $table->find()->all()->count();
if ($table->behaviors()->has('Timestamp') && $includeTimeline) {
$statistics['timeline'] = $this->buildTimeline($table, $days, $field);
$statistics['variation'] = $table->find()->where(["{$field} >" => new \DateTime("-{$days} days")])->all()->count();
-$statistics['amount'];
$startCount = $table->find()->where(["{$field} >" => new \DateTime("-{$days} days")])->all()->count();
$endCount = $statistics['amount'];
$statistics['variation'] = $startCount - $endCount;
} else {
$statistics['timeline'] = [];
$statistics['variation'] = 0;

View File

@ -0,0 +1,250 @@
<?php
namespace App\Model\Table;
use App\Model\Table\AppTable;
use Cake\ORM\Table;
use Cake\Validation\Validator;
use Cake\Datasource\EntityInterface;
use Cake\Event\Event;
use Cake\Event\EventInterface;
use Cake\Auth\DefaultPasswordHasher;
use Cake\Utility\Security;
use Cake\Core\Configure;
use Cake\Routing\Router;
use Cake\Http\Exception\MethodNotAllowedException;
use ArrayObject;
/**
* @property Event $Event
* @property User $User
* @property Organisation $Organisation
*/
class AuditLogsTable extends AppTable
{
const BROTLI_HEADER = "\xce\xb2\xcf\x81";
const BROTLI_MIN_LENGTH = 200;
const REQUEST_TYPE_DEFAULT = 0,
REQUEST_TYPE_API = 1,
REQUEST_TYPE_CLI = 2;
/** @var array|null */
private $user = null;
/** @var bool */
private $compressionEnabled;
/**
* Null when not defined, false when not enabled
* @var Syslog|null|false
*/
private $syslog;
public function initialize(array $config): void
{
parent::initialize($config);
$this->addBehavior('UUID');
$this->addBehavior('Timestamp', [
'Model.beoreSave' => [
'created_at' => 'new'
]
]);
$this->belongsTo('Users');
$this->setDisplayField('info');
$this->compressionEnabled = Configure::read('Cerebrate.log_new_audit_compress') && function_exists('brotli_compress');
}
public function beforeMarshal(EventInterface $event, ArrayObject $data, ArrayObject $options)
{
if (!isset($data['request_ip'])) {
$ipHeader = 'REMOTE_ADDR';
if (isset($_SERVER[$ipHeader])) {
$data['request_ip'] = $_SERVER[$ipHeader];
} else {
$data['request_ip'] = '127.0.0.1';
}
}
foreach (['user_id', 'request_type', 'authkey_id'] as $field) {
if (!isset($data[$field])) {
if (!isset($userInfo)) {
$userInfo = $this->userInfo();
}
if (!empty($userInfo[$field])) {
$data[$field] = $userInfo[$field];
} else {
$data[$field] = 0;
}
}
}
if (!isset($data['request_id'] ) && isset($_SERVER['HTTP_X_REQUEST_ID'])) {
$data['request_id'] = $_SERVER['HTTP_X_REQUEST_ID'];
}
// Truncate request_id
if (isset($data['request_id']) && strlen($data['request_id']) > 255) {
$data['request_id'] = substr($data['request_id'], 0, 255);
}
// Truncate model title
if (isset($data['model_title']) && mb_strlen($data['model_title']) > 255) {
$data['model_title'] = mb_substr($data['model_title'], 0, 252) . '...';
}
if (isset($data['changed'])) {
$changed = json_encode($data['changed'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if ($this->compressionEnabled && strlen($changed) >= self::BROTLI_MIN_LENGTH) {
$changed = self::BROTLI_HEADER . brotli_compress($changed, 4, BROTLI_TEXT);
}
$data['changed'] = $changed;
}
}
public function beforeSave(EventInterface $event, EntityInterface $entity, ArrayObject $options)
{
$entity->request_ip = inet_pton($entity->request_ip);
$this->logData($entity);
return true;
}
/**
* @param array $data
* @return bool
*/
private function logData(EntityInterface $entity)
{
if (Configure::read('Plugin.ZeroMQ_enable') && Configure::read('Plugin.ZeroMQ_audit_notifications_enable')) {
$pubSubTool = $this->getPubSubTool();
$pubSubTool->publish($data, 'audit', 'log');
}
//$this->publishKafkaNotification('audit', $data, 'log');
if (Configure::read('Plugin.ElasticSearch_logging_enable')) {
// send off our logs to distributed /dev/null
$logIndex = Configure::read("Plugin.ElasticSearch_log_index");
$elasticSearchClient = $this->getElasticSearchTool();
$elasticSearchClient->pushDocument($logIndex, "log", $data);
}
// write to syslogd as well if enabled
if ($this->syslog === null) {
if (Configure::read('Security.syslog')) {
$options = [];
$syslogToStdErr = Configure::read('Security.syslog_to_stderr');
if ($syslogToStdErr !== null) {
$options['to_stderr'] = $syslogToStdErr;
}
$syslogIdent = Configure::read('Security.syslog_ident');
if ($syslogIdent) {
$options['ident'] = $syslogIdent;
}
$this->syslog = new SysLog($options);
} else {
$this->syslog = false;
}
}
if ($this->syslog) {
$entry = $data['request_action'];
$title = $entity->generateUserFriendlyTitle();
if ($title) {
$entry .= " -- $title";
}
$this->syslog->write('info', $entry);
}
return true;
}
/**
* @return array
*/
public function userInfo()
{
if ($this->user !== null) {
return $this->user;
}
$this->user = ['id' => 0, /*'org_id' => 0, */'authkey_id' => 0, 'request_type' => self::REQUEST_TYPE_DEFAULT, 'name' => ''];
$isShell = (php_sapi_name() === 'cli');
if ($isShell) {
// do not start session for shell commands and fetch user info from configuration
$this->user['request_type'] = self::REQUEST_TYPE_CLI;
$currentUserId = Configure::read('CurrentUserId');
if (!empty($currentUserId)) {
$this->user['id'] = $currentUserId;
$userFromDb = $this->Users->find()->where(['id' => $currentUserId])->first();
$this->user['name'] = $userFromDb['name'];
$this->user['org_id'] = $userFromDb['org_id'];
}
} else {
$authUser = Router::getRequest()->getSession()->read('authUser');
if (!empty($authUser)) {
$this->user['id'] = $authUser['id'];
$this->user['user_id'] = $authUser['id'];
$this->user['name'] = $authUser['name'];
//$this->user['org_id'] = $authUser['org_id'];
if (isset($authUser['logged_by_authkey']) && $authUser['logged_by_authkey']) {
$this->user['request_type'] = self::REQUEST_TYPE_API;
}
if (isset($authUser['authkey_id'])) {
$this->user['authkey_id'] = $authUser['authkey_id'];
}
}
}
return $this->user;
}
public function insert(array $data)
{
$logEntity = $this->newEntity($data);
if ($logEntity->getErrors()) {
throw new Exception($logEntity->getErrors());
} else {
$this->save($logEntity);
}
}
/**
* @param string|int $org
* @return array
*/
public function returnDates($org = 'all')
{
$conditions = [];
if ($org !== 'all') {
$org = $this->Organisation->fetchOrg($org);
if (empty($org)) {
throw new NotFoundException('Invalid organisation.');
}
$conditions['org_id'] = $org['id'];
}
$dataSource = ConnectionManager::getDataSource('default')->config['datasource'];
if ($dataSource === 'Database/Mysql' || $dataSource === 'Database/MysqlObserver') {
$validDates = $this->find('all', [
'recursive' => -1,
'fields' => ['DISTINCT UNIX_TIMESTAMP(DATE(created)) AS Date', 'count(id) AS count'],
'conditions' => $conditions,
'group' => ['Date'],
'order' => ['Date'],
]);
} elseif ($dataSource === 'Database/Postgres') {
if (!empty($conditions['org_id'])) {
$condOrg = sprintf('WHERE org_id = %s', intval($conditions['org_id']));
} else {
$condOrg = '';
}
$sql = 'SELECT DISTINCT EXTRACT(EPOCH FROM CAST(created AS DATE)) AS "Date", COUNT(id) AS count
FROM audit_logs
' . $condOrg . '
GROUP BY "Date" ORDER BY "Date"';
$validDates = $this->query($sql);
}
$data = [];
foreach ($validDates as $date) {
$data[(int)$date[0]['Date']] = (int)$date[0]['count'];
}
return $data;
}
}

View File

@ -19,10 +19,11 @@ class AuthKeysTable extends AppTable
{
parent::initialize($config);
$this->addBehavior('UUID');
$this->addBehavior('AuditLog');
$this->belongsTo(
'Users'
);
$this->setDisplayField('authkey');
$this->setDisplayField('comment');
}
public function beforeMarshal(EventInterface $event, ArrayObject $data, ArrayObject $options)

View File

@ -19,6 +19,7 @@ class BroodsTable extends AppTable
parent::initialize($config);
$this->addBehavior('UUID');
$this->addBehavior('Timestamp');
$this->addBehavior('AuditLog');
$this->BelongsTo(
'Organisations'
);

View File

@ -14,6 +14,7 @@ class EncryptionKeysTable extends AppTable
{
parent::initialize($config);
$this->addBehavior('UUID');
$this->addBehavior('AuditLog');
$this->addBehavior('Timestamp');
$this->belongsTo(
'Individuals',

View File

@ -19,7 +19,7 @@ class InboxTable extends AppTable
parent::initialize($config);
$this->addBehavior('UUID');
$this->addBehavior('Timestamp');
$this->addBehavior('AuditLog');
$this->belongsTo('Users');
$this->setDisplayField('title');
}

View File

@ -15,6 +15,7 @@ class IndividualsTable extends AppTable
$this->addBehavior('Timestamp');
$this->addBehavior('Tags.Tag');
$this->addBehavior('MetaFields');
$this->addBehavior('AuditLog');
$this->hasMany(
'Alignments',

View File

@ -18,6 +18,8 @@ class InstanceTable extends AppTable
public function initialize(array $config): void
{
parent::initialize($config);
$this->addBehavior('AuditLog');
$this->setDisplayField('name');
}
public function validationDefault(Validator $validator): Validator
@ -38,6 +40,18 @@ class InstanceTable extends AppTable
public function searchAll($value, $limit=5, $model=null)
{
$results = [];
// search in metafields. FIXME: To be replaced by the meta-template system
$metaFieldTable = TableRegistry::get('MetaFields');
$query = $metaFieldTable->find()->where([
'value LIKE' => '%' . $value . '%'
]);
$results['MetaFields']['amount'] = $query->count();
$result = $query->limit($limit)->all()->toList();
if (!empty($result)) {
$results['MetaFields']['entries'] = $result;
}
$models = $this->seachAllTables;
if (!is_null($model)) {
if (in_array($model, $this->seachAllTables)) {

View File

@ -30,6 +30,7 @@ class LocalToolsTable extends AppTable
public function initialize(array $config): void
{
parent::initialize($config);
$this->addBehavior('AuditLog');
$this->addBehavior('Timestamp');
}
@ -142,7 +143,8 @@ class LocalToolsTable extends AppTable
'connector' => $connector_type,
'connector_version' => $connector_class->version,
'connector_description' => $connector_class->description,
'connector_settings' => $connector_class->settings ?? []
'connector_settings' => $connector_class->settings ?? [],
'connector_settings_placeholder' => $connector_class->settingsPlaceholder ?? [],
];
if ($includeConnections) {
$connector['connections'] = $this->healthCheck($connector_type, $connector_class);
@ -287,6 +289,7 @@ class LocalToolsTable extends AppTable
return $jsonReply;
}
/*
public function findConnectable($local_tool): array
{
$connectors = $this->getInterconnectors($local_tool['connector']);
@ -296,8 +299,8 @@ class LocalToolsTable extends AppTable
$validTargets[$connector['connects'][1]] = 1;
}
}
}
*/
public function fetchConnection($id): object
{

View File

@ -18,6 +18,7 @@ class MetaFieldsTable extends AppTable
'MetaTemplateFields' => ['counter']
]);
$this->addBehavior('AuditLog');
$this->belongsTo('MetaTemplates');
$this->belongsTo('MetaTemplateFields');

View File

@ -16,8 +16,10 @@ class OrganisationsTable extends AppTable
public function initialize(array $config): void
{
parent::initialize($config);
$this->addBehavior('UUID');
$this->addBehavior('Timestamp');
$this->addBehavior('Tags.Tag');
$this->addBehavior('AuditLog');
$this->hasMany(
'Alignments',
[

View File

@ -28,6 +28,7 @@ class OutboxProcessorsTable extends AppTable
if (empty($this->outboxProcessors)) {
$this->loadProcessors();
}
$this->addBehavior('AuditLog');
}
public function getProcessor($scope, $action=null)

View File

@ -19,8 +19,8 @@ class OutboxTable extends AppTable
parent::initialize($config);
$this->addBehavior('UUID');
$this->addBehavior('Timestamp');
$this->belongsTo('Users');
$this->addBehavior('AuditLog');
$this->setDisplayField('title');
}

View File

@ -18,6 +18,7 @@ class RemoteToolConnectionsTable extends AppTable
'LocalTools'
);
$this->setDisplayField('id');
$this->addBehavior('AuditLog');
}
public function validationDefault(Validator $validator): Validator

View File

@ -12,6 +12,7 @@ class RolesTable extends AppTable
{
parent::initialize($config);
$this->addBehavior('UUID');
$this->addBehavior('AuditLog');
$this->hasMany(
'Users',
[

View File

@ -211,6 +211,13 @@ class CerebrateSettingsProvider extends BaseSettingsProvider
},
'dependsOn' => 'keycloak.enabled'
],
'keycloak.screw' => [
'name' => 'Screw',
'type' => 'string',
'severity' => 'info',
'default' => 0,
'description' => __('The misalignment allowed when validating JWT tokens between cerebrate and keycloak. Whilst crisp timings are essential for any timing push, perfect timing is only achievable by GSL participants. (given in seconds)')
],
'keycloak.mapping.org_uuid' => [
'name' => 'org_uuid mapping',
'type' => 'string',

View File

@ -26,6 +26,7 @@ class SettingsTable extends AppTable
parent::initialize($config);
$this->setTable(false);
$this->SettingsProvider = new CerebrateSettingsProvider();
$this->addBehavior('AuditLog');
}
public function getSettings($full=false): array

View File

@ -15,6 +15,7 @@ class SharingGroupsTable extends AppTable
parent::initialize($config);
$this->addBehavior('UUID');
$this->addBehavior('Timestamp');
$this->addBehavior('AuditLog');
$this->belongsTo(
'Users'
);

View File

@ -11,7 +11,7 @@ use App\Settings\SettingsProvider\UserSettingsProvider;
class UserSettingsTable extends AppTable
{
protected $BOOKMARK_SETTING_NAME = 'ui.bookmarks';
public $BOOKMARK_SETTING_NAME = 'ui.bookmarks';
public function initialize(array $config): void
{

View File

@ -12,6 +12,7 @@ use \Cake\Http\Session;
use Cake\Http\Client;
use Cake\Utility\Security;
use Cake\Core\Configure;
use Cake\Utility\Text;
class UsersTable extends AppTable
{
@ -20,6 +21,7 @@ class UsersTable extends AppTable
parent::initialize($config);
$this->addBehavior('Timestamp');
$this->addBehavior('UUID');
$this->addBehavior('AuditLog');
$this->initAuthBehaviors();
$this->belongsTo(
'Individuals',
@ -35,6 +37,13 @@ class UsersTable extends AppTable
'cascadeCallbacks' => false
]
);
$this->belongsTo(
'Organisations',
[
'dependent' => false,
'cascadeCallbacks' => false
]
);
$this->hasMany(
'UserSettings',
[
@ -88,15 +97,35 @@ class UsersTable extends AppTable
return $rules;
}
public function test()
{
$this->Roles = TableRegistry::get('Roles');
$role = $this->Roles->newEntity([
'name' => 'admin',
'perm_admin' => 1,
'perm_org_admin' => 1,
'perm_sync' => 1
]);
$this->Roles->save($role);
}
public function checkForNewInstance(): bool
{
if (empty($this->find()->first())) {
$this->Roles = TableRegistry::get('Roles');
$role = $this->Roles->newEntity([
'name' => 'admin',
'perm_admin' => 1
'perm_admin' => 1,
'perm_org_admin' => 1,
'perm_sync' => 1
]);
$this->Roles->save($role);
$this->Organisations = TableRegistry::get('Organisations');
$organisation = $this->Organisations->newEntity([
'name' => 'default_organisation',
'uuid' => Text::uuid()
]);
$this->Organisations->save($organisation);
$this->Individuals = TableRegistry::get('Individuals');
$individual = $this->Individuals->newEntity([
'email' => 'admin@admin.test',
@ -108,6 +137,7 @@ class UsersTable extends AppTable
'username' => 'admin',
'password' => 'Password1234',
'individual_id' => $individual->id,
'organisation_id' => $organisation->id,
'role_id' => $role->id
]);
$this->save($user);

View File

@ -44,5 +44,6 @@ class AppView extends View
$this->loadHelper('FormFieldMassage');
$this->loadHelper('Paginator', ['templates' => 'cerebrate-pagination-templates']);
$this->loadHelper('Tags.Tag');
$this->loadHelper('ACL');
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\View\Helper;
use Cake\View\Helper;
// This helper helps determining the brightness of a colour (initially only used for the tagging) in order to decide
// what text colour to use against the background (black or white)
class ACLHelper extends Helper {
private $roleAccess = [];
public function checkAccess($controller, $action) {
if (empty($this->roleAccess)) {
$this->roleAccess = $this->getView()->get('roleAccess');
}
if (
in_array($action, $this->roleAccess['*']) ||
(isset($this->roleAccess[$controller]) && in_array($action, $this->roleAccess[$controller]))
) {
return true;
} else {
return false;
}
}
}

View File

@ -651,13 +651,13 @@ class BoostrapTable extends BootstrapGeneric
],
]);
foreach ($this->items as $i => $row) {
$body .= $this->genRow($row);
$body .= $this->genRow($row, $i);
}
$body .= $this->closeNode('tbody');
return $body;
}
private function genRow($row)
private function genRow($row, $rowIndex)
{
$html = $this->openNode('tr', [
'class' => [
@ -672,21 +672,21 @@ class BoostrapTable extends BootstrapGeneric
$key = $field;
}
$cellValue = Hash::get($row, $key);
$html .= $this->genCell($cellValue, $field, $row, $i);
$html .= $this->genCell($cellValue, $field, $row, $rowIndex);
}
} else { // indexed array
foreach ($row as $cellValue) {
$html .= $this->genCell($cellValue, $field, $row, $i);
foreach ($row as $i => $cellValue) {
$html .= $this->genCell($cellValue, 'index', $row, $rowIndex);
}
}
$html .= $this->closeNode('tr');
return $html;
}
private function genCell($value, $field = [], $row = [], $i = 0)
private function genCell($value, $field=[], $row=[], $rowIndex=0)
{
if (isset($field['formatter'])) {
$cellContent = $field['formatter']($value, $row, $i);
$cellContent = $field['formatter']($value, $row, $rowIndex);
} else if (isset($field['element'])) {
$cellContent = $this->btHelper->getView()->element($field['element'], [
'data' => [$value],

2
templates/Api/index.php Normal file
View File

@ -0,0 +1,2 @@
<redoc spec-url='<?php echo $url ?>'></redoc>
<script src="https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"> </script>

View File

@ -0,0 +1,65 @@
<?php
echo $this->element('genericElements/IndexTable/index_table', [
'data' => [
'data' => $data,
'top_bar' => [
'children' => [
[
'type' => 'search',
'button' => __('Filter'),
'placeholder' => __('Enter value to search'),
'data' => '',
'searchKey' => 'value'
]
]
],
'fields' => [
[
'name' => '#',
'sort' => 'id',
'data_path' => 'id',
],
[
'name' => __('IP'),
'sort' => 'request_ip',
'data_path' => 'request_ip',
],
[
'name' => __('Username'),
'sort' => 'user.username',
'data_path' => 'user.username',
],
[
'name' => __('Title'),
'data_path' => 'title',
],
[
'name' => __('Model'),
'sort' => 'model',
'data_path' => 'model',
],
[
'name' => __('Model ID'),
'sort' => 'model',
'data_path' => 'model_id',
],
[
'name' => __('Action'),
'sort' => 'request_action',
'data_path' => 'request_action',
],
[
'name' => __('Changed'),
'sort' => 'changed',
'data_path' => 'changed',
'element' => 'json'
],
],
'title' => __('Logs'),
'description' => null,
'pull' => 'right',
'actions' => []
]
]);
echo '</div>';
?>

View File

@ -0,0 +1,32 @@
<?php
echo $this->element(
'/genericElements/SingleViews/single_view',
[
'data' => $entity,
'fields' => [
[
'key' => __('ID'),
'path' => 'id'
],
[
'key' => __('Type'),
'path' => 'type'
],
[
'key' => __('Owner'),
'path' => 'owner_id',
'owner_model_path' => 'owner_model',
'type' => 'owner'
],
[
'key' => __('Revoked'),
'path' => 'revoked'
],
[
'key' => __('Key'),
'path' => 'encryption_key'
]
]
]
);

View File

@ -23,7 +23,8 @@
),
array(
'field' => 'tag_list',
'type' => 'tags'
'type' => 'tags',
'requirements' => $this->request->getParam('action') === 'edit'
),
),
'submit' => array(

View File

@ -19,14 +19,25 @@
</span>', h($tableName), $tableResult['amount']);
foreach ($tableResult['entries'] as $entry) {
$section .= sprintf('<a class="dropdown-item" href="%s">%s</a>',
Cake\Routing\Router::URL([
'controller' => Cake\Utility\Inflector::pluralize($entry->getSource()),
'action' => 'view',
h($entry['id'])
]),
h($entry[$fieldPath])
);
if ($entry->getSource() == 'MetaFields') {
$section .= sprintf('<a class="dropdown-item" href="%s">%s</a>',
Cake\Routing\Router::URL([
'controller' => Cake\Utility\Inflector::pluralize($entry->scope),
'action' => 'view',
h($entry->parent_id)
]),
sprintf('%s (%s::%s)', h($entry->value), h($entry->scope), h($entry->field))
);
} else {
$section .= sprintf('<a class="dropdown-item" href="%s">%s</a>',
Cake\Routing\Router::URL([
'controller' => Cake\Utility\Inflector::pluralize($entry->getSource()),
'action' => 'view',
h($entry['id'])
]),
h($entry[$fieldPath])
);
}
}
$remaining = $tableResult['amount'] - count($tableResult['entries']);
if ($remaining > 0) {

View File

@ -22,7 +22,8 @@
'codemirror' => [
'height' => '10rem',
'hints' => $connectors[0]['connector_settings']
]
],
'placeholder' => json_encode($connectors[0]['connector_settings_placeholder'], JSON_FORCE_OBJECT | JSON_PRETTY_PRINT)
],
[
'field' => 'description',

View File

@ -89,12 +89,14 @@ echo $this->element('genericElements/IndexTable/index_table', [
'url_params_data_paths' => ['id'],
'icon' => 'eye'
],
/*
[
'open_modal' => '/localTools/connectLocal/[onclick_params_data_path]',
'modal_params_data_path' => 'id',
'reload_url' => sprintf('/localTools/connectorIndex/%s', h($connectorName)),
'icon' => 'plug'
],
*/
[
'open_modal' => '/localTools/edit/[onclick_params_data_path]',
'modal_params_data_path' => 'id',

View File

@ -7,17 +7,13 @@
array(
'field' => 'name'
),
array(
'field' => 'description',
'type' => 'textarea'
),
array(
'field' => 'uuid',
'label' => 'UUID',
'type' => 'uuid'
),
array(
'field' => 'URL'
'field' => 'url'
),
array(
'field' => 'nationality'

View File

@ -97,12 +97,14 @@ echo $this->element('genericElements/IndexTable/index_table', [
[
'open_modal' => '/organisations/edit/[onclick_params_data_path]',
'modal_params_data_path' => 'id',
'icon' => 'edit'
'icon' => 'edit',
'requirement' => $loggedUser['role']['perm_admin']
],
[
'open_modal' => '/organisations/delete/[onclick_params_data_path]',
'modal_params_data_path' => 'id',
'icon' => 'trash'
'icon' => 'trash',
'requirement' => $loggedUser['role']['perm_admin']
],
]
]

View File

@ -78,12 +78,14 @@ echo $this->element('genericElements/IndexTable/index_table', [
[
'open_modal' => '/roles/edit/[onclick_params_data_path]',
'modal_params_data_path' => 'id',
'icon' => 'edit'
'icon' => 'edit',
'requirement' => !empty($loggedUser['role']['perm_admin'])
],
[
'open_modal' => '/roles/delete/[onclick_params_data_path]',
'modal_params_data_path' => 'id',
'icon' => 'trash'
'icon' => 'trash',
'requirement' => !empty($loggedUser['role']['perm_admin'])
],
]
]

View File

@ -10,7 +10,8 @@ echo $this->element('genericElements/IndexTable/index_table', [
'data' => [
'type' => 'simple',
'text' => __('Add sharing group'),
'popover_url' => '/SharingGroups/add'
'popover_url' => '/SharingGroups/add',
'requirement' => $this->ACL->checkAccess('SharingGroups', 'add')
]
]
],
@ -35,6 +36,11 @@ echo $this->element('genericElements/IndexTable/index_table', [
'class' => 'short',
'data_path' => 'name',
],
[
'name' => __('Owner'),
'class' => 'short',
'data_path' => 'organisation.name'
],
[
'name' => __('UUID'),
'sort' => 'uuid',
@ -43,9 +49,9 @@ echo $this->element('genericElements/IndexTable/index_table', [
],
[
'name' => __('Members'),
'data_path' => 'alignments',
'data_path' => 'sharing_group_orgs',
'element' => 'count_summary',
'url' => '/sharingGroups/view/{{id}}',
'url' => '/sharingGroups/view/{{url_data}}',
'url_data_path' => 'id'
]
],

View File

@ -14,12 +14,19 @@
'field' => 'username',
'autocomplete' => 'off'
],
[
'field' => 'organisation_id',
'type' => 'dropdown',
'label' => __('Associated organisation'),
'options' => $dropdownData['organisation']
],
[
'field' => 'password',
'label' => __('Password'),
'type' => 'password',
'required' => $this->request->getParam('action') === 'add' ? 'required' : false,
'autocomplete' => 'new-password'
'autocomplete' => 'new-password',
'value' => ''
],
[
'field' => 'confirm_password',

View File

@ -51,6 +51,13 @@ echo $this->element('genericElements/IndexTable/index_table', [
'sort' => 'username',
'data_path' => 'username',
],
[
'name' => __('Organisation'),
'sort' => 'organisation.name',
'data_path' => 'organisation.name',
'url' => '/organisations/view/{{0}}',
'url_vars' => ['organisation.id']
],
[
'name' => __('Email'),
'sort' => 'individual.email',

View File

@ -24,7 +24,7 @@ use Cake\Core\Configure;
echo $this->Form->control('password', ['type' => 'password', 'label' => 'Password', 'class' => 'form-control mb-3', 'placeholder' => __('Password')]);
echo $this->Form->control(__('Login'), ['type' => 'submit', 'class' => 'btn btn-primary']);
echo $this->Form->end();
if (!empty(Configure::read('Cerebrate')['security.registration.self-registration'])) {
if (!empty(Configure::read('security.registration.self-registration'))) {
echo '<div class="text-end">';
echo sprintf('<span class="text-secondary ms-auto" style="font-size: 0.8rem">%s <a href="/users/register" class="text-decoration-none link-primary fw-bold">%s</a></span>', __('Doesn\'t have an account?'), __('Sign up'));
echo '</div>';

View File

@ -21,6 +21,14 @@ echo $this->element(
'path' => 'individual.email'
],
[
'type' => 'generic',
'key' => __('Organisation'),
'path' => 'organisation.name',
'url' => '/organisations/view/{{0}}',
'url_vars' => 'organisation.id'
],
[
'type' => 'generic',
'key' => __('Role'),
'path' => 'role.name',
'url' => '/roles/view/{{0}}',
@ -48,8 +56,8 @@ echo $this->element(
'title' => __('Authentication keys')
],
[
'url' => '/EncryptionKeys/index?Users.id={{0}}',
'url_params' => ['id'],
'url' => '/EncryptionKeys/index?owner_id={{0}}',
'url_params' => ['individual_id'],
'title' => __('Encryption keys')
],
[

View File

@ -2,7 +2,7 @@
$controlParams = [
'options' => $fieldData['options'],
'empty' => $fieldData['empty'] ?? false,
'value' => $fieldData['value'] ?? [],
'value' => $fieldData['value'] ?? null,
'multiple' => $fieldData['multiple'] ?? false,
'disabled' => $fieldData['disabled'] ?? false,
'class' => ($fieldData['class'] ?? '') . ' formDropdown form-select'

View File

@ -0,0 +1,6 @@
<?php
$params['div'] = false;
$params['class'] .= ' form-control';
$params['value'] = '';
echo $this->FormFieldMassage->prepareFormElement($this->Form, $params, $fieldData);
?>

View File

@ -1,9 +1,12 @@
<?php
$data = h($this->Hash->extract($row, $field['data_path']));
$data = $this->Hash->extract($row, $field['data_path']);
// I feed dirty for this...
if (is_array($data) && count($data) === 1 && isset($data[0])) {
$data = $data[0];
}
if (!is_array($data)) {
$data = json_decode($data, true);
}
echo sprintf(
'<div class="json_container_%s"></div>',
h($k)

Some files were not shown because too many files have changed in this diff Show More