Merge branch 'develop' of github.com:cerebrate-project/cerebrate into develop-unstable
commit
62ca877f0b
|
@ -0,0 +1,52 @@
|
|||
name: test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, develop]
|
||||
pull_request:
|
||||
branches: [main, develop]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 5
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-20.04]
|
||||
php: ["7.4"]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Create config files
|
||||
run: |
|
||||
cp ./config/app_local.example.php ./config/app_local.php
|
||||
cp ./config/config.example.json ./config/config.json
|
||||
|
||||
- name: Setup MariaDB
|
||||
uses: getong/mariadb-action@v1.1
|
||||
with:
|
||||
host port: 3306
|
||||
container port: 3306
|
||||
mysql database: "cerebrate_test"
|
||||
mysql user: "cerebrate"
|
||||
mysql password: "cerebrate"
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
extensions: pdo, pdo_mysql, mysqli, simplexml
|
||||
|
||||
- name: Install dependencies
|
||||
env:
|
||||
php_version: ${{ matrix.php }}
|
||||
run: |
|
||||
sudo apt-get -y update
|
||||
sudo apt-get install -y --no-install-recommends curl git zip unzip libicu-dev libxml2-dev
|
||||
composer install --no-progress --no-interaction
|
||||
|
||||
- name: Run tests
|
||||
run: composer test
|
||||
env:
|
||||
DEBUG: true
|
|
@ -1,7 +1,9 @@
|
|||
## Requirements
|
||||
|
||||
An Ubuntu server (18.04/20.04 should both work fine) - though other linux installations should work too.
|
||||
|
||||
- apache2 (or nginx), mysql/mariadb, sqlite need to be installed and running
|
||||
- php version 8+ is required
|
||||
- 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
|
||||
|
|
|
@ -55,7 +55,7 @@
|
|||
"cs-fix": "phpcbf --colors --standard=vendor/cakephp/cakephp-codesniffer/CakePHP src/ tests/",
|
||||
"stan": "phpstan analyse src/",
|
||||
"test": [
|
||||
"sh ./tests/Helper/wiremock/start.sh",
|
||||
"nohup sh ./tests/Helper/wiremock/start.sh >/dev/null 2>&1 &",
|
||||
"phpunit",
|
||||
"sh ./tests/Helper/wiremock/stop.sh"
|
||||
]
|
||||
|
|
|
@ -9,6 +9,10 @@ class InitialSchema extends AbstractMigration
|
|||
{
|
||||
public function change()
|
||||
{
|
||||
$exists = $this->hasTable('broods');
|
||||
if ($exists) {
|
||||
return true;
|
||||
}
|
||||
$this->execute('SET unique_checks=0; SET foreign_key_checks=0;');
|
||||
$this->execute("ALTER DATABASE CHARACTER SET 'utf8mb4';");
|
||||
$this->execute("ALTER DATABASE COLLATE='utf8mb4_general_ci';");
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Migrations\AbstractMigration;
|
||||
|
||||
final class RegistrationFloodProtection extends AbstractMigration
|
||||
{
|
||||
/**
|
||||
* Change Method.
|
||||
*
|
||||
* Write your reversible migrations using this method.
|
||||
*
|
||||
* More information on writing migrations is available here:
|
||||
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
|
||||
*
|
||||
* Remember to call "create()" or "update()" and NOT "save()" when working
|
||||
* with the Table class.
|
||||
*/
|
||||
public function change(): void
|
||||
{
|
||||
$exists = $this->hasTable('flood_protections');
|
||||
if (!$exists) {
|
||||
$table = $this->table('flood_protections', [
|
||||
'signed' => false,
|
||||
'collation' => 'utf8mb4_unicode_ci',
|
||||
]);
|
||||
$table
|
||||
->addColumn('remote_ip', 'string', [
|
||||
'null' => false,
|
||||
'length' => 45,
|
||||
])
|
||||
->addColumn('request_action', 'string', [
|
||||
'null' => false,
|
||||
'length' => 191,
|
||||
])
|
||||
->addColumn('expiration', 'integer', [
|
||||
'null' => false,
|
||||
'signed' => false,
|
||||
'length' => 10,
|
||||
])
|
||||
->addIndex('remote_ip')
|
||||
->addIndex('request_action')
|
||||
->addIndex('expiration');
|
||||
$table->create();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -73,12 +73,10 @@ return [
|
|||
* The test connection is used during the test suite.
|
||||
*/
|
||||
'test' => [
|
||||
'host' => 'localhost',
|
||||
//'port' => 'non_standard_port_number',
|
||||
'username' => 'my_app',
|
||||
'password' => 'secret',
|
||||
'database' => 'test_myapp',
|
||||
//'schema' => 'myapp',
|
||||
'host' => '127.0.0.1',
|
||||
'username' => 'cerebrate',
|
||||
'password' => 'cerebrate',
|
||||
'database' => 'cerebrate_test',
|
||||
],
|
||||
],
|
||||
|
||||
|
|
|
@ -41,6 +41,7 @@ class AppController extends Controller
|
|||
public $restResponsePayload = null;
|
||||
public $user = null;
|
||||
public $breadcrumb = [];
|
||||
public $request_ip = null;
|
||||
|
||||
/**
|
||||
* Initialization hook method.
|
||||
|
@ -86,6 +87,7 @@ class AppController extends Controller
|
|||
Configure::write('DebugKit.forceEnable', true);
|
||||
}
|
||||
$this->loadComponent('CustomPagination');
|
||||
$this->loadComponent('FloodProtection');
|
||||
/*
|
||||
* Enable the following component for recommended CakePHP form protection settings.
|
||||
* see https://book.cakephp.org/4/en/controllers/components/form-protection.html
|
||||
|
@ -135,7 +137,6 @@ class AppController extends Controller
|
|||
|
||||
$this->ACL->checkAccess();
|
||||
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'));
|
||||
|
@ -149,6 +150,9 @@ class AppController extends Controller
|
|||
$this->set('metaGroup', !empty($this->isAdmin) ? 'Administration' : 'Cerebrate');
|
||||
}
|
||||
}
|
||||
if (mt_rand(1, 50) === 1) {
|
||||
$this->FloodProtection->cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
public function beforeRender(EventInterface $event)
|
||||
|
|
|
@ -188,12 +188,12 @@ class ACLComponent extends Component
|
|||
'add' => ['*'],
|
||||
'edit' => ['*'],
|
||||
'delete' => ['*'],
|
||||
'getSettingByName' => ['*'],
|
||||
'setSetting' => ['*'],
|
||||
'getMySettingByName' => ['*'],
|
||||
'setMySetting' => ['*'],
|
||||
'saveSetting' => ['*'],
|
||||
'getBookmarks' => ['*'],
|
||||
'saveBookmark' => ['*'],
|
||||
'deleteBookmark' => ['*']
|
||||
'getMyBookmarks' => ['*'],
|
||||
'saveMyBookmark' => ['*'],
|
||||
'deleteMyBookmark' => ['*']
|
||||
],
|
||||
'Api' => [
|
||||
'index' => ['*']
|
||||
|
@ -277,9 +277,32 @@ class ACLComponent extends Component
|
|||
$this->user = $user;
|
||||
}
|
||||
|
||||
public function getUser(): User
|
||||
public function getUser(): ?User
|
||||
{
|
||||
return $this->user;
|
||||
if (!empty($this->user)) {
|
||||
return $this->user;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function canEditUser(User $currentUser, User $user): bool
|
||||
{
|
||||
if (empty($user) || empty($currentUser)) {
|
||||
return false;
|
||||
}
|
||||
if (!$currentUser['role']['perm_admin']) {
|
||||
if ($user['role']['perm_admin']) {
|
||||
return false; // org_admins cannot edit admins
|
||||
}
|
||||
if (!$currentUser['role']['perm_org_admin']) {
|
||||
return false;
|
||||
} else {
|
||||
if ($currentUser['organisation_id'] !== $user['organisation_id']) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
@ -223,9 +223,6 @@ class CRUDComponent extends Component
|
|||
$metaTemplates = $this->getMetaTemplates();
|
||||
$data = $this->attachMetaTemplatesIfNeeded($data, $metaTemplates->toArray());
|
||||
}
|
||||
if (!empty($params['fields'])) {
|
||||
$this->Controller->set('fields', $params['fields']);
|
||||
}
|
||||
if ($this->request->is('post')) {
|
||||
$patchEntityParams = [
|
||||
'associated' => [],
|
||||
|
@ -291,6 +288,9 @@ class CRUDComponent extends Component
|
|||
}
|
||||
}
|
||||
}
|
||||
if (!empty($params['fields'])) {
|
||||
$this->Controller->set('fields', $params['fields']);
|
||||
}
|
||||
$this->Controller->entity = $data;
|
||||
$this->Controller->set('entity', $data);
|
||||
}
|
||||
|
@ -461,7 +461,10 @@ class CRUDComponent extends Component
|
|||
if (!empty($params['conditions'])) {
|
||||
$query->where($params['conditions']);
|
||||
}
|
||||
$data = $query->first();
|
||||
$data = $data->first();
|
||||
if (isset($params['afterFind'])) {
|
||||
$data = $params['afterFind']($data, $params);
|
||||
}
|
||||
if (empty($data)) {
|
||||
throw new NotFoundException(__('Invalid {0}.', $this->ObjectAlias));
|
||||
}
|
||||
|
@ -469,9 +472,6 @@ class CRUDComponent extends Component
|
|||
$metaTemplates = $this->getMetaTemplates();
|
||||
$data = $this->attachMetaTemplatesIfNeeded($data, $metaTemplates->toArray());
|
||||
}
|
||||
if (!empty($params['fields'])) {
|
||||
$this->Controller->set('fields', $params['fields']);
|
||||
}
|
||||
if ($this->request->is(['post', 'put'])) {
|
||||
$patchEntityParams = [
|
||||
'associated' => []
|
||||
|
@ -527,6 +527,9 @@ class CRUDComponent extends Component
|
|||
}
|
||||
}
|
||||
}
|
||||
if (!empty($params['fields'])) {
|
||||
$this->Controller->set('fields', $params['fields']);
|
||||
}
|
||||
$this->Controller->entity = $data;
|
||||
$this->Controller->set('entity', $data);
|
||||
}
|
||||
|
@ -658,11 +661,16 @@ class CRUDComponent extends Component
|
|||
}
|
||||
|
||||
$data = $this->Table->get($id, $params);
|
||||
if (empty($data)) {
|
||||
throw new NotFoundException(__('Invalid {0}.', $this->ObjectAlias));
|
||||
}
|
||||
$data = $this->attachMetaTemplatesIfNeeded($data);
|
||||
|
||||
if (isset($params['afterFind'])) {
|
||||
$data = $params['afterFind']($data);
|
||||
}
|
||||
if (empty($data)) {
|
||||
throw new NotFoundException(__('Invalid {0}.', $this->ObjectAlias));
|
||||
}
|
||||
if ($this->Controller->ParamHandler->isRest()) {
|
||||
$this->Controller->restResponsePayload = $this->RestResponse->viewData($data, 'json');
|
||||
}
|
||||
|
@ -730,9 +738,13 @@ class CRUDComponent extends Component
|
|||
}
|
||||
$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));
|
||||
try {
|
||||
$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));
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$data = false;
|
||||
}
|
||||
}
|
||||
if (!empty($data)) {
|
||||
|
@ -1291,6 +1303,9 @@ class CRUDComponent extends Component
|
|||
}
|
||||
|
||||
$data = $this->Table->get($id, $params);
|
||||
if (isset($params['afterFind'])) {
|
||||
$data = $params['afterFind']($data, $params);
|
||||
}
|
||||
if ($this->request->is(['post', 'put'])) {
|
||||
if (isset($params['force_state'])) {
|
||||
$data->{$fieldName} = $params['force_state'];
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
namespace App\Controller\Component;
|
||||
|
||||
use Cake\Controller\Component;
|
||||
use App\Model\Entity\User;
|
||||
use App\Http\Exception\TooManyRequestsException;
|
||||
use Cake\ORM\TableRegistry;
|
||||
use Cake\Core\Configure;
|
||||
use Cake\Core\Configure\Engine\PhpConfig;
|
||||
|
||||
class FloodProtectionComponent extends Component
|
||||
{
|
||||
private $remote_ip = null;
|
||||
private $FloodProtections = null;
|
||||
|
||||
public function initialize(array $config): void
|
||||
{
|
||||
$ip_source = Configure::check('security.logging.ip_source') ? Configure::read('security.logging.ip_source') : 'REMOTE_ADDR';
|
||||
$this->remote_ip = $_SERVER[$ip_source];
|
||||
$temp = explode(PHP_EOL, $_SERVER[$ip_source]);
|
||||
if (count($temp) > 1) {
|
||||
$this->remote_ip = $temp[0];
|
||||
}
|
||||
$this->FloodProtections = TableRegistry::getTableLocator()->get('FloodProtections');
|
||||
}
|
||||
|
||||
public function check(string $action, int $limit = 5, int $expiration_time = 300): bool
|
||||
{
|
||||
$results = $this->FloodProtections->find('all')->where(['request_action' => $action, 'remote_ip' => $this->remote_ip, 'expiration' > time()])->toList();
|
||||
if (count($results) >= $limit) {
|
||||
throw new TooManyRequestsException(__('Too many {0} requests have been issued ({1} requests allowed ever {2} seconds)', [$action, $limit, $expiration_time]));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function set(string $action, int $expiration_time = 300): bool
|
||||
{
|
||||
$entry = $this->FloodProtections->newEmptyEntity();
|
||||
$entry->expiration = time() + $expiration_time;
|
||||
$entry->remote_ip = $this->remote_ip;
|
||||
$entry->request_action = $action;
|
||||
return (bool)$this->FloodProtections->save($entry);
|
||||
|
||||
}
|
||||
|
||||
public function checkAndSet(string $action, int $limit = 5, int $expiration_time = 300): bool
|
||||
{
|
||||
$result = $this->check($action, $limit, $expiration_time);
|
||||
$this->set($action, $expiration_time);
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function cleanup(): void
|
||||
{
|
||||
$this->FloodProtections->deleteAll(['expiration <' => time()]);
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
namespace BreadcrumbNavigation;
|
||||
|
||||
require_once(APP . 'Controller' . DS . 'Component' . DS . 'Navigation' . DS . 'base.php');
|
||||
require_once(APP . 'Controller' . DS . 'Component' . DS . 'Navigation' . DS . 'base.php');
|
||||
|
||||
class UsersNavigation extends BaseNavigation
|
||||
{
|
||||
|
@ -24,7 +24,8 @@ class UsersNavigation extends BaseNavigation
|
|||
$bcf = $this->bcf;
|
||||
$request = $this->request;
|
||||
$passedData = $this->request->getParam('pass');
|
||||
$this->bcf->addLink('Users', 'view', 'UserSettings', 'index', function ($config) use ($bcf, $request, $passedData) {
|
||||
$currentUser = $this->currentUser;
|
||||
$this->bcf->addLink('Users', 'view', 'UserSettings', 'index', function ($config) use ($bcf, $request, $passedData, $currentUser) {
|
||||
if (!empty($passedData[0])) {
|
||||
$user_id = $passedData[0];
|
||||
$linkData = [
|
||||
|
|
|
@ -6,6 +6,7 @@ class BaseNavigation
|
|||
protected $bcf;
|
||||
protected $request;
|
||||
protected $viewVars;
|
||||
public $currentUser;
|
||||
|
||||
public function __construct($bcf, $request, $viewVars)
|
||||
{
|
||||
|
@ -14,8 +15,13 @@ class BaseNavigation
|
|||
$this->viewVars = $viewVars;
|
||||
}
|
||||
|
||||
public function setCurrentUser($currentUser)
|
||||
{
|
||||
$this->currentUser = $currentUser;
|
||||
}
|
||||
|
||||
public function addRoutes() {}
|
||||
public function addParents() {}
|
||||
public function addLinks() {}
|
||||
public function addActions() {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,8 +17,9 @@ require_once(APP . 'Controller' . DS . 'Component' . DS . 'Navigation' . DS . 's
|
|||
|
||||
class NavigationComponent extends Component
|
||||
{
|
||||
private $user = null;
|
||||
private $currentUser = null;
|
||||
public $breadcrumb = null;
|
||||
public $fullBreadcrumb = null;
|
||||
public $iconToTableMapping = [
|
||||
'Individuals' => 'address-book',
|
||||
'Organisations' => 'building',
|
||||
|
@ -46,7 +47,6 @@ class NavigationComponent extends Component
|
|||
public function beforeRender($event)
|
||||
{
|
||||
$this->fullBreadcrumb = $this->genBreadcrumb();
|
||||
$this->breadcrumb = $this->getBreadcrumb();
|
||||
}
|
||||
|
||||
public function getSideMenu(): array
|
||||
|
@ -57,7 +57,7 @@ class NavigationComponent extends Component
|
|||
return $sidemenu;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function addUserBookmarks($sidemenu): array
|
||||
{
|
||||
$bookmarks = $this->getUserBookmarks();
|
||||
|
@ -82,7 +82,7 @@ class NavigationComponent extends Component
|
|||
}, $bookmarks);
|
||||
return $links;
|
||||
}
|
||||
|
||||
|
||||
public function getBreadcrumb(): array
|
||||
{
|
||||
$controller = $this->request->getParam('controller');
|
||||
|
@ -143,6 +143,7 @@ class NavigationComponent extends Component
|
|||
$reflection = new \ReflectionClass("BreadcrumbNavigation\\{$navigationClassname}Navigation");
|
||||
$viewVars = $this->_registry->getController()->viewBuilder()->getVars();
|
||||
$navigationClasses[$navigationClassname] = $reflection->newInstance($bcf, $request, $viewVars);
|
||||
$navigationClasses[$navigationClassname]->setCurrentUser($this->currentUser);
|
||||
}
|
||||
return $navigationClasses;
|
||||
}
|
||||
|
@ -288,7 +289,7 @@ class BreadcrumbFactory
|
|||
$this->addLink($controller, 'view', $controller, 'edit');
|
||||
$this->addLink($controller, 'edit', $controller, 'view');
|
||||
$this->addSelfLink($controller, 'edit');
|
||||
|
||||
|
||||
$this->addAction($controller, 'view', $controller, 'add');
|
||||
$this->addAction($controller, 'view', $controller, 'delete');
|
||||
$this->addAction($controller, 'edit', $controller, 'add');
|
||||
|
|
|
@ -48,7 +48,7 @@ class ParamHandlerComponent extends Component
|
|||
return $this->isRest;
|
||||
}
|
||||
if ($this->request->is('json')) {
|
||||
if (!empty((string)$this->request->getBody()) && empty($this->request->getParsedBody())) {
|
||||
if (!empty((string)$this->request->getBody()) && !is_array($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;
|
||||
|
|
|
@ -11,13 +11,17 @@ use Cake\Http\Exception\NotFoundException;
|
|||
use Cake\Http\Exception\MethodNotAllowedException;
|
||||
use Cake\Http\Exception\ForbiddenException;
|
||||
use Cake\Event\EventInterface;
|
||||
use Cake\Core\Configure;
|
||||
|
||||
class IndividualsController extends AppController
|
||||
{
|
||||
public function beforeFilter(EventInterface $event)
|
||||
{
|
||||
parent::beforeFilter($event);
|
||||
$this->Authentication->allowUnauthenticated(['index']);
|
||||
$open = Configure::read('Cerebrate.open');
|
||||
if (!empty($open) && in_array('individuals', $open)) {
|
||||
$this->Authentication->allowUnauthenticated(['index']);
|
||||
}
|
||||
}
|
||||
|
||||
public function index()
|
||||
|
|
|
@ -10,13 +10,17 @@ use Cake\Http\Exception\NotFoundException;
|
|||
use Cake\Http\Exception\MethodNotAllowedException;
|
||||
use Cake\Http\Exception\ForbiddenException;
|
||||
use Cake\Event\EventInterface;
|
||||
use Cake\Core\Configure;
|
||||
|
||||
class OrganisationsController extends AppController
|
||||
{
|
||||
public function beforeFilter(EventInterface $event)
|
||||
{
|
||||
parent::beforeFilter($event);
|
||||
$this->Authentication->allowUnauthenticated(['index']);
|
||||
$open = Configure::read('Cerebrate.open');
|
||||
if (!empty($open) && in_array('organisations', $open)) {
|
||||
$this->Authentication->allowUnauthenticated(['index']);
|
||||
}
|
||||
}
|
||||
|
||||
public function index()
|
||||
|
|
|
@ -31,7 +31,15 @@ class RolesController extends AppController
|
|||
|
||||
public function add()
|
||||
{
|
||||
$this->CRUD->add();
|
||||
$rolesModel = $this->Roles;
|
||||
$this->CRUD->add([
|
||||
'afterSave' => function ($data) use ($rolesModel) {
|
||||
if ($data['is_default']) {
|
||||
$rolesModel->query()->update()->set(['is_default' => false])->where(['id !=' => $data->id])->execute();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
]);
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
if (!empty($responsePayload)) {
|
||||
return $responsePayload;
|
||||
|
@ -51,7 +59,15 @@ class RolesController extends AppController
|
|||
|
||||
public function edit($id)
|
||||
{
|
||||
$this->CRUD->edit($id);
|
||||
$rolesModel = $this->Roles;
|
||||
$this->CRUD->edit($id, [
|
||||
'afterSave' => function ($data) use ($rolesModel) {
|
||||
if ($data['is_default']) {
|
||||
$rolesModel->query()->update()->set(['is_default' => false])->where(['id !=' => $data->id])->execute();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
]);
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
if (!empty($responsePayload)) {
|
||||
return $responsePayload;
|
||||
|
|
|
@ -7,6 +7,7 @@ use Cake\Utility\Hash;
|
|||
use Cake\Utility\Text;
|
||||
use \Cake\Database\Expression\QueryExpression;
|
||||
use Cake\Error\Debugger;
|
||||
use Cake\Http\Exception\NotFoundException;
|
||||
|
||||
class SharingGroupsController extends AppController
|
||||
{
|
||||
|
@ -52,8 +53,25 @@ class SharingGroupsController extends AppController
|
|||
|
||||
public function view($id)
|
||||
{
|
||||
$currentUser = $this->ACL->getUser();
|
||||
$this->CRUD->view($id, [
|
||||
'contain' => ['SharingGroupOrgs', 'Organisations', 'Users' => ['fields' => ['id', 'username']]]
|
||||
'contain' => ['SharingGroupOrgs', 'Organisations', 'Users' => ['fields' => ['id', 'username']]],
|
||||
'afterFind' => function($data) use ($currentUser) {
|
||||
if (empty($currentUser['role']['perm_admin'])) {
|
||||
$orgFround = false;
|
||||
if (!empty($data['sharing_group_orgs'])) {
|
||||
foreach ($data['sharing_group_orgs'] as $org) {
|
||||
if ($org['id'] === $currentUser['organisation_id']) {
|
||||
$orgFound = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($data['organisation_id'] !== $currentUser['organisation_id'] && !$orgFround) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
]);
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
if (!empty($responsePayload)) {
|
||||
|
@ -68,6 +86,7 @@ class SharingGroupsController extends AppController
|
|||
if (empty($currentUser['role']['perm_admin'])) {
|
||||
$params['conditions'] = ['organisation_id' => $currentUser['organisation_id']];
|
||||
}
|
||||
$params['fields'] = ['name', 'releasability', 'description', 'active'];
|
||||
$this->CRUD->edit($id, $params);
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
if (!empty($responsePayload)) {
|
||||
|
@ -82,7 +101,11 @@ class SharingGroupsController extends AppController
|
|||
|
||||
public function delete($id)
|
||||
{
|
||||
$this->CRUD->delete($id);
|
||||
$currentUser = $this->ACL->getUser();
|
||||
if (empty($currentUser['role']['perm_admin'])) {
|
||||
$params['conditions'] = ['organisation_id' => $currentUser['organisation_id']];
|
||||
}
|
||||
$this->CRUD->delete($id, $params);
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
if (!empty($responsePayload)) {
|
||||
return $responsePayload;
|
||||
|
@ -91,9 +114,18 @@ class SharingGroupsController extends AppController
|
|||
|
||||
public function addOrg($id)
|
||||
{
|
||||
$currentUser = $this->ACL->getUser();
|
||||
$sharingGroup = $this->SharingGroups->get($id, [
|
||||
'contain' => 'SharingGroupOrgs'
|
||||
]);
|
||||
if (empty($currentUser['role']['perm_admin'])) {
|
||||
if ($sharingGroup['organisation_id'] !== $currentUser['organisation_id']) {
|
||||
$sharingGroup = null;
|
||||
}
|
||||
}
|
||||
if (empty($sharingGroup)) {
|
||||
throw new NotFoundException(__('Invalid SharingGroup.'));
|
||||
}
|
||||
$conditions = [];
|
||||
$containedOrgIds = array_values(\Cake\Utility\Hash::extract($sharingGroup, 'sharing_group_orgs.{n}.id'));
|
||||
if (!empty($containedOrgIds)) {
|
||||
|
@ -150,9 +182,18 @@ class SharingGroupsController extends AppController
|
|||
|
||||
public function removeOrg($id, $org_id)
|
||||
{
|
||||
$currentUser = $this->ACL->getUser();
|
||||
$sharingGroup = $this->SharingGroups->get($id, [
|
||||
'contain' => 'SharingGroupOrgs'
|
||||
]);
|
||||
if (empty($currentUser['role']['perm_admin'])) {
|
||||
if ($sharingGroup['organisation_id'] !== $currentUser['organisation_id']) {
|
||||
$sharingGroup = null;
|
||||
}
|
||||
}
|
||||
if (empty($sharingGroup)) {
|
||||
throw new NotFoundException(__('Invalid SharingGroup.'));
|
||||
}
|
||||
if ($this->request->is('post')) {
|
||||
$org = $this->SharingGroups->SharingGroupOrgs->get($org_id);
|
||||
$result = (bool)$this->SharingGroups->SharingGroupOrgs->unlink($sharingGroup, [$org]);
|
||||
|
|
|
@ -36,9 +36,16 @@ class UserSettingsController extends AppController
|
|||
return $responsePayload;
|
||||
}
|
||||
if (!empty($this->request->getQuery('Users_id'))) {
|
||||
$settingsForUser = $this->UserSettings->Users->find()->where([
|
||||
$conditions = [
|
||||
'id' => $this->request->getQuery('Users_id')
|
||||
])->first();
|
||||
];
|
||||
if (empty($currentUser['role']['perm_admin'])) {
|
||||
$conditions['organisation_id'] = $currentUser['organisation_id'];
|
||||
}
|
||||
$settingsForUser = $this->UserSettings->Users->find()->where($conditions)->first();
|
||||
if (empty($settingsForUser)) {
|
||||
throw new NotFoundException(__('Invalid {0}.', __('user')));
|
||||
}
|
||||
$this->set('settingsForUser', $settingsForUser);
|
||||
}
|
||||
}
|
||||
|
@ -57,7 +64,7 @@ class UserSettingsController extends AppController
|
|||
}
|
||||
}
|
||||
|
||||
public function add($user_id = false)
|
||||
public function add($user_id=null)
|
||||
{
|
||||
$currentUser = $this->ACL->getUser();
|
||||
$this->CRUD->add([
|
||||
|
@ -77,6 +84,8 @@ class UserSettingsController extends AppController
|
|||
if (empty($currentUser['role']['perm_admin'])) {
|
||||
$allUsers->where(['id' => $currentUser->id]);
|
||||
$user_id = $currentUser->id;
|
||||
} else if (!is_null($user_id)) {
|
||||
$allUsers->where(['id' => $user_id]);
|
||||
}
|
||||
$dropdownData = [
|
||||
'user' => $allUsers->all()->toArray(),
|
||||
|
@ -124,7 +133,13 @@ class UserSettingsController extends AppController
|
|||
}
|
||||
}
|
||||
|
||||
public function getSettingByName($settingsName)
|
||||
/**
|
||||
* Get a setting by name for the currently logged-in user
|
||||
*
|
||||
* @param [type] $settingsName
|
||||
* @return void
|
||||
*/
|
||||
public function getMySettingByName($settingsName)
|
||||
{
|
||||
$setting = $this->UserSettings->getSettingByName($this->ACL->getUser(), $settingsName);
|
||||
if (is_null($setting)) {
|
||||
|
@ -140,7 +155,7 @@ class UserSettingsController extends AppController
|
|||
$this->render('view');
|
||||
}
|
||||
|
||||
public function setSetting($settingsName = false)
|
||||
public function setMySetting($settingsName = false)
|
||||
{
|
||||
if (!$this->request->is('get')) {
|
||||
$setting = $this->UserSettings->getSettingByName($this->ACL->getUser(), $settingsName);
|
||||
|
@ -160,22 +175,23 @@ class UserSettingsController extends AppController
|
|||
$this->set('settingName', $settingsName);
|
||||
}
|
||||
|
||||
public function saveSetting()
|
||||
public function saveSetting($user_id = false)
|
||||
{
|
||||
$user = $this->getRequestedUserIfAllowed($user_id);
|
||||
if ($this->request->is('post')) {
|
||||
$data = $this->ParamHandler->harvestParams([
|
||||
'name',
|
||||
'value'
|
||||
]);
|
||||
$setting = $this->UserSettings->getSettingByName($this->ACL->getUser(), $data['name']);
|
||||
$setting = $this->UserSettings->getSettingByName($user, $data['name']);
|
||||
if (is_null($setting)) { // setting not found, create it
|
||||
$result = $this->UserSettings->createSetting($this->ACL->getUser(), $data['name'], $data['value']);
|
||||
$result = $this->UserSettings->createSetting($user, $data['name'], $data['value']);
|
||||
} else {
|
||||
$result = $this->UserSettings->editSetting($this->ACL->getUser(), $data['name'], $data['value']);
|
||||
$result = $this->UserSettings->editSetting($user, $data['name'], $data['value']);
|
||||
}
|
||||
$success = !empty($result);
|
||||
$message = $success ? __('Setting saved') : __('Could not save setting');
|
||||
$this->CRUD->setResponseForController('setSetting', $success, $message, $result);
|
||||
$this->CRUD->setResponseForController('saveSetting', $success, $message, $result);
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
if (!empty($responsePayload)) {
|
||||
return $responsePayload;
|
||||
|
@ -183,7 +199,7 @@ class UserSettingsController extends AppController
|
|||
}
|
||||
}
|
||||
|
||||
public function getBookmarks($forSidebar = false)
|
||||
public function getMyBookmarks($forSidebar = false)
|
||||
{
|
||||
$bookmarks = $this->UserSettings->getSettingByName($this->ACL->getUser(), $this->UserSettings->BOOKMARK_SETTING_NAME);
|
||||
$bookmarks = json_decode($bookmarks['value'], true);
|
||||
|
@ -193,7 +209,7 @@ class UserSettingsController extends AppController
|
|||
$this->render('/element/UserSettings/saved-bookmarks');
|
||||
}
|
||||
|
||||
public function saveBookmark()
|
||||
public function saveMyBookmark()
|
||||
{
|
||||
if (!$this->request->is('get')) {
|
||||
$result = $this->UserSettings->saveBookmark($this->ACL->getUser(), $this->request->getData());
|
||||
|
@ -208,7 +224,7 @@ class UserSettingsController extends AppController
|
|||
$this->set('user_id', $this->ACL->getUser()->id);
|
||||
}
|
||||
|
||||
public function deleteBookmark()
|
||||
public function deleteMyBookmark()
|
||||
{
|
||||
if (!$this->request->is('get')) {
|
||||
$result = $this->UserSettings->deleteBookmark($this->ACL->getUser(), $this->request->getData());
|
||||
|
@ -224,7 +240,7 @@ class UserSettingsController extends AppController
|
|||
}
|
||||
|
||||
/**
|
||||
* isLoggedUserAllowedToEdit
|
||||
* isLoggedUserAllowedToEdit
|
||||
*
|
||||
* @param int|\App\Model\Entity\UserSetting $setting
|
||||
* @return boolean
|
||||
|
@ -248,4 +264,26 @@ class UserSettingsController extends AppController
|
|||
}
|
||||
return $isAllowed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the requested user if user permissions allow it. Otherwise, return the user currently logged-in
|
||||
*
|
||||
* @param bool|int $user_id
|
||||
* @return void
|
||||
*/
|
||||
private function getRequestedUserIfAllowed($user_id = false)
|
||||
{
|
||||
$currentUser = $this->ACL->getUser();
|
||||
if (is_bool($user_id)) {
|
||||
return $currentUser;
|
||||
}
|
||||
if (!empty($currentUser['role']['perm_admin'])) {
|
||||
$user = $this->Users->get($user_id, [
|
||||
'contain' => ['Roles', 'Individuals' => 'Organisations']
|
||||
]);
|
||||
} else {
|
||||
$user = $currentUser;
|
||||
}
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,16 +33,34 @@ class UsersController extends AppController
|
|||
if (!empty($responsePayload)) {
|
||||
return $responsePayload;
|
||||
}
|
||||
$this->set(
|
||||
'validRoles',
|
||||
$this->Users->Roles->find('list')->select(['id', 'name'])->order(['name' => 'asc'])->where(['perm_admin' => 0])->all()->toArray()
|
||||
);
|
||||
$this->set('metaGroup', $this->isAdmin ? 'Administration' : 'Cerebrate');
|
||||
}
|
||||
|
||||
public function add()
|
||||
{
|
||||
$currentUser = $this->ACL->getUser();
|
||||
$validRoles = [];
|
||||
if (!$currentUser['role']['perm_admin']) {
|
||||
$validRoles = $this->Users->Roles->find('list')->select(['id', 'name'])->order(['name' => 'asc'])->where(['perm_admin' => 0])->all()->toArray();
|
||||
} else {
|
||||
$validRoles = $this->Users->Roles->find('list')->order(['name' => 'asc'])->all()->toArray();
|
||||
}
|
||||
$defaultRole = $this->Users->Roles->find()->select(['id'])->first()->toArray();
|
||||
|
||||
$this->CRUD->add([
|
||||
'beforeSave' => function($data) use ($currentUser) {
|
||||
'beforeSave' => function($data) use ($currentUser, $validRoles, $defaultRole) {
|
||||
if (!isset($data['role_id']) && !empty($defaultRole)) {
|
||||
$data['role_id'] = $defaultRole['id'];
|
||||
}
|
||||
if (!$currentUser['role']['perm_admin']) {
|
||||
$data['organisation_id'] = $currentUser['organisation_id'];
|
||||
if (!in_array($data['role_id'], array_keys($validRoles))) {
|
||||
throw new MethodNotAllowedException(__('You do not have permission to assign that role.'));
|
||||
}
|
||||
}
|
||||
$this->Users->enrollUserRouter($data);
|
||||
return $data;
|
||||
|
@ -65,9 +83,7 @@ class UsersController extends AppController
|
|||
$org_conditions = ['id' => $currentUser['organisation_id']];
|
||||
}
|
||||
$dropdownData = [
|
||||
'role' => $this->Users->Roles->find('list', [
|
||||
'sort' => ['name' => 'asc']
|
||||
]),
|
||||
'role' => $validRoles,
|
||||
'individual' => $this->Users->Individuals->find('list', [
|
||||
'sort' => ['email' => 'asc']
|
||||
]),
|
||||
|
@ -77,12 +93,14 @@ class UsersController extends AppController
|
|||
])
|
||||
];
|
||||
$this->set(compact('dropdownData'));
|
||||
$this->set('defaultRole', $defaultRole['id'] ?? null);
|
||||
$this->set('metaGroup', $this->isAdmin ? 'Administration' : 'Cerebrate');
|
||||
}
|
||||
|
||||
public function view($id = false)
|
||||
{
|
||||
if (empty($id) || empty($this->ACL->getUser()['role']['perm_admin'])) {
|
||||
$currentUser = $this->ACL->getUser();
|
||||
if (empty($id) || (empty($currentUser['role']['perm_org_admin']) && empty($currentUser['role']['perm_admin']))) {
|
||||
$id = $this->ACL->getUser()['id'];
|
||||
}
|
||||
$this->CRUD->view($id, [
|
||||
|
@ -98,6 +116,12 @@ class UsersController extends AppController
|
|||
public function edit($id = false)
|
||||
{
|
||||
$currentUser = $this->ACL->getUser();
|
||||
$validRoles = [];
|
||||
if (!$currentUser['role']['perm_admin']) {
|
||||
$validRoles = $this->Users->Roles->find('list')->select(['id', 'name'])->order(['name' => 'asc'])->where(['perm_admin' => 0])->all()->toArray();
|
||||
} else {
|
||||
$validRoles = $this->Users->Roles->find('list')->order(['name' => 'asc'])->all()->toArray();
|
||||
}
|
||||
if (empty($id)) {
|
||||
$id = $currentUser['id'];
|
||||
} else {
|
||||
|
@ -128,6 +152,21 @@ class UsersController extends AppController
|
|||
$params['fields'][] = 'role_id';
|
||||
$params['fields'][] = 'organisation_id';
|
||||
$params['fields'][] = 'disabled';
|
||||
} else if (!empty($this->ACL->getUser()['role']['perm_org_admin'])) {
|
||||
$params['fields'][] = 'username';
|
||||
$params['fields'][] = 'role_id';
|
||||
$params['fields'][] = 'disabled';
|
||||
if (!$currentUser['role']['perm_admin']) {
|
||||
$params['afterFind'] = function ($data, &$params) use ($currentUser, $validRoles) {
|
||||
if (!in_array($data['role_id'], array_keys($validRoles))) {
|
||||
throw new MethodNotAllowedException(__('You cannot edit the given privileged user.'));
|
||||
}
|
||||
if ($data['organisation_id'] !== $currentUser['organisation_id']) {
|
||||
throw new MethodNotAllowedException(__('You cannot edit the given user.'));
|
||||
}
|
||||
return $data;
|
||||
};
|
||||
}
|
||||
}
|
||||
$this->CRUD->edit($id, $params);
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
|
@ -135,9 +174,7 @@ class UsersController extends AppController
|
|||
return $responsePayload;
|
||||
}
|
||||
$dropdownData = [
|
||||
'role' => $this->Users->Roles->find('list', [
|
||||
'sort' => ['name' => 'asc']
|
||||
]),
|
||||
'role' => $validRoles,
|
||||
'individual' => $this->Users->Individuals->find('list', [
|
||||
'sort' => ['email' => 'asc']
|
||||
]),
|
||||
|
@ -152,7 +189,19 @@ class UsersController extends AppController
|
|||
|
||||
public function toggle($id, $fieldName = 'disabled')
|
||||
{
|
||||
$this->CRUD->toggle($id, $fieldName);
|
||||
$params = [
|
||||
'contain' => 'Roles'
|
||||
];
|
||||
$currentUser = $this->ACL->getUser();
|
||||
if (!$currentUser['role']['perm_admin']) {
|
||||
$params['afterFind'] = function ($user, &$params) use ($currentUser) {
|
||||
if (!$this->ACL->canEditUser($currentUser, $user)) {
|
||||
throw new MethodNotAllowedException(__('You cannot edit the given user.'));
|
||||
}
|
||||
return $user;
|
||||
};
|
||||
}
|
||||
$this->CRUD->toggle($id, $fieldName, $params);
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
if (!empty($responsePayload)) {
|
||||
return $responsePayload;
|
||||
|
@ -161,6 +210,24 @@ class UsersController extends AppController
|
|||
|
||||
public function delete($id)
|
||||
{
|
||||
$currentUser = $this->ACL->getUser();
|
||||
$validRoles = [];
|
||||
if (!$currentUser['role']['perm_admin']) {
|
||||
$validRoles = $this->Users->Roles->find('list')->order(['name' => 'asc'])->all()->toArray();
|
||||
}
|
||||
$params = [
|
||||
'beforeSave' => function($data) use ($currentUser, $validRoles) {
|
||||
if (!$currentUser['role']['perm_admin']) {
|
||||
if ($data['organisation_id'] !== $currentUser['organisation_id']) {
|
||||
throw new MethodNotAllowedException(__('You do not have permission to remove the given user.'));
|
||||
}
|
||||
if (!in_array($data['role_id'], array_keys($validRoles))) {
|
||||
throw new MethodNotAllowedException(__('You do not have permission to remove the given user.'));
|
||||
}
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
];
|
||||
$this->CRUD->delete($id);
|
||||
$responsePayload = $this->CRUD->getResponsePayload();
|
||||
if (!empty($responsePayload)) {
|
||||
|
@ -218,10 +285,21 @@ class UsersController extends AppController
|
|||
}
|
||||
}
|
||||
|
||||
public function settings()
|
||||
public function settings($user_id=false)
|
||||
{
|
||||
$this->set('user', $this->ACL->getUser());
|
||||
$all = $this->Users->UserSettings->getSettingsFromProviderForUser($this->ACL->getUser()['id'], true);
|
||||
$editingAnotherUser = false;
|
||||
$currentUser = $this->ACL->getUser();
|
||||
if (empty($currentUser['role']['perm_admin']) || $user_id == $currentUser->id) {
|
||||
$user = $currentUser;
|
||||
} else {
|
||||
$user = $this->Users->get($user_id, [
|
||||
'contain' => ['Roles', 'Individuals' => 'Organisations', 'Organisations', 'UserSettings']
|
||||
]);
|
||||
$editingAnotherUser = true;
|
||||
}
|
||||
$this->set('editingAnotherUser', $editingAnotherUser);
|
||||
$this->set('user', $user);
|
||||
$all = $this->Users->UserSettings->getSettingsFromProviderForUser($user->id, true);
|
||||
$this->set('settingsProvider', $all['settingsProvider']);
|
||||
$this->set('settings', $all['settings']);
|
||||
$this->set('settingsFlattened', $all['settingsFlattened']);
|
||||
|
@ -233,6 +311,9 @@ class UsersController extends AppController
|
|||
if (empty(Configure::read('security.registration.self-registration'))) {
|
||||
throw new UnauthorizedException(__('User self-registration is not open.'));
|
||||
}
|
||||
if (!empty(Configure::read('security.registration.floodProtection'))) {
|
||||
$this->FloodProtection->check('register');
|
||||
}
|
||||
if ($this->request->is('post')) {
|
||||
$data = $this->request->getData();
|
||||
$this->InboxProcessors = TableRegistry::getTableLocator()->get('InboxProcessors');
|
||||
|
@ -249,6 +330,9 @@ class UsersController extends AppController
|
|||
],
|
||||
];
|
||||
$processorResult = $processor->create($data);
|
||||
if (!empty(Configure::read('security.registration.floodProtection'))) {
|
||||
$this->FloodProtection->set('register');
|
||||
}
|
||||
return $processor->genHTTPReply($this, $processorResult, ['controller' => 'Inbox', 'action' => 'index']);
|
||||
}
|
||||
$this->viewBuilder()->setLayout('login');
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
|
||||
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
|
||||
*
|
||||
* Licensed under The MIT License
|
||||
* Redistributions of files must retain the above copyright notice.
|
||||
*
|
||||
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
|
||||
* @since 3.0.0
|
||||
* @license https://opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
namespace App\Http\Exception;
|
||||
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Represents an HTTP 404 error.
|
||||
*/
|
||||
class TooManyRequestsException extends \Cake\Http\Exception\HttpException
|
||||
{
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected $_defaultCode = 429;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string|null $message If no message is given 'Too Many Requests' will be the message
|
||||
* @param int|null $code Status code, defaults to 429
|
||||
* @param \Throwable|null $previous The previous exception.
|
||||
*/
|
||||
public function __construct(?string $message = null, ?int $code = null, ?Throwable $previous = null)
|
||||
{
|
||||
if (empty($message)) {
|
||||
$message = 'Too Many Requests';
|
||||
}
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
}
|
|
@ -527,6 +527,7 @@ class MispConnector extends CommonConnectorTools
|
|||
]
|
||||
],
|
||||
'actions' => [
|
||||
/*
|
||||
[
|
||||
'open_modal' => '/localTools/action/' . h($params['connection']['id']) . '/editUser?id={{0}}',
|
||||
'modal_params_data_path' => ['User.id'],
|
||||
|
@ -539,6 +540,7 @@ class MispConnector extends CommonConnectorTools
|
|||
'icon' => 'trash',
|
||||
'reload_url' => '/localTools/action/' . h($params['connection']['id']) . '/serversAction'
|
||||
]
|
||||
*/
|
||||
],
|
||||
'title' => false,
|
||||
'description' => false,
|
||||
|
|
|
@ -7,5 +7,4 @@ use Cake\ORM\Entity;
|
|||
|
||||
class EncryptionKey extends AppModel
|
||||
{
|
||||
|
||||
}
|
||||
|
|
|
@ -11,10 +11,12 @@ class Individual extends AppModel
|
|||
'*' => true,
|
||||
'id' => false,
|
||||
'uuid' => false,
|
||||
'created' => false,
|
||||
];
|
||||
|
||||
protected $_accessibleOnNew = [
|
||||
'uuid' => true,
|
||||
'created' => true,
|
||||
];
|
||||
|
||||
protected $_virtual = ['full_name', 'alternate_emails'];
|
||||
|
|
|
@ -10,5 +10,10 @@ class Organisation extends AppModel
|
|||
protected $_accessible = [
|
||||
'*' => true,
|
||||
'id' => false,
|
||||
'created' => false
|
||||
];
|
||||
|
||||
protected $_accessibleOnNew = [
|
||||
'created' => true
|
||||
];
|
||||
}
|
||||
|
|
|
@ -13,11 +13,13 @@ class SharingGroup extends AppModel
|
|||
'uuid' => false,
|
||||
'organisation_id' => false,
|
||||
'user_id' => false,
|
||||
'created' => false
|
||||
];
|
||||
|
||||
protected $_accessibleOnNew = [
|
||||
'uuid' => true,
|
||||
'organisation_id' => true,
|
||||
'user_id' => true,
|
||||
'created' => true
|
||||
];
|
||||
}
|
||||
|
|
|
@ -185,4 +185,9 @@ class AppTable extends Table
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function isValidUrl($value, array $context): bool
|
||||
{
|
||||
return filter_var($value, FILTER_VALIDATE_URL);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,7 +33,11 @@ class BroodsTable extends AppTable
|
|||
->requirePresence(['name', 'url', 'organisation_id'], 'create')
|
||||
->notEmptyString('name')
|
||||
->notEmptyString('url')
|
||||
->url('url', __('The provided value is not a valid URL'))
|
||||
->add('url', 'isValidUrl', [
|
||||
'rule' => 'isValidUrl',
|
||||
'message' => __('The provided value is not a valid URL'),
|
||||
'provider' => 'table'
|
||||
])
|
||||
->naturalNumber('organisation_id', false);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace App\Model\Table;
|
||||
|
||||
use App\Model\Table\AppTable;
|
||||
use Cake\ORM\Table;
|
||||
use Cake\Validation\Validator;
|
||||
|
||||
class FloodProtectionsTable extends AppTable
|
||||
{
|
||||
public function initialize(array $config): void
|
||||
{
|
||||
parent::initialize($config);
|
||||
$this->setDisplayField('request_ip');
|
||||
}
|
||||
|
||||
public function validationDefault(Validator $validator): Validator
|
||||
{
|
||||
return $validator;
|
||||
}
|
||||
}
|
|
@ -70,6 +70,7 @@ class IndividualsTable extends AppTable
|
|||
$this->patchEntity($existingIndividual, $individual);
|
||||
$entityToSave = $existingIndividual;
|
||||
}
|
||||
$entityToSave->setDirty('modified', false);
|
||||
$savedEntity = $this->save($entityToSave, ['associated' => false]);
|
||||
if (!$savedEntity) {
|
||||
return null;
|
||||
|
|
|
@ -66,6 +66,7 @@ class OrganisationsTable extends AppTable
|
|||
$this->patchEntity($existingOrg, $org);
|
||||
$entityToSave = $existingOrg;
|
||||
}
|
||||
$entityToSave->setDirty('modified', false);
|
||||
$savedEntity = $this->save($entityToSave, ['associated' => false]);
|
||||
if (!$savedEntity) {
|
||||
return null;
|
||||
|
|
|
@ -188,7 +188,7 @@ class BaseSettingsProvider
|
|||
* @param array $setting
|
||||
* @return mixed
|
||||
*/
|
||||
public function evaluateFunctionForSetting($fun, $setting)
|
||||
public function evaluateFunctionForSetting($fun, &$setting)
|
||||
{
|
||||
$functionResult = true;
|
||||
if (is_callable($fun)) { // Validate with anonymous function
|
||||
|
|
|
@ -188,7 +188,11 @@ class CerebrateSettingsProvider extends BaseSettingsProvider
|
|||
'severity' => 'info',
|
||||
'default' => '',
|
||||
'description' => __('The baseurl of the keycloak authentication endpoint, such as https://foo.bar/baz/auth.'),
|
||||
'dependsOn' => 'keycloak.enabled'
|
||||
'dependsOn' => 'keycloak.enabled',
|
||||
'beforeSave' => function (&$value, $setting, $validator) {
|
||||
$value = rtrim($value, '/');
|
||||
return true;
|
||||
}
|
||||
],
|
||||
'keycloak.authoritative' => [
|
||||
'name' => 'Authoritative',
|
||||
|
@ -270,6 +274,21 @@ class CerebrateSettingsProvider extends BaseSettingsProvider
|
|||
]
|
||||
],
|
||||
'Security' => [
|
||||
'Logging' => [
|
||||
'Logging' => [
|
||||
'security.logging.ip_source' => [
|
||||
'name' => __('Set IP source'),
|
||||
'type' => 'select',
|
||||
'description' => __('Select where the harvested IP should come from. This defaults to REMOTE_ADDR, but for instances behind a proxy HTTP_X_FORWARDED_FOR or HTTP_CLIENT_IP might make more sense.'),
|
||||
'default' => 'REMOTE_ADDR',
|
||||
'options' => [
|
||||
'REMOTE_ADDR' => 'REMOTE_ADDR',
|
||||
'HTTP_X_FORWARDED_FOR' => 'HTTP_X_FORWARDED_FOR',
|
||||
'HTTP_CLIENT_IP' => __('HTTP_CLIENT_IP'),
|
||||
],
|
||||
],
|
||||
]
|
||||
],
|
||||
'Registration' => [
|
||||
'Registration' => [
|
||||
'security.registration.self-registration' => [
|
||||
|
@ -278,6 +297,12 @@ class CerebrateSettingsProvider extends BaseSettingsProvider
|
|||
'description' => __('Enable the self-registration feature where user can request account creation. Admin can view the request and accept it in the application inbox.'),
|
||||
'default' => false,
|
||||
],
|
||||
'security.registration.floodProtection' => [
|
||||
'name' => __('Enable registration flood-protection'),
|
||||
'type' => 'boolean',
|
||||
'description' => __('Enabling this setting will only allow 5 registrations / IP address every 15 minutes (rolling time-frame).'),
|
||||
'default' => false,
|
||||
],
|
||||
]
|
||||
],
|
||||
'Development' => [
|
||||
|
|
|
@ -3,6 +3,7 @@ namespace App\Model\Table;
|
|||
|
||||
use App\Model\Table\AppTable;
|
||||
use Cake\ORM\Table;
|
||||
use Cake\Filesystem\File;
|
||||
use Cake\Core\Configure;
|
||||
use Cake\Error\Debugger;
|
||||
|
||||
|
@ -72,19 +73,29 @@ class SettingsTable extends AppTable
|
|||
}
|
||||
}
|
||||
}
|
||||
$setting['value'] = $value ?? '';
|
||||
if (isset($setting['test'])) {
|
||||
$validationResult = $this->SettingsProvider->evaluateFunctionForSetting($setting['test'], $setting);
|
||||
if ($validationResult !== true) {
|
||||
$errors[] = $validationResult;
|
||||
$setting['errorMessage'] = $validationResult;
|
||||
}
|
||||
}
|
||||
if (empty($errors) && !empty($setting['beforeSave'])) {
|
||||
$setting['value'] = $value ?? '';
|
||||
$beforeSaveResult = $this->SettingsProvider->evaluateFunctionForSetting($setting['beforeSave'], $setting);
|
||||
if ($beforeSaveResult !== true) {
|
||||
$errors[] = $beforeSaveResult;
|
||||
$setting['errorMessage'] = $validationResult;
|
||||
}
|
||||
}
|
||||
if (empty($errors)) {
|
||||
$saveResult = $this->saveSettingOnDisk($name, $value);
|
||||
$saveResult = $this->saveSettingOnDisk($name, $setting['value']);
|
||||
if ($saveResult) {
|
||||
if (!empty($setting['afterSave'])) {
|
||||
$this->SettingsProvider->evaluateFunctionForSetting($setting['afterSave'], $setting);
|
||||
}
|
||||
} else {
|
||||
$errors[] = __('Could not save settings on disk');
|
||||
}
|
||||
}
|
||||
return $errors;
|
||||
|
@ -129,8 +140,16 @@ class SettingsTable extends AppTable
|
|||
$settings = $this->readSettings();
|
||||
$settings[$name] = $value;
|
||||
$settings = json_encode($settings, JSON_PRETTY_PRINT);
|
||||
file_put_contents(CONFIG . 'config.json', $settings);
|
||||
$this->loadSettings();
|
||||
return true;
|
||||
$path = CONFIG . 'config.json';
|
||||
$file = new File($path);
|
||||
if ($file->writable()) {
|
||||
$success = file_put_contents($path, $settings);
|
||||
if ($success) {
|
||||
$this->loadSettings();
|
||||
}
|
||||
} else {
|
||||
$success = false;
|
||||
}
|
||||
return $success;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -66,6 +66,7 @@ class SharingGroupsTable extends AppTable
|
|||
$this->patchEntity($existingSG, $input);
|
||||
$entityToSave = $existingSG;
|
||||
}
|
||||
$entityToSave->setDirty('modified', false);
|
||||
$savedEntity = $this->save($entityToSave, ['associated' => false]);
|
||||
if (!$savedEntity) {
|
||||
return null;
|
||||
|
|
|
@ -135,4 +135,18 @@ class UserSettingsTable extends AppTable
|
|||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* validURI - Ensure the provided URI can be safely put as a link
|
||||
*
|
||||
* @param String $uri
|
||||
* @return bool if the URI is safe to be put as a link
|
||||
*/
|
||||
public function validURI(String $uri): bool
|
||||
{
|
||||
$parsed = parse_url($uri);
|
||||
$isLocalPath = empty($parsed['scheme']) && empty($parsed['domain']) && !empty($parsed['path']);
|
||||
$isValidURL = !empty($parsed['scheme']) && in_array($parsed['scheme'], ['http', 'https']) && filter_var($uri, FILTER_SANITIZE_URL);
|
||||
return $isLocalPath || $isValidURL;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
"version": "0.1",
|
||||
"version": "1.4",
|
||||
"application": "Cerebrate"
|
||||
}
|
||||
|
|
|
@ -10,7 +10,8 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
'data' => [
|
||||
'type' => 'simple',
|
||||
'text' => __('Add authentication key'),
|
||||
'popover_url' => '/authKeys/add'
|
||||
'popover_url' => '/authKeys/add',
|
||||
'reload_url' => $this->request->getRequestTarget()
|
||||
]
|
||||
]
|
||||
],
|
||||
|
@ -65,7 +66,8 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
[
|
||||
'open_modal' => '/authKeys/delete/[onclick_params_data_path]',
|
||||
'modal_params_data_path' => 'id',
|
||||
'icon' => 'trash'
|
||||
'icon' => 'trash',
|
||||
'reload_url' => $this->request->getRequestTarget()
|
||||
]
|
||||
]
|
||||
]
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
<?php
|
||||
|
||||
use Cake\ORM\TableRegistry;
|
||||
|
||||
$bookmarks = !empty($loggedUser->user_settings_by_name['ui.bookmarks']['value']) ? json_decode($loggedUser->user_settings_by_name['ui.bookmarks']['value'], true) : [];
|
||||
$this->userSettingsTable = TableRegistry::getTableLocator()->get('UserSettings');
|
||||
?>
|
||||
|
||||
<h3>
|
||||
|
@ -9,18 +13,24 @@ $bookmarks = !empty($loggedUser->user_settings_by_name['ui.bookmarks']['value'])
|
|||
<?= __('Bookmarks') ?>
|
||||
</h3>
|
||||
<div class="row">
|
||||
<?php if (!empty($bookmarks)): ?>
|
||||
<?php if (!empty($bookmarks)) : ?>
|
||||
<ul class="col-sm-12 col-md-10 col-l-8 col-xl-8 mb-3">
|
||||
<?php foreach ($bookmarks as $bookmark) : ?>
|
||||
<li class="list-group-item">
|
||||
<a href="<?= h($bookmark['url']) ?>" class="w-bold">
|
||||
<?= h($bookmark['label']) ?>
|
||||
</a>
|
||||
<?php if ($this->userSettingsTable->validURI($bookmark['url'])): ?>
|
||||
<a href="<?= h($bookmark['url']) ?>" class="w-bold">
|
||||
<?= h($bookmark['label']) ?>
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<span class="w-bold">
|
||||
<?= h($bookmark['url']) ?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
<span class="ms-3 fw-light"><?= h($bookmark['name']) ?></span>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php else: ?>
|
||||
<?php else : ?>
|
||||
<p class="fw-light"><?= __('No bookmarks') ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
|
|
@ -11,7 +11,8 @@
|
|||
'field' => 'organisation_id',
|
||||
'type' => 'dropdown',
|
||||
'label' => __('Owner organisation'),
|
||||
'options' => $dropdownData['organisation']
|
||||
'options' => $dropdownData['organisation'],
|
||||
'default' => $loggedUser['organisation_id']
|
||||
],
|
||||
array(
|
||||
'field' => 'releasability',
|
||||
|
|
|
@ -9,8 +9,7 @@
|
|||
'type' => 'dropdown',
|
||||
'label' => __('User'),
|
||||
'options' => $dropdownData['user'],
|
||||
'value' => !empty($user_id) ? $user_id : '',
|
||||
'disabled' => !empty($user_id),
|
||||
'value' => !is_null($user_id) ? $user_id : '',
|
||||
],
|
||||
[
|
||||
'field' => 'name',
|
||||
|
|
|
@ -18,7 +18,8 @@
|
|||
'field' => 'organisation_id',
|
||||
'type' => 'dropdown',
|
||||
'label' => __('Associated organisation'),
|
||||
'options' => $dropdownData['organisation']
|
||||
'options' => $dropdownData['organisation'],
|
||||
'default' => $loggedUser['organisation_id']
|
||||
],
|
||||
[
|
||||
'field' => 'password',
|
||||
|
@ -39,7 +40,8 @@
|
|||
'field' => 'role_id',
|
||||
'type' => 'dropdown',
|
||||
'label' => __('Role'),
|
||||
'options' => $dropdownData['role']
|
||||
'options' => $dropdownData['role'],
|
||||
'default' => $defaultRole ?? null
|
||||
],
|
||||
[
|
||||
'field' => 'disabled',
|
||||
|
|
|
@ -102,12 +102,48 @@ echo $this->element('genericElements/IndexTable/index_table', [
|
|||
[
|
||||
'open_modal' => '/users/edit/[onclick_params_data_path]',
|
||||
'modal_params_data_path' => 'id',
|
||||
'icon' => 'edit'
|
||||
'icon' => 'edit',
|
||||
'complex_requirement' => [
|
||||
'options' => [
|
||||
'datapath' => [
|
||||
'role_id' => 'role_id'
|
||||
]
|
||||
],
|
||||
'function' => function ($row, $options) use ($loggedUser, $validRoles) {
|
||||
if (empty($loggedUser['role']['perm_admin'])) {
|
||||
if (empty($loggedUser['role']['perm_org_admin'])) {
|
||||
return false;
|
||||
}
|
||||
if (!isset($validRoles[$options['datapath']['role_id']])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
]
|
||||
],
|
||||
[
|
||||
'open_modal' => '/users/delete/[onclick_params_data_path]',
|
||||
'modal_params_data_path' => 'id',
|
||||
'icon' => 'trash'
|
||||
'icon' => 'trash',
|
||||
'complex_requirement' => [
|
||||
'options' => [
|
||||
'datapath' => [
|
||||
'role_id' => 'role_id'
|
||||
]
|
||||
],
|
||||
'function' => function ($row, $options) use ($loggedUser, $validRoles) {
|
||||
if (empty($loggedUser['role']['perm_admin'])) {
|
||||
if (empty($loggedUser['role']['perm_org_admin'])) {
|
||||
return false;
|
||||
}
|
||||
if (!isset($validRoles[$options['datapath']['role_id']])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
]
|
||||
],
|
||||
]
|
||||
]
|
||||
|
|
|
@ -30,7 +30,7 @@ use Cake\Core\Configure;
|
|||
echo '</div>';
|
||||
}
|
||||
|
||||
if (!empty(Configure::read('keycloak'))) {
|
||||
if (!empty(Configure::read('keycloak.enabled'))) {
|
||||
echo sprintf('<div class="d-flex align-items-center my-2"><hr class="d-inline-block flex-grow-1"/><span class="mx-3 fw-light">%s</span><hr class="d-inline-block flex-grow-1"/></div>', __('Or'));
|
||||
echo $this->Form->create(null, [
|
||||
'url' => Cake\Routing\Router::url([
|
||||
|
|
|
@ -10,10 +10,12 @@ foreach ($settingsProvider as $settingTitle => $settingContent) {
|
|||
]);
|
||||
}
|
||||
|
||||
$navLinks[] = __('Bookmarks');
|
||||
$tabContents[] = $this->element('UserSettings/saved-bookmarks', [
|
||||
'bookmarks' => !empty($user->user_settings_by_name['ui.bookmarks']['value']) ? json_decode($user->user_settings_by_name['ui.bookmarks']['value'], true) : []
|
||||
]);
|
||||
if (empty($editingAnotherUser)) {
|
||||
$navLinks[] = __('Bookmarks');
|
||||
$tabContents[] = $this->element('UserSettings/saved-bookmarks', [
|
||||
'bookmarks' => !empty($user->user_settings_by_name['ui.bookmarks']['value']) ? json_decode($user->user_settings_by_name['ui.bookmarks']['value'], true) : []
|
||||
]);
|
||||
}
|
||||
|
||||
$tabsOptions = [
|
||||
'vertical' => true,
|
||||
|
@ -29,11 +31,15 @@ $tabsOptions = [
|
|||
];
|
||||
$tabs = $this->Bootstrap->tabs($tabsOptions);
|
||||
echo $this->Html->script('settings');
|
||||
$saveUrl = '/userSettings/saveSetting';
|
||||
if(!empty($editingAnotherUser)) {
|
||||
$saveUrl .= '/' . h($user->id);
|
||||
}
|
||||
?>
|
||||
|
||||
<script>
|
||||
window.settingsFlattened = <?= json_encode($settingsFlattened) ?>;
|
||||
window.saveSettingURL = '/userSettings/saveSetting'
|
||||
window.saveSettingURL = '<?= $saveUrl ?>'
|
||||
</script>
|
||||
|
||||
<h2 class="fw-light"><?= __('Account settings') ?></h2>
|
||||
|
@ -43,7 +49,17 @@ echo $this->Html->script('settings');
|
|||
<span class="fw-bold font-monospace me-2 fs-5"><?= h($user->username) ?></span>
|
||||
<span><?= h($user->individual->full_name) ?></span>
|
||||
</div>
|
||||
<div class="fw-light"><?= __('Your personnal account') ?></div>
|
||||
<?php if (!empty($editingAnotherUser)): ?>
|
||||
<?=
|
||||
$this->Bootstrap->alert([
|
||||
'text' => __('Currently editing the account settings of another user.'),
|
||||
'variant' => 'warning',
|
||||
'dismissible' => false
|
||||
])
|
||||
?>
|
||||
<?php else: ?>
|
||||
<div class="fw-light"><?= __('Your personnal account') ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<?= $tabs ?>
|
||||
|
|
|
@ -57,6 +57,6 @@ $mainPanelHeight = 'calc(100vh - 42px - 1rem - 56px - 38px - 1rem)';
|
|||
</div>
|
||||
<?php else: ?>
|
||||
<div>
|
||||
<?= $contentHtml ?>
|
||||
<?= !empty($contentHtml) ? $contentHtml : sprintf('<p class="text-center mt-3">%s</p>', __('No settings available for this category')) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
|
@ -13,11 +13,11 @@
|
|||
(!empty($setting['error']) ? $appView->get('variantFromSeverity')[$setting['severity']] : ''),
|
||||
],
|
||||
($setting['type'] == 'textarea' ? '' : 'type') => ($setting['type'] == 'textarea' ? '' : 'text'),
|
||||
'id' => $settingId,
|
||||
'data-setting-name' => $settingName,
|
||||
'value' => isset($setting['value']) ? $setting['value'] : "",
|
||||
'placeholder' => $setting['default'] ?? '',
|
||||
'aria-describedby' => "{$settingId}Help"
|
||||
'id' => h($settingId),
|
||||
'data-setting-name' => h($settingName),
|
||||
'value' => isset($setting['value']) ? h($setting['value']) : "",
|
||||
'placeholder' => empty($setting['default']) ? '' : h($setting['default']),
|
||||
'aria-describedby' => h("{$settingId}Help")
|
||||
]
|
||||
);
|
||||
})($settingName, $setting, $this);
|
||||
|
@ -28,13 +28,13 @@
|
|||
return $this->Bootstrap->switch([
|
||||
'label' => h($setting['description']),
|
||||
'checked' => !empty($setting['value']),
|
||||
'id' => $settingId,
|
||||
'id' => h($settingId),
|
||||
'class' => [
|
||||
(!empty($setting['error']) ? 'is-invalid' : ''),
|
||||
(!empty($setting['error']) ? $appView->get('variantFromSeverity')[$setting['severity']] : ''),
|
||||
],
|
||||
'attrs' => [
|
||||
'data-setting-name' => $settingName
|
||||
'data-setting-name' => h($settingName)
|
||||
]
|
||||
]);
|
||||
})($settingName, $setting, $this);
|
||||
|
@ -53,16 +53,16 @@
|
|||
'type' => 'number',
|
||||
'min' => '0',
|
||||
'step' => 1,
|
||||
'id' => $settingId,
|
||||
'data-setting-name' => $settingName,
|
||||
'aria-describedby' => "{$settingId}Help"
|
||||
'id' => h($settingId),
|
||||
'data-setting-name' => h($settingName),
|
||||
'aria-describedby' => h("{$settingId}Help")
|
||||
]);
|
||||
})($settingName, $setting, $this);
|
||||
|
||||
} elseif ($setting['type'] == 'select' || $setting['type'] == 'multi-select') {
|
||||
$input = (function ($settingName, $setting, $appView) {
|
||||
$settingId = str_replace('.', '_', $settingName);
|
||||
$setting['value'] = $setting['value'] ?? '';
|
||||
$setting['value'] = empty($setting['value']) ? '' : h($setting['value']);
|
||||
if ($setting['type'] == 'multi-select') {
|
||||
if (!is_array($setting['value'])) {
|
||||
$firstChar = substr($setting['value'], 0, 1);
|
||||
|
@ -77,7 +77,7 @@
|
|||
foreach ($setting['options'] as $key => $value) {
|
||||
$optionParam = [
|
||||
'class' => [],
|
||||
'value' => $key,
|
||||
'value' => h($key),
|
||||
];
|
||||
if ($setting['type'] == 'multi-select') {
|
||||
if (in_array($key, $setting['value'])) {
|
||||
|
@ -100,10 +100,10 @@
|
|||
(!empty($setting['error']) ? $appView->get('variantFromSeverity')[$setting['severity']] : ''),
|
||||
],
|
||||
($setting['type'] == 'multi-select' ? 'multiple' : '') => ($setting['type'] == 'multi-select' ? 'multiple' : ''),
|
||||
'id' => $settingId,
|
||||
'data-setting-name' => $settingName,
|
||||
'placeholder' => $setting['default'] ?? '',
|
||||
'aria-describedby' => "{$settingId}Help"
|
||||
'id' => h($settingId),
|
||||
'data-setting-name' => h($settingName),
|
||||
'placeholder' => empty($setting['default']) ? '' : h($setting['default']),
|
||||
'aria-describedby' => h("{$settingId}Help")
|
||||
], $options);
|
||||
})($settingName, $setting, $this);
|
||||
}
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
'value' => $fieldData['value'] ?? null,
|
||||
'multiple' => $fieldData['multiple'] ?? false,
|
||||
'disabled' => $fieldData['disabled'] ?? false,
|
||||
'class' => ($fieldData['class'] ?? '') . ' formDropdown form-select'
|
||||
'class' => ($fieldData['class'] ?? '') . ' formDropdown form-select',
|
||||
'default' => ($fieldData['default'] ?? null)
|
||||
];
|
||||
if (!empty($fieldData['label'])) {
|
||||
$controlParams['label'] = $fieldData['label'];
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<?= $ajaxFlashMessage ?>
|
||||
<?php if (!empty($data['description'])) : ?>
|
||||
<div class="pb-3 fw-light">
|
||||
<?= $data['description'] ?>
|
||||
<?= h($data['description']) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="panel col-lg-8">
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?php if (!empty($data['description'])) : ?>
|
||||
<div class="pb-3 fw-light">
|
||||
<?= $data['description'] ?>
|
||||
<?= h($data['description']) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?= $ajaxFlashMessage ?>
|
||||
|
|
|
@ -18,13 +18,13 @@
|
|||
if ($field['fields']['allow_picture'] && !empty($org['id'])) {
|
||||
echo sprintf(
|
||||
'<a href="%s">%s</a>',
|
||||
$baseurl . 'organisations/view/' . h($org['id']),
|
||||
$baseurl . '/organisations/view/' . h($org['id']),
|
||||
h($org['name'])
|
||||
);
|
||||
//echo $this->OrgImg->getOrgImg(array('name' => $org['name'], 'id' => $org['id'], 'size' => 24));
|
||||
} else {
|
||||
echo sprintf(
|
||||
'<a href="%sorganisations/view/%s">%s</a>',
|
||||
'<a href="%s/organisations/view/%s">%s</a>',
|
||||
$baseurl,
|
||||
empty($org['id']) ? h($org['uuid']) : h($org['id']),
|
||||
h($org['name'])
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
<?php
|
||||
$seed = 'f_' . mt_rand();
|
||||
if (!isset($data['requirement']) || $data['requirement']) {
|
||||
if (!empty($data['popover_url'])) {
|
||||
$onClick = sprintf(
|
||||
'onClick="openModalForButton(this, \'%s\', \'%s\')"',
|
||||
'onClick="openModalForButton%s(this, \'%s\', \'%s\')"',
|
||||
$seed,
|
||||
h($data['popover_url']),
|
||||
h(!empty($data['reload_url']) ? $data['reload_url'] : '')
|
||||
);
|
||||
|
@ -70,7 +72,7 @@
|
|||
?>
|
||||
|
||||
<script>
|
||||
function openModalForButton(clicked, url, reloadUrl='') {
|
||||
function openModalForButton<?= $seed ?>(clicked, url, reloadUrl='') {
|
||||
const fallbackReloadUrl = '<?= $this->Url->build(['action' => 'index']); ?>'
|
||||
reloadUrl = reloadUrl != '' ? reloadUrl : fallbackReloadUrl
|
||||
UI.overlayUntilResolve(clicked, UI.submissionModalForIndex(url, reloadUrl, '<?= $tableRandomValue ?>'))
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
<?php
|
||||
use Cake\Routing\Router;
|
||||
use Cake\ORM\TableRegistry;
|
||||
|
||||
$this->userSettingsTable = TableRegistry::getTableLocator()->get('UserSettings');
|
||||
|
||||
$seed = 'sb-' . mt_rand();
|
||||
$icon = $entry['icon'] ?? '';
|
||||
|
@ -14,6 +17,8 @@
|
|||
$active = true;
|
||||
}
|
||||
|
||||
$validURI = $this->userSettingsTable->validURI($url);
|
||||
|
||||
echo $this->Bootstrap->button([
|
||||
'nodeType' => 'a',
|
||||
'text' => h($label),
|
||||
|
@ -22,9 +27,9 @@
|
|||
'outline' => !$active,
|
||||
'size' => 'sm',
|
||||
'icon' => h($icon),
|
||||
'class' => ['mb-1'],
|
||||
'class' => ['mb-1', !$validURI ? 'disabled' : ''],
|
||||
'params' => [
|
||||
'href' => h($url),
|
||||
'href' => $validURI ? h($url) : '#',
|
||||
]
|
||||
]);
|
||||
?>
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Test\Fixture;
|
||||
|
||||
use Cake\TestSuite\Fixture\TestFixture;
|
||||
|
||||
class LocalToolsFixture extends TestFixture
|
||||
{
|
||||
public $connection = 'test';
|
||||
|
||||
public function init(): void
|
||||
{
|
||||
$faker = \Faker\Factory::create();
|
||||
|
||||
$this->records = [];
|
||||
parent::init();
|
||||
}
|
||||
}
|
|
@ -11,7 +11,10 @@ class OrganisationsFixture extends TestFixture
|
|||
public $connection = 'test';
|
||||
|
||||
public const ORGANISATION_A_ID = 1;
|
||||
public const ORGANISATION_A_UUID = 'dce5017e-b6a5-4d0d-a0d7-81e9af56c82c';
|
||||
|
||||
public const ORGANISATION_B_ID = 2;
|
||||
public const ORGANISATION_B_UUID = '36d22d9a-851e-4838-a655-9999c1d19497';
|
||||
|
||||
public function init(): void
|
||||
{
|
||||
|
@ -20,7 +23,7 @@ class OrganisationsFixture extends TestFixture
|
|||
$this->records = [
|
||||
[
|
||||
'id' => self::ORGANISATION_A_ID,
|
||||
'uuid' => $faker->uuid(),
|
||||
'uuid' => self::ORGANISATION_A_UUID,
|
||||
'name' => 'Organisation A',
|
||||
'url' => $faker->url,
|
||||
'nationality' => $faker->countryCode,
|
||||
|
@ -33,7 +36,7 @@ class OrganisationsFixture extends TestFixture
|
|||
[
|
||||
'id' => self::ORGANISATION_B_ID,
|
||||
'uuid' => $faker->uuid(),
|
||||
'name' => 'Organisation B',
|
||||
'name' => self::ORGANISATION_B_UUID,
|
||||
'url' => $faker->url,
|
||||
'nationality' => $faker->countryCode,
|
||||
'sector' => 'IT',
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Test\Fixture;
|
||||
|
||||
use Cake\TestSuite\Fixture\TestFixture;
|
||||
|
||||
class RemoteToolConnectionsFixture extends TestFixture
|
||||
{
|
||||
public $connection = 'test';
|
||||
|
||||
public function init(): void
|
||||
{
|
||||
$faker = \Faker\Factory::create();
|
||||
|
||||
$this->records = [];
|
||||
parent::init();
|
||||
}
|
||||
}
|
|
@ -240,7 +240,10 @@ trait ApiTestTrait
|
|||
protected function _sendRequest($url, $method, $data = []): void
|
||||
{
|
||||
// Adding Content-Type: application/json $this->configRequest() prevents this from happening somehow
|
||||
if (in_array($method, ['POST', 'PATCH', 'PUT']) && $this->_request['headers']['Content-Type'] === 'application/json') {
|
||||
if (
|
||||
in_array($method, ['POST', 'PATCH', 'PUT'])
|
||||
&& $this->_request['headers']['Content-Type'] === 'application/json'
|
||||
) {
|
||||
$data = json_encode($data);
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,9 @@ declare(strict_types=1);
|
|||
namespace App\Test\Helper;
|
||||
|
||||
use \WireMock\Client\WireMock;
|
||||
use Exception;
|
||||
use \WireMock\Client\ValueMatchingStrategy;
|
||||
use \WireMock\Client\RequestPatternBuilder;
|
||||
use \WireMock\Stubbing\StubMapping;
|
||||
|
||||
trait WireMockTestTrait
|
||||
{
|
||||
|
@ -26,7 +28,7 @@ trait WireMockTestTrait
|
|||
);
|
||||
|
||||
if (!$this->wiremock->isAlive()) {
|
||||
throw new Exception('Failed to connect to WireMock server.');
|
||||
throw new \Exception('Failed to connect to WireMock server.');
|
||||
}
|
||||
|
||||
$this->clearWireMockStubs();
|
||||
|
@ -46,4 +48,42 @@ trait WireMockTestTrait
|
|||
{
|
||||
return sprintf('http://%s:%s', $this->config['hostname'], $this->config['port']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify all WireMock stubs were called.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function verifyAllStubsCalled(): void
|
||||
{
|
||||
$stubs = $this->wiremock->listAllStubMappings()->getMappings();
|
||||
foreach ((array)$stubs as $stub) {
|
||||
$this->verifyStubCalled($stub);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the WireMock stub was called.
|
||||
*
|
||||
* @param StubMapping $stub
|
||||
* @return void
|
||||
*/
|
||||
public function verifyStubCalled(StubMapping $stub): void
|
||||
{
|
||||
$validator = new RequestPatternBuilder($stub->getRequest()->getMethod(), $stub->getRequest()->getUrlMatchingStrategy());
|
||||
|
||||
// validate headers
|
||||
$headers = $stub->getRequest()->getHeaders();
|
||||
if (is_array($headers)) {
|
||||
foreach ($headers as $header => $rule) {
|
||||
$validator = $validator->withHeader($header, ValueMatchingStrategy::fromArray($rule));
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Add body matching
|
||||
// TODO: Add query matching
|
||||
// TODO: Add cookie matching
|
||||
|
||||
$this->wiremock->verify($validator);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,6 @@ if [ -e $pidFile ]; then
|
|||
rm $pidFile
|
||||
else
|
||||
echo WireMock is not started 2>&1
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo WireMock $instance stopped
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Test\TestCase\Api\Users;
|
||||
namespace App\Test\TestCase\Api\AuthKeys;
|
||||
|
||||
use Cake\TestSuite\TestCase;
|
||||
use App\Test\Fixture\AuthKeysFixture;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Test\TestCase\Api\Users;
|
||||
namespace App\Test\TestCase\Api\AuthKeys;
|
||||
|
||||
use Cake\TestSuite\TestCase;
|
||||
use App\Test\Fixture\AuthKeysFixture;
|
||||
|
@ -19,7 +19,7 @@ class DeleteAuthKeyApiTest extends TestCase
|
|||
'app.Individuals',
|
||||
'app.Roles',
|
||||
'app.Users',
|
||||
'app.AuthKeys',
|
||||
'app.AuthKeys'
|
||||
];
|
||||
|
||||
public function testDeleteAdminAuthKey(): void
|
||||
|
@ -34,12 +34,14 @@ class DeleteAuthKeyApiTest extends TestCase
|
|||
|
||||
public function testDeleteOrgAdminAuthKeyNotAllowedAsRegularUser(): void
|
||||
{
|
||||
$this->skipOpenApiValidations();
|
||||
$this->setAuthToken(AuthKeysFixture::REGULAR_USER_API_KEY);
|
||||
$url = sprintf('%s/%d', self::ENDPOINT, AuthKeysFixture::ORG_ADMIN_API_ID);
|
||||
|
||||
$this->delete($url);
|
||||
|
||||
$this->assertResponseCode(405);
|
||||
$this->assertDbRecordExists('AuthKeys', ['id' => AuthKeysFixture::ORG_ADMIN_API_ID]);
|
||||
|
||||
$this->markTestIncomplete('FIXME: this test returns string(4) "null", which is not a valid JSON object with 405 status code.');
|
||||
$this->assertResponseCode(405);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Test\TestCase\Api\Users;
|
||||
namespace App\Test\TestCase\Api\AuthKeys;
|
||||
|
||||
use Cake\TestSuite\TestCase;
|
||||
use App\Test\Fixture\AuthKeysFixture;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Test\TestCase\Api\Users;
|
||||
namespace App\Test\TestCase\Api\Broods;
|
||||
|
||||
use Cake\TestSuite\TestCase;
|
||||
use App\Test\Fixture\OrganisationsFixture;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Test\TestCase\Api\Users;
|
||||
namespace App\Test\TestCase\Api\Broods;
|
||||
|
||||
use Cake\TestSuite\TestCase;
|
||||
use App\Test\Fixture\AuthKeysFixture;
|
||||
|
|
|
@ -2,9 +2,8 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Test\TestCase\Api\Users;
|
||||
namespace App\Test\TestCase\Api\Broods;
|
||||
|
||||
use Cake\TestSuite\IntegrationTestTrait;
|
||||
use Cake\TestSuite\TestCase;
|
||||
use App\Test\Fixture\AuthKeysFixture;
|
||||
use App\Test\Fixture\BroodsFixture;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Test\TestCase\Api\Users;
|
||||
namespace App\Test\TestCase\Api\Broods;
|
||||
|
||||
use Cake\TestSuite\TestCase;
|
||||
use App\Test\Fixture\AuthKeysFixture;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Test\TestCase\Api\Users;
|
||||
namespace App\Test\TestCase\Api\Broods;
|
||||
|
||||
use Cake\TestSuite\TestCase;
|
||||
use App\Test\Fixture\AuthKeysFixture;
|
||||
|
@ -31,17 +31,12 @@ class TestBroodConnectionApiTest extends TestCase
|
|||
{
|
||||
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);
|
||||
$this->initializeWireMock();
|
||||
$this->mockCerebrateStatusResponse();
|
||||
$stub = $this->mockCerebrateStatusResponse();
|
||||
|
||||
$url = sprintf('%s/%d', self::ENDPOINT, BroodsFixture::BROOD_WIREMOCK_ID);
|
||||
$this->get($url);
|
||||
|
||||
$this->getWireMock()->verify(
|
||||
WireMock::getRequestedFor(WireMock::urlEqualTo('/instance/status.json'))
|
||||
->withHeader('Content-Type', WireMock::equalTo('application/json'))
|
||||
->withHeader('Authorization', WireMock::equalTo(BroodsFixture::BROOD_WIREMOCK_API_KEY))
|
||||
);
|
||||
|
||||
$this->verifyStubCalled($stub);
|
||||
$this->assertResponseOk();
|
||||
$this->assertResponseContains('"user": "wiremock"');
|
||||
}
|
||||
|
@ -52,17 +47,19 @@ class TestBroodConnectionApiTest extends TestCase
|
|||
WireMock::get(WireMock::urlEqualTo('/instance/status.json'))
|
||||
->willReturn(WireMock::aResponse()
|
||||
->withHeader('Content-Type', 'application/json')
|
||||
->withBody((string)json_encode([
|
||||
"version" => "0.1",
|
||||
"application" => "Cerebrate",
|
||||
"user" => [
|
||||
"id" => 1,
|
||||
"username" => "wiremock",
|
||||
"role" => [
|
||||
"id" => 1
|
||||
->withBody((string)json_encode(
|
||||
[
|
||||
"version" => "0.1",
|
||||
"application" => "Cerebrate",
|
||||
"user" => [
|
||||
"id" => 1,
|
||||
"username" => "wiremock",
|
||||
"role" => [
|
||||
"id" => 1
|
||||
]
|
||||
]
|
||||
]
|
||||
])))
|
||||
)))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Test\TestCase\Api\Users;
|
||||
namespace App\Test\TestCase\Api\Broods;
|
||||
|
||||
use Cake\TestSuite\TestCase;
|
||||
use App\Test\Fixture\AuthKeysFixture;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Test\TestCase\Api\Users;
|
||||
namespace App\Test\TestCase\Api\EncryptionKeys;
|
||||
|
||||
use Cake\TestSuite\TestCase;
|
||||
use App\Test\Fixture\AuthKeysFixture;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Test\TestCase\Api\Users;
|
||||
namespace App\Test\TestCase\Api\EncryptionKeys;
|
||||
|
||||
use Cake\TestSuite\TestCase;
|
||||
use App\Test\Fixture\AuthKeysFixture;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Test\TestCase\Api\Users;
|
||||
namespace App\Test\TestCase\Api\EncryptionKeys;
|
||||
|
||||
use Cake\TestSuite\TestCase;
|
||||
use App\Test\Fixture\AuthKeysFixture;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Test\TestCase\Api\Users;
|
||||
namespace App\Test\TestCase\Api\EncryptionKeys;
|
||||
|
||||
use Cake\TestSuite\TestCase;
|
||||
use App\Test\Fixture\AuthKeysFixture;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Test\TestCase\Api\Users;
|
||||
namespace App\Test\TestCase\Api\EncryptionKeys;
|
||||
|
||||
use Cake\TestSuite\TestCase;
|
||||
use App\Test\Fixture\AuthKeysFixture;
|
||||
|
|
|
@ -2,11 +2,12 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Test\TestCase\Api\Users;
|
||||
namespace App\Test\TestCase\Api\Inbox;
|
||||
|
||||
use Cake\TestSuite\TestCase;
|
||||
use App\Test\Fixture\AuthKeysFixture;
|
||||
use App\Test\Helper\ApiTestTrait;
|
||||
use Authentication\PasswordHasher\DefaultPasswordHasher;
|
||||
|
||||
class CreateInboxEntryApiTest extends TestCase
|
||||
{
|
||||
|
@ -31,24 +32,31 @@ class CreateInboxEntryApiTest extends TestCase
|
|||
$_SERVER['REMOTE_ADDR'] = '::1';
|
||||
|
||||
$url = sprintf("%s/%s/%s", self::ENDPOINT, 'User', 'Registration');
|
||||
$password = 'Password12345!';
|
||||
$email = 'john@example.com';
|
||||
$this->post(
|
||||
$url,
|
||||
[
|
||||
'email' => 'john@example.com',
|
||||
'password' => 'Password12345!'
|
||||
'email' => $email,
|
||||
'password' => $password
|
||||
]
|
||||
);
|
||||
$this->assertResponseOk();
|
||||
|
||||
$response = $this->getJsonResponseAsArray();
|
||||
$userId = $response['data']['id'];
|
||||
|
||||
$createdInboxMessage = $this->getRecordFromDb(
|
||||
'Inbox',
|
||||
[
|
||||
'id' => $userId,
|
||||
'scope' => 'User',
|
||||
'action' => 'Registration'
|
||||
]
|
||||
);
|
||||
|
||||
$this->assertResponseOk();
|
||||
$this->assertResponseContains('"email": "john@example.com"');
|
||||
$this->assertDbRecordExists(
|
||||
'Inbox',
|
||||
[
|
||||
'id' => 3, // hacky, but `data` is json string cannot verify the value because of the hashed password
|
||||
'scope' => 'User',
|
||||
'action' => 'Registration',
|
||||
]
|
||||
);
|
||||
$this->assertTrue((new DefaultPasswordHasher())->check($password, $createdInboxMessage['data']['password']));
|
||||
$this->assertEquals($email, $createdInboxMessage['data']['email']);
|
||||
}
|
||||
|
||||
public function testAddUserRegistrationInboxNotAllowedAsRegularUser(): void
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Test\TestCase\Api\Users;
|
||||
namespace App\Test\TestCase\Api\Inbox;
|
||||
|
||||
use Cake\TestSuite\TestCase;
|
||||
use App\Test\Fixture\AuthKeysFixture;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Test\TestCase\Api\Users;
|
||||
namespace App\Test\TestCase\Api\Individuals;
|
||||
|
||||
use Cake\TestSuite\TestCase;
|
||||
use App\Test\Fixture\AuthKeysFixture;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Test\TestCase\Api\Users;
|
||||
namespace App\Test\TestCase\Api\Individuals;
|
||||
|
||||
use Cake\TestSuite\TestCase;
|
||||
use App\Test\Fixture\AuthKeysFixture;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Test\TestCase\Api\Users;
|
||||
namespace App\Test\TestCase\Api\Individuals;
|
||||
|
||||
use Cake\TestSuite\TestCase;
|
||||
use App\Test\Fixture\AuthKeysFixture;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Test\TestCase\Api\Users;
|
||||
namespace App\Test\TestCase\Api\Individuals;
|
||||
|
||||
use Cake\TestSuite\TestCase;
|
||||
use App\Test\Fixture\AuthKeysFixture;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Test\TestCase\Api\Users;
|
||||
namespace App\Test\TestCase\Api\Individuals;
|
||||
|
||||
use Cake\TestSuite\TestCase;
|
||||
use App\Test\Fixture\AuthKeysFixture;
|
||||
|
|
|
@ -0,0 +1,405 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Test\TestCase\Api\LocalTools;
|
||||
|
||||
use Cake\TestSuite\TestCase;
|
||||
use App\Test\Fixture\OrganisationsFixture;
|
||||
use App\Test\Fixture\AuthKeysFixture;
|
||||
use App\Test\Fixture\UsersFixture;
|
||||
use App\Test\Fixture\RolesFixture;
|
||||
use App\Test\Helper\ApiTestTrait;
|
||||
use App\Test\Helper\WireMockTestTrait;
|
||||
use \WireMock\Client\WireMock;
|
||||
|
||||
class MispInterConnectionTest extends TestCase
|
||||
{
|
||||
use ApiTestTrait;
|
||||
use WireMockTestTrait;
|
||||
|
||||
protected $fixtures = [
|
||||
'app.Organisations',
|
||||
'app.Individuals',
|
||||
'app.Roles',
|
||||
'app.Users',
|
||||
'app.AuthKeys',
|
||||
'app.Broods',
|
||||
'app.LocalTools',
|
||||
'app.RemoteToolConnections',
|
||||
'app.Inbox'
|
||||
];
|
||||
|
||||
/** constants related to the local Cerebrate instance */
|
||||
private const LOCAL_CEREBRATE_URL = 'http://127.0.0.1';
|
||||
|
||||
/** constants related to the local MISP instance */
|
||||
private const LOCAL_MISP_INSTANCE_URL = 'http://localhost:8080/MISP_LOCAL';
|
||||
private const LOCAL_MISP_ADMIN_USER_AUTHKEY = 'b17ce79ac0f05916f382ab06ea4790665dbc174c';
|
||||
|
||||
/** constants related to the remote Cerebrate instance */
|
||||
private const REMOTE_CEREBRATE_URL = 'http://127.0.0.1:8080/CEREBRATE_REMOTE';
|
||||
private const REMOTE_CEREBRATE_AUTHKEY = 'a192ba3c749b545f9cec6b6bba0643736f6c3022';
|
||||
|
||||
/** constants related to the remote MISP instance */
|
||||
private const REMOTE_MISP_SYNC_USER_ID = 333;
|
||||
private const REMOTE_MISP_SYNC_USER_EMAIL = 'sync@misp.remote';
|
||||
private const REMOTE_MISP_INSTANCE_URL = 'http://localhost:8080/MISP_REMOTE';
|
||||
private const REMOTE_MISP_AUTHKEY = '19ca57ecebd2fe34c1c17d729980678eb648d541';
|
||||
|
||||
|
||||
public function testInterConnectMispViaCerebrate(): void
|
||||
{
|
||||
$this->initializeWireMock();
|
||||
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);
|
||||
|
||||
$faker = \Faker\Factory::create();
|
||||
|
||||
/**
|
||||
* 1. Create LocalTool connection to `MISP LOCAL` (local MISP instance)
|
||||
*/
|
||||
$this->post(
|
||||
sprintf('%s/localTools/add', self::LOCAL_CEREBRATE_URL),
|
||||
[
|
||||
'name' => 'MISP_LOCAL',
|
||||
'connector' => 'MispConnector',
|
||||
'settings' => json_encode([
|
||||
'url' => self::LOCAL_MISP_INSTANCE_URL,
|
||||
'authkey' => self::LOCAL_MISP_ADMIN_USER_AUTHKEY,
|
||||
'skip_ssl' => true,
|
||||
]),
|
||||
'description' => 'MISP local instance',
|
||||
'exposed' => true
|
||||
]
|
||||
);
|
||||
$this->assertResponseOk();
|
||||
$this->assertDbRecordExists('LocalTools', ['name' => 'MISP_LOCAL']);
|
||||
|
||||
/**
|
||||
* 2. Create a new Brood (connect to a remote Cerebrate instance)
|
||||
* This step assumes that the remote Cerebrate instance is already
|
||||
* running and has a user created for the local Cerebrate instance.
|
||||
*
|
||||
* NOTE: Uses OrganisationsFixture::ORGANISATION_A_ID from the
|
||||
* fixtures as the local Organisation.
|
||||
*/
|
||||
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);
|
||||
$LOCAL_BROOD_UUID = $faker->uuid;
|
||||
$this->post(
|
||||
'/broods/add',
|
||||
[
|
||||
'uuid' => $LOCAL_BROOD_UUID,
|
||||
'name' => 'Local Brood',
|
||||
'url' => self::REMOTE_CEREBRATE_URL,
|
||||
'description' => $faker->text,
|
||||
'organisation_id' => OrganisationsFixture::ORGANISATION_A_ID,
|
||||
'trusted' => true,
|
||||
'pull' => true,
|
||||
'skip_proxy' => true,
|
||||
'authkey' => self::REMOTE_CEREBRATE_AUTHKEY,
|
||||
]
|
||||
);
|
||||
$this->assertResponseOk();
|
||||
$this->assertDbRecordExists('Broods', ['uuid' => $LOCAL_BROOD_UUID]);
|
||||
$brood = $this->getJsonResponseAsArray();
|
||||
|
||||
/**
|
||||
* 3. Create a new Cerebrate local user for the remote Cerebrate
|
||||
* These includes:
|
||||
* - 3.a: Create a new Organisation
|
||||
* - 3.b: Create a new Individual
|
||||
* - 3.c: Create a new User
|
||||
* - 3.d: Create a new Authkey
|
||||
*/
|
||||
// Create Organisation
|
||||
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);
|
||||
$remoteOrgUuid = $faker->uuid;
|
||||
$this->post(
|
||||
'/organisations/add',
|
||||
[
|
||||
'name' => 'Remote Organisation',
|
||||
'description' => $faker->text,
|
||||
'uuid' => $remoteOrgUuid,
|
||||
'url' => 'http://cerebrate.remote',
|
||||
'nationality' => 'US',
|
||||
'sector' => 'sector',
|
||||
'type' => 'type',
|
||||
]
|
||||
);
|
||||
$this->assertResponseOk();
|
||||
$this->assertDbRecordExists('Organisations', ['uuid' => $remoteOrgUuid]);
|
||||
$remoteOrg = $this->getJsonResponseAsArray();
|
||||
|
||||
// Create Individual
|
||||
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);
|
||||
$this->post(
|
||||
'/individuals/add',
|
||||
[
|
||||
'email' => 'sync@cerebrate.remote',
|
||||
'first_name' => 'Remote',
|
||||
'last_name' => 'Cerebrate'
|
||||
]
|
||||
);
|
||||
$this->assertResponseOk();
|
||||
$this->assertDbRecordExists('Individuals', ['email' => 'sync@cerebrate.remote']);
|
||||
$remoteIndividual = $this->getJsonResponseAsArray();
|
||||
|
||||
// Create User
|
||||
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);
|
||||
$this->post(
|
||||
'/users/add',
|
||||
[
|
||||
'individual_id' => $remoteIndividual['id'],
|
||||
'organisation_id' => $remoteOrg['id'],
|
||||
'role_id' => RolesFixture::ROLE_SYNC_ID,
|
||||
'disabled' => false,
|
||||
'username' => 'remote_cerebrate',
|
||||
'password' => 'Password123456!',
|
||||
]
|
||||
);
|
||||
$this->assertResponseOk();
|
||||
$this->assertDbRecordExists('Users', ['username' => 'remote_cerebrate']);
|
||||
$user = $this->getJsonResponseAsArray();
|
||||
|
||||
// Create Authkey
|
||||
$remoteCerebrateAuthkey = $faker->sha1;
|
||||
$remoteAuthkeyUuid = $faker->uuid;
|
||||
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);
|
||||
$this->post(
|
||||
'/authKeys/add',
|
||||
[
|
||||
'uuid' => $remoteAuthkeyUuid,
|
||||
'authkey' => $remoteCerebrateAuthkey,
|
||||
'expiration' => 0,
|
||||
'user_id' => $user['id'],
|
||||
'comment' => $faker->text
|
||||
]
|
||||
);
|
||||
$this->assertResponseOk();
|
||||
$this->assertDbRecordExists('AuthKeys', ['uuid' => $remoteAuthkeyUuid]);
|
||||
|
||||
/**
|
||||
* 4. Get remote Cerebrate exposed tools
|
||||
*/
|
||||
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);
|
||||
$this->mockCerebrateGetExposedToolsResponse('CEREBRATE_REMOTE', self::REMOTE_CEREBRATE_AUTHKEY);
|
||||
$this->get(sprintf('/localTools/broodTools/%s', $brood['id']));
|
||||
$this->assertResponseOk();
|
||||
$tools = $this->getJsonResponseAsArray();
|
||||
|
||||
/**
|
||||
* 5. Issue a connection request to the remote MISP instance
|
||||
*/
|
||||
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);
|
||||
$this->mockCerebrateGetExposedToolsResponse('CEREBRATE_REMOTE', self::REMOTE_CEREBRATE_AUTHKEY);
|
||||
$this->mockMispViewOrganisationByUuid(
|
||||
'MISP_LOCAL',
|
||||
self::LOCAL_MISP_ADMIN_USER_AUTHKEY,
|
||||
OrganisationsFixture::ORGANISATION_A_UUID,
|
||||
OrganisationsFixture::ORGANISATION_A_ID
|
||||
);
|
||||
$this->mockMispCreateSyncUser(
|
||||
'MISP_LOCAL',
|
||||
self::LOCAL_MISP_ADMIN_USER_AUTHKEY,
|
||||
self::REMOTE_MISP_SYNC_USER_ID,
|
||||
self::REMOTE_MISP_SYNC_USER_EMAIL
|
||||
);
|
||||
$this->mockCerebrateCreateMispIncommingConnectionRequest(
|
||||
'CEREBRATE_REMOTE',
|
||||
UsersFixture::USER_ADMIN_ID,
|
||||
self::LOCAL_CEREBRATE_URL,
|
||||
self::REMOTE_CEREBRATE_AUTHKEY,
|
||||
self::LOCAL_MISP_INSTANCE_URL
|
||||
);
|
||||
$this->post(
|
||||
sprintf('/localTools/connectionRequest/%s/%s', $brood['id'], $tools[0]['id']),
|
||||
[
|
||||
'local_tool_id' => 1
|
||||
]
|
||||
);
|
||||
$this->assertResponseOk();
|
||||
|
||||
/**
|
||||
* 6. Remote Cerebrate accepts the connection request
|
||||
*/
|
||||
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);
|
||||
$this->post(
|
||||
'/inbox/createEntry/LocalTool/AcceptedRequest',
|
||||
[
|
||||
'email' => self::REMOTE_MISP_SYNC_USER_EMAIL,
|
||||
'authkey' => self::REMOTE_MISP_AUTHKEY,
|
||||
'url' => self::REMOTE_MISP_INSTANCE_URL,
|
||||
'reflected_user_id' => self::REMOTE_MISP_SYNC_USER_ID,
|
||||
'connectorName' => 'MispConnector',
|
||||
'cerebrateURL' => self::REMOTE_CEREBRATE_URL,
|
||||
'local_tool_id' => 1,
|
||||
'remote_tool_id' => 1,
|
||||
'tool_name' => 'MISP_REMOTE'
|
||||
]
|
||||
);
|
||||
$this->assertResponseOk();
|
||||
$acceptRequest = $this->getJsonResponseAsArray();
|
||||
|
||||
/**
|
||||
* 7. Finalize the connection
|
||||
*/
|
||||
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);
|
||||
$this->mockEnableMispSyncUser('MISP_LOCAL', self::LOCAL_MISP_ADMIN_USER_AUTHKEY, self::REMOTE_MISP_SYNC_USER_ID);
|
||||
$this->mockAddMispServer(
|
||||
'MISP_LOCAL',
|
||||
self::LOCAL_MISP_ADMIN_USER_AUTHKEY,
|
||||
[
|
||||
'authkey' => self::REMOTE_MISP_AUTHKEY,
|
||||
'url' => self::REMOTE_MISP_INSTANCE_URL,
|
||||
'name' => 'MISP_REMOTE',
|
||||
'remote_org_id' => OrganisationsFixture::ORGANISATION_A_ID
|
||||
]
|
||||
);
|
||||
$this->post(sprintf('/inbox/process/%s', $acceptRequest['data']['id']));
|
||||
$this->assertResponseOk();
|
||||
$this->assertResponseContains('"success": true');
|
||||
$this->verifyAllStubsCalled();
|
||||
}
|
||||
|
||||
private function mockCerebrateGetExposedToolsResponse(string $instance, string $cerebrateAuthkey): \WireMock\Stubbing\StubMapping
|
||||
{
|
||||
return $this->getWireMock()->stubFor(
|
||||
WireMock::get(WireMock::urlEqualTo("/$instance/localTools/exposedTools"))
|
||||
->withHeader('Authorization', WireMock::equalTo($cerebrateAuthkey))
|
||||
->willReturn(WireMock::aResponse()
|
||||
->withHeader('Content-Type', 'application/json')
|
||||
->withBody((string)json_encode(
|
||||
[
|
||||
[
|
||||
"id" => 1,
|
||||
"name" => "MISP ($instance)",
|
||||
"connector" => "MispConnector",
|
||||
]
|
||||
]
|
||||
)))
|
||||
);
|
||||
}
|
||||
|
||||
private function mockMispViewOrganisationByUuid(string $instance, string $mispAuthkey, string $orgUuid, int $orgId): \WireMock\Stubbing\StubMapping
|
||||
{
|
||||
return $this->getWireMock()->stubFor(
|
||||
WireMock::get(WireMock::urlEqualTo("/$instance/organisations/view/$orgUuid/limit:50"))
|
||||
->withHeader('Authorization', WireMock::equalTo($mispAuthkey))
|
||||
->willReturn(WireMock::aResponse()
|
||||
->withHeader('Content-Type', 'application/json')
|
||||
->withBody((string)json_encode(
|
||||
[
|
||||
"Organisation" => [
|
||||
"id" => $orgId,
|
||||
"name" => $instance . ' Organisation',
|
||||
"uuid" => $orgUuid,
|
||||
"local" => true
|
||||
]
|
||||
]
|
||||
)))
|
||||
);
|
||||
}
|
||||
|
||||
private function mockMispCreateSyncUser(string $instance, string $mispAuthkey, int $userId, string $email): \WireMock\Stubbing\StubMapping
|
||||
{
|
||||
$faker = \Faker\Factory::create();
|
||||
return $this->getWireMock()->stubFor(
|
||||
WireMock::post(WireMock::urlEqualTo("/$instance/admin/users/add"))
|
||||
->withHeader('Authorization', WireMock::equalTo($mispAuthkey))
|
||||
->willReturn(WireMock::aResponse()
|
||||
->withHeader('Content-Type', 'application/json')
|
||||
->withBody((string)json_encode(
|
||||
[
|
||||
"User" => [
|
||||
"id" => $userId,
|
||||
"email" => $email,
|
||||
"authkey" => $faker->sha1
|
||||
]
|
||||
]
|
||||
)))
|
||||
);
|
||||
}
|
||||
|
||||
private function mockCerebrateCreateMispIncommingConnectionRequest(
|
||||
string $instance,
|
||||
int $userId,
|
||||
string $cerebrateUrl,
|
||||
string $cerebrateAuthkey,
|
||||
string $mispUrl
|
||||
): \WireMock\Stubbing\StubMapping {
|
||||
$faker = \Faker\Factory::create();
|
||||
|
||||
return $this->getWireMock()->stubFor(
|
||||
WireMock::post(WireMock::urlEqualTo("/$instance/inbox/createEntry/LocalTool/IncomingConnectionRequest"))
|
||||
->withHeader('Authorization', WireMock::equalTo($cerebrateAuthkey))
|
||||
->willReturn(WireMock::aResponse()
|
||||
->withHeader('Content-Type', 'application/json')
|
||||
->withBody((string)json_encode(
|
||||
[
|
||||
'data' => [
|
||||
'id' => $faker->randomNumber(),
|
||||
'uuid' => $faker->uuid,
|
||||
'origin' => $cerebrateUrl,
|
||||
'user_id' => $userId,
|
||||
'data' => [
|
||||
'connectorName' => 'MispConnector',
|
||||
'cerebrateURL' => $cerebrateUrl,
|
||||
'url' => $mispUrl,
|
||||
'tool_connector' => 'MispConnector',
|
||||
'local_tool_id' => 1,
|
||||
'remote_tool_id' => 1,
|
||||
],
|
||||
'title' => 'Request for MISP Inter-connection',
|
||||
'scope' => 'LocalTool',
|
||||
'action' => 'IncomingConnectionRequest',
|
||||
'description' => 'Handle Phase I of inter-connection when another cerebrate instance performs the request.',
|
||||
'local_tool_connector_name' => 'MispConnector',
|
||||
'created' => date('c'),
|
||||
'modified' => date('c')
|
||||
],
|
||||
'success' => true,
|
||||
'message' => 'LocalTool request for IncomingConnectionRequest created',
|
||||
'errors' => [],
|
||||
]
|
||||
)))
|
||||
);
|
||||
}
|
||||
|
||||
private function mockEnableMispSyncUser(string $instance, string $mispAuthkey, int $userId): \WireMock\Stubbing\StubMapping
|
||||
{
|
||||
return $this->getWireMock()->stubFor(
|
||||
WireMock::post(WireMock::urlEqualTo("/$instance/admin/users/edit/$userId"))
|
||||
->withHeader('Authorization', WireMock::equalTo($mispAuthkey))
|
||||
->withRequestBody(WireMock::equalToJson(json_encode(['disabled' => false])))
|
||||
->willReturn(WireMock::aResponse()
|
||||
->withHeader('Content-Type', 'application/json')
|
||||
->withBody((string)json_encode(
|
||||
[
|
||||
"User" => [
|
||||
"id" => $userId,
|
||||
]
|
||||
]
|
||||
)))
|
||||
);
|
||||
}
|
||||
|
||||
private function mockAddMispServer(string $instance, string $mispAuthkey, array $body): \WireMock\Stubbing\StubMapping
|
||||
{
|
||||
$faker = \Faker\Factory::create();
|
||||
|
||||
return $this->getWireMock()->stubFor(
|
||||
WireMock::post(WireMock::urlEqualTo("/$instance/servers/add"))
|
||||
->withHeader('Authorization', WireMock::equalTo($mispAuthkey))
|
||||
->withRequestBody(WireMock::equalToJson(json_encode($body)))
|
||||
->willReturn(WireMock::aResponse()
|
||||
->withHeader('Content-Type', 'application/json')
|
||||
->withBody((string)json_encode(
|
||||
[
|
||||
'Server' => [
|
||||
'id' => $faker->randomNumber()
|
||||
]
|
||||
]
|
||||
)))
|
||||
);
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Test\TestCase\Api\Users;
|
||||
namespace App\Test\TestCase\Api\Organisations;
|
||||
|
||||
use Cake\TestSuite\TestCase;
|
||||
use App\Test\Fixture\AuthKeysFixture;
|
||||
|
|
|
@ -2,9 +2,8 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Test\TestCase\Api\Users;
|
||||
namespace App\Test\TestCase\Api\Organisations;
|
||||
|
||||
use Cake\TestSuite\IntegrationTestTrait;
|
||||
use Cake\TestSuite\TestCase;
|
||||
use App\Test\Fixture\AuthKeysFixture;
|
||||
use App\Test\Fixture\OrganisationsFixture;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Test\TestCase\Api\Users;
|
||||
namespace App\Test\TestCase\Api\Organisations;
|
||||
|
||||
use Cake\TestSuite\TestCase;
|
||||
use App\Test\Fixture\AuthKeysFixture;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Test\TestCase\Api\Users;
|
||||
namespace App\Test\TestCase\Api\Organisations;
|
||||
|
||||
use Cake\TestSuite\TestCase;
|
||||
use App\Test\Fixture\AuthKeysFixture;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Test\TestCase\Api\Users;
|
||||
namespace App\Test\TestCase\Api\Organisations;
|
||||
|
||||
use Cake\TestSuite\TestCase;
|
||||
use App\Test\Fixture\AuthKeysFixture;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Test\TestCase\Api\Users;
|
||||
namespace App\Test\TestCase\Api\Organisations;
|
||||
|
||||
use Cake\TestSuite\TestCase;
|
||||
use App\Test\Fixture\AuthKeysFixture;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Test\TestCase\Api\Users;
|
||||
namespace App\Test\TestCase\Api\Organisations;
|
||||
|
||||
use Cake\TestSuite\TestCase;
|
||||
use App\Test\Fixture\AuthKeysFixture;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Test\TestCase\Api\Users;
|
||||
namespace App\Test\TestCase\Api\SharingGroups;
|
||||
|
||||
use Cake\TestSuite\TestCase;
|
||||
use App\Test\Fixture\OrganisationsFixture;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Test\TestCase\Api\Users;
|
||||
namespace App\Test\TestCase\Api\SharingGroups;
|
||||
|
||||
use Cake\TestSuite\TestCase;
|
||||
use App\Test\Fixture\AuthKeysFixture;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Test\TestCase\Api\Users;
|
||||
namespace App\Test\TestCase\Api\SharingGroups;
|
||||
|
||||
use Cake\TestSuite\TestCase;
|
||||
use App\Test\Fixture\AuthKeysFixture;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Test\TestCase\Api\Users;
|
||||
namespace App\Test\TestCase\Api\SharingGroups;
|
||||
|
||||
use Cake\TestSuite\TestCase;
|
||||
use App\Test\Fixture\AuthKeysFixture;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Test\TestCase\Api\Users;
|
||||
namespace App\Test\TestCase\Api\SharingGroups;
|
||||
|
||||
use Cake\TestSuite\TestCase;
|
||||
use App\Test\Fixture\AuthKeysFixture;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Test\TestCase\Api\Users;
|
||||
namespace App\Test\TestCase\Api\Tags;
|
||||
|
||||
use Cake\TestSuite\TestCase;
|
||||
use App\Test\Fixture\AuthKeysFixture;
|
||||
|
|
|
@ -27,6 +27,8 @@ tags:
|
|||
description: "Assign encryption keys to the user, used to securely communicate or validate messages coming from the user."
|
||||
- name: AuthKeys
|
||||
description: "Authkeys are used for API access. A user can have more than one authkey, so if you would like to use separate keys per tool that queries Cerebrate, add additional keys. Use the comment field to make identifying your keys easier."
|
||||
- name: LocalTools
|
||||
description: "Cerebrate can connect to local tools via individual connectors, built to expose the various functionalities of the given tool via Cerebrate. Simply view the connectors' details and the accompanying instance list to manage the connections using the given connector."
|
||||
|
||||
paths:
|
||||
/individuals/index:
|
||||
|
@ -418,7 +420,7 @@ paths:
|
|||
/inbox/createEntry/User/Registration:
|
||||
post:
|
||||
summary: "Create user registration inbox entry"
|
||||
operationId: createInboxEntry
|
||||
operationId: createUserRegistrationInboxEntry
|
||||
tags:
|
||||
- Inbox
|
||||
requestBody:
|
||||
|
@ -433,6 +435,42 @@ paths:
|
|||
default:
|
||||
$ref: "#/components/responses/ApiErrorResponse"
|
||||
|
||||
/inbox/createEntry/LocalTool/AcceptedRequest:
|
||||
post:
|
||||
summary: "Create accepted connection request inbox entry"
|
||||
operationId: createAcceptedRequestInboxEntry
|
||||
tags:
|
||||
- Inbox
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/CreateAcceptedRequestInboxEntryRequest"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/AcceptedRequestInboxResponse"
|
||||
"403":
|
||||
$ref: "#/components/responses/UnauthorizedApiErrorResponse"
|
||||
"405":
|
||||
$ref: "#/components/responses/MethodNotAllowedApiErrorResponse"
|
||||
default:
|
||||
$ref: "#/components/responses/ApiErrorResponse"
|
||||
|
||||
/inbox/process/{inboxId}:
|
||||
post:
|
||||
summary: "Process inbox entry"
|
||||
operationId: processInboxEntry
|
||||
tags:
|
||||
- Inbox
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/inboxId"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/ProcessInboxResponse"
|
||||
"403":
|
||||
$ref: "#/components/responses/UnauthorizedApiErrorResponse"
|
||||
"405":
|
||||
$ref: "#/components/responses/MethodNotAllowedApiErrorResponse"
|
||||
default:
|
||||
$ref: "#/components/responses/ApiErrorResponse"
|
||||
|
||||
/sharingGroups/index:
|
||||
get:
|
||||
summary: "Get a sharing groups list"
|
||||
|
@ -783,6 +821,63 @@ paths:
|
|||
default:
|
||||
$ref: "#/components/responses/ApiErrorResponse"
|
||||
|
||||
/localTools/add:
|
||||
post:
|
||||
summary: "Add a local tool connection"
|
||||
operationId: addLocalTool
|
||||
tags:
|
||||
- LocalTools
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/CreateLocalToolConnectionRequest"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/LocalToolResponse"
|
||||
"403":
|
||||
$ref: "#/components/responses/UnauthorizedApiErrorResponse"
|
||||
"405":
|
||||
$ref: "#/components/responses/MethodNotAllowedApiErrorResponse"
|
||||
default:
|
||||
$ref: "#/components/responses/ApiErrorResponse"
|
||||
|
||||
/localTools/broodTools/{broodId}:
|
||||
get:
|
||||
summary: "Get brood exposed tools"
|
||||
operationId: getBroodExposedTools
|
||||
tags:
|
||||
- LocalTools
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/broodId"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/GetExposedBroodToolsResponse"
|
||||
"403":
|
||||
$ref: "#/components/responses/UnauthorizedApiErrorResponse"
|
||||
"405":
|
||||
$ref: "#/components/responses/MethodNotAllowedApiErrorResponse"
|
||||
default:
|
||||
$ref: "#/components/responses/ApiErrorResponse"
|
||||
|
||||
/localTools/connectionRequest/{broodId}/{localToolId}:
|
||||
post:
|
||||
summary: "Issue a local tool connection request"
|
||||
operationId: issueLocalToolConnectionRequest
|
||||
tags:
|
||||
- LocalTools
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/broodId"
|
||||
- $ref: "#/components/parameters/localToolId"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/IssueLocalToolConnectionRequest"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/IncomingConnectionRequestInboxResponse"
|
||||
"403":
|
||||
$ref: "#/components/responses/UnauthorizedApiErrorResponse"
|
||||
"405":
|
||||
$ref: "#/components/responses/MethodNotAllowedApiErrorResponse"
|
||||
default:
|
||||
$ref: "#/components/responses/ApiErrorResponse"
|
||||
|
||||
components:
|
||||
schemas:
|
||||
# General
|
||||
|
@ -1129,8 +1224,37 @@ components:
|
|||
user:
|
||||
$ref: "#/components/schemas/User"
|
||||
local_tool_connector_name:
|
||||
type: string
|
||||
nullable: true
|
||||
$ref: "#/components/schemas/LocalToolConnector"
|
||||
|
||||
AcceptedRequestInbox:
|
||||
type: object
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/Inbox"
|
||||
- type: object
|
||||
properties:
|
||||
data:
|
||||
type: object
|
||||
properties:
|
||||
email:
|
||||
$ref: "#/components/schemas/Email"
|
||||
authkey:
|
||||
$ref: "#/components/schemas/AuthKeyRaw"
|
||||
url:
|
||||
type: string
|
||||
reflected_user_id:
|
||||
$ref: "#/components/schemas/ID"
|
||||
connectorName:
|
||||
$ref: "#/components/schemas/LocalToolConnector"
|
||||
cerebrateURL:
|
||||
type: string
|
||||
local_tool_id:
|
||||
$ref: "#/components/schemas/ID"
|
||||
remote_tool_id:
|
||||
$ref: "#/components/schemas/ID"
|
||||
tool_name:
|
||||
type: string
|
||||
local_tool_connector_name:
|
||||
$ref: "#/components/schemas/LocalToolConnector"
|
||||
|
||||
IncomingConnectionRequestInbox:
|
||||
type: object
|
||||
|
@ -1142,9 +1266,7 @@ components:
|
|||
type: object
|
||||
properties:
|
||||
connectorName:
|
||||
type: string
|
||||
enum:
|
||||
- "MispConnector"
|
||||
$ref: "#/components/schemas/LocalToolConnector"
|
||||
cerebrateURL:
|
||||
type: string
|
||||
example: "http://192.168.0.1"
|
||||
|
@ -1159,6 +1281,7 @@ components:
|
|||
anyOf:
|
||||
- $ref: "#/components/schemas/UserRegistrationInbox"
|
||||
- $ref: "#/components/schemas/IncomingConnectionRequestInbox"
|
||||
- $ref: "#/components/schemas/AcceptedRequestInbox"
|
||||
|
||||
# SharingGroups
|
||||
SharingGroupName:
|
||||
|
@ -1362,6 +1485,45 @@ components:
|
|||
items:
|
||||
$ref: "#/components/schemas/AuthKey"
|
||||
|
||||
# LocalTools
|
||||
LocalToolName:
|
||||
type: string
|
||||
|
||||
LocalToolConnector:
|
||||
type: string
|
||||
nullable: true
|
||||
enum:
|
||||
- "MispConnector"
|
||||
|
||||
LocalToolSettings:
|
||||
type: string
|
||||
description: "Stringified JSON representing tool settings"
|
||||
|
||||
LocalToolDescription:
|
||||
type: string
|
||||
|
||||
LocalToolIsExposed:
|
||||
type: boolean
|
||||
|
||||
LocalTool:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
$ref: "#/components/schemas/LocalToolName"
|
||||
connector:
|
||||
$ref: "#/components/schemas/LocalToolConnector"
|
||||
settings:
|
||||
$ref: "#/components/schemas/LocalToolSettings"
|
||||
description:
|
||||
$ref: "#/components/schemas/LocalToolDescription"
|
||||
exposed:
|
||||
$ref: "#/components/schemas/LocalToolIsExposed"
|
||||
|
||||
LocalToolList:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/LocalTool"
|
||||
|
||||
# Errors
|
||||
ApiError:
|
||||
type: object
|
||||
|
@ -1487,6 +1649,22 @@ components:
|
|||
schema:
|
||||
$ref: "#/components/schemas/ID"
|
||||
|
||||
localToolId:
|
||||
name: localToolId
|
||||
in: path
|
||||
description: "Numeric ID of the local tool"
|
||||
required: true
|
||||
schema:
|
||||
$ref: "#/components/schemas/ID"
|
||||
|
||||
inboxId:
|
||||
name: inboxId
|
||||
in: path
|
||||
description: "Numeric ID of the local tool"
|
||||
required: true
|
||||
schema:
|
||||
$ref: "#/components/schemas/ID"
|
||||
|
||||
quickFilter:
|
||||
name: quickFilter
|
||||
in: query
|
||||
|
@ -1669,6 +1847,32 @@ components:
|
|||
password:
|
||||
type: string
|
||||
|
||||
CreateAcceptedRequestInboxEntryRequest:
|
||||
description: "Create accepted connection request inbox entry request"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
email:
|
||||
$ref: "#/components/schemas/Email"
|
||||
authkey:
|
||||
$ref: "#/components/schemas/AuthKeyRaw"
|
||||
url:
|
||||
type: string
|
||||
reflected_user_id:
|
||||
$ref: "#/components/schemas/ID"
|
||||
connectorName:
|
||||
$ref: "#/components/schemas/LocalToolConnector"
|
||||
cerebrateURL:
|
||||
type: string
|
||||
local_tool_id:
|
||||
$ref: "#/components/schemas/ID"
|
||||
remote_tool_id:
|
||||
$ref: "#/components/schemas/ID"
|
||||
tool_name:
|
||||
type: string
|
||||
|
||||
# SharingGroups
|
||||
CreateSharingGroupRequest:
|
||||
required: true
|
||||
|
@ -1834,6 +2038,34 @@ components:
|
|||
comment:
|
||||
$ref: "#/components/schemas/AuthKeyComment"
|
||||
|
||||
# LocalTools
|
||||
CreateLocalToolConnectionRequest:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
$ref: "#/components/schemas/LocalToolName"
|
||||
connector:
|
||||
$ref: "#/components/schemas/LocalToolConnector"
|
||||
settings:
|
||||
$ref: "#/components/schemas/LocalToolSettings"
|
||||
description:
|
||||
$ref: "#/components/schemas/LocalToolDescription"
|
||||
exposed:
|
||||
$ref: "#/components/schemas/LocalToolIsExposed"
|
||||
|
||||
IssueLocalToolConnectionRequest:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
local_tool_id:
|
||||
type: integer
|
||||
responses:
|
||||
# Individuals
|
||||
IndividualResponse:
|
||||
|
@ -1910,6 +2142,34 @@ components:
|
|||
schema:
|
||||
$ref: "#/components/schemas/IncomingConnectionRequestInbox"
|
||||
|
||||
AcceptedRequestInboxResponse:
|
||||
description: "Accepted connection request inbox response"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/IncomingConnectionRequestInbox"
|
||||
|
||||
ProcessInboxResponse:
|
||||
description: "Process inbox response"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
success:
|
||||
type: boolean
|
||||
message:
|
||||
type: string
|
||||
example: "Interconnection for `http://cerebrate.remote`'s finalised"
|
||||
errors:
|
||||
type: string
|
||||
nullable: true
|
||||
|
||||
InboxListResponse:
|
||||
description: "Inbox list response"
|
||||
content:
|
||||
|
@ -1918,7 +2178,7 @@ components:
|
|||
$ref: "#/components/schemas/InboxList"
|
||||
|
||||
CreateUserRegistrationInboxEntryResponse:
|
||||
description: "Inbox response"
|
||||
description: "Create user registration inbox response"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
|
@ -1942,6 +2202,31 @@ components:
|
|||
type: object
|
||||
# TODO: describe
|
||||
|
||||
AcceptedRequestInboxEntryResponse:
|
||||
description: "Accepted request inbox response"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/AcceptedRequestInbox"
|
||||
- properties:
|
||||
local_tool_connector_name:
|
||||
type: string
|
||||
nullable: true
|
||||
success:
|
||||
type: boolean
|
||||
message:
|
||||
type: string
|
||||
example: "User account creation requested. Please wait for an admin to approve your account."
|
||||
errors:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
# TODO: describe
|
||||
|
||||
# SharingGroups
|
||||
SharingGroupResponse:
|
||||
description: "Sharing group response"
|
||||
|
@ -2029,6 +2314,21 @@ components:
|
|||
schema:
|
||||
$ref: "#/components/schemas/AuthKeyList"
|
||||
|
||||
# LocalTools
|
||||
LocalToolResponse:
|
||||
description: "Local tool response"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/LocalTool"
|
||||
|
||||
GetExposedBroodToolsResponse:
|
||||
description: "Local tool response"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/LocalToolList"
|
||||
|
||||
# Errors
|
||||
ApiErrorResponse:
|
||||
description: "Unexpected API error"
|
||||
|
|
|
@ -167,7 +167,7 @@ function saveSetting(statusNode, settingName, settingValue) {
|
|||
}
|
||||
|
||||
function openSaveBookmarkModal(bookmark_url = '') {
|
||||
const url = '/user-settings/saveBookmark';
|
||||
const url = '/user-settings/saveMyBookmark';
|
||||
UI.submissionModal(url).then(([modalFactory, ajaxApi]) => {
|
||||
const $input = modalFactory.$modal.find('input[name="bookmark_url"]')
|
||||
$input.val(bookmark_url)
|
||||
|
@ -175,7 +175,7 @@ function openSaveBookmarkModal(bookmark_url = '') {
|
|||
}
|
||||
|
||||
function deleteBookmark(bookmark, forSidebar=false) {
|
||||
const url = '/user-settings/deleteBookmark'
|
||||
const url = '/user-settings/deleteMyBookmark'
|
||||
AJAXApi.quickFetchAndPostForm(url, {
|
||||
bookmark_name: bookmark.name,
|
||||
bookmark_url: bookmark.url,
|
||||
|
@ -183,7 +183,7 @@ function deleteBookmark(bookmark, forSidebar=false) {
|
|||
provideFeedback: true,
|
||||
statusNode: $('.bookmark-table-container'),
|
||||
}).then((apiResult) => {
|
||||
const url = `/userSettings/getBookmarks/${forSidebar ? '1' : '0'}`
|
||||
const url = `/userSettings/getMyBookmarks/${forSidebar ? '1' : '0'}`
|
||||
UI.reload(url, $('.bookmark-table-container').parent())
|
||||
const theToast = UI.toast({
|
||||
variant: 'success',
|
||||
|
@ -191,7 +191,7 @@ function deleteBookmark(bookmark, forSidebar=false) {
|
|||
bodyHtml: $('<div/>').append(
|
||||
$('<span/>').text('Cancel deletion operation.'),
|
||||
$('<button/>').addClass(['btn', 'btn-primary', 'btn-sm', 'ms-3']).text('Restore bookmark').click(function () {
|
||||
const urlRestore = '/user-settings/saveBookmark'
|
||||
const urlRestore = '/user-settings/saveMyBookmark'
|
||||
AJAXApi.quickFetchAndPostForm(urlRestore, {
|
||||
bookmark_label: bookmark.label,
|
||||
bookmark_name: bookmark.name,
|
||||
|
@ -200,7 +200,7 @@ function deleteBookmark(bookmark, forSidebar=false) {
|
|||
provideFeedback: true,
|
||||
statusNode: $('.bookmark-table-container')
|
||||
}).then(() => {
|
||||
const url = `/userSettings/getBookmarks/${forSidebar ? '1' : '0'}`
|
||||
const url = `/userSettings/getMyBookmarks/${forSidebar ? '1' : '0'}`
|
||||
UI.reload(url, $('.bookmark-table-container').parent())
|
||||
})
|
||||
}),
|
||||
|
@ -297,7 +297,7 @@ $(document).ready(() => {
|
|||
$sidebar.addClass('expanded')
|
||||
}
|
||||
const settingName = 'ui.sidebar.expanded';
|
||||
const url = `/user-settings/setSetting/${settingName}`
|
||||
const url = `/user-settings/setMySetting/${settingName}`
|
||||
AJAXApi.quickFetchAndPostForm(url, {
|
||||
value: expanded ? 0 : 1
|
||||
}, { provideFeedback: false})
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
function mergeAndSaveSettings(table_setting_id, newTableSettings, automaticFeedback=true) {
|
||||
const settingName = 'ui.table_setting'
|
||||
const urlGet = `/user-settings/getSettingByName/${settingName}`
|
||||
const urlGet = `/user-settings/getMySettingByName/${settingName}`
|
||||
return AJAXApi.quickFetchJSON(urlGet).then(tableSettings => {
|
||||
tableSettings = JSON.parse(tableSettings.value)
|
||||
newTableSettings = mergeNewTableSettingsIntoOld(table_setting_id, tableSettings, newTableSettings)
|
||||
|
@ -17,8 +17,8 @@ function mergeNewTableSettingsIntoOld(table_setting_id, oldTableSettings, newTab
|
|||
return tableSettings
|
||||
}
|
||||
|
||||
function saveTableSetting(settingName, newTableSettings, automaticFeedback=true) {
|
||||
const urlSet = `/user-settings/setSetting/${settingName}`
|
||||
function saveTableSetting(settingName, newTableSettings) {
|
||||
const urlSet = `/user-settings/setMySetting/${settingName}`
|
||||
return AJAXApi.quickFetchAndPostForm(urlSet, {
|
||||
value: JSON.stringify(newTableSettings)
|
||||
}, {
|
||||
|
|
Loading…
Reference in New Issue