From 219f739b54a731e33843f895d5b46408fd2c6570 Mon Sep 17 00:00:00 2001 From: Jeppe Bundgaard Date: Thu, 19 Mar 2026 17:13:45 +0100 Subject: [PATCH] Invalidate user permission and session caches when group permissions change; add tests. --- .../app/objects/groups_permissions_o.php | 60 ++++++++++++++++++- ...ermissionSessionInvalidationWiringTest.php | 13 ++++ 2 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 services/nginx/app/tests/Unit/Permissions/GroupPermissionSessionInvalidationWiringTest.php diff --git a/services/nginx/app/objects/groups_permissions_o.php b/services/nginx/app/objects/groups_permissions_o.php index 2d0ffec3..efb5b5b0 100644 --- a/services/nginx/app/objects/groups_permissions_o.php +++ b/services/nginx/app/objects/groups_permissions_o.php @@ -6,6 +6,7 @@ use classes\db; use classes\object_property; use Exception; use traits\db_object_t; +use Throwable; class groups_permissions_o extends db { @@ -48,10 +49,11 @@ class groups_permissions_o extends db ]); $this->id = $tmp_id; self::getObjectProperties(); - self::objectChanged(); if (!$this->id) { throw new Exception('The permission was not created successfully.'); } + $this->invalidateGroupSessionCaches($group_id); + self::objectChanged(); } public function getObjectProperties(): void @@ -62,7 +64,7 @@ class groups_permissions_o extends db public function objectChanged(): void { - //TODO: Add cache invalidation + // Cache invalidation is handled in add/remove where group context is guaranteed. } /** @@ -96,6 +98,58 @@ class groups_permissions_o extends db $tmp_id->select((int)$id); $tmp_id->requireSelected(); $tmp_id->delete(); + $this->invalidateGroupSessionCaches($group_id); + } + + /** + * Invalidate cached permissions and session payloads for users associated with a group. + */ + private function invalidateGroupSessionCaches(int $group_id): void + { + if ($group_id <= 0 || !defined('redis')) { + return; + } + + try { + $userRows = (new users_o())->getFieldsWhere([ + 'group_id' => $group_id, + ], ['id']); + + if (count($userRows) === 0) { + return; + } + + $userIds = []; + foreach ($userRows as $userRow) { + $id = (int)($userRow['id'] ?? 0); + if ($id > 0) { + $userIds[] = $id; + } + } + $userIds = array_values(array_unique($userIds)); + + if (count($userIds) === 0) { + return; + } + + foreach ($userIds as $userId) { + redis->clear_keys('perm:user:' . $userId . ':*'); + } + + $tokenRows = (new tokens_o())->getFieldsWhere([ + 'user_id' => $userIds, + ], ['token']); + + foreach ($tokenRows as $tokenRow) { + $token = (string)($tokenRow['token'] ?? ''); + if ($token === '') { + continue; + } + redis->clear_auth_session($token); + } + } catch (Throwable) { + // Cache invalidation must not block permission updates. + } } public function asArray(): array @@ -145,4 +199,4 @@ class groups_permissions_o extends db return preg_match($regex, $permission['permission']); }); } -} \ No newline at end of file +} diff --git a/services/nginx/app/tests/Unit/Permissions/GroupPermissionSessionInvalidationWiringTest.php b/services/nginx/app/tests/Unit/Permissions/GroupPermissionSessionInvalidationWiringTest.php new file mode 100644 index 00000000..2e7e4f8f --- /dev/null +++ b/services/nginx/app/tests/Unit/Permissions/GroupPermissionSessionInvalidationWiringTest.php @@ -0,0 +1,13 @@ +not->toBeFalse(); + expect($content)->toContain('private function invalidateGroupSessionCaches(int $group_id): void'); + expect($content)->toContain('$this->invalidateGroupSessionCaches($group_id);'); + expect(substr_count((string)$content, '$this->invalidateGroupSessionCaches($group_id);'))->toBeGreaterThanOrEqual(2); + expect($content)->toContain("redis->clear_keys('perm:user:' . \$userId . ':*');"); + expect($content)->toContain('redis->clear_auth_session($token);'); +}); +