Merge branch 'main' into develop

pull/79/head
iglocska 2021-11-17 16:00:07 +01:00
commit 0def46149f
No known key found for this signature in database
GPG Key ID: BEA224F1FEF113AC
47 changed files with 1276 additions and 40 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"

1
.gitignore vendored
View File

@ -6,3 +6,4 @@ tmp
vendor
webroot/theme/node_modules
.vscode
docker/run/

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:
@ -71,7 +88,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 +108,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 +121,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

@ -390,6 +390,27 @@ CREATE TABLE `meta_template_fields` (
KEY `type` (`type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `audit_logs` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`created` datetime NOT NULL,
`user_id` int(10) unsigned DEFAULT NULL,
`authkey_id` int(10) unsigned DEFAULT NULL,
`request_ip` varbinary(16) DEFAULT NULL,
`request_type` tinyint NOT NULL,
`request_id` varchar(191) DEFAULT NULL,
`request_action` varchar(20) NOT NULL,
`model` varchar(80) NOT NULL,
`model_id` int(10) unsigned DEFAULT NULL,
`model_title` text DEFAULT NULL,
`change` blob,
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`),
KEY `ip` (`ip`),
KEY `model` (`model`),
KEY `action` (`action`),
KEY `model_id` (`model_id`)
KEY `created` (`created`)
) 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 */;

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

@ -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

@ -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
]
];

80
docker/Dockerfile Normal file
View File

@ -0,0 +1,80 @@
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 \
--ignore-platform-reqs \
--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" ]

47
docker/README.md Normal file
View File

