From 6e4dc3a6cda038ffadad183f876f13ce68af0b9d Mon Sep 17 00:00:00 2001 From: Luciano Righetti Date: Thu, 20 Jan 2022 16:23:48 +0100 Subject: [PATCH 01/73] add: github action test workflow --- .github/workflows/test.yml | 53 ++++++++++++++++++++++++++++++++++++ config/app_local.example.php | 10 +++---- 2 files changed, 57 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..09c18d4 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,53 @@ +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 + + - name: Run tests + run: | + composer install --no-progress --no-interaction + composer test + env: + DEBUG: true diff --git a/config/app_local.example.php b/config/app_local.example.php index 1ec0f4a..ed0c5ec 100644 --- a/config/app_local.example.php +++ b/config/app_local.example.php @@ -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', ], ], From b2ffc822e97f817754b26137edf97a7b137d6ad1 Mon Sep 17 00:00:00 2001 From: Luciano Righetti Date: Thu, 20 Jan 2022 16:29:31 +0100 Subject: [PATCH 02/73] fix: run wiremock in background --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index f51bc6f..6d48735 100644 --- a/composer.json +++ b/composer.json @@ -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" ] From 8d4eabf255cdb863d2d166201cbba77849d99320 Mon Sep 17 00:00:00 2001 From: Luciano Righetti Date: Thu, 20 Jan 2022 16:33:28 +0100 Subject: [PATCH 03/73] chg: minor improv --- .github/workflows/test.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 09c18d4..a2c763a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -44,10 +44,9 @@ jobs: 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 install --no-progress --no-interaction - composer test + run: composer test env: DEBUG: true From 932a28288d8cf5e4baf6108adafd7093df569133 Mon Sep 17 00:00:00 2001 From: iglocska Date: Fri, 21 Jan 2022 13:41:29 +0100 Subject: [PATCH 04/73] new: [CRUD] added some new useful features - afterFind for the edit functions to make last minute decisions on the modification after already having loaded the data to be modified - moved the field restrictions to be able to pass it to the view - try/catch for bulk deletions. A single failure in the beforeSave call will no longer block the entire saving process --- src/Controller/Component/CRUDComponent.php | 24 +++++++++++++--------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/Controller/Component/CRUDComponent.php b/src/Controller/Component/CRUDComponent.php index d1c78fc..47dc5bf 100644 --- a/src/Controller/Component/CRUDComponent.php +++ b/src/Controller/Component/CRUDComponent.php @@ -157,9 +157,6 @@ class CRUDComponent extends Component { $this->getMetaTemplates(); $data = $this->Table->newEmptyEntity(); - if (!empty($params['fields'])) { - $this->Controller->set('fields', $params['fields']); - } if ($this->request->is('post')) { $patchEntityParams = [ 'associated' => [], @@ -223,6 +220,9 @@ class CRUDComponent extends Component } } } + if (!empty($params['fields'])) { + $this->Controller->set('fields', $params['fields']); + } $this->Controller->entity = $data; $this->Controller->set('entity', $data); } @@ -295,21 +295,18 @@ class CRUDComponent extends Component $data->where($params['conditions']); } $data = $data->first(); + if (isset($params['afterFind'])) { + $data = $params['afterFind']($data, $params); + } if (empty($data)) { throw new NotFoundException(__('Invalid {0}.', $this->ObjectAlias)); } $data = $this->getMetaFields($id, $data); - if (!empty($params['fields'])) { - $this->Controller->set('fields', $params['fields']); - } if ($this->request->is(['post', 'put'])) { $patchEntityParams = [ 'associated' => [] ]; $input = $this->__massageInput($params); - if (!empty($params['fields'])) { - $patchEntityParams['fields'] = $params['fields']; - } $data = $this->Table->patchEntity($data, $input, $patchEntityParams); if (isset($params['beforeSave'])) { $data = $params['beforeSave']($data); @@ -352,6 +349,9 @@ class CRUDComponent extends Component } } } + if (!empty($params['fields'])) { + $this->Controller->set('fields', $params['fields']); + } $this->Controller->entity = $data; $this->Controller->set('entity', $data); } @@ -469,7 +469,11 @@ class CRUDComponent extends Component } $data = $data->first(); if (isset($params['beforeSave'])) { - $data = $params['beforeSave']($data); + try { + $data = $params['beforeSave']($data); + } catch (Exception $e) { + $data = false; + } } if (!empty($data)) { $success = $this->Table->delete($data); From b556f7f22a860d5050481417634c6867668d51f9 Mon Sep 17 00:00:00 2001 From: Andras Iklody Date: Fri, 21 Jan 2022 14:39:43 +0100 Subject: [PATCH 05/73] Update VERSION.json --- src/VERSION.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/VERSION.json b/src/VERSION.json index 5ddd625..f790db8 100644 --- a/src/VERSION.json +++ b/src/VERSION.json @@ -1,4 +1,4 @@ { - "version": "0.1", + "version": "1.3", "application": "Cerebrate" } From 6479fd6183128c40ae6d16d6c7a7865ea0511025 Mon Sep 17 00:00:00 2001 From: Luciano Righetti Date: Mon, 24 Jan 2022 11:43:40 +0100 Subject: [PATCH 06/73] chg: clean test --- .../Api/Inbox/CreateInboxEntryApiTest.php | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/tests/TestCase/Api/Inbox/CreateInboxEntryApiTest.php b/tests/TestCase/Api/Inbox/CreateInboxEntryApiTest.php index 8434d62..a1fd077 100644 --- a/tests/TestCase/Api/Inbox/CreateInboxEntryApiTest.php +++ b/tests/TestCase/Api/Inbox/CreateInboxEntryApiTest.php @@ -7,6 +7,7 @@ namespace App\Test\TestCase\Api\Users; 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 From 57e2c753523ec792bbdd8974c58a5d3bf0969bf8 Mon Sep 17 00:00:00 2001 From: iglocska Date: Tue, 25 Jan 2022 11:34:22 +0100 Subject: [PATCH 07/73] fix: [users] role based action filtering added - to avoid annoying clickable, but blocked actions for og admins --- src/Controller/UsersController.php | 57 ++++++++++++++++++++++++++---- templates/Users/index.php | 40 +++++++++++++++++++-- 2 files changed, 88 insertions(+), 9 deletions(-) diff --git a/src/Controller/UsersController.php b/src/Controller/UsersController.php index 9ffb2fe..6fa1067 100644 --- a/src/Controller/UsersController.php +++ b/src/Controller/UsersController.php @@ -33,16 +33,28 @@ 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')->order(['name' => 'asc'])->all()->toArray(); + } + $this->CRUD->add([ - 'beforeSave' => function($data) use ($currentUser) { + 'beforeSave' => function($data) use ($currentUser, $validRoles) { 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 +77,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'] ]), @@ -98,6 +108,10 @@ 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(); + } if (empty($id)) { $id = $currentUser['id']; } else { @@ -128,6 +142,20 @@ 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 (!$currentUser['role']['perm_admin'] && $currentUser['role']['perm_org_admin']) { + if (!in_array($data['role_id'], array_keys($validRoles))) { + throw new MethodNotAllowedException(__('You cannot edit the given privileged user.')); + } + } + return $data; + }; + } } $this->CRUD->edit($id, $params); $responsePayload = $this->CRUD->getResponsePayload(); @@ -135,9 +163,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'] ]), @@ -161,6 +187,23 @@ class UsersController extends AppController public function delete($id) { + $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)) { diff --git a/templates/Users/index.php b/templates/Users/index.php index 1ff36bf..21afdc5 100644 --- a/templates/Users/index.php +++ b/templates/Users/index.php @@ -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; + } + ] ], ] ] From a7e2fb2ea7454eb72299662b267374c9f09c9bf8 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Thu, 20 Jan 2022 14:24:03 +0100 Subject: [PATCH 08/73] chg: [auditlog:index] Break text in changed column --- templates/AuditLogs/index.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/templates/AuditLogs/index.php b/templates/AuditLogs/index.php index c4657cb..8b7b70e 100644 --- a/templates/AuditLogs/index.php +++ b/templates/AuditLogs/index.php @@ -52,7 +52,8 @@ echo $this->element('genericElements/IndexTable/index_table', [ 'name' => __('Changed'), 'sort' => 'changed', 'data_path' => 'changed', - 'element' => 'json' + 'element' => 'json', + 'class' => 'text-break' ], ], 'title' => __('Logs'), From 6005552e76037fbf14a45f948367d901d6c10360 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Thu, 20 Jan 2022 14:37:19 +0100 Subject: [PATCH 09/73] fix: [genericElements:tags] List tags when editing an entity --- templates/element/genericElements/Form/Fields/tagsField.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/element/genericElements/Form/Fields/tagsField.php b/templates/element/genericElements/Form/Fields/tagsField.php index 1dcbc39..da3114a 100644 --- a/templates/element/genericElements/Form/Fields/tagsField.php +++ b/templates/element/genericElements/Form/Fields/tagsField.php @@ -1,6 +1,6 @@ Tag->tags($entity['tags'], [ - 'allTags' => [], + 'allTags' => $allTags ?? [], 'picker' => true, 'editable' => true, ]); From dc2bfcb6b2a0cc7d964f951b8e8310407d715f3e Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Fri, 21 Jan 2022 09:35:55 +0100 Subject: [PATCH 10/73] fix: [components:CRUD] Support of controller's paginate public variable --- src/Controller/Component/CRUDComponent.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Controller/Component/CRUDComponent.php b/src/Controller/Component/CRUDComponent.php index d1c78fc..316168e 100644 --- a/src/Controller/Component/CRUDComponent.php +++ b/src/Controller/Component/CRUDComponent.php @@ -88,7 +88,7 @@ class CRUDComponent extends Component $this->Controller->restResponsePayload = $this->RestResponse->viewData($data, 'json'); } else { $this->Controller->loadComponent('Paginator'); - $data = $this->Controller->Paginator->paginate($query); + $data = $this->Controller->Paginator->paginate($query, $this->Controller->paginate ?? []); if (isset($options['afterFind'])) { $function = $options['afterFind']; if (is_callable($options['afterFind'])) { From 7d227a43875ac13c799023d3939614ec48527d4d Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Fri, 21 Jan 2022 09:48:53 +0100 Subject: [PATCH 11/73] chg: [inbox:index] Sort messages by created datetime --- src/Controller/InboxController.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Controller/InboxController.php b/src/Controller/InboxController.php index 6532e01..0d07fb9 100644 --- a/src/Controller/InboxController.php +++ b/src/Controller/InboxController.php @@ -20,6 +20,12 @@ class InboxController extends AppController public $quickFilterFields = ['scope', 'action', ['title' => true], ['comment' => true]]; public $containFields = ['Users']; + public $paginate = [ + 'order' => [ + 'Inbox.created' => 'desc' + ] + ]; + public function beforeFilter(EventInterface $event) { parent::beforeFilter($event); From 4f8b663b87d6f89a35ec580a9d68df25c7219d71 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Mon, 24 Jan 2022 16:12:46 +0100 Subject: [PATCH 12/73] chg: [localtTools:connectionRequest] Provide more info on exception --- src/Controller/LocalToolsController.php | 12 +++++++++++- templates/LocalTools/connection_request.php | 1 - 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Controller/LocalToolsController.php b/src/Controller/LocalToolsController.php index 12d9d62..baf8995 100644 --- a/src/Controller/LocalToolsController.php +++ b/src/Controller/LocalToolsController.php @@ -304,7 +304,17 @@ class LocalToolsController extends AppController throw new MethodNotAllowedException(__('No local tool ID supplied.')); } $params['local_tool_id'] = $postParams['local_tool_id']; - $encodingResult = $this->LocalTools->encodeConnection($params); + try { + $encodingResult = $this->LocalTools->encodeConnection($params); + } catch (\Exception $e) { + $encodingResult = [ + 'inboxResult' => [ + 'success' => false, + 'message' => __('Error while trying to encode connection'), + 'errors' => [$e->getMessage()], + ], + ]; + } $inboxResult = $encodingResult['inboxResult']; if ($inboxResult['success']) { if ($this->ParamHandler->isRest()) { diff --git a/templates/LocalTools/connection_request.php b/templates/LocalTools/connection_request.php index 0b8a52e..035af55 100644 --- a/templates/LocalTools/connection_request.php +++ b/templates/LocalTools/connection_request.php @@ -29,4 +29,3 @@ ] ]); ?> - From eef09f44c43505935dabbbf9942b457d96e81e10 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Mon, 24 Jan 2022 16:35:42 +0100 Subject: [PATCH 13/73] chg: [brood:connectionTest] Correctly handles network exceptions --- src/Model/Table/BroodsTable.php | 10 +++++++++- webroot/js/main.js | 4 +++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Model/Table/BroodsTable.php b/src/Model/Table/BroodsTable.php index b0d3dca..9798260 100644 --- a/src/Model/Table/BroodsTable.php +++ b/src/Model/Table/BroodsTable.php @@ -9,6 +9,7 @@ use Cake\Core\Configure; use Cake\Http\Client; use Cake\Http\Client\Response; use Cake\Http\Exception\NotFoundException; +use Cake\Http\Client\Exception\NetworkException; use Cake\ORM\TableRegistry; use Cake\Error\Debugger; @@ -69,7 +70,14 @@ class BroodsTable extends AppTable { $brood = $this->find()->where(['id' => $id])->first(); $start = microtime(true); - $response = $this->HTTPClientGET('/instance/status.json', $brood); + try { + $response = $this->HTTPClientGET('/instance/status.json', $brood); + } catch (NetworkException $e) { + return [ + 'error' => __('Could not query status'), + 'reason' => $e->getMessage(), + ]; + } $ping = ((int)(100 * (microtime(true) - $start))); $errors = [ 403 => [ diff --git a/webroot/js/main.js b/webroot/js/main.js index c4383ff..fc2a4dc 100644 --- a/webroot/js/main.js +++ b/webroot/js/main.js @@ -55,8 +55,10 @@ function attachTestConnectionResultHtml(result, $container) { $testResultDiv.append(getKVHtml('Internal error', result, ['text-danger fw-bold'])) } else { if (result['error']) { + if (result['ping']) { + $testResultDiv.append('Status', 'OK', ['text-danger'], `${result['ping']} ms`); + } $testResultDiv.append( - getKVHtml('Status', 'OK', ['text-danger'], `${result['ping']} ms`), getKVHtml('Status', `Error: ${result['error']}`, ['text-danger']), getKVHtml('Reason', result['reason'], ['text-danger']) ) From 7faca9452028d97d0443b818e173737c7a4b07dd Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Mon, 24 Jan 2022 16:48:58 +0100 Subject: [PATCH 14/73] chg: [outboxProcessors:broods] Provide errors while trying to re-send a message --- libraries/default/OutboxProcessors/BroodsOutboxProcessor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/default/OutboxProcessors/BroodsOutboxProcessor.php b/libraries/default/OutboxProcessors/BroodsOutboxProcessor.php index 946e548..9019857 100644 --- a/libraries/default/OutboxProcessors/BroodsOutboxProcessor.php +++ b/libraries/default/OutboxProcessors/BroodsOutboxProcessor.php @@ -126,7 +126,7 @@ class ResendFailedMessageProcessor extends BroodsOutboxProcessor implements Gene [], $success, $success ? $messageSuccess : $messageFail, - [] + $jsonReply['errors'] ?? [] ); } From 88313679a6fb494f06eb1adafe5d251521d7a0a6 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Mon, 24 Jan 2022 17:36:12 +0100 Subject: [PATCH 15/73] chg: [outboxProcessors:brood] Gracefully catch server errors on remote broods --- .../default/OutboxProcessors/BroodsOutboxProcessor.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/libraries/default/OutboxProcessors/BroodsOutboxProcessor.php b/libraries/default/OutboxProcessors/BroodsOutboxProcessor.php index 9019857..9bf79ae 100644 --- a/libraries/default/OutboxProcessors/BroodsOutboxProcessor.php +++ b/libraries/default/OutboxProcessors/BroodsOutboxProcessor.php @@ -110,6 +110,14 @@ class ResendFailedMessageProcessor extends BroodsOutboxProcessor implements Gene $dataSent = $outboxRequest->data['sent']; $response = $this->Broods->sendRequest($brood, $url, true, $dataSent); $jsonReply = $response->getJson(); + if (is_null($jsonReply)) { + $jsonReply = [ + 'success' => false, + 'errors' => [ + __('Brood returned an invalid JSON.') + ] + ]; + } $success = !empty($jsonReply['success']); $messageSuccess = __('Message successfully sent to `{0}`', $brood->name); $messageFail = __('Could not send message to `{0}`.', $brood->name); From e05bf612515f05e49c72eb1349ef59090864127c Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Mon, 24 Jan 2022 17:37:32 +0100 Subject: [PATCH 16/73] chg: [inbox:createEntry] Checks for remote back connection is more flexible Handle the case of trailing slash --- src/Model/Table/InboxTable.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Model/Table/InboxTable.php b/src/Model/Table/InboxTable.php index 18faf47..a2c0e2c 100644 --- a/src/Model/Table/InboxTable.php +++ b/src/Model/Table/InboxTable.php @@ -62,8 +62,11 @@ class InboxTable extends AppTable $this->Broods = \Cake\ORM\TableRegistry::getTableLocator()->get('Broods'); $this->Individuals = \Cake\ORM\TableRegistry::getTableLocator()->get('Individuals'); $errors = []; + $originUrl = trim($entryData['origin'], '/'); $brood = $this->Broods->find() - ->where(['url' => $entryData['origin']]) + ->where([ + 'url IN' => [$originUrl, "{$originUrl}/"] + ]) ->first(); if (empty($brood)) { $errors[] = __('Unkown brood `{0}`', $entryData['data']['cerebrateURL']); From 578eacfd891b8d7a221f28b65dfebc423aae267e Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Tue, 25 Jan 2022 14:03:48 +0100 Subject: [PATCH 17/73] fix: [templates:common] Removed extra closing tag --- templates/Common/index.php | 1 - 1 file changed, 1 deletion(-) diff --git a/templates/Common/index.php b/templates/Common/index.php index 1f0a5f6..20e795e 100644 --- a/templates/Common/index.php +++ b/templates/Common/index.php @@ -1,4 +1,3 @@ element('genericElements/IndexTable/index_table', $data); - echo ''; ?> From 44913c5ed77a9fe7d2921808e70c6c3a3053d29e Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Tue, 25 Jan 2022 15:27:34 +0100 Subject: [PATCH 18/73] fix: [users:settings] Allow admin to see account settings of other users --- src/Controller/UsersController.php | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Controller/UsersController.php b/src/Controller/UsersController.php index 9ffb2fe..74aec0d 100644 --- a/src/Controller/UsersController.php +++ b/src/Controller/UsersController.php @@ -218,10 +218,18 @@ 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); + $currentUser = $this->ACL->getUser(); + if (empty($currentUser['role']['perm_admin'])) { + $user = $currentUser; + } else { + $user = $this->Users->get($user_id, [ + 'contain' => ['Roles', 'Individuals' => 'Organisations'] + ]); + } + $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']); From 55782af52b8536878384ed6f59edaac7e53699b3 Mon Sep 17 00:00:00 2001 From: iglocska Date: Tue, 25 Jan 2022 15:58:31 +0100 Subject: [PATCH 19/73] fix: [users] add - fixed role selection --- src/Controller/UsersController.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Controller/UsersController.php b/src/Controller/UsersController.php index 6fa1067..f03f329 100644 --- a/src/Controller/UsersController.php +++ b/src/Controller/UsersController.php @@ -45,6 +45,8 @@ class UsersController extends AppController $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(); } @@ -111,6 +113,8 @@ class UsersController extends AppController $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']; From 1086e41086f7184097e2cb70421e63710249118b Mon Sep 17 00:00:00 2001 From: iglocska Date: Tue, 25 Jan 2022 17:01:27 +0100 Subject: [PATCH 20/73] fix: [modified] saving fixed for sync captures - set the field as not dirty to force an update - stops the exceptions thrown on pulling these objects in --- src/Model/Table/IndividualsTable.php | 1 + src/Model/Table/OrganisationsTable.php | 1 + src/Model/Table/SharingGroupsTable.php | 1 + 3 files changed, 3 insertions(+) diff --git a/src/Model/Table/IndividualsTable.php b/src/Model/Table/IndividualsTable.php index 70f63fd..16a3da2 100644 --- a/src/Model/Table/IndividualsTable.php +++ b/src/Model/Table/IndividualsTable.php @@ -66,6 +66,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; diff --git a/src/Model/Table/OrganisationsTable.php b/src/Model/Table/OrganisationsTable.php index c56720e..8cd7dec 100644 --- a/src/Model/Table/OrganisationsTable.php +++ b/src/Model/Table/OrganisationsTable.php @@ -71,6 +71,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; diff --git a/src/Model/Table/SharingGroupsTable.php b/src/Model/Table/SharingGroupsTable.php index ff39220..e976e8c 100644 --- a/src/Model/Table/SharingGroupsTable.php +++ b/src/Model/Table/SharingGroupsTable.php @@ -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; From 19c81b7c114d875453fe202b7d2c7e882be21aec Mon Sep 17 00:00:00 2001 From: iglocska Date: Tue, 25 Jan 2022 17:09:29 +0100 Subject: [PATCH 21/73] fix: [Sharing groups] UUID and owner org shouldn't be editable --- src/Controller/SharingGroupsController.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Controller/SharingGroupsController.php b/src/Controller/SharingGroupsController.php index 4e98df8..764f0e6 100644 --- a/src/Controller/SharingGroupsController.php +++ b/src/Controller/SharingGroupsController.php @@ -71,6 +71,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)) { From 5da61f15dd9b399a9f464bb6858e05ec2569800f Mon Sep 17 00:00:00 2001 From: Luciano Righetti Date: Tue, 25 Jan 2022 18:01:51 +0100 Subject: [PATCH 22/73] add: initial version of cerebrate->cerebrate misp interconnection --- tests/Fixture/LocalToolsFixture.php | 20 ++ tests/Fixture/OrganisationsFixture.php | 7 +- .../Fixture/RemoteToolConnectionsFixture.php | 20 ++ tests/Helper/ApiTestTrait.php | 5 +- .../Api/Broods/TestBroodConnectionApiTest.php | 20 +- .../LocalTools/MispInterConnectionTest.php | 312 ++++++++++++++++++ 6 files changed, 372 insertions(+), 12 deletions(-) create mode 100644 tests/Fixture/LocalToolsFixture.php create mode 100644 tests/Fixture/RemoteToolConnectionsFixture.php create mode 100644 tests/TestCase/Api/LocalTools/MispInterConnectionTest.php diff --git a/tests/Fixture/LocalToolsFixture.php b/tests/Fixture/LocalToolsFixture.php new file mode 100644 index 0000000..bdc8334 --- /dev/null +++ b/tests/Fixture/LocalToolsFixture.php @@ -0,0 +1,20 @@ +records = []; + parent::init(); + } +} diff --git a/tests/Fixture/OrganisationsFixture.php b/tests/Fixture/OrganisationsFixture.php index 8531d0c..9e62495 100644 --- a/tests/Fixture/OrganisationsFixture.php +++ b/tests/Fixture/OrganisationsFixture.php @@ -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', diff --git a/tests/Fixture/RemoteToolConnectionsFixture.php b/tests/Fixture/RemoteToolConnectionsFixture.php new file mode 100644 index 0000000..9246836 --- /dev/null +++ b/tests/Fixture/RemoteToolConnectionsFixture.php @@ -0,0 +1,20 @@ +records = []; + parent::init(); + } +} diff --git a/tests/Helper/ApiTestTrait.php b/tests/Helper/ApiTestTrait.php index a55268c..4b3986f 100644 --- a/tests/Helper/ApiTestTrait.php +++ b/tests/Helper/ApiTestTrait.php @@ -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); } diff --git a/tests/TestCase/Api/Broods/TestBroodConnectionApiTest.php b/tests/TestCase/Api/Broods/TestBroodConnectionApiTest.php index ee1117f..abc8be8 100644 --- a/tests/TestCase/Api/Broods/TestBroodConnectionApiTest.php +++ b/tests/TestCase/Api/Broods/TestBroodConnectionApiTest.php @@ -52,17 +52,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 + ] ] ] - ]))) + ))) ); } } diff --git a/tests/TestCase/Api/LocalTools/MispInterConnectionTest.php b/tests/TestCase/Api/LocalTools/MispInterConnectionTest.php new file mode 100644 index 0000000..e79431b --- /dev/null +++ b/tests/TestCase/Api/LocalTools/MispInterConnectionTest.php @@ -0,0 +1,312 @@ +skipOpenApiValidations(); + $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']); + $connection = $this->getJsonResponseAsArray(); + // print_r($connection); + + // 2. Create a new Brood (connect to a remote Cerebrate instance) + $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(); + // print_r($brood); + + // 3. 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(); + // print_r($tools); + + // 4. 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', OrganisationsFixture::ORGANISATION_A_UUID); + $this->mockMispCreateSyncUser( + 'MISP_LOCAL', + self::LOCAL_MISP_ADMIN_USER_AUTHKEY, + self::REMOTE_MISP_SYNC_USER_ID, + self::REMOTE_MISP_SYNC_USER_EMAIL, + self::REMOTE_MISP_SYNC_USER_AUTHKEY + ); + $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(); + // $connectionRequest = $this->getJsonResponseAsArray(); + // print_r($connectionRequest); + + // 5. Remote Cerebrate accepts the connection request + $this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY); // TODO: use the Cerebrate admin authkey + $this->post( + '/inbox/createEntry/LocalTool/AcceptedRequest', + [ + 'email' => self::REMOTE_MISP_SYNC_USER_EMAIL, + 'authkey' => self::REMOTE_MISP_SYNC_USER_AUTHKEY, + 'url' => self::LOCAL_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_LOCAL' + ] + ); + $this->assertResponseOk(); + $acceptRequest = $this->getJsonResponseAsArray(); + // print_r($acceptRequest); + + // 6. 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); + $stub = $this->mockAddMispServer( + 'MISP_LOCAL', + self::LOCAL_MISP_ADMIN_USER_AUTHKEY, + [ + 'authkey' => self::REMOTE_MISP_SYNC_USER_AUTHKEY, + 'url' => self::LOCAL_MISP_INSTANCE_URL, + 'name' => 'MISP_LOCAL', + 'remote_org_id' => 1 + ] + ); + $this->post(sprintf('/inbox/process/%s', $acceptRequest['data']['id'])); + // $finalizeConnection = $this->getJsonResponseAsArray(); + // print_r($finalizeConnection); + $this->assertResponseOk(); + $this->assertResponseContains('"success": true'); + } + + 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_REMOTE", + "connector" => "MispConnector", + "description" => "Remote MISP instance" + ] + ] + ))) + ); + } + + private function mockMispViewOrganisationByUuid(string $instance, string $orgUuid): \WireMock\Stubbing\StubMapping + { + return $this->getWireMock()->stubFor( + WireMock::get(WireMock::urlEqualTo("/$instance/organisations/view/$orgUuid/limit:50")) + ->willReturn(WireMock::aResponse() + ->withHeader('Content-Type', 'application/json') + ->withBody((string)json_encode( + [ + "Organisation" => [ + "id" => 1, + "name" => "Local Organisation", + "uuid" => $orgUuid, + "local" => true + ] + ] + ))) + ); + } + + private function mockMispCreateSyncUser(string $instance, string $mispAuthkey, int $userId, string $email, string $authkey): \WireMock\Stubbing\StubMapping + { + 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, + "authkey" => $authkey, + "email" => $email + ] + ] + ))) + ); + } + + 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() + ] + ] + ))) + ); + } +} From d18471ba9591cdf7f09bd4753af435b150058363 Mon Sep 17 00:00:00 2001 From: Luciano Righetti Date: Tue, 25 Jan 2022 18:02:41 +0100 Subject: [PATCH 23/73] fix: failing when request is empty json object --- src/Controller/Component/ParamHandlerComponent.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Controller/Component/ParamHandlerComponent.php b/src/Controller/Component/ParamHandlerComponent.php index e52e5f8..92260fd 100644 --- a/src/Controller/Component/ParamHandlerComponent.php +++ b/src/Controller/Component/ParamHandlerComponent.php @@ -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; From 4c60fa00177066d5558bbf479b2c49bf6e51a7dd Mon Sep 17 00:00:00 2001 From: Luciano Righetti Date: Wed, 26 Jan 2022 11:00:48 +0100 Subject: [PATCH 24/73] chg: tighten tests assertions --- .../LocalTools/MispInterConnectionTest.php | 153 ++++++++++++++---- 1 file changed, 121 insertions(+), 32 deletions(-) diff --git a/tests/TestCase/Api/LocalTools/MispInterConnectionTest.php b/tests/TestCase/Api/LocalTools/MispInterConnectionTest.php index e79431b..dea3ceb 100644 --- a/tests/TestCase/Api/LocalTools/MispInterConnectionTest.php +++ b/tests/TestCase/Api/LocalTools/MispInterConnectionTest.php @@ -8,6 +8,7 @@ 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; @@ -28,18 +29,19 @@ class MispInterConnectionTest extends TestCase 'app.RemoteToolConnections' ]; + /** 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_SYNC_USER_ID = 999; - private const LOCAL_MISP_SYNC_USER_AUTHKEY = '7f59533a2f792b389f18b086d88f6d7af02cba3e'; - private const LOCAL_MISP_SYNC_USER_EMAIL = 'sync@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'; - private const REMOTE_MISP_INSTANCE_URL = 'http://localhost:8080/MISP_REMOTE'; + + /** constants related to the remote MISP instance */ private const REMOTE_MISP_SYNC_USER_ID = 333; - private const REMOTE_MISP_SYNC_USER_AUTHKEY = '429f629abf98f7bf79e5a7f3a8fc694ca19ed357'; private const REMOTE_MISP_SYNC_USER_EMAIL = 'sync@misp.remote'; public function testInterConnectMispViaCerebrate(): void @@ -50,7 +52,9 @@ class MispInterConnectionTest extends TestCase $faker = \Faker\Factory::create(); - // 1. Create LocalTool connection to `MISP LOCAL` (local MISP instance) + /** + * 1. Create LocalTool connection to `MISP LOCAL` (local MISP instance) + */ $this->post( sprintf('%s/localTools/add', self::LOCAL_CEREBRATE_URL), [ @@ -67,13 +71,17 @@ class MispInterConnectionTest extends TestCase ); $this->assertResponseOk(); $this->assertDbRecordExists('LocalTools', ['name' => 'MISP_LOCAL']); - $connection = $this->getJsonResponseAsArray(); - // print_r($connection); - // 2. Create a new Brood (connect to a remote Cerebrate instance) + /** + * 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', [ @@ -91,9 +99,85 @@ class MispInterConnectionTest extends TestCase $this->assertResponseOk(); $this->assertDbRecordExists('Broods', ['uuid' => $LOCAL_BROOD_UUID]); $brood = $this->getJsonResponseAsArray(); - // print_r($brood); - // 3. Get remote Cerebrate exposed tools + /** + * 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'])); @@ -101,16 +185,22 @@ class MispInterConnectionTest extends TestCase $tools = $this->getJsonResponseAsArray(); // print_r($tools); - // 4. Issue a connection request to the remote MISP instance + /** + * 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', OrganisationsFixture::ORGANISATION_A_UUID); + $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, - self::REMOTE_MISP_SYNC_USER_AUTHKEY + self::REMOTE_MISP_SYNC_USER_EMAIL ); $this->mockCerebrateCreateMispIncommingConnectionRequest( 'CEREBRATE_REMOTE', @@ -126,16 +216,16 @@ class MispInterConnectionTest extends TestCase ] ); $this->assertResponseOk(); - // $connectionRequest = $this->getJsonResponseAsArray(); - // print_r($connectionRequest); - // 5. Remote Cerebrate accepts the connection request - $this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY); // TODO: use the Cerebrate admin authkey + /** + * 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_SYNC_USER_AUTHKEY, + 'authkey' => $remoteCerebrateAuthkey, 'url' => self::LOCAL_MISP_INSTANCE_URL, 'reflected_user_id' => self::REMOTE_MISP_SYNC_USER_ID, 'connectorName' => 'MispConnector', @@ -147,24 +237,23 @@ class MispInterConnectionTest extends TestCase ); $this->assertResponseOk(); $acceptRequest = $this->getJsonResponseAsArray(); - // print_r($acceptRequest); - // 6. Finalize the connection + /** + * 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); - $stub = $this->mockAddMispServer( + $this->mockAddMispServer( 'MISP_LOCAL', self::LOCAL_MISP_ADMIN_USER_AUTHKEY, [ - 'authkey' => self::REMOTE_MISP_SYNC_USER_AUTHKEY, + 'authkey' => $remoteCerebrateAuthkey, 'url' => self::LOCAL_MISP_INSTANCE_URL, 'name' => 'MISP_LOCAL', 'remote_org_id' => 1 ] ); $this->post(sprintf('/inbox/process/%s', $acceptRequest['data']['id'])); - // $finalizeConnection = $this->getJsonResponseAsArray(); - // print_r($finalizeConnection); $this->assertResponseOk(); $this->assertResponseContains('"success": true'); } @@ -189,17 +278,18 @@ class MispInterConnectionTest extends TestCase ); } - private function mockMispViewOrganisationByUuid(string $instance, string $orgUuid): \WireMock\Stubbing\StubMapping + 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" => 1, - "name" => "Local Organisation", + "id" => $orgId, + "name" => $instance . ' Organisation', "uuid" => $orgUuid, "local" => true ] @@ -208,7 +298,7 @@ class MispInterConnectionTest extends TestCase ); } - private function mockMispCreateSyncUser(string $instance, string $mispAuthkey, int $userId, string $email, string $authkey): \WireMock\Stubbing\StubMapping + private function mockMispCreateSyncUser(string $instance, string $mispAuthkey, int $userId, string $email): \WireMock\Stubbing\StubMapping { return $this->getWireMock()->stubFor( WireMock::post(WireMock::urlEqualTo("/$instance/admin/users/add")) @@ -219,7 +309,6 @@ class MispInterConnectionTest extends TestCase [ "User" => [ "id" => $userId, - "authkey" => $authkey, "email" => $email ] ] From f53b458103827c231b976ccb796ed669242caf68 Mon Sep 17 00:00:00 2001 From: Sami Mokaddem Date: Wed, 26 Jan 2022 12:11:44 +0100 Subject: [PATCH 25/73] fix: [userSettings] Allow admin to edit other user's settings --- src/Controller/Component/ACLComponent.php | 10 ++--- src/Controller/UserSettingsController.php | 49 ++++++++++++++++++----- src/Controller/UsersController.php | 7 +++- templates/Users/settings.php | 28 ++++++++++--- templates/element/Settings/category.php | 2 +- webroot/js/main.js | 12 +++--- webroot/js/table-settings.js | 4 +- 7 files changed, 80 insertions(+), 32 deletions(-) diff --git a/src/Controller/Component/ACLComponent.php b/src/Controller/Component/ACLComponent.php index fb51f49..8e50a67 100644 --- a/src/Controller/Component/ACLComponent.php +++ b/src/Controller/Component/ACLComponent.php @@ -188,12 +188,12 @@ class ACLComponent extends Component 'add' => ['*'], 'edit' => ['*'], 'delete' => ['*'], - 'getSettingByName' => ['*'], - 'setSetting' => ['*'], + 'getMySettingByName' => ['*'], + 'setMySetting' => ['*'], 'saveSetting' => ['*'], - 'getBookmarks' => ['*'], - 'saveBookmark' => ['*'], - 'deleteBookmark' => ['*'] + 'getMyBookmarks' => ['*'], + 'saveMyBookmark' => ['*'], + 'deleteMyBookmark' => ['*'] ], 'Api' => [ 'index' => ['*'] diff --git a/src/Controller/UserSettingsController.php b/src/Controller/UserSettingsController.php index d28f6ca..bced32b 100644 --- a/src/Controller/UserSettingsController.php +++ b/src/Controller/UserSettingsController.php @@ -124,7 +124,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 +146,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 +166,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 +190,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 +200,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 +215,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()); @@ -248,4 +255,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; + } } diff --git a/src/Controller/UsersController.php b/src/Controller/UsersController.php index 74aec0d..d484b5c 100644 --- a/src/Controller/UsersController.php +++ b/src/Controller/UsersController.php @@ -220,14 +220,17 @@ class UsersController extends AppController public function settings($user_id=false) { + $editingAnotherUser = false; $currentUser = $this->ACL->getUser(); - if (empty($currentUser['role']['perm_admin'])) { + if (empty($currentUser['role']['perm_admin']) || $user_id == $currentUser->id) { $user = $currentUser; } else { $user = $this->Users->get($user_id, [ - 'contain' => ['Roles', 'Individuals' => 'Organisations'] + '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']); diff --git a/templates/Users/settings.php b/templates/Users/settings.php index da014ca..4681def 100644 --- a/templates/Users/settings.php +++ b/templates/Users/settings.php @@ -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); +} ?>

@@ -43,7 +49,17 @@ echo $this->Html->script('settings'); username) ?> individual->full_name) ?> -
+ + Bootstrap->alert([ + 'text' => __('Currently editing the account setting of another user.'), + 'variant' => 'warning', + 'dismissible' => false + ]) + ?> + +
+
diff --git a/templates/element/Settings/category.php b/templates/element/Settings/category.php index 03e17e4..317dac0 100644 --- a/templates/element/Settings/category.php +++ b/templates/element/Settings/category.php @@ -57,6 +57,6 @@ $mainPanelHeight = 'calc(100vh - 42px - 1rem - 56px - 38px - 1rem)';
- + %s

', __('No settings available for this category')) ?>
\ No newline at end of file diff --git a/webroot/js/main.js b/webroot/js/main.js index fc2a4dc..420d1bf 100644 --- a/webroot/js/main.js +++ b/webroot/js/main.js @@ -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: $('
').append( $('').text('Cancel deletion operation.'), $('