From 9d1892cf9cae1a7c1b2dd713cc03576c4df14b2a Mon Sep 17 00:00:00 2001 From: Lukas Schaefer Date: Thu, 17 Sep 2026 08:52:31 -0400 Subject: [PATCH] fix: Delete user from rules when user is deleted Signed-off-by: Lukas Schaefer --- appinfo/info.xml | 2 +- lib/AppInfo/Application.php | 3 + lib/Listener/UserDeletedListener.php | 31 ++++++++++ .../Version030302Date20260820101934.php | 56 +++++++++++++++++++ lib/Service/ApprovalService.php | 1 - lib/Service/RuleService.php | 22 ++++++++ tests/unit/Service/ApprovalServiceTest.php | 9 +++ 7 files changed, 122 insertions(+), 2 deletions(-) create mode 100644 lib/Listener/UserDeletedListener.php create mode 100644 lib/Migration/Version030302Date20260820101934.php diff --git a/appinfo/info.xml b/appinfo/info.xml index 99675579..b41b25d9 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -13,7 +13,7 @@ Approve/reject files based on workflows defined by admins. **Warning**: The DocuSign integration is no longer part of this app and can be installed with [this app](https://apps.nextcloud.com/apps/integration_docusign). ]]> - 3.3.1 + 3.3.2-dev.0 agpl Julien Veyssier Approval diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 88aa49d8..af7e4df0 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -12,6 +12,7 @@ use OCA\Approval\Listener\LoadAdditionalScriptsListener; use OCA\Approval\Listener\LoadSidebarScripts; use OCA\Approval\Listener\UpdateFilesListener; +use OCA\Approval\Listener\UserDeletedListener; use OCA\Approval\Notification\Notifier; use OCA\Approval\Service\ApprovalService; use OCA\DAV\Events\SabrePluginAddEvent; @@ -24,6 +25,7 @@ use OCP\EventDispatcher\IEventDispatcher; use OCP\FilesMetadata\Event\MetadataBackgroundEvent; use OCP\SystemTag\TagAssignedEvent; +use OCP\User\Events\UserDeletedEvent; use Override; class Application extends App implements IBootstrap { @@ -68,6 +70,7 @@ public function register(IRegistrationContext $context): void { $context->registerNotifierService(Notifier::class); $context->registerDashboardWidget(ApprovalPendingWidget::class); $context->registerEventListener(MetadataBackgroundEvent::class, UpdateFilesListener::class); + $context->registerEventListener(UserDeletedEvent::class, UserDeletedListener::class); } #[Override] diff --git a/lib/Listener/UserDeletedListener.php b/lib/Listener/UserDeletedListener.php new file mode 100644 index 00000000..9d4f6401 --- /dev/null +++ b/lib/Listener/UserDeletedListener.php @@ -0,0 +1,31 @@ + */ +class UserDeletedListener implements IEventListener { + public function __construct( + private RuleService $ruleService, + ) { + } + + public function handle(Event $event): void { + if (!($event instanceof UserDeletedEvent)) { + return; + } + + $this->ruleService->deleteUserFromRules($event->getUser()->getUID()); + } +} diff --git a/lib/Migration/Version030302Date20260820101934.php b/lib/Migration/Version030302Date20260820101934.php new file mode 100644 index 00000000..b2150b5e --- /dev/null +++ b/lib/Migration/Version030302Date20260820101934.php @@ -0,0 +1,56 @@ +connection->getQueryBuilder(); + $qb->selectDistinct('entity_id') + ->from('approval_rule_' . $role) + ->where( + $qb->expr()->eq('entity_type', $qb->createNamedParameter(Application::TYPE_USER, IQueryBuilder::PARAM_INT)) + ); + + $result = $qb->executeQuery(); + $userIds = $result->fetchAll(\PDO::FETCH_COLUMN); + $result->closeCursor(); + + foreach ($userIds as $userId) { + if ($this->userManager->get($userId) === null) { + $deleteQb = $this->connection->getQueryBuilder(); + $deleteQb->delete('approval_rule_' . $role) + ->where( + $deleteQb->expr()->eq('entity_type', $deleteQb->createNamedParameter(Application::TYPE_USER, IQueryBuilder::PARAM_INT)) + ) + ->andWhere( + $deleteQb->expr()->eq('entity_id', $deleteQb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)) + ); + $deleteQb->executeStatement(); + } + } + } + } +} diff --git a/lib/Service/ApprovalService.php b/lib/Service/ApprovalService.php index e32b2d31..6bb4f5a1 100644 --- a/lib/Service/ApprovalService.php +++ b/lib/Service/ApprovalService.php @@ -853,7 +853,6 @@ public function propFind(int $nodeId): int { return $state['state']; } - /** * Get approval state for multiple files and loads all the tags at once * diff --git a/lib/Service/RuleService.php b/lib/Service/RuleService.php index d5d091c4..d975432a 100644 --- a/lib/Service/RuleService.php +++ b/lib/Service/RuleService.php @@ -572,6 +572,28 @@ public function clearRuleCaches(): void { $cache->remove('approval_tags'); } + /** + * Remove a user from approval approvers and requesters. + */ + public function deleteUserFromRules(string $userId): void { + $this->cachedRules = null; + $qb = $this->db->getQueryBuilder(); + + foreach (['approvers', 'requesters'] as $role) { + $qb->delete('approval_rule_' . $role) + ->where( + $qb->expr()->eq('entity_type', $qb->createNamedParameter(Application::TYPE_USER, IQueryBuilder::PARAM_INT)) + ) + ->andWhere( + $qb->expr()->eq('entity_id', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)) + ); + $qb->executeStatement(); + $qb = $qb->resetQueryParts(); + } + + $this->clearRuleCaches(); + } + /** * Checks that the approval of the file was after the time given. * This does not verify that the file was actually approved. diff --git a/tests/unit/Service/ApprovalServiceTest.php b/tests/unit/Service/ApprovalServiceTest.php index 84aabeca..40da6fea 100644 --- a/tests/unit/Service/ApprovalServiceTest.php +++ b/tests/unit/Service/ApprovalServiceTest.php @@ -240,6 +240,15 @@ public function testGetRuleAuthorizedUserIds() { $this->assertEquals('user1', $uidRequesters[0]); } + public function testDeleteUserFromRules(): void { + $this->ruleService->deleteUserFromRules('user1'); + + $rule = $this->ruleService->getRule($this->idRule1); + $this->assertNotNull($rule); + $this->assertCount(0, $rule['approvers']); + $this->assertCount(0, $rule['requesters']); + } + public function testGetApprovalState(): void { $uf = $this->root->getUserFolder('user1'); $file1 = $uf->newFile('file1.txt', 'content');