@ -0,0 +1,47 @@
# Database init
For the `docker-compose` setup to work you must initialize database with
what is in `../INSTALL/mysql.sql`
```
mkdir -p run/dbinit/
cp ../INSTALL/mysql.sql run/dbinit/
```
The MariaDB container has a volume mounted as follow
`- ./run/dbinit:/docker-entrypoint-initdb.d/:ro`
So that on startup the container will source files in this directory to seed
the database. Once it's done the container will run normally and Cerebrate will
be able to roll its database migration scripts
# 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) \
.
```

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

@ -0,0 +1,28 @@
version: "3"
services:
database:
image: mariadb:10.6
restart: always
volumes:
- ./run/database:/var/lib/mysql
- ./run/dbinit:/docker-entrypoint-initdb.d/:ro
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
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

20
docker/entrypoint.sh Executable file
View File

@ -0,0 +1,20 @@
#!/bin/sh
set -e
run_all_migrations() {
./bin/cake migrations migrate
./bin/cake migrations migrate -p tags
./bin/cake migrations migrate -p ADmad/SocialAuth
}
# 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]

1
logs/.gitkeep Normal file
View File

@ -0,0 +1 @@

View File

@ -2,7 +2,7 @@
/**
* Generic importer to feed data to cerebrate from JSON or CSV.
*
*
* - JSON configuration file must have the `format` key which can either take the value `json` or `csv`
* - If `csv` is provided, the file must contains the header.
* - If `json` is provided, a `mapping` key on how to reach each fields using the cakephp4's Hash syntax must be provided.
@ -10,7 +10,7 @@
* - The key is the field name
* - The value
* - Can either be the string representing the path from which to get the value
* - Or a JSON containg the `path`, the optional `override` parameter specifying if the existing data should be overriden
* - Or a JSON containg the `path`, the optional `override` parameter specifying if the existing data should be overriden
* and an optional `massage` function able to alter the data.
* - Example
* {
@ -22,7 +22,7 @@
* },
*
* - The optional primary key argument provides a way to make import replayable. It can typically be used when an ID or UUID is not provided in the source file but can be replaced by something else (e.g. team-name or other type of unique data).
*
*
*/
namespace App\Command;
@ -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;
}
}
@ -529,4 +529,4 @@ class ImporterCommand extends Command
{
return is_null($value) ? '' : $value;
}
}
}

View File

@ -111,6 +111,7 @@ 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());
@ -147,13 +148,31 @@ 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']);
$user = $logModel->userInfo();
$logModel->insert([
'action' => 'login',
'model' => 'Users',
'model_id' => $user['id'],
'model_title' => $user['name'],
'change' => []
]);
if (!empty($user)) {
$this->Authentication->setIdentity($user);
}
} else {
$user = $logModel->userInfo();
$logModel->insert([
'action' => 'login',
'model' => 'Users',
'model_id' => $user['id'],
'model_title' => $user['name'],
'change' => []
]);
}
}
}

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', 'action', 'user_id', 'title'];
public $quickFilterFields = ['model', '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['change'] = stream_get_contents($data['change']);
return $data;
}
]);
$responsePayload = $this->CRUD->getResponsePayload();
if (!empty($responsePayload)) {
return $responsePayload;
}
$this->set('metaGroup', 'Administration');
}
}

View File

@ -61,11 +61,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');
@ -73,10 +87,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);

View File

@ -113,6 +113,11 @@ class Sidemenu {
'url' => '/instance/migrationIndex',
'icon' => 'database',
],
'AuditLogs' => [
'label' => __('Audit Logs'),
'url' => '/auditLogs/index',
'icon' => 'history',
],
]
],
],
@ -144,4 +149,4 @@ class Sidemenu {
]
];
}
}
}

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

@ -130,11 +130,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'],
'change' => []
]);
$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',
'change' => []
]);
$this->Flash->error(__('Invalid username or password'));
}
$this->viewBuilder()->setLayout('login');
@ -144,6 +160,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'],
'change' => []
]);
$this->Authentication->logout();
$this->Flash->success(__('Goodbye.'));
return $this->redirect(\Cake\Routing\Router::url('/users/login'));

View File

@ -0,0 +1,214 @@
<?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,
'change' => $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];
}
$logEntity = $this->auditLogs()->newEntity([
'request_action' => $entity->getConstant('ACTION_DELETE'),
'model' => $entity->getSource(),
'model_id' => $this->old->id,
'model_title' => $modelTitle,
'change' => $this->changedFields($entity)
]);
$logEntity->save();
}
/**
* @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

@ -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

@ -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

@ -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['change'])) {
$change = json_encode($data['change'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if ($this->compressionEnabled && strlen($change) >= self::BROTLI_MIN_LENGTH) {
$change = self::BROTLI_HEADER . brotli_compress($change, 4, BROTLI_TEXT);
}
$data['change'] = $change;
}
}
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];
$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'
);
@ -278,7 +279,7 @@ class BroodsTable extends AppTable
}
return $jsonReply;
}
/**
* handleSendingFailed - Handle the case if the request could not be sent or if the remote rejected the connection request
*
@ -302,7 +303,7 @@ class BroodsTable extends AppTable
];
return $creationResult;
}
/**
* handleMessageNotCreated - Handle the case if the request was sent but the remote brood did not save the message in the inbox
*

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');
}
@ -68,7 +68,7 @@ class InboxTable extends AppTable
if (empty($brood)) {
$errors[] = __('Unkown brood `{0}`', $entryData['data']['cerebrateURL']);
}
// $found = false;
// foreach ($user->individual->organisations as $organisations) {
// if ($organisations->id == $brood->organisation_id) {

View File

@ -16,6 +16,7 @@ class IndividualsTable extends AppTable
$this->addBehavior('UUID');
$this->addBehavior('Timestamp');
$this->addBehavior('Tags.Tag');
$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
@ -54,7 +56,7 @@ class InstanceTable extends AppTable
$timeline[$entry->date]['count'] = $entry->count;
}
$statistics[$model]['timeline'] = array_values($timeline);
$startCount = $table->find()->where(['modified <' => new \DateTime("-{$days} days")])->all()->count();
$endCount = $statistics[$model]['amount'];
$statistics[$model]['variation'] = $endCount - $startCount;

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');
}

View File

@ -12,6 +12,7 @@ class MetaFieldsTable extends AppTable
{
parent::initialize($config);
$this->addBehavior('UUID');
$this->addBehavior('AuditLog');
$this->setDisplayField('field');
$this->belongsTo('MetaTemplates');
$this->belongsTo('MetaTemplateFields');

View File

@ -20,6 +20,7 @@ class OrganisationsTable extends AppTable
parent::initialize($config);
$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)
@ -87,7 +88,7 @@ class OutboxProcessorsTable extends AppTable
}
}
}
/**
* getProcessorClass
*
@ -112,7 +113,7 @@ class OutboxProcessorsTable extends AppTable
return $e->getMessage();
}
}
/**
* createOutboxEntry
*

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

@ -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

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

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' => 'action',
'data_path' => 'action',
],
[
'name' => __('Change'),
'sort' => 'change',
'data_path' => 'change',
'element' => 'json'
],
],
'title' => __('Logs'),
'description' => null,
'pull' => 'right',
'actions' => []
]
]);
echo '</div>';
?>

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)