Skip to content

Commit bf9fdd4

Browse files
feat: allow SCIM user deletion (#9190)
Co-authored-by: Gastón Fournier <[email protected]>
1 parent cdeb515 commit bf9fdd4

File tree

20 files changed

+308
-15
lines changed

20 files changed

+308
-15
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { Alert, styled, Typography } from '@mui/material';
2+
import { Dialogue } from 'component/common/Dialogue/Dialogue';
3+
4+
const StyledAlert = styled(Alert)(({ theme }) => ({
5+
marginBottom: theme.spacing(3),
6+
}));
7+
8+
export type EntityType = 'Users' | 'Groups';
9+
10+
interface IScimDeleteUsersProps {
11+
open: boolean;
12+
entityType: EntityType;
13+
closeDialog: () => void;
14+
deleteEntities: () => void;
15+
}
16+
17+
export const ScimDeleteEntityDialog = ({
18+
open,
19+
closeDialog,
20+
deleteEntities: removeUser,
21+
entityType,
22+
}: IScimDeleteUsersProps) => (
23+
<Dialogue
24+
open={open}
25+
primaryButtonText={`Delete SCIM ${entityType}`}
26+
secondaryButtonText='Cancel'
27+
title={`Do you really want to delete ALL SCIM ${entityType}?`}
28+
onClose={closeDialog}
29+
onClick={removeUser}
30+
>
31+
<Typography variant='body1'>
32+
This will delete all {entityType.toLocaleLowerCase()} created or
33+
managed by SCIM.
34+
</Typography>
35+
</Dialogue>
36+
);

frontend/src/component/admin/auth/ScimSettings/ScimSettings.tsx

+100
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import { formatUnknownError } from 'utils/formatUnknownError';
99
import useToast from 'hooks/useToast';
1010
import { useScimSettingsApi } from 'hooks/api/actions/useScimSettingsApi/useScimSettingsApi';
1111
import { useScimSettings } from 'hooks/api/getters/useScimSettings/useScimSettings';
12+
import { ScimDeleteEntityDialog } from './ScimDeleteUsersDialog';
13+
import useAdminUsersApi from 'hooks/api/actions/useAdminUsersApi/useAdminUsersApi';
14+
import { useGroupApi } from 'hooks/api/actions/useGroupApi/useGroupApi';
1215

1316
const StyledContainer = styled('div')(({ theme }) => ({
1417
padding: theme.spacing(3),
@@ -25,8 +28,12 @@ export const ScimSettings = () => {
2528
const { setToastData, setToastApiError } = useToast();
2629
const [newToken, setNewToken] = useState('');
2730
const [tokenGenerationDialog, setTokenGenerationDialog] = useState(false);
31+
const [deleteGroupsDialog, setDeleteGroupsDialog] = useState(false);
32+
const [deleteUsersDialog, setDeleteUsersDialog] = useState(false);
2833
const [tokenDialog, setTokenDialog] = useState(false);
2934
const { settings, refetch } = useScimSettings();
35+
const { deleteScimUsers } = useAdminUsersApi();
36+
const { deleteScimGroups } = useGroupApi();
3037
const [enabled, setEnabled] = useState(settings.enabled ?? true);
3138

3239
useEffect(() => {
@@ -40,6 +47,34 @@ export const ScimSettings = () => {
4047
setTokenGenerationDialog(true);
4148
};
4249

50+
const onDeleteScimGroups = async () => {
51+
try {
52+
await deleteScimGroups();
53+
setToastData({
54+
text: 'Scim Groups have been deleted',
55+
type: 'success',
56+
});
57+
setDeleteGroupsDialog(false);
58+
refetch();
59+
} catch (error: unknown) {
60+
setToastApiError(formatUnknownError(error));
61+
}
62+
};
63+
64+
const onDeleteScimUsers = async () => {
65+
try {
66+
await deleteScimUsers();
67+
setToastData({
68+
text: 'Scim Users have been deleted',
69+
type: 'success',
70+
});
71+
setDeleteUsersDialog(false);
72+
refetch();
73+
} catch (error: unknown) {
74+
setToastApiError(formatUnknownError(error));
75+
}
76+
};
77+
4378
const onGenerateNewTokenConfirm = async () => {
4479
setTokenGenerationDialog(false);
4580
const token = await generateNewToken();
@@ -138,6 +173,57 @@ export const ScimSettings = () => {
138173
/>
139174
</Grid>
140175
</Grid>
176+
177+
<Grid container spacing={3}>
178+
<Grid item md={10.5} mb={2}>
179+
<StyledTitleDiv>
180+
<strong>Delete SCIM Users</strong>
181+
</StyledTitleDiv>
182+
<p>
183+
This will remove all SCIM users from the Unleash
184+
database. This action cannot be undone through
185+
Unleash but the upstream SCIM provider may re sync
186+
these users.
187+
</p>
188+
</Grid>
189+
<Grid item md={1.5}>
190+
<Button
191+
variant='outlined'
192+
color='error'
193+
disabled={loading}
194+
onClick={() => {
195+
setDeleteUsersDialog(true);
196+
}}
197+
>
198+
Delete Users
199+
</Button>
200+
</Grid>
201+
<Grid item md={10.5} mb={2}>
202+
<StyledTitleDiv>
203+
<strong>Delete SCIM Groups</strong>
204+
</StyledTitleDiv>
205+
<p>
206+
This will remove all SCIM groups from the Unleash
207+
database. This action cannot be undone through
208+
Unleash but the upstream SCIM provider may re sync
209+
these groups. Note that this may affect the
210+
permissions of users present in those groups.
211+
</p>
212+
</Grid>
213+
<Grid item md={1.5}>
214+
<Button
215+
variant='outlined'
216+
color='error'
217+
disabled={loading}
218+
onClick={() => {
219+
setDeleteGroupsDialog(true);
220+
}}
221+
>
222+
Delete Groups
223+
</Button>
224+
</Grid>
225+
</Grid>
226+
141227
<ScimTokenGenerationDialog
142228
open={tokenGenerationDialog}
143229
setOpen={setTokenGenerationDialog}
@@ -148,6 +234,20 @@ export const ScimSettings = () => {
148234
setOpen={setTokenDialog}
149235
token={newToken}
150236
/>
237+
238+
<ScimDeleteEntityDialog
239+
open={deleteUsersDialog}
240+
closeDialog={() => setDeleteUsersDialog(false)}
241+
deleteEntities={onDeleteScimUsers}
242+
entityType='Users'
243+
/>
244+
245+
<ScimDeleteEntityDialog
246+
open={deleteGroupsDialog}
247+
closeDialog={() => setDeleteGroupsDialog(false)}
248+
deleteEntities={onDeleteScimGroups}
249+
entityType='Groups'
250+
/>
151251
</StyledContainer>
152252
</>
153253
);

frontend/src/component/admin/groups/Group/Group.tsx

+1-4
Original file line numberDiff line numberDiff line change
@@ -270,11 +270,8 @@ export const Group: VFC = () => {
270270
onClick={() => setRemoveOpen(true)}
271271
permission={ADMIN}
272272
tooltipProps={{
273-
title: isScimGroup
274-
? scimGroupTooltip
275-
: 'Delete group',
273+
title: 'Delete group',
276274
}}
277-
disabled={isScimGroup}
278275
>
279276
<Delete />
280277
</PermissionIconButton>

frontend/src/component/admin/groups/GroupsList/GroupCard/GroupCardActions.tsx

-1
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,6 @@ export const GroupCardActions: FC<IGroupCardActions> = ({
125125
onRemove();
126126
handleClose();
127127
}}
128-
disabled={isScimGroup}
129128
>
130129
<ListItemIcon>
131130
<Delete />

frontend/src/component/admin/users/UsersList/DeleteUser/DeleteUser.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ const DeleteUser = ({
7777
})`
7878
: ''}
7979
?
80+
{user.scimId
81+
? ' This user is currently managed by SCIM and may be re-added by your SCIM provider.'
82+
: ''}
8083
</Typography>
8184
</div>
8285
</Dialogue>

frontend/src/component/admin/users/UsersList/UsersActionsCell/UsersActionsCell.tsx

+1-2
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,8 @@ export const UsersActionsCell: VFC<IUsersActionsCellProps> = ({
9191
onClick={onDelete}
9292
permission={ADMIN}
9393
tooltipProps={{
94-
title: isScimUser ? scimTooltip : 'Remove user',
94+
title: 'Remove user',
9595
}}
96-
disabled={isScimUser}
9796
>
9897
<Delete />
9998
</PermissionIconButton>

frontend/src/hooks/api/actions/useAdminUsersApi/useAdminUsersApi.ts

+14
Original file line numberDiff line numberDiff line change
@@ -94,13 +94,27 @@ const useAdminUsersApi = () => {
9494
return makeRequest(req.caller, req.id);
9595
};
9696

97+
const deleteScimUsers = async () => {
98+
const requestId = 'deleteScimUsers';
99+
const req = createRequest(
100+
'api/admin/user-admin/scim-users',
101+
{
102+
method: 'DELETE',
103+
},
104+
requestId,
105+
);
106+
107+
return makeRequest(req.caller, req.id);
108+
};
109+
97110
return {
98111
addUser,
99112
updateUser,
100113
removeUser,
101114
changePassword,
102115
validatePassword,
103116
resetPassword,
117+
deleteScimUsers,
104118
userApiErrors: errors,
105119
userLoading: loading,
106120
};

frontend/src/hooks/api/actions/useGroupApi/useGroupApi.ts

+10
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,20 @@ export const useGroupApi = () => {
4646
await makeRequest(req.caller, req.id);
4747
};
4848

49+
const deleteScimGroups = async () => {
50+
const path = `api/admin/groups/scim-groups`;
51+
const req = createRequest(path, {
52+
method: 'DELETE',
53+
});
54+
55+
await makeRequest(req.caller, req.id);
56+
};
57+
4958
return {
5059
createGroup,
5160
updateGroup,
5261
removeGroup,
62+
deleteScimGroups,
5363
errors,
5464
loading,
5565
};

src/lib/db/group-store.ts

+4
Original file line numberDiff line numberDiff line change
@@ -354,4 +354,8 @@ export default class GroupStore implements IGroupStore {
354354
const { present } = result.rows[0];
355355
return present;
356356
}
357+
358+
async deleteScimGroups(): Promise<void> {
359+
await this.db(T.GROUPS).whereNotNull('scim_id').del();
360+
}
357361
}

src/lib/db/user-store.ts

+4
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,10 @@ class UserStore implements IUserStore {
274274
await this.activeUsers().del();
275275
}
276276

277+
async deleteScimUsers(): Promise<void> {
278+
await this.db(TABLE).whereNotNull('scim_id').del();
279+
}
280+
277281
async count(): Promise<number> {
278282
return this.activeUsers()
279283
.count('*')

src/lib/routes/admin-api/user-admin.ts

+26-2
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,26 @@ export default class UserAdminController extends Controller {
411411
],
412412
});
413413

414+
this.route({
415+
method: 'delete',
416+
path: '/scim-users',
417+
acceptAnyContentType: true,
418+
handler: this.deleteScimUsers,
419+
permission: ADMIN,
420+
middleware: [
421+
openApiService.validPath({
422+
tags: ['Users'],
423+
operationId: 'deleteScimUsers',
424+
summary: 'Delete all SCIM users',
425+
description: 'Deletes all users managed by SCIM',
426+
responses: {
427+
200: emptyResponse,
428+
...getStandardResponses(401, 403),
429+
},
430+
}),
431+
],
432+
});
433+
414434
this.route({
415435
method: 'delete',
416436
path: '/:id',
@@ -654,8 +674,6 @@ export default class UserAdminController extends Controller {
654674
const { user, params } = req;
655675
const { id } = params;
656676

657-
await this.throwIfScimUser({ id });
658-
659677
await this.userService.deleteUser(+id, req.audit);
660678
res.status(200).send();
661679
}
@@ -766,4 +784,10 @@ export default class UserAdminController extends Controller {
766784
Boolean((await this.userService.getUser(id)).scimId)
767785
);
768786
}
787+
788+
async deleteScimUsers(req: IAuthRequest, res: Response): Promise<void> {
789+
const { audit } = req;
790+
await this.userService.deleteScimUsers(audit);
791+
res.status(200).send();
792+
}
769793
}

src/lib/services/group-service.ts

+11
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
GROUP_CREATED,
2222
GroupUserAdded,
2323
GroupUserRemoved,
24+
ScimGroupsDeleted,
2425
type IBaseEvent,
2526
} from '../types/events';
2627
import NameExistsError from '../error/name-exists-error';
@@ -310,6 +311,16 @@ export class GroupService {
310311
}
311312
}
312313

314+
async deleteScimGroups(auditUser: IAuditUser): Promise<void> {
315+
await this.groupStore.deleteScimGroups();
316+
await this.eventService.storeEvent(
317+
new ScimGroupsDeleted({
318+
data: null,
319+
auditUser,
320+
}),
321+
);
322+
}
323+
313324
private mapGroupWithUsers(
314325
group: IGroup,
315326
allGroupUsers: IGroupUser[],

src/lib/services/user-service.ts

+12
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import type SessionService from './session-service';
2424
import type { IUnleashStores } from '../types/stores';
2525
import PasswordUndefinedError from '../error/password-undefined';
2626
import {
27+
ScimUsersDeleted,
2728
UserCreatedEvent,
2829
UserDeletedEvent,
2930
UserUpdatedEvent,
@@ -401,6 +402,17 @@ class UserService {
401402
);
402403
}
403404

405+
async deleteScimUsers(auditUser: IAuditUser): Promise<void> {
406+
await this.store.deleteScimUsers();
407+
408+
await this.eventService.storeEvent(
409+
new ScimUsersDeleted({
410+
data: null,
411+
auditUser,
412+
}),
413+
);
414+
}
415+
404416
async loginUser(
405417
usernameOrEmail: string,
406418
password: string,

0 commit comments

Comments
 (0)