Skip to content

Commit c0efbac

Browse files
authored
Merge pull request #7 from OS2web/feature/entities
ITKDev: Entity audit module
2 parents 8b58dc0 + 84e2716 commit c0efbac

11 files changed

+331
-2
lines changed

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88

99
## [Unreleased]
1010

11+
- Add new module to track user accessing webform submissions.
12+
- Added remote ip to all log lines.
13+
1114
## [0.1.1] - 2024-11-19
1215

1316
- Made Watchdog default logger
14-
- Updated Watchlog logger
17+
- Updated watchdog logger
1518

1619
## [0.1.0] - 2024-10-21
1720

modules/os2web_audit_entity/README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# OS2web audit entity
2+
3+
This module tries to log information about entity access and changes.
4+
5+
## Webform submission
6+
7+
This module integrates with [OS2Forms][os2forms-link], which utilizes the Webform module.
8+
9+
If you are logging users who have accessed Webform submissions but no data is being recorded, ensure the patches
10+
provided by this module are applied to the Webform module.
11+
12+
**Note:** The patch cannot be applied via Composer because Composer does not support relative paths to patches outside
13+
the webroot. Additionally, as the location of this module within the site can vary, applying the patch automatically
14+
could potentially break the Composer installation.
15+
16+
### Why this patch
17+
18+
When implementing audit logging for webform submissions in Drupal, particularly to track who accessed the data:
19+
20+
- Using `hook_entity_storage_load()` presents challenges with webform submissions due to their reliance on revisions.
21+
- This is because the hook gets triggered before the storage handler finishes loading the submission data.
22+
23+
To address this issue, a custom hook, `hook_webform_post_load_data()`, is introduced.
24+
This custom hook is invoked after the webform has successfully loaded the submission data for a given submission
25+
revision.
26+
27+
[os2forms-link]: https://github.com/OS2Forms/os2forms
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
name: "OS2web Audit logging entity access"
2+
description: "Logs CRUD events for entities"
3+
type: module
4+
core_version_requirement: ^8 || ^9 || ^10
5+
dependencies:
6+
- os2web_audit:os2web_audit
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
/**
4+
* @file
5+
* This module enabled os2web audit entity default options.
6+
*/
7+
8+
/**
9+
* Implements hook_install().
10+
*
11+
* We need to change the modules weight to ensure that all other changes to
12+
* webform submission data have been executed before this module.
13+
*
14+
* The class is set in os2forms_encrypt_entity_type_alter().
15+
*/
16+
function os2web_audit_entity_install(): void {
17+
module_set_weight('os2web_audit_entity', 19999);
18+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
os2web_audit_entity.admin_settings:
2+
title: 'OS2web Audit entity settings'
3+
parent: system.admin_config_system
4+
description: 'Settings for the OS2web Audit entity module'
5+
route_name: os2web_audit_entity.settings
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
<?php
2+
3+
/**
4+
* @file
5+
* Hooks into drupal and collect logging data.
6+
*/
7+
8+
use Drupal\Core\Entity\EntityInterface;
9+
use Drupal\Core\Session\AccountInterface;
10+
use Drupal\os2web_audit_entity\Form\SettingsForm;
11+
12+
/**
13+
* Implements hook_entity_insert().
14+
*/
15+
function os2web_audit_entity_entity_insert(EntityInterface $entity): void {
16+
$msg = sprintf('Entity (%d) of type "%s" created.', $entity->id(), $entity->getEntityTypeId());
17+
os2web_audit_entity_log($msg);
18+
}
19+
20+
/**
21+
* Implements hook_entity_update().
22+
*/
23+
function os2web_audit_entity_entity_update(EntityInterface $entity): void {
24+
$msg = sprintf('Entity (%d) of type "%s" updated.', $entity->id(), $entity->getEntityTypeId());
25+
os2web_audit_entity_log($msg);
26+
}
27+
28+
/**
29+
* Implements hook_entity_delete().
30+
*/
31+
function os2web_audit_entity_entity_delete(EntityInterface $entity): void {
32+
$msg = sprintf('Entity (%d) of type "%s" deleted.', $entity->id(), $entity->getEntityTypeId());
33+
os2web_audit_entity_log($msg);
34+
}
35+
36+
/**
37+
* Implements hook_entity_storage_load().
38+
*
39+
* Logs access for file entities.
40+
*/
41+
function os2web_audit_entity_entity_storage_load(mixed $entities, string $entity_type): void {
42+
foreach ($entities as $entity) {
43+
if ($entity_type == 'file') {
44+
/** @var \Drupal\file\Entity\File $entity */
45+
$fid = $entity->id();
46+
$uri = $entity->getFileUri();
47+
$msg = sprintf('File (%d) accessed. Uri "%s"', $fid, $uri);
48+
os2web_audit_entity_log($msg);
49+
}
50+
}
51+
}
52+
53+
/**
54+
* Implements hook_webform_post_load_data().
55+
*/
56+
function os2web_audit_entity_webform_post_load_data(mixed $submissions): void {
57+
foreach ($submissions as $submission) {
58+
// Try to check for _cpr field for extra logging information.
59+
$personal = '';
60+
$filterFields = [];
61+
62+
// Detect field of type that contains "cpr" in name or where field name
63+
// contains "cpr".
64+
$webform = $submission->getWebform();
65+
$elements = $webform->getElementsDecoded();
66+
foreach ($elements as $fieldName => $element) {
67+
if (str_contains(strtolower($element['#type']), 'cpr') || str_contains(strtolower($fieldName), 'cpr')) {
68+
$filterFields[] = $fieldName;
69+
}
70+
}
71+
72+
$submissionData = $submission->getData();
73+
if (!empty($filterFields)) {
74+
foreach ($filterFields as $field) {
75+
$cpr = $submissionData[$field];
76+
$personal .= sprintf(' CPR "%s" in field "%s".', $cpr ?: 'null', $field);
77+
}
78+
}
79+
80+
// Attachments download.
81+
$request = \Drupal::request();
82+
if (preg_match('~(.*)/print/pdf/(.*)|(.*)\d.*/attachment(.*)~', $request->getPathInfo())) {
83+
// We know that a webform submission has been loaded and this is a print
84+
// pdf path. This indicates that this is an attachment download action.
85+
$msg = sprintf('Webform submission (%d) downloaded as attachment.%s Webform id "%s".', $submission->id(), $personal, $submission->getWebform()->id());
86+
os2web_audit_entity_log($msg);
87+
88+
// Exit to prevent double log entry.
89+
return;
90+
}
91+
92+
$msg = sprintf('Webform submission (%d) looked up.%s Webform id "%s".', $submission->id(), $personal, $submission->getWebform()->id());
93+
os2web_audit_entity_log($msg);
94+
}
95+
}
96+
97+
/**
98+
* Check if the accounts roles are in the array of API roles.
99+
*
100+
* @param \Drupal\Core\Session\AccountInterface $account
101+
* User account.
102+
*
103+
* @return bool
104+
* If roles found TRUE else FALSE.
105+
*/
106+
function os2web_audit_entity_is_api_user(AccountInterface $account): bool {
107+
$roles = $account->getRoles();
108+
109+
$config = \Drupal::config(SettingsForm::$configName);
110+
$selectedRoles = $config->get('roles');
111+
112+
return !empty(array_intersect($roles, array_keys(array_filter($selectedRoles))));
113+
}
114+
115+
/**
116+
* Simple logger wrapper.
117+
*
118+
* @param string $message
119+
* Message to log.
120+
*/
121+
function os2web_audit_entity_log(string $message): void {
122+
/** @var \Drupal\os2web_audit\Service\Logger $logger */
123+
$logger = \Drupal::service('os2web_audit.logger');
124+
125+
// Detect user type.
126+
$account = \Drupal::currentUser();
127+
$metadata['userId'] = $account->getEmail();
128+
$metadata['userType'] = os2web_audit_entity_is_api_user($account) ? 'api' : 'web';
129+
$logger->info('Entity', $message, FALSE, $metadata);
130+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
os2web_audit_entity.settings:
2+
path: '/admin/config/os2web_audit/entity'
3+
defaults:
4+
_form: '\Drupal\os2web_audit_entity\Form\SettingsForm'
5+
_title: 'OS2web Audit entity settings'
6+
requirements:
7+
_permission: 'administer site'
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
diff --git a/src/WebformSubmissionStorage.php b/src/WebformSubmissionStorage.php
2+
index 4e14c3c..4c2d1c9 100644
3+
--- a/src/WebformSubmissionStorage.php
4+
+++ b/src/WebformSubmissionStorage.php
5+
@@ -168,6 +168,9 @@ class WebformSubmissionStorage extends SqlContentEntityStorage implements Webfor
6+
/** @var \Drupal\webform\WebformSubmissionInterface[] $webform_submissions */
7+
$webform_submissions = parent::doLoadMultiple($ids);
8+
$this->loadData($webform_submissions);
9+
+
10+
+ \Drupal::moduleHandler()->invokeAll('webform_post_load_data', [$webform_submissions]);
11+
+
12+
return $webform_submissions;
13+
}
14+
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
<?php
2+
3+
namespace Drupal\os2web_audit_entity\Form;
4+
5+
use Drupal\Core\Config\ConfigFactoryInterface;
6+
use Drupal\Core\Entity\EntityTypeManagerInterface;
7+
use Drupal\Core\Form\ConfigFormBase;
8+
use Drupal\Core\Form\FormStateInterface;
9+
use Symfony\Component\DependencyInjection\ContainerInterface;
10+
11+
/**
12+
* Class SettingsForm.
13+
*
14+
* This is the settings for the module.
15+
*/
16+
class SettingsForm extends ConfigFormBase {
17+
18+
/**
19+
* {@inheritdoc}
20+
*/
21+
public function __construct(
22+
ConfigFactoryInterface $configFactory,
23+
private EntityTypeManagerInterface $entityTypeManager,
24+
) {
25+
parent::__construct($configFactory);
26+
}
27+
28+
/**
29+
* {@inheritdoc}
30+
*/
31+
public static function create(ContainerInterface $container): static {
32+
return new static(
33+
$container->get('config.factory'),
34+
$container->get('entity_type.manager')
35+
);
36+
}
37+
38+
/**
39+
* The name of the configuration setting.
40+
*
41+
* @var string
42+
*/
43+
public static string $configName = 'os2web_audit_entity.settings';
44+
45+
/**
46+
* {@inheritdoc}
47+
*/
48+
protected function getEditableConfigNames(): array {
49+
return [self::$configName];
50+
}
51+
52+
/**
53+
* {@inheritdoc}
54+
*/
55+
public function getFormId(): string {
56+
return 'os2web_audit_entity_admin_form';
57+
}
58+
59+
/**
60+
* {@inheritdoc}
61+
*/
62+
public function buildForm(array $form, FormStateInterface $form_state): array {
63+
$items = [];
64+
$roles = $this->getRoles();
65+
foreach ($roles as $role) {
66+
$items[$role->id()] = $role->label();
67+
}
68+
69+
$config = $this->config(self::$configName);
70+
71+
$form['roles'] = [
72+
'#type' => 'checkboxes',
73+
'#title' => $this->t('Select API access roles'),
74+
'#description' => $this->t('The selected roles will be use to determine who is accessing entities through the API.'),
75+
'#options' => $items,
76+
'#default_value' => $config->get('roles') ?? [],
77+
'#required' => TRUE,
78+
];
79+
80+
return parent::buildForm($form, $form_state);
81+
}
82+
83+
/**
84+
* {@inheritdoc}
85+
*/
86+
public function submitForm(array &$form, FormStateInterface $form_state): void {
87+
parent::submitForm($form, $form_state);
88+
89+
$this->config(self::$configName)
90+
->set('roles', $form_state->getValue('roles'))
91+
->save();
92+
}
93+
94+
/**
95+
* Get all roles.
96+
*
97+
* @return array<\Drupal\Core\Entity\EntityInterface>
98+
* An array of role entities.
99+
*
100+
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
101+
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
102+
*/
103+
private function getRoles() {
104+
// Use the role storage to load roles.
105+
$roleStorage = $this->entityTypeManager->getStorage('user_role');
106+
107+
return $roleStorage->loadMultiple();
108+
}
109+
110+
}

os2web_audit.services.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@ services:
55

66
os2web_audit.logger:
77
class: Drupal\os2web_audit\Service\Logger
8-
arguments: ['@plugin.manager.os2web_audit_logger', '@config.factory', '@current_user', '@logger.factory']
8+
arguments: ['@plugin.manager.os2web_audit_logger', '@config.factory', '@current_user', '@logger.factory', '@request_stack']

0 commit comments

Comments
 (0)