-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #7 from OS2web/feature/entities
ITKDev: Entity audit module
- Loading branch information
Showing
11 changed files
with
331 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
name: "OS2web Audit logging entity access" | ||
description: "Logs CRUD events for entities" | ||
type: module | ||
core_version_requirement: ^8 || ^9 || ^10 | ||
dependencies: | ||
- os2web_audit:os2web_audit |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
<?php | ||
|
||
/** | ||
* @file | ||
* This module enabled os2web audit entity default options. | ||
*/ | ||
|
||
/** | ||
* Implements hook_install(). | ||
* | ||
* We need to change the modules weight to ensure that all other changes to | ||
* webform submission data have been executed before this module. | ||
* | ||
* The class is set in os2forms_encrypt_entity_type_alter(). | ||
*/ | ||
function os2web_audit_entity_install(): void { | ||
module_set_weight('os2web_audit_entity', 19999); | ||
} |
5 changes: 5 additions & 0 deletions
5
modules/os2web_audit_entity/os2web_audit_entity.links.menu.yml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
os2web_audit_entity.admin_settings: | ||
title: 'OS2web Audit entity settings' | ||
parent: system.admin_config_system | ||
description: 'Settings for the OS2web Audit entity module' | ||
route_name: os2web_audit_entity.settings |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
<?php | ||
|
||
/** | ||
* @file | ||
* Hooks into drupal and collect logging data. | ||
*/ | ||
|
||
use Drupal\Core\Entity\EntityInterface; | ||
use Drupal\Core\Session\AccountInterface; | ||
use Drupal\os2web_audit_entity\Form\SettingsForm; | ||
|
||
/** | ||
* 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); | ||
} | ||
|
||
/** | ||
* Implements hook_entity_update(). | ||
*/ | ||
function os2web_audit_entity_entity_update(EntityInterface $entity): void { | ||
$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 { | ||
$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(mixed $entities, string $entity_type): void { | ||
foreach ($entities as $entity) { | ||
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); | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* 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 = ''; | ||
$filterFields = []; | ||
|
||
// Detect field of type that contains "cpr" in name or where field name | ||
// contains "cpr". | ||
$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(); | ||
if (!empty($filterFields)) { | ||
foreach ($filterFields as $field) { | ||
$cpr = $submissionData[$field]; | ||
$personal .= sprintf(' CPR "%s" in field "%s".', $cpr ?: 'null', $field); | ||
} | ||
} | ||
|
||
// 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 | ||
// 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); | ||
} | ||
} | ||
|
||
/** | ||
* 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(); | ||
|
||
$config = \Drupal::config(SettingsForm::$configName); | ||
$selectedRoles = $config->get('roles'); | ||
|
||
return !empty(array_intersect($roles, array_keys(array_filter($selectedRoles)))); | ||
} | ||
|
||
/** | ||
* Simple logger wrapper. | ||
* | ||
* @param string $message | ||
* Message to log. | ||
*/ | ||
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(); | ||
$metadata['userId'] = $account->getEmail(); | ||
$metadata['userType'] = os2web_audit_entity_is_api_user($account) ? 'api' : 'web'; | ||
$logger->info('Entity', $message, FALSE, $metadata); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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' |
14 changes: 14 additions & 0 deletions
14
modules/os2web_audit_entity/patches/webform_hook_webform_post_load_data.diff
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
<?php | ||
|
||
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 Symfony\Component\DependencyInjection\ContainerInterface; | ||
|
||
/** | ||
* Class SettingsForm. | ||
* | ||
* This is the settings for the module. | ||
*/ | ||
class SettingsForm extends ConfigFormBase { | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function __construct( | ||
ConfigFactoryInterface $configFactory, | ||
private EntityTypeManagerInterface $entityTypeManager, | ||
) { | ||
parent::__construct($configFactory); | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public static function create(ContainerInterface $container): static { | ||
return new static( | ||
$container->get('config.factory'), | ||
$container->get('entity_type.manager') | ||
); | ||
} | ||
|
||
/** | ||
* 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 = $this->getRoles(); | ||
foreach ($roles as $role) { | ||
$items[$role->id()] = $role->label(); | ||
} | ||
|
||
$config = $this->config(self::$configName); | ||
|
||
$form['roles'] = [ | ||
'#type' => 'checkboxes', | ||
'#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, | ||
]; | ||
|
||
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(); | ||
} | ||
|
||
/** | ||
* 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(); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters