[permissions] forbid deletion of last admin user (#10504)

A user should not be able to delete their account if they are the last
admin of a workspace.

It means that if a user wants to sign out of twenty, they should delete
their workspace, not their account
This commit is contained in:
Marie
2025-02-27 12:44:51 +01:00
committed by GitHub
parent fb38828943
commit 17dbb634ca
8 changed files with 158 additions and 65 deletions

View File

@ -16,9 +16,11 @@ export enum PermissionsExceptionCode {
WORKSPACE_MEMBER_NOT_FOUND = 'WORKSPACE_MEMBER_NOT_FOUND',
ROLE_NOT_FOUND = 'ROLE_NOT_FOUND',
CANNOT_UNASSIGN_LAST_ADMIN = 'CANNOT_UNASSIGN_LAST_ADMIN',
CANNOT_DELETE_LAST_ADMIN_USER = 'CANNOT_DELETE_LAST_ADMIN_USER',
UNKNOWN_OPERATION_NAME = 'UNKNOWN_OPERATION_NAME',
UNKNOWN_REQUIRED_PERMISSION = 'UNKNOWN_REQUIRED_PERMISSION',
CANNOT_UPDATE_SELF_ROLE = 'CANNOT_UPDATE_SELF_ROLE',
NO_ROLE_FOUND_FOR_USER_WORKSPACE = 'NO_ROLE_FOUND_FOR_USER_WORKSPACE',
}
export enum PermissionsExceptionMessage {
@ -31,7 +33,9 @@ export enum PermissionsExceptionMessage {
WORKSPACE_MEMBER_NOT_FOUND = 'Workspace member not found',
ROLE_NOT_FOUND = 'Role not found',
CANNOT_UNASSIGN_LAST_ADMIN = 'Cannot unassign admin role from last admin of the workspace',
CANNOT_DELETE_LAST_ADMIN_USER = 'Cannot delete account: user is the unique admin of a workspace',
UNKNOWN_OPERATION_NAME = 'Unknown operation name, cannot determine required permission',
UNKNOWN_REQUIRED_PERMISSION = 'Unknown required permission',
CANNOT_UPDATE_SELF_ROLE = 'Cannot update self role',
NO_ROLE_FOUND_FOR_USER_WORKSPACE = 'No role found for userWorkspace',
}

View File

@ -15,6 +15,7 @@ export const permissionGraphqlApiExceptionHandler = (
case PermissionsExceptionCode.PERMISSION_DENIED:
case PermissionsExceptionCode.CANNOT_UNASSIGN_LAST_ADMIN:
case PermissionsExceptionCode.CANNOT_UPDATE_SELF_ROLE:
case PermissionsExceptionCode.CANNOT_DELETE_LAST_ADMIN_USER:
throw new ForbiddenError(error.message);
case PermissionsExceptionCode.ROLE_NOT_FOUND:
case PermissionsExceptionCode.USER_WORKSPACE_NOT_FOUND:

View File

@ -137,6 +137,35 @@ export class UserRoleService {
return workspaceMembers;
}
public async validateUserWorkspaceIsNotUniqueAdminOrThrow({
userWorkspaceId,
workspaceId,
}: {
userWorkspaceId: string;
workspaceId: string;
}) {
const roleOfUserWorkspace = await this.getRolesByUserWorkspaces({
userWorkspaceIds: [userWorkspaceId],
workspaceId,
}).then((roles) => roles.get(userWorkspaceId)?.[0]);
if (!isDefined(roleOfUserWorkspace)) {
throw new PermissionsException(
PermissionsExceptionMessage.NO_ROLE_FOUND_FOR_USER_WORKSPACE,
PermissionsExceptionCode.NO_ROLE_FOUND_FOR_USER_WORKSPACE,
);
}
if (roleOfUserWorkspace.label === ADMIN_ROLE_LABEL) {
const adminRole = roleOfUserWorkspace;
await this.validateMoreThanOneWorkspaceMemberHasAdminRoleOrThrow({
adminRoleId: adminRole.id,
workspaceId,
});
}
}
private async validateAssignRoleInput({
userWorkspaceId,
workspaceId,
@ -187,8 +216,21 @@ export class UserRoleService {
return;
}
await this.validateMoreThanOneWorkspaceMemberHasAdminRoleOrThrow({
workspaceId,
adminRoleId: currentRole.id,
});
}
private async validateMoreThanOneWorkspaceMemberHasAdminRoleOrThrow({
adminRoleId,
workspaceId,
}: {
adminRoleId: string;
workspaceId: string;
}) {
const workspaceMembersWithAdminRole =
await this.getWorkspaceMembersAssignedToRole(currentRole.id, workspaceId);
await this.getWorkspaceMembersAssignedToRole(adminRoleId, workspaceId);
if (workspaceMembersWithAdminRole.length === 1) {
throw new PermissionsException(