diff --git a/src/lib/db/user-store.ts b/src/lib/db/user-store.ts index 04d2ab73a6..5f22610335 100644 --- a/src/lib/db/user-store.ts +++ b/src/lib/db/user-store.ts @@ -272,6 +272,10 @@ class UserStore implements IUserStore { await this.activeUsers().del(); } + async deleteScimUsers(): Promise { + await this.db(TABLE).whereNotNull('scim_id').del(); + } + async count(): Promise { return this.activeUsers() .count('*') diff --git a/src/lib/routes/admin-api/user-admin.ts b/src/lib/routes/admin-api/user-admin.ts index 344dc09e69..8ff1f08671 100644 --- a/src/lib/routes/admin-api/user-admin.ts +++ b/src/lib/routes/admin-api/user-admin.ts @@ -441,6 +441,26 @@ export default class UserAdminController extends Controller { }), ], }); + + //add a method to delete all scim users + this.route({ + method: 'delete', + path: '/scim-users', + handler: this.deleteScimUsers, + permission: ADMIN, + middleware: [ + openApiService.validPath({ + tags: ['Users'], + operationId: 'deleteScimUsers', + summary: 'Delete all SCIM users', + description: 'Deletes all users managed by SCIM', + responses: { + 200: emptyResponse, + ...getStandardResponses(401, 403), + }, + }), + ], + }); } async resetUserPassword( @@ -766,4 +786,10 @@ export default class UserAdminController extends Controller { Boolean((await this.userService.getUser(id)).scimId) ); } + + async deleteScimUsers(req: IAuthRequest, res: Response): Promise { + const { audit } = req; + await this.userService.deleteScimUsers(audit); + res.status(200).send(); + } } diff --git a/src/lib/services/user-service.ts b/src/lib/services/user-service.ts index e38f52d1b8..5fb8f92c29 100644 --- a/src/lib/services/user-service.ts +++ b/src/lib/services/user-service.ts @@ -24,6 +24,7 @@ import type SessionService from './session-service'; import type { IUnleashStores } from '../types/stores'; import PasswordUndefinedError from '../error/password-undefined'; import { + ScimUsersDeleted, UserCreatedEvent, UserDeletedEvent, UserUpdatedEvent, @@ -398,6 +399,17 @@ class UserService { ); } + async deleteScimUsers(auditUser: IAuditUser): Promise { + await this.store.deleteScimUsers(); + + await this.eventService.storeEvent( + new ScimUsersDeleted({ + data: null, + auditUser, + }), + ); + } + async loginUser( usernameOrEmail: string, password: string, diff --git a/src/lib/types/events.ts b/src/lib/types/events.ts index 956f204d32..3fffaf3509 100644 --- a/src/lib/types/events.ts +++ b/src/lib/types/events.ts @@ -217,6 +217,8 @@ export const RELEASE_PLAN_MILESTONE_STARTED = 'release-plan-milestone-started' as const; export const USER_PREFERENCE_UPDATED = 'user-preference-updated' as const; +export const SCIM_USERS_DELETED = 'scim-users-deleted' as const; +export const SCIM_GROUPS_DELETED = 'scim-groups-deleted' as const; export const IEventTypes = [ APPLICATION_CREATED, @@ -372,6 +374,8 @@ export const IEventTypes = [ RELEASE_PLAN_REMOVED, RELEASE_PLAN_MILESTONE_STARTED, USER_PREFERENCE_UPDATED, + SCIM_USERS_DELETED, + SCIM_GROUPS_DELETED, ] as const; export type IEventType = (typeof IEventTypes)[number]; @@ -1608,6 +1612,30 @@ export class UserDeletedEvent extends BaseEvent { } } +export class ScimUsersDeleted extends BaseEvent { + readonly data: any; + + constructor(eventData: { + data: any; + auditUser: IAuditUser; + }) { + super(SCIM_USERS_DELETED, eventData.auditUser); + this.data = eventData.data; + } +} + +export class ScimGroupsDeleted extends BaseEvent { + readonly data: any; + + constructor(eventData: { + data: any; + auditUser: IAuditUser; + }) { + super(SCIM_GROUPS_DELETED, eventData.auditUser); + this.data = eventData.data; + } +} + export class TagTypeCreatedEvent extends BaseEvent { readonly data: any; diff --git a/src/lib/types/stores/user-store.ts b/src/lib/types/stores/user-store.ts index e3fe1aea44..17332935e8 100644 --- a/src/lib/types/stores/user-store.ts +++ b/src/lib/types/stores/user-store.ts @@ -40,4 +40,5 @@ export interface IUserStore extends Store { count(): Promise; countRecentlyDeleted(): Promise; countServiceAccounts(): Promise; + deleteScimUsers(): Promise; } diff --git a/src/test/fixtures/fake-user-store.ts b/src/test/fixtures/fake-user-store.ts index f37b6c67d3..4802ad0810 100644 --- a/src/test/fixtures/fake-user-store.ts +++ b/src/test/fixtures/fake-user-store.ts @@ -159,6 +159,10 @@ class UserStoreMock implements IUserStore { return Promise.resolve(undefined); } + deleteScimUsers(): Promise { + throw new Error('Method not implemented.'); + } + upsert(user: ICreateUser): Promise { this.data.splice(this.data.findIndex((u) => u.email === user.email)); const userToReturn = {