From 55781f7c3068fdebf0f9aa3d76845779ccebd899 Mon Sep 17 00:00:00 2001 From: Ghassen kefi Date: Fri, 2 Oct 2026 11:43:36 +0200 Subject: [PATCH] feat(admin_audit): add stable operation identifier to audit log entries Every audit log entry now carries a machine-readable "operation" field (e.g. "files.file.read") in its data, next to the human-readable message. Log consumers can filter on it instead of matching message text, which may change between releases. The names are defined once in the new OCA\AdminAudit\Operation enum. CriticalActionPerformedEvent gets an optional $operation parameter so other apps can provide their own. Refs nextcloud-gmbh/governance#199 Assisted-by: ClaudeCode:claude-opus-5-5 Signed-off-by: Ghassen kefi --- .../composer/composer/autoload_classmap.php | 1 + .../composer/composer/autoload_static.php | 1 + apps/admin_audit/lib/Actions/Action.php | 12 ++- apps/admin_audit/lib/Actions/Files.php | 7 ++ apps/admin_audit/lib/Actions/Sharing.php | 7 ++ apps/admin_audit/lib/Actions/Trashbin.php | 6 +- apps/admin_audit/lib/Actions/Versions.php | 4 +- .../Listener/AppManagementEventListener.php | 9 +- .../lib/Listener/AuthEventListener.php | 5 + .../lib/Listener/CacheEventListener.php | 5 +- .../lib/Listener/ConsoleEventListener.php | 3 +- .../CriticalActionPerformedEventListener.php | 1 + .../lib/Listener/FileEventListener.php | 4 +- .../Listener/GroupManagementEventListener.php | 9 +- .../lib/Listener/SecurityEventListener.php | 3 + .../lib/Listener/SharingEventListener.php | 19 ++++ .../lib/Listener/TagEventListener.php | 3 +- .../Listener/UserManagementEventListener.php | 13 ++- apps/admin_audit/lib/Operation.php | 75 +++++++++++++ apps/admin_audit/tests/Actions/ActionTest.php | 87 +++++++++++++++ apps/admin_audit/tests/Actions/FilesTest.php | 79 ++++++++++++++ .../admin_audit/tests/Actions/SharingTest.php | 79 ++++++++++++++ .../tests/Actions/TrashbinTest.php | 43 ++++++++ .../tests/Actions/VersionsTest.php | 35 ++++++ .../AppManagementEventListenerTest.php | 56 ++++++++++ .../tests/Listener/AuthEventListenerTest.php | 68 ++++++++++++ .../tests/Listener/CacheEventListenerTest.php | 48 +++++++++ .../Listener/ConsoleEventListenerTest.php | 36 +++++++ ...iticalActionPerformedEventListenerTest.php | 16 +++ .../tests/Listener/FileEventListenerTest.php | 58 ++++++++++ .../GroupManagementEventListenerTest.php | 66 ++++++++++++ .../Listener/SecurityEventListenerTest.php | 4 +- .../Listener/SharingEventListenerTest.php | 102 ++++++++++++++++++ .../tests/Listener/TagEventListenerTest.php | 42 ++++++++ .../UserManagementEventListenerTest.php | 52 ++++++++- .../Audit/CriticalActionPerformedEvent.php | 15 ++- 36 files changed, 1046 insertions(+), 27 deletions(-) create mode 100644 apps/admin_audit/lib/Operation.php create mode 100644 apps/admin_audit/tests/Actions/ActionTest.php create mode 100644 apps/admin_audit/tests/Actions/FilesTest.php create mode 100644 apps/admin_audit/tests/Actions/SharingTest.php create mode 100644 apps/admin_audit/tests/Actions/TrashbinTest.php create mode 100644 apps/admin_audit/tests/Actions/VersionsTest.php create mode 100644 apps/admin_audit/tests/Listener/AppManagementEventListenerTest.php create mode 100644 apps/admin_audit/tests/Listener/AuthEventListenerTest.php create mode 100644 apps/admin_audit/tests/Listener/CacheEventListenerTest.php create mode 100644 apps/admin_audit/tests/Listener/ConsoleEventListenerTest.php create mode 100644 apps/admin_audit/tests/Listener/FileEventListenerTest.php create mode 100644 apps/admin_audit/tests/Listener/GroupManagementEventListenerTest.php create mode 100644 apps/admin_audit/tests/Listener/SharingEventListenerTest.php create mode 100644 apps/admin_audit/tests/Listener/TagEventListenerTest.php diff --git a/apps/admin_audit/composer/composer/autoload_classmap.php b/apps/admin_audit/composer/composer/autoload_classmap.php index 9f496d408eccb..72bf58847b828 100644 --- a/apps/admin_audit/composer/composer/autoload_classmap.php +++ b/apps/admin_audit/composer/composer/autoload_classmap.php @@ -27,4 +27,5 @@ 'OCA\\AdminAudit\\Listener\\SharingEventListener' => $baseDir . '/../lib/Listener/SharingEventListener.php', 'OCA\\AdminAudit\\Listener\\TagEventListener' => $baseDir . '/../lib/Listener/TagEventListener.php', 'OCA\\AdminAudit\\Listener\\UserManagementEventListener' => $baseDir . '/../lib/Listener/UserManagementEventListener.php', + 'OCA\\AdminAudit\\Operation' => $baseDir . '/../lib/Operation.php', ); diff --git a/apps/admin_audit/composer/composer/autoload_static.php b/apps/admin_audit/composer/composer/autoload_static.php index 4be8380ff543a..25b67cae98869 100644 --- a/apps/admin_audit/composer/composer/autoload_static.php +++ b/apps/admin_audit/composer/composer/autoload_static.php @@ -42,6 +42,7 @@ class ComposerStaticInitAdminAudit 'OCA\\AdminAudit\\Listener\\SharingEventListener' => __DIR__ . '/..' . '/../lib/Listener/SharingEventListener.php', 'OCA\\AdminAudit\\Listener\\TagEventListener' => __DIR__ . '/..' . '/../lib/Listener/TagEventListener.php', 'OCA\\AdminAudit\\Listener\\UserManagementEventListener' => __DIR__ . '/..' . '/../lib/Listener/UserManagementEventListener.php', + 'OCA\\AdminAudit\\Operation' => __DIR__ . '/..' . '/../lib/Operation.php', ); public static function getInitializer(ClassLoader $loader) diff --git a/apps/admin_audit/lib/Actions/Action.php b/apps/admin_audit/lib/Actions/Action.php index 7d41eff4df6e4..cedf4a78fcb90 100644 --- a/apps/admin_audit/lib/Actions/Action.php +++ b/apps/admin_audit/lib/Actions/Action.php @@ -9,6 +9,7 @@ namespace OCA\AdminAudit\Actions; use OCA\AdminAudit\IAuditLogger; +use OCA\AdminAudit\Operation; class Action { @@ -20,21 +21,28 @@ public function __construct( /** * Log a single action with a log level of info * + * @param Operation|string|null $operation Stable identifier of the action. A string in the form `app.entity.action` is only expected from other apps via CriticalActionPerformedEvent * @param string $text * @param array $params * @param list $elements * @param bool $obfuscateParameters */ public function log( + Operation|string|null $operation, string $text, array $params, array $elements, bool $obfuscateParameters = false, ): void { + $baseContext = ['app' => 'admin_audit']; + if ($operation !== null) { + $baseContext['operation'] = $operation instanceof Operation ? $operation->value : $operation; + } + foreach ($elements as $element) { if (!array_key_exists($element, $params)) { $message = '$params["' . $element . '"] was missing.'; - $context = ['app' => 'admin_audit']; + $context = $baseContext; if (!$obfuscateParameters) { $message .= ' Transferred value: {params}'; @@ -47,7 +55,7 @@ public function log( } $replaceArray = []; - $context = ['app' => 'admin_audit']; + $context = $baseContext; foreach ($elements as $element) { $value = $params[$element]; if ($value instanceof \DateTimeInterface) { diff --git a/apps/admin_audit/lib/Actions/Files.php b/apps/admin_audit/lib/Actions/Files.php index 71048345f8f76..c6a8c122ecdbf 100644 --- a/apps/admin_audit/lib/Actions/Files.php +++ b/apps/admin_audit/lib/Actions/Files.php @@ -9,6 +9,7 @@ namespace OCA\AdminAudit\Actions; use OC\Files\Node\NonExistingFile; +use OCA\AdminAudit\Operation; use OCP\Files\Events\Node\BeforeNodeDeletedEvent; use OCP\Files\Events\Node\BeforeNodeReadEvent; use OCP\Files\Events\Node\NodeCopiedEvent; @@ -43,6 +44,7 @@ public function read(BeforeNodeReadEvent $event): void { return; } $this->log( + Operation::FileRead, 'File with id "%s" accessed: "%s"', $params, array_keys($params) @@ -69,6 +71,7 @@ public function afterRename(NodeRenamedEvent $event): void { } $this->log( + Operation::FileRenamed, 'File renamed with id "%s" from "%s" to "%s"', $params, array_keys($params) @@ -95,6 +98,7 @@ public function create(NodeCreatedEvent $event): void { return; } $this->log( + Operation::FileCreated, 'File with id "%s" created: "%s"', $params, array_keys($params) @@ -121,6 +125,7 @@ public function copy(NodeCopiedEvent $event): void { return; } $this->log( + Operation::FileCopied, 'File id copied from: "%s" to "%s", path from "%s" to "%s"', $params, array_keys($params) @@ -148,6 +153,7 @@ public function write(NodeWrittenEvent $event): void { } $this->log( + Operation::FileWritten, 'File with id "%s" written to: "%s"', $params, array_keys($params) @@ -171,6 +177,7 @@ public function delete(BeforeNodeDeletedEvent $event): void { return; } $this->log( + Operation::FileDeleted, 'File with id "%s" deleted: "%s"', $params, array_keys($params) diff --git a/apps/admin_audit/lib/Actions/Sharing.php b/apps/admin_audit/lib/Actions/Sharing.php index 424fe63757c58..226364ad83df9 100644 --- a/apps/admin_audit/lib/Actions/Sharing.php +++ b/apps/admin_audit/lib/Actions/Sharing.php @@ -8,6 +8,8 @@ namespace OCA\AdminAudit\Actions; +use OCA\AdminAudit\Operation; + /** * Class Sharing logs the sharing actions * @@ -22,6 +24,7 @@ class Sharing extends Action { */ public function updatePermissions(array $params): void { $this->log( + Operation::SharePermissionsUpdated, 'The permissions of the shared %s "%s" with ID "%s" have been changed to "%s"', $params, [ @@ -40,6 +43,7 @@ public function updatePermissions(array $params): void { */ public function updatePassword(array $params): void { $this->log( + Operation::SharePasswordUpdated, 'The password of the publicly shared %s "%s" with ID "%s" has been changed', $params, [ @@ -58,6 +62,7 @@ public function updatePassword(array $params): void { public function updateExpirationDate(array $params): void { if ($params['date'] === null) { $this->log( + Operation::ShareExpirationRemoved, 'The expiration date of the publicly shared %s with ID "%s" has been removed', $params, [ @@ -67,6 +72,7 @@ public function updateExpirationDate(array $params): void { ); } else { $this->log( + Operation::ShareExpirationUpdated, 'The expiration date of the publicly shared %s with ID "%s" has been changed to "%s"', $params, [ @@ -85,6 +91,7 @@ public function updateExpirationDate(array $params): void { */ public function shareAccessed(array $params): void { $this->log( + Operation::ShareLinkAccessed, 'The shared %s with the token "%s" by "%s" has been accessed.', $params, [ diff --git a/apps/admin_audit/lib/Actions/Trashbin.php b/apps/admin_audit/lib/Actions/Trashbin.php index c7503ed1becc4..83463a1624f70 100644 --- a/apps/admin_audit/lib/Actions/Trashbin.php +++ b/apps/admin_audit/lib/Actions/Trashbin.php @@ -8,15 +8,17 @@ namespace OCA\AdminAudit\Actions; +use OCA\AdminAudit\Operation; + class Trashbin extends Action { public function delete(array $params): void { - $this->log('File "%s" deleted from trash bin.', + $this->log(Operation::TrashbinFileDeleted, 'File "%s" deleted from trash bin.', ['path' => $params['path']], ['path'] ); } public function restore(array $params): void { - $this->log('File "%s" restored from trash bin.', + $this->log(Operation::TrashbinFileRestored, 'File "%s" restored from trash bin.', ['path' => $params['filePath']], ['path'] ); } diff --git a/apps/admin_audit/lib/Actions/Versions.php b/apps/admin_audit/lib/Actions/Versions.php index a7b18119ea28e..88cdb50a6ddc4 100644 --- a/apps/admin_audit/lib/Actions/Versions.php +++ b/apps/admin_audit/lib/Actions/Versions.php @@ -8,9 +8,11 @@ namespace OCA\AdminAudit\Actions; +use OCA\AdminAudit\Operation; + class Versions extends Action { public function delete(array $params): void { - $this->log('Version "%s" was deleted.', + $this->log(Operation::VersionDeleted, 'Version "%s" was deleted.', ['path' => $params['path']], ['path'] ); diff --git a/apps/admin_audit/lib/Listener/AppManagementEventListener.php b/apps/admin_audit/lib/Listener/AppManagementEventListener.php index 87b3f538fd3ab..bdd348fa5297b 100644 --- a/apps/admin_audit/lib/Listener/AppManagementEventListener.php +++ b/apps/admin_audit/lib/Listener/AppManagementEventListener.php @@ -10,6 +10,7 @@ namespace OCA\AdminAudit\Listener; use OCA\AdminAudit\Actions\Action; +use OCA\AdminAudit\Operation; use OCP\App\Events\AppDisableEvent; use OCP\App\Events\AppEnableEvent; use OCP\App\Events\AppUpdateEvent; @@ -33,12 +34,12 @@ public function handle(Event $event): void { private function appEnable(AppEnableEvent $event): void { if (empty($event->getGroupIds())) { - $this->log('App "%s" enabled', + $this->log(Operation::AppEnabled, 'App "%s" enabled', ['app' => $event->getAppId()], ['app'] ); } else { - $this->log('App "%1$s" enabled for groups: %2$s', + $this->log(Operation::AppEnabled, 'App "%1$s" enabled for groups: %2$s', ['app' => $event->getAppId(), 'groups' => implode(', ', $event->getGroupIds())], ['app', 'groups'] ); @@ -46,14 +47,14 @@ private function appEnable(AppEnableEvent $event): void { } private function appDisable(AppDisableEvent $event): void { - $this->log('App "%s" disabled', + $this->log(Operation::AppDisabled, 'App "%s" disabled', ['app' => $event->getAppId()], ['app'] ); } private function appUpdate(AppUpdateEvent $event): void { - $this->log('App "%s" updated', + $this->log(Operation::AppUpdated, 'App "%s" updated', ['app' => $event->getAppId()], ['app'] ); diff --git a/apps/admin_audit/lib/Listener/AuthEventListener.php b/apps/admin_audit/lib/Listener/AuthEventListener.php index dc4d857f59cfe..10f19f71ac43c 100644 --- a/apps/admin_audit/lib/Listener/AuthEventListener.php +++ b/apps/admin_audit/lib/Listener/AuthEventListener.php @@ -10,6 +10,7 @@ namespace OCA\AdminAudit\Listener; use OCA\AdminAudit\Actions\Action; +use OCA\AdminAudit\Operation; use OCP\Authentication\Events\AnyLoginFailedEvent; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; @@ -37,6 +38,7 @@ public function handle(Event $event): void { private function beforeUserLoggedIn(BeforeUserLoggedInEvent $event): void { $this->log( + Operation::LoginAttempted, 'Login attempt: "%s"', [ 'uid' => $event->getUsername() @@ -50,6 +52,7 @@ private function beforeUserLoggedIn(BeforeUserLoggedInEvent $event): void { private function userLoggedIn(UserLoggedInWithCookieEvent|UserLoggedInEvent $event): void { $this->log( + Operation::LoginSucceeded, 'Login successful: "%s"', [ 'uid' => $event->getUser()->getUID() @@ -63,6 +66,7 @@ private function userLoggedIn(UserLoggedInWithCookieEvent|UserLoggedInEvent $eve private function beforeUserLogout(BeforeUserLoggedOutEvent $event): void { $this->log( + Operation::LogoutPerformed, 'Logout occurred', [], [] @@ -71,6 +75,7 @@ private function beforeUserLogout(BeforeUserLoggedOutEvent $event): void { private function anyLoginFailed(AnyLoginFailedEvent $event): void { $this->log( + Operation::LoginFailed, 'Login failed: "%s"', [ 'loginName' => $event->getLoginName() diff --git a/apps/admin_audit/lib/Listener/CacheEventListener.php b/apps/admin_audit/lib/Listener/CacheEventListener.php index 76b0f94e4d7e2..e4eed67762ef6 100644 --- a/apps/admin_audit/lib/Listener/CacheEventListener.php +++ b/apps/admin_audit/lib/Listener/CacheEventListener.php @@ -10,6 +10,7 @@ namespace OCA\AdminAudit\Listener; use OCA\AdminAudit\Actions\Action; +use OCA\AdminAudit\Operation; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; use OCP\Files\Cache\CacheEntryInsertedEvent; @@ -29,7 +30,7 @@ public function handle(Event $event): void { } private function entryInserted(CacheEntryInsertedEvent $event): void { - $this->log('Cache entry inserted for fileid "%1$d", path "%2$s" on storageid "%3$d"', + $this->log(Operation::CacheEntryInserted, 'Cache entry inserted for fileid "%1$d", path "%2$s" on storageid "%3$d"', [ 'fileid' => $event->getFileId(), 'path' => $event->getPath(), @@ -40,7 +41,7 @@ private function entryInserted(CacheEntryInsertedEvent $event): void { } private function entryRemoved(CacheEntryRemovedEvent $event): void { - $this->log('Cache entry removed for fileid "%1$d", path "%2$s" on storageid "%3$d"', + $this->log(Operation::CacheEntryRemoved, 'Cache entry removed for fileid "%1$d", path "%2$s" on storageid "%3$d"', [ 'fileid' => $event->getFileId(), 'path' => $event->getPath(), diff --git a/apps/admin_audit/lib/Listener/ConsoleEventListener.php b/apps/admin_audit/lib/Listener/ConsoleEventListener.php index 8527c6331fd77..2ed8aa028561c 100644 --- a/apps/admin_audit/lib/Listener/ConsoleEventListener.php +++ b/apps/admin_audit/lib/Listener/ConsoleEventListener.php @@ -10,6 +10,7 @@ namespace OCA\AdminAudit\Listener; use OCA\AdminAudit\Actions\Action; +use OCA\AdminAudit\Operation; use OCP\Console\ConsoleEvent; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; @@ -35,7 +36,7 @@ private function runCommand(ConsoleEvent $event): void { // Remove `./occ` array_shift($arguments); - $this->log('Console command executed: %s', + $this->log(Operation::ConsoleCommandExecuted, 'Console command executed: %s', ['arguments' => implode(' ', $arguments)], ['arguments'] ); diff --git a/apps/admin_audit/lib/Listener/CriticalActionPerformedEventListener.php b/apps/admin_audit/lib/Listener/CriticalActionPerformedEventListener.php index 38c6922b015d3..743cabbc0c936 100644 --- a/apps/admin_audit/lib/Listener/CriticalActionPerformedEventListener.php +++ b/apps/admin_audit/lib/Listener/CriticalActionPerformedEventListener.php @@ -23,6 +23,7 @@ public function handle(Event $event): void { } $this->log( + $event->getOperation(), $event->getLogMessage(), $event->getParameters(), array_keys($event->getParameters()), diff --git a/apps/admin_audit/lib/Listener/FileEventListener.php b/apps/admin_audit/lib/Listener/FileEventListener.php index 20c0a2831f922..1d8902673e26c 100644 --- a/apps/admin_audit/lib/Listener/FileEventListener.php +++ b/apps/admin_audit/lib/Listener/FileEventListener.php @@ -10,6 +10,7 @@ namespace OCA\AdminAudit\Listener; use OCA\AdminAudit\Actions\Action; +use OCA\AdminAudit\Operation; use OCA\Files_Versions\Events\VersionRestoredEvent; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; @@ -47,6 +48,7 @@ private function beforePreviewFetched(BeforePreviewFetchedEvent $event): void { 'path' => $file->getPath(), ]; $this->log( + Operation::PreviewAccessed, 'Preview accessed: (id: "%s", width: "%s", height: "%s" crop: "%s", mode: "%s", path: "%s")', $params, array_keys($params) @@ -64,7 +66,7 @@ private function beforePreviewFetched(BeforePreviewFetchedEvent $event): void { */ private function versionRestored(VersionRestoredEvent $event): void { $version = $event->getVersion(); - $this->log('Version "%s" of "%s" was restored.', + $this->log(Operation::VersionRestored, 'Version "%s" of "%s" was restored.', [ 'version' => $version->getRevisionId(), 'path' => $version->getVersionPath() diff --git a/apps/admin_audit/lib/Listener/GroupManagementEventListener.php b/apps/admin_audit/lib/Listener/GroupManagementEventListener.php index b17ff74829602..25d123ca39c6e 100644 --- a/apps/admin_audit/lib/Listener/GroupManagementEventListener.php +++ b/apps/admin_audit/lib/Listener/GroupManagementEventListener.php @@ -10,6 +10,7 @@ namespace OCA\AdminAudit\Listener; use OCA\AdminAudit\Actions\Action; +use OCA\AdminAudit\Operation; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; use OCP\Group\Events\GroupCreatedEvent; @@ -35,7 +36,7 @@ public function handle(Event $event): void { } private function userAdded(UserAddedEvent $event): void { - $this->log('User "%s" added to group "%s"', + $this->log(Operation::GroupMemberAdded, 'User "%s" added to group "%s"', [ 'group' => $event->getGroup()->getGID(), 'user' => $event->getUser()->getUID() @@ -47,7 +48,7 @@ private function userAdded(UserAddedEvent $event): void { } private function userRemoved(UserRemovedEvent $event): void { - $this->log('User "%s" removed from group "%s"', + $this->log(Operation::GroupMemberRemoved, 'User "%s" removed from group "%s"', [ 'group' => $event->getGroup()->getGID(), 'user' => $event->getUser()->getUID() @@ -59,7 +60,7 @@ private function userRemoved(UserRemovedEvent $event): void { } private function groupCreated(GroupCreatedEvent $event): void { - $this->log('Group created: "%s"', + $this->log(Operation::GroupCreated, 'Group created: "%s"', [ 'group' => $event->getGroup()->getGID() ], @@ -70,7 +71,7 @@ private function groupCreated(GroupCreatedEvent $event): void { } private function groupDeleted(GroupDeletedEvent $event): void { - $this->log('Group deleted: "%s"', + $this->log(Operation::GroupDeleted, 'Group deleted: "%s"', [ 'group' => $event->getGroup()->getGID() ], diff --git a/apps/admin_audit/lib/Listener/SecurityEventListener.php b/apps/admin_audit/lib/Listener/SecurityEventListener.php index 192433c54b9bc..994867c0fcaf8 100644 --- a/apps/admin_audit/lib/Listener/SecurityEventListener.php +++ b/apps/admin_audit/lib/Listener/SecurityEventListener.php @@ -10,6 +10,7 @@ namespace OCA\AdminAudit\Listener; use OCA\AdminAudit\Actions\Action; +use OCA\AdminAudit\Operation; use OCP\Authentication\TwoFactorAuth\TwoFactorProviderChallengeFailed; use OCP\Authentication\TwoFactorAuth\TwoFactorProviderChallengePassed; use OCP\EventDispatcher\Event; @@ -30,6 +31,7 @@ public function handle(Event $event): void { private function twoFactorProviderChallengePassed(TwoFactorProviderChallengePassed $event): void { $this->log( + Operation::TwoFactorPassed, 'Successful two factor attempt by user %s (%s) with provider %s', [ 'uid' => $event->getUser()->getUID(), @@ -46,6 +48,7 @@ private function twoFactorProviderChallengePassed(TwoFactorProviderChallengePass private function twoFactorProviderChallengeFailed(TwoFactorProviderChallengeFailed $event): void { $this->log( + Operation::TwoFactorFailed, 'Failed two factor attempt by user %s (%s) with provider %s', [ 'uid' => $event->getUser()->getUID(), diff --git a/apps/admin_audit/lib/Listener/SharingEventListener.php b/apps/admin_audit/lib/Listener/SharingEventListener.php index 3131a09aaab99..96599c1dc42e5 100644 --- a/apps/admin_audit/lib/Listener/SharingEventListener.php +++ b/apps/admin_audit/lib/Listener/SharingEventListener.php @@ -10,6 +10,7 @@ namespace OCA\AdminAudit\Listener; use OCA\AdminAudit\Actions\Action; +use OCA\AdminAudit\Operation; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; use OCP\Share\Events\ShareCreatedEvent; @@ -43,6 +44,7 @@ private function shareCreated(ShareCreatedEvent $event): void { match ($share->getShareType()) { IShare::TYPE_LINK => $this->log( + Operation::ShareCreated, 'The %s "%s" with ID "%s" has been shared via link with permissions "%s" (Share ID: %s)', $params, [ @@ -54,6 +56,7 @@ private function shareCreated(ShareCreatedEvent $event): void { ] ), IShare::TYPE_USER => $this->log( + Operation::ShareCreated, 'The %s "%s" with ID "%s" has been shared to the user "%s" with permissions "%s" (Share ID: %s)', $params, [ @@ -66,6 +69,7 @@ private function shareCreated(ShareCreatedEvent $event): void { ] ), IShare::TYPE_GROUP => $this->log( + Operation::ShareCreated, 'The %s "%s" with ID "%s" has been shared to the group "%s" with permissions "%s" (Share ID: %s)', $params, [ @@ -78,6 +82,7 @@ private function shareCreated(ShareCreatedEvent $event): void { ] ), IShare::TYPE_ROOM => $this->log( + Operation::ShareCreated, 'The %s "%s" with ID "%s" has been shared to the room "%s" with permissions "%s" (Share ID: %s)', $params, [ @@ -90,6 +95,7 @@ private function shareCreated(ShareCreatedEvent $event): void { ] ), IShare::TYPE_EMAIL => $this->log( + Operation::ShareCreated, 'The %s "%s" with ID "%s" has been shared to the email recipient "%s" with permissions "%s" (Share ID: %s)', $params, [ @@ -102,6 +108,7 @@ private function shareCreated(ShareCreatedEvent $event): void { ] ), IShare::TYPE_CIRCLE => $this->log( + Operation::ShareCreated, 'The %s "%s" with ID "%s" has been shared to the circle "%s" with permissions "%s" (Share ID: %s)', $params, [ @@ -114,6 +121,7 @@ private function shareCreated(ShareCreatedEvent $event): void { ] ), IShare::TYPE_REMOTE => $this->log( + Operation::ShareCreated, 'The %s "%s" with ID "%s" has been shared to the remote user "%s" with permissions "%s" (Share ID: %s)', $params, [ @@ -126,6 +134,7 @@ private function shareCreated(ShareCreatedEvent $event): void { ] ), IShare::TYPE_REMOTE_GROUP => $this->log( + Operation::ShareCreated, 'The %s "%s" with ID "%s" has been shared to the remote group "%s" with permissions "%s" (Share ID: %s)', $params, [ @@ -138,6 +147,7 @@ private function shareCreated(ShareCreatedEvent $event): void { ] ), IShare::TYPE_DECK => $this->log( + Operation::ShareCreated, 'The %s "%s" with ID "%s" has been shared to the deck card "%s" with permissions "%s" (Share ID: %s)', $params, [ @@ -166,6 +176,7 @@ private function shareDeleted(ShareDeletedEvent $event): void { match ($share->getShareType()) { IShare::TYPE_LINK => $this->log( + Operation::ShareDeleted, 'The %s "%s" with ID "%s" has been unshared (Share ID: %s)', $params, [ @@ -176,6 +187,7 @@ private function shareDeleted(ShareDeletedEvent $event): void { ] ), IShare::TYPE_USER => $this->log( + Operation::ShareDeleted, 'The %s "%s" with ID "%s" has been unshared from the user "%s" (Share ID: %s)', $params, [ @@ -187,6 +199,7 @@ private function shareDeleted(ShareDeletedEvent $event): void { ] ), IShare::TYPE_GROUP => $this->log( + Operation::ShareDeleted, 'The %s "%s" with ID "%s" has been unshared from the group "%s" (Share ID: %s)', $params, [ @@ -198,6 +211,7 @@ private function shareDeleted(ShareDeletedEvent $event): void { ] ), IShare::TYPE_ROOM => $this->log( + Operation::ShareDeleted, 'The %s "%s" with ID "%s" has been unshared from the room "%s" (Share ID: %s)', $params, [ @@ -209,6 +223,7 @@ private function shareDeleted(ShareDeletedEvent $event): void { ] ), IShare::TYPE_EMAIL => $this->log( + Operation::ShareDeleted, 'The %s "%s" with ID "%s" has been unshared from the email recipient "%s" (Share ID: %s)', $params, [ @@ -220,6 +235,7 @@ private function shareDeleted(ShareDeletedEvent $event): void { ] ), IShare::TYPE_CIRCLE => $this->log( + Operation::ShareDeleted, 'The %s "%s" with ID "%s" has been unshared from the circle "%s" (Share ID: %s)', $params, [ @@ -231,6 +247,7 @@ private function shareDeleted(ShareDeletedEvent $event): void { ] ), IShare::TYPE_REMOTE => $this->log( + Operation::ShareDeleted, 'The %s "%s" with ID "%s" has been unshared from the remote user "%s" (Share ID: %s)', $params, [ @@ -242,6 +259,7 @@ private function shareDeleted(ShareDeletedEvent $event): void { ] ), IShare::TYPE_REMOTE_GROUP => $this->log( + Operation::ShareDeleted, 'The %s "%s" with ID "%s" has been unshared from the remote group "%s" (Share ID: %s)', $params, [ @@ -253,6 +271,7 @@ private function shareDeleted(ShareDeletedEvent $event): void { ] ), IShare::TYPE_DECK => $this->log( + Operation::ShareDeleted, 'The %s "%s" with ID "%s" has been unshared from the deck card "%s" (Share ID: %s)', $params, [ diff --git a/apps/admin_audit/lib/Listener/TagEventListener.php b/apps/admin_audit/lib/Listener/TagEventListener.php index ca90ce10f621e..4330393dd98ab 100644 --- a/apps/admin_audit/lib/Listener/TagEventListener.php +++ b/apps/admin_audit/lib/Listener/TagEventListener.php @@ -11,6 +11,7 @@ namespace OCA\AdminAudit\Listener; use OCA\AdminAudit\Actions\Action; +use OCA\AdminAudit\Operation; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; use OCP\SystemTag\Events\TagCreatedEvent; @@ -28,7 +29,7 @@ public function handle(Event $event): void { $tag = $event->getTag(); - $this->log('System tag "%s" (%s, %s) created', + $this->log(Operation::SystemTagCreated, 'System tag "%s" (%s, %s) created', [ 'name' => $tag->getName(), 'visibility' => $tag->isUserVisible() ? 'visible' : 'invisible', diff --git a/apps/admin_audit/lib/Listener/UserManagementEventListener.php b/apps/admin_audit/lib/Listener/UserManagementEventListener.php index 37767d11f4b5c..661fef2351705 100644 --- a/apps/admin_audit/lib/Listener/UserManagementEventListener.php +++ b/apps/admin_audit/lib/Listener/UserManagementEventListener.php @@ -10,6 +10,7 @@ namespace OCA\AdminAudit\Listener; use OCA\AdminAudit\Actions\Action; +use OCA\AdminAudit\Operation; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; use OCP\User\Events\PasswordUpdatedEvent; @@ -42,6 +43,7 @@ public function handle(Event $event): void { private function userCreated(UserCreatedEvent $event): void { $this->log( + Operation::UserCreated, 'User created: "%s"', [ 'uid' => $event->getUid() @@ -54,6 +56,7 @@ private function userCreated(UserCreatedEvent $event): void { private function userDeleted(UserDeletedEvent $event): void { $this->log( + Operation::UserDeleted, 'User deleted: "%s"', [ 'uid' => $event->getUser()->getUID() @@ -67,10 +70,10 @@ private function userDeleted(UserDeletedEvent $event): void { private function userChanged(UserChangedEvent $event): void { switch ($event->getFeature()) { case 'enabled': + $enabled = $event->getValue() === true; $this->log( - $event->getValue() === true - ? 'User enabled: "%s"' - : 'User disabled: "%s"', + $enabled ? Operation::UserEnabled : Operation::UserDisabled, + $enabled ? 'User enabled: "%s"' : 'User disabled: "%s"', ['user' => $event->getUser()->getUID()], [ 'user', @@ -79,6 +82,7 @@ private function userChanged(UserChangedEvent $event): void { break; case 'eMailAddress': $this->log( + Operation::UserEmailChanged, 'Email address changed for user %s', ['user' => $event->getUser()->getUID()], [ @@ -92,6 +96,7 @@ private function userChanged(UserChangedEvent $event): void { private function passwordUpdated(PasswordUpdatedEvent $event): void { if ($event->getUser()->getBackendClassName() === 'Database') { $this->log( + Operation::UserPasswordChanged, 'Password of user "%s" has been changed', [ 'user' => $event->getUser()->getUID(), @@ -108,6 +113,7 @@ private function passwordUpdated(PasswordUpdatedEvent $event): void { */ private function userIdAssigned(UserIdAssignedEvent $event): void { $this->log( + Operation::UserIdAssigned, 'UserID assigned: "%s"', [ 'uid' => $event->getUserId() ], [ 'uid' ] @@ -119,6 +125,7 @@ private function userIdAssigned(UserIdAssignedEvent $event): void { */ private function userIdUnassigned(UserIdUnassignedEvent $event): void { $this->log( + Operation::UserIdUnassigned, 'UserID unassigned: "%s"', [ 'uid' => $event->getUserId() ], [ 'uid' ] diff --git a/apps/admin_audit/lib/Operation.php b/apps/admin_audit/lib/Operation.php new file mode 100644 index 0000000000000..c88bffdff2178 --- /dev/null +++ b/apps/admin_audit/lib/Operation.php @@ -0,0 +1,75 @@ +logger = $this->createMock(IAuditLogger::class); + $this->action = new Action($this->logger); + } + + public function testLogAddsOperationToContext(): void { + $this->logger->expects($this->once()) + ->method('info') + ->with('File "/a.txt" deleted', ['app' => 'admin_audit', 'operation' => 'files.file.deleted']); + + $this->action->log(Operation::FileDeleted, 'File "%s" deleted', ['path' => '/a.txt'], ['path']); + } + + public function testLogAcceptsOperationString(): void { + $this->logger->expects($this->once()) + ->method('info') + ->with('Share "42" accepted', ['app' => 'admin_audit', 'operation' => 'federatedfilesharing.share.accepted']); + + $this->action->log('federatedfilesharing.share.accepted', 'Share "%s" accepted', ['id' => '42'], ['id']); + } + + public function testLogWithoutOperation(): void { + $this->logger->expects($this->once()) + ->method('info') + ->with('File "/a.txt" deleted', ['app' => 'admin_audit']); + + $this->action->log(null, 'File "%s" deleted', ['path' => '/a.txt'], ['path']); + } + + public function testLogWithOperationAndNamedPlaceholders(): void { + $this->logger->expects($this->once()) + ->method('info') + ->with( + 'File "{path}" deleted', + ['app' => 'admin_audit', 'operation' => 'files.file.deleted', 'path' => '/a.txt'], + ); + + $this->action->log(Operation::FileDeleted, 'File "{path}" deleted', ['path' => '/a.txt'], ['path']); + } + + public function testMissingParameterKeepsOperation(): void { + $this->logger->expects($this->never()) + ->method('info'); + $this->logger->expects($this->once()) + ->method('critical') + ->with( + '$params["path"] was missing. Transferred value: {params}', + ['app' => 'admin_audit', 'operation' => 'files.file.deleted', 'params' => ['id' => 42]], + ); + + $this->action->log(Operation::FileDeleted, 'File "%s" deleted', ['id' => 42], ['path']); + } + + public function testMissingParameterObfuscatedKeepsOperation(): void { + $this->logger->expects($this->once()) + ->method('critical') + ->with( + '$params["uid"] was missing.', + ['app' => 'admin_audit', 'operation' => 'auth.login.failed'], + ); + + $this->action->log(Operation::LoginFailed, 'Login failed: "%s"', [], ['uid'], true); + } +} diff --git a/apps/admin_audit/tests/Actions/FilesTest.php b/apps/admin_audit/tests/Actions/FilesTest.php new file mode 100644 index 0000000000000..39da7533e600e --- /dev/null +++ b/apps/admin_audit/tests/Actions/FilesTest.php @@ -0,0 +1,79 @@ +logger = $this->createMock(IAuditLogger::class); + $this->files = new Files($this->logger); + + $this->source = $this->createMock(File::class); + $this->source->method('getId')->willReturn(41); + $this->source->method('getPath')->willReturn('/alice/files/a.txt'); + $this->target = $this->createMock(File::class); + $this->target->method('getId')->willReturn(42); + $this->target->method('getPath')->willReturn('/alice/files/b.txt'); + } + + private function expectInfo(string $message, string $operation): void { + $this->logger->expects($this->once()) + ->method('info') + ->with($message, ['app' => 'admin_audit', 'operation' => $operation]); + } + + public function testRead(): void { + $this->expectInfo('File with id "42" accessed: "/alice/files/b.txt"', 'files.file.read'); + $this->files->read(new BeforeNodeReadEvent($this->target)); + } + + public function testRename(): void { + $this->expectInfo('File renamed with id "42" from "/alice/files/a.txt" to "/alice/files/b.txt"', 'files.file.renamed'); + $this->files->afterRename(new NodeRenamedEvent($this->source, $this->target)); + } + + public function testCreate(): void { + $this->expectInfo('File with id "42" created: "/alice/files/b.txt"', 'files.file.created'); + $this->files->create(new NodeCreatedEvent($this->target)); + } + + public function testCopy(): void { + $this->expectInfo('File id copied from: "41" to "42", path from "/alice/files/a.txt" to "/alice/files/b.txt"', 'files.file.copied'); + $this->files->copy(new NodeCopiedEvent($this->source, $this->target)); + } + + public function testWrite(): void { + $this->expectInfo('File with id "42" written to: "/alice/files/b.txt"', 'files.file.written'); + $this->files->write(new NodeWrittenEvent($this->target)); + } + + public function testDelete(): void { + $this->expectInfo('File with id "42" deleted: "/alice/files/b.txt"', 'files.file.deleted'); + $this->files->delete(new BeforeNodeDeletedEvent($this->target)); + } +} diff --git a/apps/admin_audit/tests/Actions/SharingTest.php b/apps/admin_audit/tests/Actions/SharingTest.php new file mode 100644 index 0000000000000..4aafa0c0b687c --- /dev/null +++ b/apps/admin_audit/tests/Actions/SharingTest.php @@ -0,0 +1,79 @@ +logger = $this->createMock(IAuditLogger::class); + $this->sharing = new Sharing($this->logger); + } + + private function expectInfo(string $message, string $operation): void { + $this->logger->expects($this->once()) + ->method('info') + ->with($message, ['app' => 'admin_audit', 'operation' => $operation]); + } + + public function testUpdatePermissions(): void { + $this->expectInfo('The permissions of the shared file "/a.txt" with ID "42" have been changed to "1"', 'sharing.share.permissions_updated'); + $this->sharing->updatePermissions([ + 'itemType' => 'file', + 'path' => '/a.txt', + 'itemSource' => 42, + 'permissions' => 1, + ]); + } + + public function testUpdatePassword(): void { + $this->expectInfo('The password of the publicly shared file "abc" with ID "42" has been changed', 'sharing.share.password_updated'); + $this->sharing->updatePassword([ + 'itemType' => 'file', + 'token' => 'abc', + 'itemSource' => 42, + ]); + } + + public function testExpirationDateRemoved(): void { + $this->expectInfo('The expiration date of the publicly shared file with ID "42" has been removed', 'sharing.share.expiration_removed'); + $this->sharing->updateExpirationDate([ + 'itemType' => 'file', + 'itemSource' => 42, + 'date' => null, + ]); + } + + public function testExpirationDateUpdated(): void { + $this->expectInfo('The expiration date of the publicly shared file with ID "42" has been changed to "2026-10-31 00:00:00"', 'sharing.share.expiration_updated'); + $this->sharing->updateExpirationDate([ + 'itemType' => 'file', + 'itemSource' => 42, + 'date' => new \DateTimeImmutable('2026-10-31 00:00:00'), + ]); + } + + public function testShareAccessed(): void { + $this->expectInfo('The shared file with the token "abc" by "alice" has been accessed.', 'sharing.share.link_accessed'); + $this->sharing->shareAccessed([ + 'itemType' => 'file', + 'token' => 'abc', + 'uidOwner' => 'alice', + ]); + } +} diff --git a/apps/admin_audit/tests/Actions/TrashbinTest.php b/apps/admin_audit/tests/Actions/TrashbinTest.php new file mode 100644 index 0000000000000..c78b4be6d86b7 --- /dev/null +++ b/apps/admin_audit/tests/Actions/TrashbinTest.php @@ -0,0 +1,43 @@ +logger = $this->createMock(IAuditLogger::class); + $this->trashbin = new Trashbin($this->logger); + } + + public function testDelete(): void { + $this->logger->expects($this->once()) + ->method('info') + ->with('File "/a.txt.d1700000000" deleted from trash bin.', ['app' => 'admin_audit', 'operation' => 'trashbin.file.deleted']); + + $this->trashbin->delete(['path' => '/a.txt.d1700000000']); + } + + public function testRestore(): void { + $this->logger->expects($this->once()) + ->method('info') + ->with('File "/a.txt" restored from trash bin.', ['app' => 'admin_audit', 'operation' => 'trashbin.file.restored']); + + $this->trashbin->restore(['filePath' => '/a.txt', 'trashPath' => '/a.txt.d1700000000']); + } +} diff --git a/apps/admin_audit/tests/Actions/VersionsTest.php b/apps/admin_audit/tests/Actions/VersionsTest.php new file mode 100644 index 0000000000000..3d909fc8412ac --- /dev/null +++ b/apps/admin_audit/tests/Actions/VersionsTest.php @@ -0,0 +1,35 @@ +logger = $this->createMock(IAuditLogger::class); + $this->versions = new Versions($this->logger); + } + + public function testDelete(): void { + $this->logger->expects($this->once()) + ->method('info') + ->with('Version "/a.txt.v1700000000" was deleted.', ['app' => 'admin_audit', 'operation' => 'versions.version.deleted']); + + $this->versions->delete(['path' => '/a.txt.v1700000000']); + } +} diff --git a/apps/admin_audit/tests/Listener/AppManagementEventListenerTest.php b/apps/admin_audit/tests/Listener/AppManagementEventListenerTest.php new file mode 100644 index 0000000000000..aa8c895a2160a --- /dev/null +++ b/apps/admin_audit/tests/Listener/AppManagementEventListenerTest.php @@ -0,0 +1,56 @@ +logger = $this->createMock(IAuditLogger::class); + $this->listener = new AppManagementEventListener($this->logger); + } + + private function expectInfo(string $message, string $operation): void { + $this->logger->expects($this->once()) + ->method('info') + ->with($message, ['app' => 'admin_audit', 'operation' => $operation]); + } + + public function testAppEnabled(): void { + $this->expectInfo('App "files" enabled', 'apps.app.enabled'); + $this->listener->handle(new AppEnableEvent('files')); + } + + public function testAppEnabledForGroups(): void { + $this->expectInfo('App "files" enabled for groups: admin, staff', 'apps.app.enabled'); + $this->listener->handle(new AppEnableEvent('files', ['admin', 'staff'])); + } + + public function testAppDisabled(): void { + $this->expectInfo('App "files" disabled', 'apps.app.disabled'); + $this->listener->handle(new AppDisableEvent('files')); + } + + public function testAppUpdated(): void { + $this->expectInfo('App "files" updated', 'apps.app.updated'); + $this->listener->handle(new AppUpdateEvent('files')); + } +} diff --git a/apps/admin_audit/tests/Listener/AuthEventListenerTest.php b/apps/admin_audit/tests/Listener/AuthEventListenerTest.php new file mode 100644 index 0000000000000..c5296e139ca77 --- /dev/null +++ b/apps/admin_audit/tests/Listener/AuthEventListenerTest.php @@ -0,0 +1,68 @@ +logger = $this->createMock(IAuditLogger::class); + $this->listener = new AuthEventListener($this->logger); + + $this->user = $this->createMock(IUser::class); + $this->user->method('getUID')->willReturn('alice'); + } + + private function expectInfo(string $message, string $operation): void { + $this->logger->expects($this->once()) + ->method('info') + ->with($message, ['app' => 'admin_audit', 'operation' => $operation]); + } + + public function testLoginAttempt(): void { + $this->expectInfo('Login attempt: "alice"', 'auth.login.attempted'); + $this->listener->handle(new BeforeUserLoggedInEvent('alice', 'password')); + } + + public function testLoginSucceeded(): void { + $this->expectInfo('Login successful: "alice"', 'auth.login.succeeded'); + $this->listener->handle(new UserLoggedInEvent($this->user, 'alice', 'password', false)); + } + + public function testLoginWithCookieSucceeded(): void { + $this->expectInfo('Login successful: "alice"', 'auth.login.succeeded'); + $this->listener->handle(new UserLoggedInWithCookieEvent($this->user, null)); + } + + public function testLogout(): void { + $this->expectInfo('Logout occurred', 'auth.logout.performed'); + $this->listener->handle(new BeforeUserLoggedOutEvent($this->user)); + } + + public function testLoginFailed(): void { + $this->expectInfo('Login failed: "alice"', 'auth.login.failed'); + $this->listener->handle(new AnyLoginFailedEvent('alice', 'password')); + } +} diff --git a/apps/admin_audit/tests/Listener/CacheEventListenerTest.php b/apps/admin_audit/tests/Listener/CacheEventListenerTest.php new file mode 100644 index 0000000000000..408deb04c47a7 --- /dev/null +++ b/apps/admin_audit/tests/Listener/CacheEventListenerTest.php @@ -0,0 +1,48 @@ +logger = $this->createMock(IAuditLogger::class); + $this->listener = new CacheEventListener($this->logger); + $this->storage = $this->createMock(IStorage::class); + } + + private function expectInfo(string $message, string $operation): void { + $this->logger->expects($this->once()) + ->method('info') + ->with($message, ['app' => 'admin_audit', 'operation' => $operation]); + } + + public function testEntryInserted(): void { + $this->expectInfo('Cache entry inserted for fileid "42", path "files/a.txt" on storageid "3"', 'files.cache_entry.inserted'); + $this->listener->handle(new CacheEntryInsertedEvent($this->storage, 'files/a.txt', 42, 3)); + } + + public function testEntryRemoved(): void { + $this->expectInfo('Cache entry removed for fileid "42", path "files/a.txt" on storageid "3"', 'files.cache_entry.removed'); + $this->listener->handle(new CacheEntryRemovedEvent($this->storage, 'files/a.txt', 42, 3)); + } +} diff --git a/apps/admin_audit/tests/Listener/ConsoleEventListenerTest.php b/apps/admin_audit/tests/Listener/ConsoleEventListenerTest.php new file mode 100644 index 0000000000000..98d768c904c54 --- /dev/null +++ b/apps/admin_audit/tests/Listener/ConsoleEventListenerTest.php @@ -0,0 +1,36 @@ +logger = $this->createMock(IAuditLogger::class); + $this->listener = new ConsoleEventListener($this->logger); + } + + public function testCommandExecuted(): void { + $this->logger->expects($this->once()) + ->method('info') + ->with('Console command executed: user:disable carol', ['app' => 'admin_audit', 'operation' => 'console.command.executed']); + + $this->listener->handle(new ConsoleEvent(ConsoleEvent::EVENT_RUN, ['occ', 'user:disable', 'carol'])); + } +} diff --git a/apps/admin_audit/tests/Listener/CriticalActionPerformedEventListenerTest.php b/apps/admin_audit/tests/Listener/CriticalActionPerformedEventListenerTest.php index 7279a0aec25d5..0777dee5991a4 100644 --- a/apps/admin_audit/tests/Listener/CriticalActionPerformedEventListenerTest.php +++ b/apps/admin_audit/tests/Listener/CriticalActionPerformedEventListenerTest.php @@ -47,4 +47,20 @@ public function testNamedPlaceholders(): void { ['ip' => '10.0.0.1', 'action' => 'login'], )); } + + public function testOperationIsPassedThrough(): void { + $this->logger->expects($this->once()) + ->method('info') + ->with( + 'Federated share with id "42" was accepted', + ['app' => 'admin_audit', 'operation' => 'federatedfilesharing.share.accepted'], + ); + + $this->listener->handle(new CriticalActionPerformedEvent( + 'Federated share with id "%s" was accepted', + ['42'], + false, + 'federatedfilesharing.share.accepted', + )); + } } diff --git a/apps/admin_audit/tests/Listener/FileEventListenerTest.php b/apps/admin_audit/tests/Listener/FileEventListenerTest.php new file mode 100644 index 0000000000000..6673ba2d86855 --- /dev/null +++ b/apps/admin_audit/tests/Listener/FileEventListenerTest.php @@ -0,0 +1,58 @@ +logger = $this->createMock(IAuditLogger::class); + $this->listener = new FileEventListener($this->logger); + } + + public function testPreviewAccessed(): void { + $file = $this->createMock(File::class); + $file->method('getId')->willReturn(42); + $file->method('getPath')->willReturn('/alice/files/a.jpg'); + + $this->logger->expects($this->once()) + ->method('info') + ->with( + 'Preview accessed: (id: "42", width: "64", height: "64" crop: "1", mode: "fill", path: "/alice/files/a.jpg")', + ['app' => 'admin_audit', 'operation' => 'files.preview.accessed'], + ); + + $this->listener->handle(new BeforePreviewFetchedEvent($file, 64, 64, true, 'fill')); + } + + public function testVersionRestored(): void { + $version = $this->createMock(IVersion::class); + $version->method('getRevisionId')->willReturn(1700000000); + $version->method('getVersionPath')->willReturn('/a.txt'); + + $this->logger->expects($this->once()) + ->method('info') + ->with('Version "1700000000" of "/a.txt" was restored.', ['app' => 'admin_audit', 'operation' => 'versions.version.restored']); + + $this->listener->handle(new VersionRestoredEvent($version)); + } +} diff --git a/apps/admin_audit/tests/Listener/GroupManagementEventListenerTest.php b/apps/admin_audit/tests/Listener/GroupManagementEventListenerTest.php new file mode 100644 index 0000000000000..875aff806b442 --- /dev/null +++ b/apps/admin_audit/tests/Listener/GroupManagementEventListenerTest.php @@ -0,0 +1,66 @@ +logger = $this->createMock(IAuditLogger::class); + $this->listener = new GroupManagementEventListener($this->logger); + + $this->group = $this->createMock(IGroup::class); + $this->group->method('getGID')->willReturn('admins'); + $this->user = $this->createMock(IUser::class); + $this->user->method('getUID')->willReturn('alice'); + } + + private function expectInfo(string $message, string $operation): void { + $this->logger->expects($this->once()) + ->method('info') + ->with($message, ['app' => 'admin_audit', 'operation' => $operation]); + } + + public function testUserAdded(): void { + $this->expectInfo('User "alice" added to group "admins"', 'groups.member.added'); + $this->listener->handle(new UserAddedEvent($this->group, $this->user)); + } + + public function testUserRemoved(): void { + $this->expectInfo('User "alice" removed from group "admins"', 'groups.member.removed'); + $this->listener->handle(new UserRemovedEvent($this->group, $this->user)); + } + + public function testGroupCreated(): void { + $this->expectInfo('Group created: "admins"', 'groups.group.created'); + $this->listener->handle(new GroupCreatedEvent($this->group)); + } + + public function testGroupDeleted(): void { + $this->expectInfo('Group deleted: "admins"', 'groups.group.deleted'); + $this->listener->handle(new GroupDeletedEvent($this->group)); + } +} diff --git a/apps/admin_audit/tests/Listener/SecurityEventListenerTest.php b/apps/admin_audit/tests/Listener/SecurityEventListenerTest.php index 482301085308d..25d5ca4fd6dcf 100644 --- a/apps/admin_audit/tests/Listener/SecurityEventListenerTest.php +++ b/apps/admin_audit/tests/Listener/SecurityEventListenerTest.php @@ -46,7 +46,7 @@ public function testTwofactorFailed(): void { ->method('info') ->with( $this->equalTo('Failed two factor attempt by user mydisplayname (myuid) with provider myprovider'), - ['app' => 'admin_audit'] + ['app' => 'admin_audit', 'operation' => 'auth.twofactor.failed'] ); $this->security->handle(new twoFactorProviderChallengeFailed($this->user, $this->provider)); @@ -57,7 +57,7 @@ public function testTwofactorSuccess(): void { ->method('info') ->with( $this->equalTo('Successful two factor attempt by user mydisplayname (myuid) with provider myprovider'), - ['app' => 'admin_audit'] + ['app' => 'admin_audit', 'operation' => 'auth.twofactor.passed'] ); $this->security->handle(new TwoFactorProviderChallengePassed($this->user, $this->provider)); diff --git a/apps/admin_audit/tests/Listener/SharingEventListenerTest.php b/apps/admin_audit/tests/Listener/SharingEventListenerTest.php new file mode 100644 index 0000000000000..1282d98db9cfb --- /dev/null +++ b/apps/admin_audit/tests/Listener/SharingEventListenerTest.php @@ -0,0 +1,102 @@ +logger = $this->createMock(IAuditLogger::class); + $this->listener = new SharingEventListener($this->logger); + } + + private function createShare(int $shareType): IShare&MockObject { + $node = $this->createMock(File::class); + $node->method('getPath')->willReturn('/alice/files/a.txt'); + + $share = $this->createMock(IShare::class); + $share->method('getShareType')->willReturn($shareType); + $share->method('getNodeType')->willReturn('file'); + $share->method('getNode')->willReturn($node); + $share->method('getNodeId')->willReturn(42); + $share->method('getSharedWith')->willReturn('bob'); + $share->method('getPermissions')->willReturn(19); + $share->method('getId')->willReturn('7'); + $share->method('getTarget')->willReturn('/a.txt'); + return $share; + } + + public static function shareTypeProvider(): array { + return [ + 'link' => [IShare::TYPE_LINK], + 'user' => [IShare::TYPE_USER], + 'group' => [IShare::TYPE_GROUP], + 'room' => [IShare::TYPE_ROOM], + 'email' => [IShare::TYPE_EMAIL], + 'circle' => [IShare::TYPE_CIRCLE], + 'remote' => [IShare::TYPE_REMOTE], + 'remote group' => [IShare::TYPE_REMOTE_GROUP], + 'deck' => [IShare::TYPE_DECK], + ]; + } + + #[DataProvider('shareTypeProvider')] + public function testShareCreatedOperation(int $shareType): void { + $this->logger->expects($this->once()) + ->method('info') + ->with($this->isString(), ['app' => 'admin_audit', 'operation' => 'sharing.share.created']); + + $this->listener->handle(new ShareCreatedEvent($this->createShare($shareType))); + } + + #[DataProvider('shareTypeProvider')] + public function testShareDeletedOperation(int $shareType): void { + $this->logger->expects($this->once()) + ->method('info') + ->with($this->isString(), ['app' => 'admin_audit', 'operation' => 'sharing.share.deleted']); + + $this->listener->handle(new ShareDeletedEvent($this->createShare($shareType))); + } + + public function testUserShareCreatedMessage(): void { + $this->logger->expects($this->once()) + ->method('info') + ->with( + 'The file "/alice/files/a.txt" with ID "42" has been shared to the user "bob" with permissions "19" (Share ID: 7)', + ['app' => 'admin_audit', 'operation' => 'sharing.share.created'], + ); + + $this->listener->handle(new ShareCreatedEvent($this->createShare(IShare::TYPE_USER))); + } + + public function testLinkShareDeletedMessage(): void { + $this->logger->expects($this->once()) + ->method('info') + ->with( + 'The file "/a.txt" with ID "42" has been unshared (Share ID: 7)', + ['app' => 'admin_audit', 'operation' => 'sharing.share.deleted'], + ); + + $this->listener->handle(new ShareDeletedEvent($this->createShare(IShare::TYPE_LINK))); + } +} diff --git a/apps/admin_audit/tests/Listener/TagEventListenerTest.php b/apps/admin_audit/tests/Listener/TagEventListenerTest.php new file mode 100644 index 0000000000000..75953f9a25067 --- /dev/null +++ b/apps/admin_audit/tests/Listener/TagEventListenerTest.php @@ -0,0 +1,42 @@ +logger = $this->createMock(IAuditLogger::class); + $this->listener = new TagEventListener($this->logger); + } + + public function testTagCreated(): void { + $tag = $this->createMock(ISystemTag::class); + $tag->method('getName')->willReturn('confidential'); + $tag->method('isUserVisible')->willReturn(false); + $tag->method('isUserAssignable')->willReturn(false); + + $this->logger->expects($this->once()) + ->method('info') + ->with('System tag "confidential" (invisible, system only) created', ['app' => 'admin_audit', 'operation' => 'systemtags.tag.created']); + + $this->listener->handle(new TagCreatedEvent($tag)); + } +} diff --git a/apps/admin_audit/tests/Listener/UserManagementEventListenerTest.php b/apps/admin_audit/tests/Listener/UserManagementEventListenerTest.php index feb67ad0166e4..00fada24f8474 100644 --- a/apps/admin_audit/tests/Listener/UserManagementEventListenerTest.php +++ b/apps/admin_audit/tests/Listener/UserManagementEventListenerTest.php @@ -11,7 +11,12 @@ use OCA\AdminAudit\IAuditLogger; use OCA\AdminAudit\Listener\UserManagementEventListener; use OCP\IUser; +use OCP\User\Events\PasswordUpdatedEvent; use OCP\User\Events\UserChangedEvent; +use OCP\User\Events\UserCreatedEvent; +use OCP\User\Events\UserDeletedEvent; +use OCP\User\Events\UserIdAssignedEvent; +use OCP\User\Events\UserIdUnassignedEvent; use PHPUnit\Framework\MockObject\MockObject; use Test\TestCase; @@ -49,7 +54,7 @@ public function testSkipUnsupported(): void { public function testUserEnabled(): void { $this->logger->expects($this->once()) ->method('info') - ->with('User enabled: "alice"', ['app' => 'admin_audit']); + ->with('User enabled: "alice"', ['app' => 'admin_audit', 'operation' => 'users.user.enabled']); $event = new UserChangedEvent( $this->user, @@ -64,7 +69,7 @@ public function testUserEnabled(): void { public function testUserDisabled(): void { $this->logger->expects($this->once()) ->method('info') - ->with('User disabled: "alice"', ['app' => 'admin_audit']); + ->with('User disabled: "alice"', ['app' => 'admin_audit', 'operation' => 'users.user.disabled']); $event = new UserChangedEvent( $this->user, @@ -79,7 +84,7 @@ public function testUserDisabled(): void { public function testEmailChanged(): void { $this->logger->expects($this->once()) ->method('info') - ->with('Email address changed for user alice', ['app' => 'admin_audit']); + ->with('Email address changed for user alice', ['app' => 'admin_audit', 'operation' => 'users.user.email_changed']); $event = new UserChangedEvent( $this->user, @@ -90,4 +95,45 @@ public function testEmailChanged(): void { $this->listener->handle($event); } + + public function testUserCreated(): void { + $this->logger->expects($this->once()) + ->method('info') + ->with('User created: "alice"', ['app' => 'admin_audit', 'operation' => 'users.user.created']); + + $this->listener->handle(new UserCreatedEvent($this->user, 'password')); + } + + public function testUserDeleted(): void { + $this->logger->expects($this->once()) + ->method('info') + ->with('User deleted: "alice"', ['app' => 'admin_audit', 'operation' => 'users.user.deleted']); + + $this->listener->handle(new UserDeletedEvent($this->user)); + } + + public function testPasswordUpdated(): void { + $this->user->method('getBackendClassName')->willReturn('Database'); + $this->logger->expects($this->once()) + ->method('info') + ->with('Password of user "alice" has been changed', ['app' => 'admin_audit', 'operation' => 'users.user.password_changed']); + + $this->listener->handle(new PasswordUpdatedEvent($this->user, 'password')); + } + + public function testUserIdAssigned(): void { + $this->logger->expects($this->once()) + ->method('info') + ->with('UserID assigned: "alice"', ['app' => 'admin_audit', 'operation' => 'users.user.id_assigned']); + + $this->listener->handle(new UserIdAssignedEvent('alice')); + } + + public function testUserIdUnassigned(): void { + $this->logger->expects($this->once()) + ->method('info') + ->with('UserID unassigned: "alice"', ['app' => 'admin_audit', 'operation' => 'users.user.id_unassigned']); + + $this->listener->handle(new UserIdUnassignedEvent('alice')); + } } diff --git a/lib/public/Log/Audit/CriticalActionPerformedEvent.php b/lib/public/Log/Audit/CriticalActionPerformedEvent.php index dbee799f5c5fe..6ace9f00f8362 100644 --- a/lib/public/Log/Audit/CriticalActionPerformedEvent.php +++ b/lib/public/Log/Audit/CriticalActionPerformedEvent.php @@ -26,19 +26,25 @@ class CriticalActionPerformedEvent extends Event { /** @var bool */ private $obfuscateParameters; + private ?string $operation; + /** * @param string $logMessage * @param array $parameters * @param bool $obfuscateParameters + * @param ?string $operation Stable identifier of the action in the form `app.entity.action`, e.g. `federatedfilesharing.share.accepted` * @since 22.0.0 + * @since 36.0.0 added the $operation parameter */ public function __construct(string $logMessage, array $parameters = [], - bool $obfuscateParameters = false) { + bool $obfuscateParameters = false, + ?string $operation = null) { parent::__construct(); $this->logMessage = $logMessage; $this->parameters = $parameters; $this->obfuscateParameters = $obfuscateParameters; + $this->operation = $operation; } /** @@ -64,4 +70,11 @@ public function getParameters(): array { public function getObfuscateParameters(): bool { return $this->obfuscateParameters; } + + /** + * @since 36.0.0 + */ + public function getOperation(): ?string { + return $this->operation; + } }