From 5901475c9eca03f56a3502e7f395fd3cfe7d7784 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gast=C3=B3n=20Fournier?= Date: Mon, 7 Jul 2025 12:17:37 +0200 Subject: [PATCH] fix: audit scim user deleted events (#10322) SCIM users deleted in bulk are not captured in the event log. We just add an event like this: ![image](https://github.com/user-attachments/assets/2d7078b2-61d6-475e-8151-63b5b5ed7449) This prevents partial user sync because we don't get an event when the user was deleted. --- src/lib/features/users/user-store.ts | 8 ++++++-- src/lib/services/user-service.ts | 26 +++++++++++++++++++------- src/lib/types/stores/user-store.ts | 2 +- src/test/fixtures/fake-user-store.ts | 2 +- 4 files changed, 27 insertions(+), 11 deletions(-) diff --git a/src/lib/features/users/user-store.ts b/src/lib/features/users/user-store.ts index 81c4e2feef..da4582e568 100644 --- a/src/lib/features/users/user-store.ts +++ b/src/lib/features/users/user-store.ts @@ -290,8 +290,12 @@ export class UserStore implements IUserStore { await this.activeUsers().del(); } - async deleteScimUsers(): Promise { - await this.db(TABLE).whereNotNull('scim_id').del(); + async deleteScimUsers(): Promise { + const rows = await this.db(TABLE) + .whereNotNull('scim_id') + .del() + .returning(USER_COLUMNS); + return rows.map(rowToUser); } async count(): Promise { diff --git a/src/lib/services/user-service.ts b/src/lib/services/user-service.ts index c0a036e7d2..6f4baef931 100644 --- a/src/lib/services/user-service.ts +++ b/src/lib/services/user-service.ts @@ -403,14 +403,26 @@ class UserService { } async deleteScimUsers(auditUser: IAuditUser): Promise { - await this.store.deleteScimUsers(); - - await this.eventService.storeEvent( - new ScimUsersDeleted({ - data: null, - auditUser, - }), + const users = await this.store.deleteScimUsers(); + // Note: after deletion we can't get the role for the user + const viewerRole = await this.accessService.getPredefinedRole( + RoleName.VIEWER, ); + if (users.length > 0) { + const deletions = users.map((user) => { + return new UserDeletedEvent({ + deletedUser: { ...user, rootRole: viewerRole.id }, + auditUser, + }); + }); + await this.eventService.storeEvents([ + ...deletions, + new ScimUsersDeleted({ + data: null, + auditUser, + }), + ]); + } } async loginUser( diff --git a/src/lib/types/stores/user-store.ts b/src/lib/types/stores/user-store.ts index 40ae4f6896..7bd7136bc5 100644 --- a/src/lib/types/stores/user-store.ts +++ b/src/lib/types/stores/user-store.ts @@ -46,5 +46,5 @@ export interface IUserStore extends Store { count(): Promise; countRecentlyDeleted(): Promise; countServiceAccounts(): Promise; - deleteScimUsers(): Promise; + deleteScimUsers(): Promise; } diff --git a/src/test/fixtures/fake-user-store.ts b/src/test/fixtures/fake-user-store.ts index 7d2cdf541a..8fef1ebaef 100644 --- a/src/test/fixtures/fake-user-store.ts +++ b/src/test/fixtures/fake-user-store.ts @@ -159,7 +159,7 @@ class UserStoreMock implements IUserStore { return Promise.resolve(undefined); } - deleteScimUsers(): Promise { + deleteScimUsers(): Promise { throw new Error('Method not implemented.'); }