From 2b476a1c03515c3cbc3555361c4ca17185619993 Mon Sep 17 00:00:00 2001 From: Jason Carvalho Date: Tue, 19 Apr 2022 10:37:55 +0100 Subject: [PATCH 1/8] Create Activity Controller on Activity dev branch --- .../src/Controller/ActivityController.php | 25 +++++++++++++++++++ .../Factory/ActivityControllerFactory.php | 21 ++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 module/apif-core/src/Controller/ActivityController.php create mode 100644 module/apif-core/src/Controller/Factory/ActivityControllerFactory.php diff --git a/module/apif-core/src/Controller/ActivityController.php b/module/apif-core/src/Controller/ActivityController.php new file mode 100644 index 0000000..194329c --- /dev/null +++ b/module/apif-core/src/Controller/ActivityController.php @@ -0,0 +1,25 @@ +_config = $config; + $this->_repository = $repository; + $this->_readLogger = $readLogger; + $this->_activityLog = $activityLog; + } +} \ No newline at end of file diff --git a/module/apif-core/src/Controller/Factory/ActivityControllerFactory.php b/module/apif-core/src/Controller/Factory/ActivityControllerFactory.php new file mode 100644 index 0000000..9910dc7 --- /dev/null +++ b/module/apif-core/src/Controller/Factory/ActivityControllerFactory.php @@ -0,0 +1,21 @@ +get("Config"); + $repository = $container->get(APIFCoreRepositoryInterface::class); + $readLogger = $container->get('apifReadLogger'); + $activityLog = $container->get(ActivityLogManagerInterface::class); + return new ActivityController($repository, $activityLog, $config, $readLogger); + } +} \ No newline at end of file From 33209c59951d0a90750d7607f7bc12637a0c24b8 Mon Sep 17 00:00:00 2001 From: Jason Carvalho Date: Tue, 19 Apr 2022 11:51:09 +0100 Subject: [PATCH 2/8] Authorization checking implemented for /activity/{dataset_id}/ method --- module/apif-core/config/module.config.php | 11 +++ .../src/Controller/ActivityController.php | 91 +++++++++++++++++++ 2 files changed, 102 insertions(+) diff --git a/module/apif-core/config/module.config.php b/module/apif-core/config/module.config.php index d34b6c8..f076e8c 100644 --- a/module/apif-core/config/module.config.php +++ b/module/apif-core/config/module.config.php @@ -10,6 +10,7 @@ namespace APIF\Core; +use APIF\Core\Controller\Factory\ActivityControllerFactory; use APIF\Core\Controller\Factory\BrowseControllerFactory; use APIF\Core\Controller\Factory\DatasetManagementControllerFactory; use APIF\Core\Controller\Factory\FileControllerFactory; @@ -98,6 +99,15 @@ ], ], ], + 'activity' => [ + 'type' => Segment::class, + 'options' => [ + 'route' => '/activity/:id', + 'defaults' => [ + 'controller' => Controller\ActivityController::class, + ], + ], + ], 'file' => [ 'type' => Segment::class, 'options' => [ @@ -180,6 +190,7 @@ Controller\ObjectController::class => ObjectControllerFactory::class, Controller\BrowseController::class => BrowseControllerFactory::class, Controller\FileController::class => FileControllerFactory::class, + Controller\ActivityController::class => ActivityControllerFactory::class, Controller\DatasetManagementController::class => DatasetManagementControllerFactory::class, Controller\PermissionsManagementController::class => PermissionsManagementControllerFactory::class, Controller\SchemaRetrievalController::class => SchemaRetrievalControllerFactory::class, diff --git a/module/apif-core/src/Controller/ActivityController.php b/module/apif-core/src/Controller/ActivityController.php index 194329c..192233c 100644 --- a/module/apif-core/src/Controller/ActivityController.php +++ b/module/apif-core/src/Controller/ActivityController.php @@ -22,4 +22,95 @@ public function __construct(APIFCoreRepositoryInterface $repository, ActivityLog $this->_readLogger = $readLogger; $this->_activityLog = $activityLog; } + + private function _getAuth() { + //Check AUTH has been passed + $request_method = $_SERVER["REQUEST_METHOD"]; + if (!isset($_SERVER['PHP_AUTH_USER'])) { + header('WWW-Authenticate: Basic realm="Realm"'); + header('HTTP/1.0 401 Unauthorized'); + echo 'Dataset key must be provided as HTTP Basic Auth username'; + exit; + } else { + $auth = [ + 'user' => $_SERVER['PHP_AUTH_USER'], + 'pwd' => $_SERVER['PHP_AUTH_PW'] + ]; + return $auth; + } + } + + private function _handleException($ex) { + if (is_a($ex, MongoDB\Driver\Exception\AuthenticationException::class) ){ + $this->getResponse()->setStatusCode(403); + }elseif(is_a($ex->getPrevious(), MongoDB\Driver\Exception\AuthenticationException::class)){ + $this->getResponse()->setStatusCode(403); + }elseif(is_a($ex, \Throwable::class)){ + $this->getResponse()->setStatusCode(500); + }else{ + // This will never happen + $this->getResponse()->setStatusCode(500); + } + } + + private function getPermissionsOnDataset($roles, $datasetId) { + $permissions = [ + 'read' => false, + 'write' => false + ]; + foreach ($roles as $role) { + $roleLabel = $role['role']; + $permissionLabel = substr($roleLabel, -1); + $datasetLabel = substr($roleLabel, 0, -2); + + // If we find a matching dataset... + if ($datasetLabel == $datasetId) { + if (($permissionLabel == 'R') AND ($permissions['read'] == false)) { + $permissions['read'] = true; + } + if (($permissionLabel == 'W') AND ($permissions['write'] == false)) { + $permissions['write'] = true; + } + } + } + return $permissions; + } + + public function get($id) { + // Credentials supplied with the request + $key = $this->_getAuth()['user']; + $pwd = $this->_getAuth()['pwd']; + + // Admin credentials for querying the permissions DB + $adminUser = $this->_config['mongodb']['adminUser']; + $adminPwd = $this->_config['mongodb']['adminPwd']; + $adminAuth = [ + 'user' => $adminUser, + 'pwd' => $adminPwd + ]; + + // CHECK PERMISSIONS ON DATASET + try { + $results = $this->_repository->getKey($key,$adminAuth); + }catch (\Throwable $ex) { + $this->_handleException($ex); + return new JsonModel(['error' => 'Failed to retrieve permissions - ' . $ex->getMessage()]); + } + + $roles = $results[0]['users'][0]['roles']; + $permissions = $this->getPermissionsOnDataset($roles, $id); + + // ###### + // Access is granted to activity log if user/key has both READ and WRITE permission on this dataset + // ###### + if ($permissions['read'] AND $permissions['write']) { + // TODO - MAKE ACTIVITY LOG REQUEST HERE... + } + else { + $this->getResponse()->setStatusCode(403); + return new JsonModel(['error' => 'Authorization failed, you do not have access to activity entries for this dataset']); + } + + return new JsonModel($permissions); + } } \ No newline at end of file From 52af8d105a4a95df2cc39344308073ad4e8f6220 Mon Sep 17 00:00:00 2001 From: Jason Carvalho Date: Tue, 19 Apr 2022 13:49:53 +0100 Subject: [PATCH 3/8] - Basic /activity/ functionality created - Bugfix for missing documentId in ActivityLog DELETE entries --- .../src/Controller/ActivityController.php | 40 ++++++++++++++++++- .../src/Controller/ObjectController.php | 2 +- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/module/apif-core/src/Controller/ActivityController.php b/module/apif-core/src/Controller/ActivityController.php index 192233c..a28d48c 100644 --- a/module/apif-core/src/Controller/ActivityController.php +++ b/module/apif-core/src/Controller/ActivityController.php @@ -105,12 +105,50 @@ public function get($id) { // ###### if ($permissions['read'] AND $permissions['write']) { // TODO - MAKE ACTIVITY LOG REQUEST HERE... + // Actions: + // - RetrieveMany + // - CreateFile + // - Create + + $activityLogId = $this->_config['activityLog']['dataset']; + $query = [ + 'al:datasetId' => $id, + '$or' => [ + ['@type' => 'al:CreateFile'], + ['@type' => 'al:Create'], + //['@type' => 'al:RetrieveMany'], + ['@type' => 'al:Update'], + ['@type' => 'al:Delete'], + //['@type' => 'al:Browse'], + ], + ]; + $projection = [ + '_id' => true, + '@id' => true, + '@context' => true, + '@type' => true, + //'al:request' => true, + 'al:request.@type' => true, + 'al:request.al:endpoint' => true, + 'al:request.al:httpRequestMethod' => true, + 'al:request.al:payload' => true, + 'al:documentId' => true, + 'al:datasetId' => true, + '_timestamp' => true, + '_timestamp_year' => true, + '_timestamp_month' => true, + '_timestamp_day' => true, + '_timestamp_hour' => true, + '_timestamp_minute' => true, + '_timestamp_second' => true, + ]; + $docs = $this->_repository->findDocs($activityLogId, $adminUser, $adminPwd, $query, $limit = null, $sort = null ,$projection); } else { $this->getResponse()->setStatusCode(403); return new JsonModel(['error' => 'Authorization failed, you do not have access to activity entries for this dataset']); } - return new JsonModel($permissions); + return new JsonModel($docs); } } \ No newline at end of file diff --git a/module/apif-core/src/Controller/ObjectController.php b/module/apif-core/src/Controller/ObjectController.php index 7cc1190..e8282d4 100644 --- a/module/apif-core/src/Controller/ObjectController.php +++ b/module/apif-core/src/Controller/ObjectController.php @@ -407,7 +407,7 @@ public function delete($id) { $datasetUUID = $id; $action = "Delete"; $summary = "Remove a document from dataset"; - $logData = $this->_assembleLogData($datasetUUID, $key, $action, $summary); + $logData = $this->_assembleLogData($datasetUUID, $key, $action, $summary, $docID); $this->_activityLog->logActivity($logData); return new JsonModel($response); From f678352dacf4e4d49530652cd799cd12038fb118 Mon Sep 17 00:00:00 2001 From: Jason Carvalho Date: Tue, 19 Apr 2022 14:01:58 +0100 Subject: [PATCH 4/8] Fixed an issue where file deletions and updates weren't being returned --- .../apif-core/src/Controller/ActivityController.php | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/module/apif-core/src/Controller/ActivityController.php b/module/apif-core/src/Controller/ActivityController.php index a28d48c..0af4d67 100644 --- a/module/apif-core/src/Controller/ActivityController.php +++ b/module/apif-core/src/Controller/ActivityController.php @@ -104,22 +104,17 @@ public function get($id) { // Access is granted to activity log if user/key has both READ and WRITE permission on this dataset // ###### if ($permissions['read'] AND $permissions['write']) { - // TODO - MAKE ACTIVITY LOG REQUEST HERE... - // Actions: - // - RetrieveMany - // - CreateFile - // - Create - $activityLogId = $this->_config['activityLog']['dataset']; + // ONLY RETRIEVE WRITE ACTIONS, NOT READS. $query = [ 'al:datasetId' => $id, '$or' => [ - ['@type' => 'al:CreateFile'], ['@type' => 'al:Create'], - //['@type' => 'al:RetrieveMany'], ['@type' => 'al:Update'], ['@type' => 'al:Delete'], - //['@type' => 'al:Browse'], + ['@type' => 'al:CreateFile'], + ['@type' => 'al:DeleteFile'], + ['@type' => 'al:OverwriteFile'], ], ]; $projection = [ From f8375c1b6b4c78d00bd56624eb04f4c89a151382 Mon Sep 17 00:00:00 2001 From: Jason Carvalho Date: Tue, 19 Apr 2022 14:10:51 +0100 Subject: [PATCH 5/8] Fixed an issue with missing items in file activity entries. All activity log attributes now returned, with specific attributes removed that contain the key used in the original request --- .../src/Controller/ActivityController.php | 21 +++---------------- 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/module/apif-core/src/Controller/ActivityController.php b/module/apif-core/src/Controller/ActivityController.php index 0af4d67..4845799 100644 --- a/module/apif-core/src/Controller/ActivityController.php +++ b/module/apif-core/src/Controller/ActivityController.php @@ -117,25 +117,10 @@ public function get($id) { ['@type' => 'al:OverwriteFile'], ], ]; + // DO NOT RETURN ANY FIELDS WHICH CONTAIN THE KEY USED IN THE ORIGINAL REQUEST $projection = [ - '_id' => true, - '@id' => true, - '@context' => true, - '@type' => true, - //'al:request' => true, - 'al:request.@type' => true, - 'al:request.al:endpoint' => true, - 'al:request.al:httpRequestMethod' => true, - 'al:request.al:payload' => true, - 'al:documentId' => true, - 'al:datasetId' => true, - '_timestamp' => true, - '_timestamp_year' => true, - '_timestamp_month' => true, - '_timestamp_day' => true, - '_timestamp_hour' => true, - '_timestamp_minute' => true, - '_timestamp_second' => true, + 'al:summary' => false, + 'al:request.al:agent' => false, ]; $docs = $this->_repository->findDocs($activityLogId, $adminUser, $adminPwd, $query, $limit = null, $sort = null ,$projection); } From ab26a05e53388afb9cd5fa48e0d8d7f4cbc96fa1 Mon Sep 17 00:00:00 2001 From: Jason Carvalho Date: Tue, 19 Apr 2022 14:49:15 +0100 Subject: [PATCH 6/8] Add timestamp URL param for only retrieving activity log entries since a certain date/time --- module/apif-core/src/Controller/ActivityController.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/module/apif-core/src/Controller/ActivityController.php b/module/apif-core/src/Controller/ActivityController.php index 4845799..fbf00c4 100644 --- a/module/apif-core/src/Controller/ActivityController.php +++ b/module/apif-core/src/Controller/ActivityController.php @@ -81,6 +81,12 @@ public function get($id) { $key = $this->_getAuth()['user']; $pwd = $this->_getAuth()['pwd']; + //Get URL params + $timestampParam = $this->params()->fromQuery('timestamp', ""); + // This should = 0 or 1 if timestamp not supplied or an invalid string supplied. + // Either way, all items will be returned. + $timestamp = intval($timestampParam); + // Admin credentials for querying the permissions DB $adminUser = $this->_config['mongodb']['adminUser']; $adminPwd = $this->_config['mongodb']['adminPwd']; @@ -116,6 +122,7 @@ public function get($id) { ['@type' => 'al:DeleteFile'], ['@type' => 'al:OverwriteFile'], ], + '_timestamp' => ['$gte' => $timestamp] ]; // DO NOT RETURN ANY FIELDS WHICH CONTAIN THE KEY USED IN THE ORIGINAL REQUEST $projection = [ From f3747113dd5914ef544d6d9df063b2b62fe2a282 Mon Sep 17 00:00:00 2001 From: Jason Carvalho Date: Tue, 19 Apr 2022 15:20:31 +0100 Subject: [PATCH 7/8] Add swagger interface components to support activity monitoring --- .../apif/core/index/swagger-config-main.json | 40 ++++++++++++++++++- .../apif/core/index/swagger-config-main.phtml | 40 ++++++++++++++++++- .../core/index/swagger-config-management.json | 2 +- .../index/swagger-config-management.phtml | 2 +- 4 files changed, 80 insertions(+), 4 deletions(-) diff --git a/module/apif-core/view/apif/core/index/swagger-config-main.json b/module/apif-core/view/apif/core/index/swagger-config-main.json index 1a612bb..4206e0f 100644 --- a/module/apif-core/view/apif/core/index/swagger-config-main.json +++ b/module/apif-core/view/apif/core/index/swagger-config-main.json @@ -11,7 +11,7 @@ "name": "Apache 2.0", "url": "http://www.apache.org/licenses/LICENSE-2.0.html" }, - "version": "0.8.4" + "version": "0.9.0" }, "tags": [ { @@ -26,6 +26,10 @@ "name": "file", "description": "Create, retrieve, update and delete files" }, + { + "name": "activity", + "description": "Activity monitoring" + }, { "name": "schemas", "description": "Retrieve a schema" @@ -521,6 +525,40 @@ } } }, + "/activity/{dataset-id}": { + "get": { + "tags": [ + "activity" + ], + "summary": "Retrieve details of write activities on a dataset", + "operationId": "getActivities", + "parameters": [ + { + "name": "dataset-id", + "in": "path", + "description": "Dataset ID", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "timestamp", + "in": "query", + "description": "Optionally only retrieve entries since this timestamp", + "required": false, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + } + }, "/schemas/{schema-id}": { "get": { "tags": [ diff --git a/module/apif-core/view/apif/core/index/swagger-config-main.phtml b/module/apif-core/view/apif/core/index/swagger-config-main.phtml index ae74407..ec634d2 100644 --- a/module/apif-core/view/apif/core/index/swagger-config-main.phtml +++ b/module/apif-core/view/apif/core/index/swagger-config-main.phtml @@ -11,7 +11,7 @@ "name": "Apache 2.0", "url": "http://www.apache.org/licenses/LICENSE-2.0.html" }, -"version": "0.8.4" +"version": "0.9.0" }, "tags": [ { @@ -27,6 +27,10 @@ "description": "Create, retrieve, update and delete files" }, { +"name": "activity", +"description": "Activity monitoring" +}, +{ "name": "schemas", "description": "Retrieve a schema" }, @@ -552,6 +556,40 @@ } } }, +"/activity/{dataset-id}": { +"get": { +"tags": [ +"activity" +], +"summary": "Retrieve details of write activities on a dataset", +"operationId": "getActivities", +"parameters": [ +{ +"name": "dataset-id", +"in": "path", +"description": "Dataset ID", +"required": true, +"schema": { +"type": "string" +} +}, +{ +"name": "timestamp", +"in": "query", +"description": "Optionally only retrieve entries since this timestamp", +"required": false, +"schema": { +"type": "integer" +} +} +], +"responses": { +"200": { +"description": "Success" +} +} +} +}, "/schemas/{schema-id}": { "get": { "tags": [ diff --git a/module/apif-core/view/apif/core/index/swagger-config-management.json b/module/apif-core/view/apif/core/index/swagger-config-management.json index 92c779f..7f7e0de 100644 --- a/module/apif-core/view/apif/core/index/swagger-config-management.json +++ b/module/apif-core/view/apif/core/index/swagger-config-management.json @@ -11,7 +11,7 @@ "name": "Apache 2.0", "url": "http://www.apache.org/licenses/LICENSE-2.0.html" }, - "version": "0.8.4" + "version": "0.9.0" }, "tags": [ { diff --git a/module/apif-core/view/apif/core/index/swagger-config-management.phtml b/module/apif-core/view/apif/core/index/swagger-config-management.phtml index 31eb9ef..e7055f9 100644 --- a/module/apif-core/view/apif/core/index/swagger-config-management.phtml +++ b/module/apif-core/view/apif/core/index/swagger-config-management.phtml @@ -11,7 +11,7 @@ "name": "Apache 2.0", "url": "http://www.apache.org/licenses/LICENSE-2.0.html" }, -"version": "0.8.4" +"version": "0.9.0" }, "tags": [ { From 6b5aa7b9fae76dc2419798e652971ad51f965ee9 Mon Sep 17 00:00:00 2001 From: Jason Carvalho Date: Tue, 19 Apr 2022 15:29:08 +0100 Subject: [PATCH 8/8] Add swagger interface components to support activity monitoring --- module/apif-core/src/Controller/ActivityController.php | 1 + 1 file changed, 1 insertion(+) diff --git a/module/apif-core/src/Controller/ActivityController.php b/module/apif-core/src/Controller/ActivityController.php index fbf00c4..c9321af 100644 --- a/module/apif-core/src/Controller/ActivityController.php +++ b/module/apif-core/src/Controller/ActivityController.php @@ -73,6 +73,7 @@ private function getPermissionsOnDataset($roles, $datasetId) { } } } + return $permissions; }