From 75188f4ea23b976b380c917d7271db405c7ca4f9 Mon Sep 17 00:00:00 2001 From: Jesper Kristensen Date: Tue, 10 Dec 2024 14:46:26 +0100 Subject: [PATCH 01/13] ITKDev: Startet on entity audit module --- .../os2web_audit_entity.info.yml | 6 ++ .../os2web_audit_entity.module | 93 +++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 modules/os2web_audit_entity/os2web_audit_entity.info.yml create mode 100644 modules/os2web_audit_entity/os2web_audit_entity.module diff --git a/modules/os2web_audit_entity/os2web_audit_entity.info.yml b/modules/os2web_audit_entity/os2web_audit_entity.info.yml new file mode 100644 index 0000000..f5d7f7c --- /dev/null +++ b/modules/os2web_audit_entity/os2web_audit_entity.info.yml @@ -0,0 +1,6 @@ +name: "OS2web Audit logging entity access" +description: "Logs CUD events for entities" +type: module +core_version_requirement: ^8 || ^9 || ^10 +dependencies: + - os2web_audit:os2web_audit diff --git a/modules/os2web_audit_entity/os2web_audit_entity.module b/modules/os2web_audit_entity/os2web_audit_entity.module new file mode 100644 index 0000000..7b51850 --- /dev/null +++ b/modules/os2web_audit_entity/os2web_audit_entity.module @@ -0,0 +1,93 @@ +bundle() == 'webform') { + # Try to check for _cpr field for extra logging information. + $t = 1; + } +} + +// Should it be more configurable in relation to types. +// Storage used instead of load, because this hook is trigger by UI and API +function os2web_audit_entity_entity_storage_load(array $entities, $entity_type): void { + // webform_submission + // file (REQUEST_URI) + foreach ($entities as $entity) { + if (in_array($entity->getEntityTypeId(), OS2WEB_AUDIT_ENTITY_TYPES)) { + $account = \Drupal::currentUser(); + + if (os2web_audit_entity_is_api_user($account)) { + $t=1; + } + + # Try to check for _cpr field for extra logging information. + $t=1; + + // Get current user + } + } +} + +/** + * Check if the accounts roles are in the array of API roles. + * + * @param \Drupal\Core\Session\AccountInterface $account + * User account. + * + * @return bool + * If roles found TRUE else FALSE. + */ +function os2web_audit_entity_is_api_user(AccountInterface $account): bool { + $roles = $account->getRoles(); + + return !empty(array_intersect($roles, OS2WEB_AUDIT_ENTITY_API_USER_ROLES)); +} + +/** + * Simple logger wrapper. + * + * @param string $message + * Message to log. + * @param string $mail + * Identify users by e-mail address. + * @param array $metadata + * Optional metadata to set. + */ +function os2web_audit_entity_log(string $message, string $mail, array $metadata = []): void { + /** @var \Drupal\os2web_audit\Service\Logger $logger */ + $logger = \Drupal::service('os2web_audit.logger'); + + $metadata['userId'] = $mail; + $logger->info('Entity', $message, FALSE, $metadata); +} From 3b96a4b2556d763ab947df96a9e330ec780da7ac Mon Sep 17 00:00:00 2001 From: Jesper Kristensen Date: Tue, 10 Dec 2024 14:49:31 +0100 Subject: [PATCH 02/13] ITKDev: Updated change log --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc99935..d54de64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,10 +8,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- Add new module to track user accessing webform submissions. + ## [0.1.1] - 2024-11-19 - Made Watchdog default logger -- Updated Watchlog logger +- Updated watchdog logger ## [0.1.0] - 2024-10-21 From d0f69b3eef7f322e403de75cc70ea00fbb0d331c Mon Sep 17 00:00:00 2001 From: Jesper Kristensen Date: Tue, 10 Dec 2024 15:59:29 +0100 Subject: [PATCH 03/13] ITKDev: Debug commit --- .../os2web_audit_entity.module | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/modules/os2web_audit_entity/os2web_audit_entity.module b/modules/os2web_audit_entity/os2web_audit_entity.module index 7b51850..fe323ac 100644 --- a/modules/os2web_audit_entity/os2web_audit_entity.module +++ b/modules/os2web_audit_entity/os2web_audit_entity.module @@ -44,21 +44,37 @@ function os2web_audit_entity_entity_storage_load(array $entities, $entity_type): // webform_submission // file (REQUEST_URI) foreach ($entities as $entity) { - if (in_array($entity->getEntityTypeId(), OS2WEB_AUDIT_ENTITY_TYPES)) { + if (in_array($entity_type, OS2WEB_AUDIT_ENTITY_TYPES)) { $account = \Drupal::currentUser(); + $data = ['API' => FALSE]; if (os2web_audit_entity_is_api_user($account)) { - $t=1; + $data['API'] = 'true'; } # Try to check for _cpr field for extra logging information. - $t=1; + if ($entity_type === 'webform_submission') { + $webform = $entity->getWebform(); + $elements = $webform->getElementsDecoded(); + $filterFields = preg_grep('/^(.*)cpr(.*)$/', array_keys($elements)); + if (!empty($filterFields)) { + // CPR field found. - // Get current user + $t = $entity->id(); + $d = $entity->getElementData($filterFields[0]); + $d1 = $entity->getOriginalData(); + $d2 = $entity->getData(); + $t=1; + foreach ($filterFields as $field) { + + } + } + } } } } + /** * Check if the accounts roles are in the array of API roles. * From 40d92291b8372f32e8b15193684ac90e2fb04f4c Mon Sep 17 00:00:00 2001 From: Jesper Kristensen Date: Wed, 11 Dec 2024 14:38:04 +0100 Subject: [PATCH 04/13] ITKDev: Added readme and patch --- modules/os2web_audit_entity/README.md | 27 +++++++++ .../os2web_audit_entity.install | 18 ++++++ .../os2web_audit_entity.module | 56 ++++++++++++------- .../webform_hook_webform_post_load_data.diff | 14 +++++ 4 files changed, 95 insertions(+), 20 deletions(-) create mode 100644 modules/os2web_audit_entity/README.md create mode 100644 modules/os2web_audit_entity/os2web_audit_entity.install create mode 100644 modules/os2web_audit_entity/patches/webform_hook_webform_post_load_data.diff diff --git a/modules/os2web_audit_entity/README.md b/modules/os2web_audit_entity/README.md new file mode 100644 index 0000000..032f472 --- /dev/null +++ b/modules/os2web_audit_entity/README.md @@ -0,0 +1,27 @@ +# OS2web audit entity + +This module tries to log information about entity access and changes. + +## Webform submission + +This module integrates with [OS2Forms][os2forms-link], which utilizes the Webform module. + +If you are logging users who have accessed Webform submissions but no data is being recorded, ensure the patches +provided by this module are applied to the Webform module. + +**Note:** The patch cannot be applied via Composer because Composer does not support relative paths to patches outside +the webroot. Additionally, as the location of this module within the site can vary, applying the patch automatically +could potentially break the Composer installation. + +### Why this patch + +When implementing audit logging for webform submissions in Drupal, particularly to track who accessed the data: + +- Using `hook_entity_storage_load()` presents challenges with webform submissions due to their reliance on revisions. +- This is because the hook gets triggered before the storage handler finishes loading the submission data. + +To address this issue, a custom hook, `hook_webform_post_load_data()`, is introduced. +This custom hook is invoked after the webform has successfully loaded the submission data for a given submission +revision. + +[os2forms-link]: https://github.com/OS2Forms/os2forms diff --git a/modules/os2web_audit_entity/os2web_audit_entity.install b/modules/os2web_audit_entity/os2web_audit_entity.install new file mode 100644 index 0000000..7879cc3 --- /dev/null +++ b/modules/os2web_audit_entity/os2web_audit_entity.install @@ -0,0 +1,18 @@ +getWebform(); - $elements = $webform->getElementsDecoded(); - $filterFields = preg_grep('/^(.*)cpr(.*)$/', array_keys($elements)); - if (!empty($filterFields)) { - // CPR field found. - - $t = $entity->id(); - $d = $entity->getElementData($filterFields[0]); - $d1 = $entity->getOriginalData(); - $d2 = $entity->getData(); - $t=1; - foreach ($filterFields as $field) { - - } - } - } } } } +/** + * Implements hook_webform_post_load_data(). + * + * @param array $submissions + */ +function os2web_audit_entity_webform_post_load_data(array $submissions): void { + + foreach ($submissions as $submission) { + $account = \Drupal::currentUser(); + + $apiUser = FALSE; + if (os2web_audit_entity_is_api_user($account)) { + $apiUser = TRUE; + } + + # Try to check for _cpr field for extra logging information. + $personal = ''; + $submissionData = $submission->getData(); + $filterFields = preg_grep('/^(.*)cpr(.*)$/', array_keys($submissionData)); + if (!empty($filterFields)) { + foreach ($filterFields as $field) { + $cpr = $submissionData[$field]; + $personal .= sprintf('CPR "%s".', $cpr); + } + } + + $msg = sprintf('Webform submission (%d) looked up. %sWebform id "%s".', $submission->id(), $personal, $submission->getWebform()->id()); + os2web_audit_entity_log($msg, $submission->getWebform()->id(), ['userType' => $apiUser ? OS2WEB_AUDIT_ENTITY_USER_API : OS2WEB_AUDIT_ENTITY_USER_WEB]); + } +} + /** * Check if the accounts roles are in the array of API roles. diff --git a/modules/os2web_audit_entity/patches/webform_hook_webform_post_load_data.diff b/modules/os2web_audit_entity/patches/webform_hook_webform_post_load_data.diff new file mode 100644 index 0000000..b28cb6d --- /dev/null +++ b/modules/os2web_audit_entity/patches/webform_hook_webform_post_load_data.diff @@ -0,0 +1,14 @@ +diff --git a/src/WebformSubmissionStorage.php b/src/WebformSubmissionStorage.php +index 4e14c3c..4c2d1c9 100644 +--- a/src/WebformSubmissionStorage.php ++++ b/src/WebformSubmissionStorage.php +@@ -168,6 +168,9 @@ class WebformSubmissionStorage extends SqlContentEntityStorage implements Webfor + /** @var \Drupal\webform\WebformSubmissionInterface[] $webform_submissions */ + $webform_submissions = parent::doLoadMultiple($ids); + $this->loadData($webform_submissions); ++ ++ \Drupal::moduleHandler()->invokeAll('webform_post_load_data', [$webform_submissions]); ++ + return $webform_submissions; + } + From a9d9ee19804a08a2703a008eed6092cab7f41087 Mon Sep 17 00:00:00 2001 From: Jesper Kristensen Date: Wed, 11 Dec 2024 15:14:40 +0100 Subject: [PATCH 05/13] ITKDev: Added remote ip to log lines --- CHANGELOG.md | 1 + .../os2web_audit_entity.module | 55 ++++++------------- os2web_audit.services.yml | 2 +- src/Service/Logger.php | 9 +++ 4 files changed, 29 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d54de64..5b30a23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] - Add new module to track user accessing webform submissions. +- Added remote ip to all log lines. ## [0.1.1] - 2024-11-19 diff --git a/modules/os2web_audit_entity/os2web_audit_entity.module b/modules/os2web_audit_entity/os2web_audit_entity.module index 44e657a..83d241e 100644 --- a/modules/os2web_audit_entity/os2web_audit_entity.module +++ b/modules/os2web_audit_entity/os2web_audit_entity.module @@ -4,10 +4,6 @@ use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Session\AccountInterface; use Drupal\webform_revisions\Entity\WebformRevisionsSubmission; -const OS2WEB_AUDIT_ENTITY_TYPES = [ - 'file', -]; - const OS2WEB_AUDIT_ENTITY_API_USER_ROLES = [ 'os2forms_rest_api_user', 'os2forms_rest_api_user_write', @@ -34,29 +30,19 @@ function os2web_audit_entity_entity_update(EntityInterface $entity): void { */ function os2web_audit_entity_entity_delete(EntityInterface $entity): void { // Your code to handle the entity delete event. - $t = 1; - if ($entity->bundle() == 'webform') { - # Try to check for _cpr field for extra logging information. - $t = 1; - } + } -// Should it be more configurable in relation to types. -// Storage used instead of load, because this hook is trigger by UI and API function os2web_audit_entity_entity_storage_load(array $entities, $entity_type): void { - // file (REQUEST_URI) - foreach ($entities as $entity) { - if (in_array($entity_type, OS2WEB_AUDIT_ENTITY_TYPES)) { - $account = \Drupal::currentUser(); - - $data = ['API' => FALSE]; - if (os2web_audit_entity_is_api_user($account)) { - $data['API'] = 'true'; - } - - } + if ($entity_type == 'file') { + /** @var \Drupal\file\Entity\File $entity */ + $fid = $entity->id(); + $uri = $entity->getFileUri(); + $msg = sprintf('File (%d) accessed. Uri "%s"', $fid, $uri); + os2web_audit_entity_log($msg); } + } } /** @@ -65,15 +51,7 @@ function os2web_audit_entity_entity_storage_load(array $entities, $entity_type): * @param array $submissions */ function os2web_audit_entity_webform_post_load_data(array $submissions): void { - foreach ($submissions as $submission) { - $account = \Drupal::currentUser(); - - $apiUser = FALSE; - if (os2web_audit_entity_is_api_user($account)) { - $apiUser = TRUE; - } - # Try to check for _cpr field for extra logging information. $personal = ''; $submissionData = $submission->getData(); @@ -86,7 +64,7 @@ function os2web_audit_entity_webform_post_load_data(array $submissions): void { } $msg = sprintf('Webform submission (%d) looked up. %sWebform id "%s".', $submission->id(), $personal, $submission->getWebform()->id()); - os2web_audit_entity_log($msg, $submission->getWebform()->id(), ['userType' => $apiUser ? OS2WEB_AUDIT_ENTITY_USER_API : OS2WEB_AUDIT_ENTITY_USER_WEB]); + os2web_audit_entity_log($msg); } } @@ -111,15 +89,18 @@ function os2web_audit_entity_is_api_user(AccountInterface $account): bool { * * @param string $message * Message to log. - * @param string $mail - * Identify users by e-mail address. - * @param array $metadata - * Optional metadata to set. */ -function os2web_audit_entity_log(string $message, string $mail, array $metadata = []): void { +function os2web_audit_entity_log(string $message): void { /** @var \Drupal\os2web_audit\Service\Logger $logger */ $logger = \Drupal::service('os2web_audit.logger'); - $metadata['userId'] = $mail; + $account = \Drupal::currentUser(); + $apiUser = FALSE; + if (os2web_audit_entity_is_api_user($account)) { + $apiUser = TRUE; + } + + $metadata['userId'] = $account->getEmail(); + $metadata['userType'] = $apiUser ? OS2WEB_AUDIT_ENTITY_USER_API : OS2WEB_AUDIT_ENTITY_USER_WEB; $logger->info('Entity', $message, FALSE, $metadata); } diff --git a/os2web_audit.services.yml b/os2web_audit.services.yml index 8d48828..34ec888 100644 --- a/os2web_audit.services.yml +++ b/os2web_audit.services.yml @@ -5,4 +5,4 @@ services: os2web_audit.logger: class: Drupal\os2web_audit\Service\Logger - arguments: ['@plugin.manager.os2web_audit_logger', '@config.factory', '@current_user', '@logger.factory'] + arguments: ['@plugin.manager.os2web_audit_logger', '@config.factory', '@current_user', '@logger.factory', '@request_stack'] diff --git a/src/Service/Logger.php b/src/Service/Logger.php index ab07551..79807d1 100644 --- a/src/Service/Logger.php +++ b/src/Service/Logger.php @@ -11,6 +11,7 @@ use Drupal\os2web_audit\Form\PluginSettingsForm; use Drupal\os2web_audit\Form\SettingsForm; use Drupal\os2web_audit\Plugin\LoggerManager; +use Symfony\Component\HttpFoundation\RequestStack; /** * Class Logger. @@ -24,6 +25,7 @@ public function __construct( private readonly ConfigFactoryInterface $configFactory, private readonly AccountProxyInterface $currentUser, private readonly LoggerChannelFactoryInterface $watchdog, + private readonly RequestStack $requestStack, ) { } @@ -86,6 +88,13 @@ private function log(string $type, int $timestamp, string $line, bool $logUser = $metadata['userId'] = $this->currentUser->getEmail(); } + // Log request IP for information more information. + $request = $this->requestStack->getCurrentRequest(); + $ip_address = $request->getClientIp(); + if (!is_null($ip_address)) { + $line .= sprintf(' Remote ip: %s',$ip_address); + } + try { /** @var \Drupal\os2web_audit\Plugin\AuditLogger\AuditLoggerInterface $logger */ $logger = $this->loggerManager->createInstance($plugin_id, $configuration ?? []); From e5cd3c2ad391c0fbd06663db165476d251cfeeaf Mon Sep 17 00:00:00 2001 From: Jesper Kristensen Date: Wed, 11 Dec 2024 15:25:51 +0100 Subject: [PATCH 06/13] ITKDev: Added insert, update and delete entity logging --- .../os2web_audit_entity.install | 2 +- .../os2web_audit_entity.module | 26 ++++++++++++++----- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/modules/os2web_audit_entity/os2web_audit_entity.install b/modules/os2web_audit_entity/os2web_audit_entity.install index 7879cc3..2737a3a 100644 --- a/modules/os2web_audit_entity/os2web_audit_entity.install +++ b/modules/os2web_audit_entity/os2web_audit_entity.install @@ -2,7 +2,7 @@ /** * @file - * This module enabled webform submission encryption as a default option. + * This module enabled os2web audit entity default options. */ /** diff --git a/modules/os2web_audit_entity/os2web_audit_entity.module b/modules/os2web_audit_entity/os2web_audit_entity.module index 83d241e..f908cca 100644 --- a/modules/os2web_audit_entity/os2web_audit_entity.module +++ b/modules/os2web_audit_entity/os2web_audit_entity.module @@ -1,9 +1,17 @@ id(), $entity->getEntityTypeId()); + os2web_audit_entity_log($msg); } /** * Implements hook_entity_update(). */ function os2web_audit_entity_entity_update(EntityInterface $entity): void { - // Your code to handle the entity update event. - $t = 1; + $msg = sprintf('Entity (%d) of type "%s" updated.', $entity->id(), $entity->getEntityTypeId()); + os2web_audit_entity_log($msg); } /** * Implements hook_entity_delete(). */ function os2web_audit_entity_entity_delete(EntityInterface $entity): void { - // Your code to handle the entity delete event. - + $msg = sprintf('Entity (%d) of type "%s" deleted.', $entity->id(), $entity->getEntityTypeId()); + os2web_audit_entity_log($msg); } +/** + * Implements hook_entity_storage_load(). + * + * Logs access for file entities. + */ function os2web_audit_entity_entity_storage_load(array $entities, $entity_type): void { foreach ($entities as $entity) { if ($entity_type == 'file') { @@ -94,6 +107,7 @@ function os2web_audit_entity_log(string $message): void { /** @var \Drupal\os2web_audit\Service\Logger $logger */ $logger = \Drupal::service('os2web_audit.logger'); + // Detect user type. $account = \Drupal::currentUser(); $apiUser = FALSE; if (os2web_audit_entity_is_api_user($account)) { From d7fdcbe15e11a19b67653dcd14e4b3241218c323 Mon Sep 17 00:00:00 2001 From: Jesper Kristensen Date: Wed, 11 Dec 2024 15:34:05 +0100 Subject: [PATCH 07/13] ITKDev: Updated code style --- modules/os2web_audit_entity/os2web_audit_entity.module | 10 +++++----- src/Service/Logger.php | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/os2web_audit_entity/os2web_audit_entity.module b/modules/os2web_audit_entity/os2web_audit_entity.module index f908cca..4c00f3d 100644 --- a/modules/os2web_audit_entity/os2web_audit_entity.module +++ b/modules/os2web_audit_entity/os2web_audit_entity.module @@ -7,7 +7,6 @@ use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Session\AccountInterface; -use Drupal\webform_revisions\Entity\WebformRevisionsSubmission; /** * @TODO: Should this be configurable? @@ -20,6 +19,9 @@ const OS2WEB_AUDIT_ENTITY_API_USER_ROLES = [ const OS2WEB_AUDIT_ENTITY_USER_API = 'api'; const OS2WEB_AUDIT_ENTITY_USER_WEB = 'web'; +/** + * Implements hook_entity_insert(). + */ function os2web_audit_entity_entity_insert(EntityInterface $entity): void { $msg = sprintf('Entity (%d) of type "%s" created.', $entity->id(), $entity->getEntityTypeId()); os2web_audit_entity_log($msg); @@ -60,12 +62,11 @@ function os2web_audit_entity_entity_storage_load(array $entities, $entity_type): /** * Implements hook_webform_post_load_data(). - * - * @param array $submissions */ function os2web_audit_entity_webform_post_load_data(array $submissions): void { + /** @var array<\Drupal\webform_revisions\Entity\WebformRevisionsSubmission> $submissions */ foreach ($submissions as $submission) { - # Try to check for _cpr field for extra logging information. + // Try to check for _cpr field for extra logging information. $personal = ''; $submissionData = $submission->getData(); $filterFields = preg_grep('/^(.*)cpr(.*)$/', array_keys($submissionData)); @@ -81,7 +82,6 @@ function os2web_audit_entity_webform_post_load_data(array $submissions): void { } } - /** * Check if the accounts roles are in the array of API roles. * diff --git a/src/Service/Logger.php b/src/Service/Logger.php index 79807d1..8d3b383 100644 --- a/src/Service/Logger.php +++ b/src/Service/Logger.php @@ -92,7 +92,7 @@ private function log(string $type, int $timestamp, string $line, bool $logUser = $request = $this->requestStack->getCurrentRequest(); $ip_address = $request->getClientIp(); if (!is_null($ip_address)) { - $line .= sprintf(' Remote ip: %s',$ip_address); + $line .= sprintf(' Remote ip: %s', $ip_address); } try { From 7a41bf9f9b6673d33cf7fbd6e040ebbb0158de5a Mon Sep 17 00:00:00 2001 From: Jesper Kristensen Date: Thu, 12 Dec 2024 10:16:55 +0100 Subject: [PATCH 08/13] ITKDev: Code style fixes --- modules/os2web_audit_entity/os2web_audit_entity.module | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/os2web_audit_entity/os2web_audit_entity.module b/modules/os2web_audit_entity/os2web_audit_entity.module index 4c00f3d..3350b11 100644 --- a/modules/os2web_audit_entity/os2web_audit_entity.module +++ b/modules/os2web_audit_entity/os2web_audit_entity.module @@ -16,8 +16,8 @@ const OS2WEB_AUDIT_ENTITY_API_USER_ROLES = [ 'os2forms_rest_api_user_write', ]; -const OS2WEB_AUDIT_ENTITY_USER_API = 'api'; -const OS2WEB_AUDIT_ENTITY_USER_WEB = 'web'; +define("OS2WEB_AUDIT_ENTITY_USER_API", 'api'); +define("OS2WEB_AUDIT_ENTITY_USER_WEB", 'web'); /** * Implements hook_entity_insert(). @@ -63,8 +63,8 @@ function os2web_audit_entity_entity_storage_load(array $entities, $entity_type): /** * Implements hook_webform_post_load_data(). */ -function os2web_audit_entity_webform_post_load_data(array $submissions): void { - /** @var array<\Drupal\webform_revisions\Entity\WebformRevisionsSubmission> $submissions */ +function os2web_audit_entity_webform_post_load_data(mixed $submissions): void { + foreach ($submissions as $submission) { // Try to check for _cpr field for extra logging information. $personal = ''; From 1c1cecd465bc81d5c437068c281440a5f251a6c0 Mon Sep 17 00:00:00 2001 From: Jesper Kristensen Date: Thu, 12 Dec 2024 11:30:12 +0100 Subject: [PATCH 09/13] ITKDev: Added settings form to entity module --- .../os2web_audit_entity.links.menu.yml | 5 + .../os2web_audit_entity.module | 37 ++++---- .../os2web_audit_entity.routing.yml | 7 ++ .../src/Form/SettingsForm.php | 92 +++++++++++++++++++ 4 files changed, 125 insertions(+), 16 deletions(-) create mode 100644 modules/os2web_audit_entity/os2web_audit_entity.links.menu.yml create mode 100644 modules/os2web_audit_entity/os2web_audit_entity.routing.yml create mode 100644 modules/os2web_audit_entity/src/Form/SettingsForm.php diff --git a/modules/os2web_audit_entity/os2web_audit_entity.links.menu.yml b/modules/os2web_audit_entity/os2web_audit_entity.links.menu.yml new file mode 100644 index 0000000..b57f18b --- /dev/null +++ b/modules/os2web_audit_entity/os2web_audit_entity.links.menu.yml @@ -0,0 +1,5 @@ +os2web_audit_entity.admin_settings: + title: 'OS2web Audit entity settings' + parent: system.admin_config_system + description: 'Settings for the OS2 Audit entity module' + route_name: os2web_audit_entity.settings diff --git a/modules/os2web_audit_entity/os2web_audit_entity.module b/modules/os2web_audit_entity/os2web_audit_entity.module index 3350b11..235c297 100644 --- a/modules/os2web_audit_entity/os2web_audit_entity.module +++ b/modules/os2web_audit_entity/os2web_audit_entity.module @@ -7,17 +7,7 @@ use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Session\AccountInterface; - -/** - * @TODO: Should this be configurable? - */ -const OS2WEB_AUDIT_ENTITY_API_USER_ROLES = [ - 'os2forms_rest_api_user', - 'os2forms_rest_api_user_write', -]; - -define("OS2WEB_AUDIT_ENTITY_USER_API", 'api'); -define("OS2WEB_AUDIT_ENTITY_USER_WEB", 'web'); +use Drupal\os2web_audit_entity\Form\SettingsForm; /** * Implements hook_entity_insert(). @@ -48,7 +38,7 @@ function os2web_audit_entity_entity_delete(EntityInterface $entity): void { * * Logs access for file entities. */ -function os2web_audit_entity_entity_storage_load(array $entities, $entity_type): void { +function os2web_audit_entity_entity_storage_load(mixed $entities, $entity_type): void { foreach ($entities as $entity) { if ($entity_type == 'file') { /** @var \Drupal\file\Entity\File $entity */ @@ -64,7 +54,6 @@ function os2web_audit_entity_entity_storage_load(array $entities, $entity_type): * Implements hook_webform_post_load_data(). */ function os2web_audit_entity_webform_post_load_data(mixed $submissions): void { - foreach ($submissions as $submission) { // Try to check for _cpr field for extra logging information. $personal = ''; @@ -77,8 +66,21 @@ function os2web_audit_entity_webform_post_load_data(mixed $submissions): void { } } - $msg = sprintf('Webform submission (%d) looked up. %sWebform id "%s".', $submission->id(), $personal, $submission->getWebform()->id()); + // Attachments - requestStack + $request = \Drupal::request(); + if (preg_match('~(.*)/print/pdf/(.*)~', $request->getPathInfo())) { + // We know that a webform submission has been loaded and this is a print + // pdf path. This indicates that this is an attachment download action. + $msg = sprintf('Webform submission (%d) downloaded as attachment. %s Webform id "%s".', $submission->id(), $personal, $submission->getWebform()->id()); + os2web_audit_entity_log($msg); + + // Exit to prevent double log entry. + return; + } + + $msg = sprintf('Webform submission (%d) looked up. %s Webform id "%s".', $submission->id(), $personal, $submission->getWebform()->id()); os2web_audit_entity_log($msg); + } } @@ -94,7 +96,10 @@ function os2web_audit_entity_webform_post_load_data(mixed $submissions): void { function os2web_audit_entity_is_api_user(AccountInterface $account): bool { $roles = $account->getRoles(); - return !empty(array_intersect($roles, OS2WEB_AUDIT_ENTITY_API_USER_ROLES)); + $config = \Drupal::config(SettingsForm::$configName); + $selectedRoles = $config->get('roles'); + + return !empty(array_intersect($roles, array_keys(array_filter($selectedRoles)))); } /** @@ -115,6 +120,6 @@ function os2web_audit_entity_log(string $message): void { } $metadata['userId'] = $account->getEmail(); - $metadata['userType'] = $apiUser ? OS2WEB_AUDIT_ENTITY_USER_API : OS2WEB_AUDIT_ENTITY_USER_WEB; + $metadata['userType'] = $apiUser ? 'api' : 'web'; $logger->info('Entity', $message, FALSE, $metadata); } diff --git a/modules/os2web_audit_entity/os2web_audit_entity.routing.yml b/modules/os2web_audit_entity/os2web_audit_entity.routing.yml new file mode 100644 index 0000000..e1795d7 --- /dev/null +++ b/modules/os2web_audit_entity/os2web_audit_entity.routing.yml @@ -0,0 +1,7 @@ +os2web_audit_entity.settings: + path: '/admin/config/os2web_audit/entity' + defaults: + _form: '\Drupal\os2web_audit_entity\Form\SettingsForm' + _title: 'OS2web Audit entity settings' + requirements: + _permission: 'administer site' diff --git a/modules/os2web_audit_entity/src/Form/SettingsForm.php b/modules/os2web_audit_entity/src/Form/SettingsForm.php new file mode 100644 index 0000000..40e9475 --- /dev/null +++ b/modules/os2web_audit_entity/src/Form/SettingsForm.php @@ -0,0 +1,92 @@ +get('config.factory'), + ); + } + + /** + * The name of the configuration setting. + * + * @var string + */ + public static string $configName = 'os2web_audit_entity.settings'; + + /** + * {@inheritdoc} + */ + protected function getEditableConfigNames(): array { + return [self::$configName]; + } + + /** + * {@inheritdoc} + */ + public function getFormId(): string { + return 'os2web_audit_entity_admin_form'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state): array { + $items = []; + $roles = Role::loadMultiple(); + foreach ($roles as $role_id => $role) { + $items[$role->id()] = $role->label(); + } + + $config = $this->config(self::$configName); + + $form['roles'] = [ + '#type' => 'checkboxes', + '#title' => $this->t('Choose an Option'), + '#description' => $this->t('Please select an option from the dropdown menu.'), + '#options' => $items, + '#default_value' => $config->get('roles') ?? [], + '#required' => TRUE, + ]; + + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state): void { + parent::submitForm($form, $form_state); + + $this->config(self::$configName) + ->set('roles', $form_state->getValue('roles')) + ->save(); + } + +} From 2e11234213d73dc9656b967fdd61439327ae5868 Mon Sep 17 00:00:00 2001 From: Jesper Kristensen Date: Thu, 12 Dec 2024 11:36:06 +0100 Subject: [PATCH 10/13] ITKDev: Match API download attachment --- modules/os2web_audit_entity/os2web_audit_entity.module | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/os2web_audit_entity/os2web_audit_entity.module b/modules/os2web_audit_entity/os2web_audit_entity.module index 235c297..46126e6 100644 --- a/modules/os2web_audit_entity/os2web_audit_entity.module +++ b/modules/os2web_audit_entity/os2web_audit_entity.module @@ -68,7 +68,7 @@ function os2web_audit_entity_webform_post_load_data(mixed $submissions): void { // Attachments - requestStack $request = \Drupal::request(); - if (preg_match('~(.*)/print/pdf/(.*)~', $request->getPathInfo())) { + if (preg_match('~(.*)/print/pdf/(.*)|(.*)\d.*/attachment(.*)~', $request->getPathInfo())) { // We know that a webform submission has been loaded and this is a print // pdf path. This indicates that this is an attachment download action. $msg = sprintf('Webform submission (%d) downloaded as attachment. %s Webform id "%s".', $submission->id(), $personal, $submission->getWebform()->id()); From d4e0e7fa3c3ff36f51c1fba1f5e66a5f23c65c36 Mon Sep 17 00:00:00 2001 From: Jesper Kristensen Date: Thu, 12 Dec 2024 12:15:23 +0100 Subject: [PATCH 11/13] ITKDev: Updated code style --- .../os2web_audit_entity.module | 4 ++-- .../src/Form/SettingsForm.php | 24 ++++++++++++++++--- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/modules/os2web_audit_entity/os2web_audit_entity.module b/modules/os2web_audit_entity/os2web_audit_entity.module index 46126e6..64ec6f8 100644 --- a/modules/os2web_audit_entity/os2web_audit_entity.module +++ b/modules/os2web_audit_entity/os2web_audit_entity.module @@ -38,7 +38,7 @@ function os2web_audit_entity_entity_delete(EntityInterface $entity): void { * * Logs access for file entities. */ -function os2web_audit_entity_entity_storage_load(mixed $entities, $entity_type): void { +function os2web_audit_entity_entity_storage_load(mixed $entities, string $entity_type): void { foreach ($entities as $entity) { if ($entity_type == 'file') { /** @var \Drupal\file\Entity\File $entity */ @@ -66,7 +66,7 @@ function os2web_audit_entity_webform_post_load_data(mixed $submissions): void { } } - // Attachments - requestStack + // Attachments download. $request = \Drupal::request(); if (preg_match('~(.*)/print/pdf/(.*)|(.*)\d.*/attachment(.*)~', $request->getPathInfo())) { // We know that a webform submission has been loaded and this is a print diff --git a/modules/os2web_audit_entity/src/Form/SettingsForm.php b/modules/os2web_audit_entity/src/Form/SettingsForm.php index 40e9475..d5b54ae 100644 --- a/modules/os2web_audit_entity/src/Form/SettingsForm.php +++ b/modules/os2web_audit_entity/src/Form/SettingsForm.php @@ -3,9 +3,9 @@ namespace Drupal\os2web_audit_entity\Form; use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Form\ConfigFormBase; use Drupal\Core\Form\FormStateInterface; -use Drupal\user\Entity\Role; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -20,6 +20,7 @@ class SettingsForm extends ConfigFormBase { */ public function __construct( ConfigFactoryInterface $configFactory, + private EntityTypeManagerInterface $entityTypeManager, ) { parent::__construct($configFactory); } @@ -30,6 +31,7 @@ public function __construct( public static function create(ContainerInterface $container): static { return new static( $container->get('config.factory'), + $container->get('entity_type.manager') ); } @@ -59,8 +61,8 @@ public function getFormId(): string { */ public function buildForm(array $form, FormStateInterface $form_state): array { $items = []; - $roles = Role::loadMultiple(); - foreach ($roles as $role_id => $role) { + $roles = $this->getRoles(); + foreach ($roles as $role) { $items[$role->id()] = $role->label(); } @@ -89,4 +91,20 @@ public function submitForm(array &$form, FormStateInterface $form_state): void { ->save(); } + /** + * Get all roles. + * + * @return array<\Drupal\Core\Entity\EntityInterface> + * An array of role entities. + * + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + */ + private function getRoles() { + // Use the role storage to load roles. + $roleStorage = $this->entityTypeManager->getStorage('user_role'); + + return $roleStorage->loadMultiple(); + } + } From 5924d6c504c0d4eb81bb09eef391300b00f4b309 Mon Sep 17 00:00:00 2001 From: Jesper Kristensen Date: Thu, 12 Dec 2024 13:21:58 +0100 Subject: [PATCH 12/13] ITKDev: Better handling of CPR fields audit --- .../os2web_audit_entity.module | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/modules/os2web_audit_entity/os2web_audit_entity.module b/modules/os2web_audit_entity/os2web_audit_entity.module index 64ec6f8..31e291f 100644 --- a/modules/os2web_audit_entity/os2web_audit_entity.module +++ b/modules/os2web_audit_entity/os2web_audit_entity.module @@ -57,12 +57,24 @@ function os2web_audit_entity_webform_post_load_data(mixed $submissions): void { foreach ($submissions as $submission) { // Try to check for _cpr field for extra logging information. $personal = ''; + $filterFields = []; + + // Detect field of type that contains "cpr" in name or where field name + // contains "cpr". + /** @var \Drupal\webform_revisions\Entity\WebformRevisions $webform */ + $webform = $submission->getWebform(); + $elements = $webform->getElementsDecoded(); + foreach ($elements as $fieldName => $element) { + if (str_contains(strtolower($element['#type']), 'cpr') || str_contains(strtolower($fieldName), 'cpr')) { + $filterFields[] = $fieldName; + } + } + $submissionData = $submission->getData(); - $filterFields = preg_grep('/^(.*)cpr(.*)$/', array_keys($submissionData)); if (!empty($filterFields)) { foreach ($filterFields as $field) { $cpr = $submissionData[$field]; - $personal .= sprintf('CPR "%s".', $cpr); + $personal .= sprintf(' CPR "%s" in field "%s".', $cpr ?: 'null', $field); } } @@ -71,16 +83,15 @@ function os2web_audit_entity_webform_post_load_data(mixed $submissions): void { if (preg_match('~(.*)/print/pdf/(.*)|(.*)\d.*/attachment(.*)~', $request->getPathInfo())) { // We know that a webform submission has been loaded and this is a print // pdf path. This indicates that this is an attachment download action. - $msg = sprintf('Webform submission (%d) downloaded as attachment. %s Webform id "%s".', $submission->id(), $personal, $submission->getWebform()->id()); + $msg = sprintf('Webform submission (%d) downloaded as attachment.%s Webform id "%s".', $submission->id(), $personal, $submission->getWebform()->id()); os2web_audit_entity_log($msg); // Exit to prevent double log entry. return; } - $msg = sprintf('Webform submission (%d) looked up. %s Webform id "%s".', $submission->id(), $personal, $submission->getWebform()->id()); + $msg = sprintf('Webform submission (%d) looked up.%s Webform id "%s".', $submission->id(), $personal, $submission->getWebform()->id()); os2web_audit_entity_log($msg); - } } From 84e27166c2307a65129604cef28455a57b996f36 Mon Sep 17 00:00:00 2001 From: Jesper Kristensen Date: Thu, 12 Dec 2024 13:24:31 +0100 Subject: [PATCH 13/13] ITKDev: Code review ajustments Co-authored-by: Jeppe Kuhlmann Andersen <78410897+jekuaitk@users.noreply.github.com> --- modules/os2web_audit_entity/os2web_audit_entity.info.yml | 2 +- .../os2web_audit_entity.links.menu.yml | 2 +- modules/os2web_audit_entity/os2web_audit_entity.module | 8 +------- modules/os2web_audit_entity/src/Form/SettingsForm.php | 4 ++-- 4 files changed, 5 insertions(+), 11 deletions(-) diff --git a/modules/os2web_audit_entity/os2web_audit_entity.info.yml b/modules/os2web_audit_entity/os2web_audit_entity.info.yml index f5d7f7c..2e421c7 100644 --- a/modules/os2web_audit_entity/os2web_audit_entity.info.yml +++ b/modules/os2web_audit_entity/os2web_audit_entity.info.yml @@ -1,5 +1,5 @@ name: "OS2web Audit logging entity access" -description: "Logs CUD events for entities" +description: "Logs CRUD events for entities" type: module core_version_requirement: ^8 || ^9 || ^10 dependencies: diff --git a/modules/os2web_audit_entity/os2web_audit_entity.links.menu.yml b/modules/os2web_audit_entity/os2web_audit_entity.links.menu.yml index b57f18b..0431128 100644 --- a/modules/os2web_audit_entity/os2web_audit_entity.links.menu.yml +++ b/modules/os2web_audit_entity/os2web_audit_entity.links.menu.yml @@ -1,5 +1,5 @@ os2web_audit_entity.admin_settings: title: 'OS2web Audit entity settings' parent: system.admin_config_system - description: 'Settings for the OS2 Audit entity module' + description: 'Settings for the OS2web Audit entity module' route_name: os2web_audit_entity.settings diff --git a/modules/os2web_audit_entity/os2web_audit_entity.module b/modules/os2web_audit_entity/os2web_audit_entity.module index 31e291f..7aef6e3 100644 --- a/modules/os2web_audit_entity/os2web_audit_entity.module +++ b/modules/os2web_audit_entity/os2web_audit_entity.module @@ -61,7 +61,6 @@ function os2web_audit_entity_webform_post_load_data(mixed $submissions): void { // Detect field of type that contains "cpr" in name or where field name // contains "cpr". - /** @var \Drupal\webform_revisions\Entity\WebformRevisions $webform */ $webform = $submission->getWebform(); $elements = $webform->getElementsDecoded(); foreach ($elements as $fieldName => $element) { @@ -125,12 +124,7 @@ function os2web_audit_entity_log(string $message): void { // Detect user type. $account = \Drupal::currentUser(); - $apiUser = FALSE; - if (os2web_audit_entity_is_api_user($account)) { - $apiUser = TRUE; - } - $metadata['userId'] = $account->getEmail(); - $metadata['userType'] = $apiUser ? 'api' : 'web'; + $metadata['userType'] = os2web_audit_entity_is_api_user($account) ? 'api' : 'web'; $logger->info('Entity', $message, FALSE, $metadata); } diff --git a/modules/os2web_audit_entity/src/Form/SettingsForm.php b/modules/os2web_audit_entity/src/Form/SettingsForm.php index d5b54ae..68ac2d3 100644 --- a/modules/os2web_audit_entity/src/Form/SettingsForm.php +++ b/modules/os2web_audit_entity/src/Form/SettingsForm.php @@ -70,8 +70,8 @@ public function buildForm(array $form, FormStateInterface $form_state): array { $form['roles'] = [ '#type' => 'checkboxes', - '#title' => $this->t('Choose an Option'), - '#description' => $this->t('Please select an option from the dropdown menu.'), + '#title' => $this->t('Select API access roles'), + '#description' => $this->t('The selected roles will be use to determine who is accessing entities through the API.'), '#options' => $items, '#default_value' => $config->get('roles') ?? [], '#required' => TRUE,