Implement updateRole (#10009)
In this PR, we are implementing the updateRole endpoint with the following rules 1. A user can only update a member's role if they have the permission (= the admin role) 2. Admin role can't be unassigned if there are no other admin in the workspace 3. (For now) as members can only have one role for now, when they are assigned a new role, they are first unassigned the other role (if any) 4. (For now) removing a member's admin role = leaving the member with no role = calling updateRole with a null roleId
This commit is contained in:
@ -14,4 +14,7 @@ export enum PermissionsExceptionCode {
|
||||
TOO_MANY_ADMIN_CANDIDATES = 'TOO_MANY_ADMIN_CANDIDATES',
|
||||
USER_WORKSPACE_ALREADY_HAS_ROLE = 'USER_WORKSPACE_ALREADY_HAS_ROLE',
|
||||
PERMISSION_DENIED = 'PERMISSION_DENIED',
|
||||
WORKSPACE_MEMBER_NOT_FOUND = 'WORKSPACE_MEMBER_NOT_FOUND',
|
||||
ROLE_NOT_FOUND = 'ROLE_NOT_FOUND',
|
||||
CANNOT_UNASSIGN_LAST_ADMIN = 'CANNOT_UNASSIGN_LAST_ADMIN',
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-works
|
||||
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/role/user-workspace-role.entity';
|
||||
import { UserRoleModule } from 'src/engine/metadata-modules/userRole/userRole.module';
|
||||
import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
||||
@ -7,7 +7,7 @@ import {
|
||||
PermissionsException,
|
||||
PermissionsExceptionCode,
|
||||
} from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||
import { UserRoleService } from 'src/engine/metadata-modules/userRole/userRole.service';
|
||||
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
|
||||
|
||||
@Injectable()
|
||||
export class PermissionsService {
|
||||
@ -21,8 +21,8 @@ export class PermissionsService {
|
||||
}: {
|
||||
userWorkspaceId: string;
|
||||
}): Promise<Record<SettingsFeatures, boolean>> {
|
||||
const roleOfUserWorkspace =
|
||||
await this.userRoleService.getRoleForUserWorkspace(userWorkspaceId);
|
||||
const [roleOfUserWorkspace] =
|
||||
await this.userRoleService.getRolesForUserWorkspace(userWorkspaceId);
|
||||
|
||||
let hasPermissionOnSettingFeature = false;
|
||||
|
||||
@ -46,8 +46,8 @@ export class PermissionsService {
|
||||
userWorkspaceId: string;
|
||||
setting: SettingsFeatures;
|
||||
}): Promise<void> {
|
||||
const userWorkspaceRole =
|
||||
await this.userRoleService.getRoleForUserWorkspace(userWorkspaceId);
|
||||
const [userWorkspaceRole] =
|
||||
await this.userRoleService.getRolesForUserWorkspace(userWorkspaceId);
|
||||
|
||||
if (userWorkspaceRole?.canUpdateAllSettings === true) {
|
||||
return;
|
||||
|
||||
@ -11,6 +11,7 @@ export const permissionsGraphqlApiExceptionHandler = (error: Error) => {
|
||||
if (error instanceof PermissionsException) {
|
||||
switch (error.code) {
|
||||
case PermissionsExceptionCode.PERMISSION_DENIED:
|
||||
case PermissionsExceptionCode.CANNOT_UNASSIGN_LAST_ADMIN:
|
||||
throw new ForbiddenError(error.message);
|
||||
default:
|
||||
throw new InternalServerError(error.message);
|
||||
|
||||
@ -2,12 +2,13 @@ import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { RoleResolver } from 'src/engine/metadata-modules/role/role.resolver';
|
||||
import { RoleService } from 'src/engine/metadata-modules/role/role.service';
|
||||
import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/role/user-workspace-role.entity';
|
||||
import { UserRoleModule } from 'src/engine/metadata-modules/userRole/userRole.module';
|
||||
import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@ -15,6 +16,7 @@ import { UserRoleModule } from 'src/engine/metadata-modules/userRole/userRole.mo
|
||||
TypeOrmModule.forFeature([UserWorkspace], 'core'),
|
||||
UserRoleModule,
|
||||
PermissionsModule,
|
||||
UserWorkspaceModule,
|
||||
],
|
||||
providers: [RoleService, RoleResolver],
|
||||
exports: [RoleService],
|
||||
|
||||
@ -1,7 +1,15 @@
|
||||
import { Parent, Query, ResolveField, Resolver } from '@nestjs/graphql';
|
||||
import {
|
||||
Args,
|
||||
Mutation,
|
||||
Parent,
|
||||
Query,
|
||||
ResolveField,
|
||||
Resolver,
|
||||
} from '@nestjs/graphql';
|
||||
|
||||
import { SettingsFeatures } from 'twenty-shared';
|
||||
import { isDefined, SettingsFeatures } from 'twenty-shared';
|
||||
|
||||
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
||||
import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-workspace-id.decorator';
|
||||
@ -10,7 +18,7 @@ import { PermissionsService } from 'src/engine/metadata-modules/permissions/perm
|
||||
import { permissionsGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception-handler';
|
||||
import { RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto';
|
||||
import { RoleService } from 'src/engine/metadata-modules/role/role.service';
|
||||
import { UserRoleService } from 'src/engine/metadata-modules/userRole/userRole.service';
|
||||
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
|
||||
@Resolver(() => RoleDTO)
|
||||
@ -19,6 +27,7 @@ export class RoleResolver {
|
||||
private readonly userRoleService: UserRoleService,
|
||||
private readonly permissionsService: PermissionsService,
|
||||
private readonly roleService: RoleService,
|
||||
private readonly userWorkspaceService: UserWorkspaceService,
|
||||
) {}
|
||||
|
||||
@Query(() => [RoleDTO])
|
||||
@ -52,6 +61,57 @@ export class RoleResolver {
|
||||
}
|
||||
}
|
||||
|
||||
@Mutation(() => WorkspaceMember)
|
||||
async updateWorkspaceMemberRole(
|
||||
@AuthUserWorkspaceId() currentUserWorkspaceId: string,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
@Args('workspaceMemberId') workspaceMemberId: string,
|
||||
@Args('roleId', { type: () => String, nullable: true })
|
||||
roleId: string | null,
|
||||
): Promise<WorkspaceMember> {
|
||||
await this.permissionsService.validateUserHasWorkspaceSettingPermissionOrThrow(
|
||||
{
|
||||
userWorkspaceId: currentUserWorkspaceId,
|
||||
setting: SettingsFeatures.ROLES,
|
||||
},
|
||||
);
|
||||
|
||||
const workspaceMember =
|
||||
await this.userWorkspaceService.getWorkspaceMemberOrThrow({
|
||||
workspaceMemberId,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
const userWorkspace =
|
||||
await this.userWorkspaceService.getUserWorkspaceForUserOrThrow({
|
||||
userId: workspaceMember.userId,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
if (!isDefined(roleId)) {
|
||||
await this.userRoleService.unassignAllRolesFromUserWorkspace({
|
||||
userWorkspaceId: userWorkspace.id,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
} else {
|
||||
await this.userRoleService.assignRoleToUserWorkspace({
|
||||
userWorkspaceId: userWorkspace.id,
|
||||
workspaceId: workspace.id,
|
||||
roleId,
|
||||
});
|
||||
}
|
||||
|
||||
const roles = await this.userRoleService.getRolesForUserWorkspace(
|
||||
userWorkspace.id,
|
||||
);
|
||||
|
||||
return {
|
||||
...workspaceMember,
|
||||
userWorkspaceId: userWorkspace.id,
|
||||
roles,
|
||||
} as WorkspaceMember;
|
||||
}
|
||||
|
||||
@ResolveField('workspaceMembers', () => [WorkspaceMember])
|
||||
async getWorkspaceMembersAssignedToRole(
|
||||
@Parent() role: RoleDTO,
|
||||
|
||||
@ -4,7 +4,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/role/user-workspace-role.entity';
|
||||
import { UserRoleService } from 'src/engine/metadata-modules/userRole/userRole.service';
|
||||
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@ -1,6 +1,5 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { In, Repository } from 'typeorm';
|
||||
|
||||
@ -9,6 +8,7 @@ import {
|
||||
PermissionsException,
|
||||
PermissionsExceptionCode,
|
||||
} from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||
import { RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/role/user-workspace-role.entity';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
@ -25,29 +25,6 @@ export class UserRoleService {
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {}
|
||||
|
||||
public async unassignRolesFromUserWorkspace({
|
||||
userWorkspaceId,
|
||||
workspaceId,
|
||||
}: {
|
||||
userWorkspaceId: string;
|
||||
workspaceId: string;
|
||||
}): Promise<void> {
|
||||
const userWorkspaceRoles = await this.userWorkspaceRoleRepository.find({
|
||||
where: {
|
||||
userWorkspaceId,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!isEmpty(userWorkspaceRoles)) {
|
||||
userWorkspaceRoles.forEach(async (userWorkspaceRole) => {
|
||||
await this.userWorkspaceRoleRepository.delete({
|
||||
id: userWorkspaceRole.id,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async assignRoleToUserWorkspace({
|
||||
workspaceId,
|
||||
userWorkspaceId,
|
||||
@ -70,7 +47,26 @@ export class UserRoleService {
|
||||
);
|
||||
}
|
||||
|
||||
await this.unassignRolesFromUserWorkspace({
|
||||
const role = await this.roleRepository.findOne({
|
||||
where: {
|
||||
id: roleId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!isDefined(role)) {
|
||||
throw new PermissionsException(
|
||||
'Role not found',
|
||||
PermissionsExceptionCode.ROLE_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const [currentRole] = await this.getRolesForUserWorkspace(userWorkspace.id);
|
||||
|
||||
if (currentRole?.id === roleId) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.unassignAllRolesFromUserWorkspace({
|
||||
userWorkspaceId: userWorkspace.id,
|
||||
workspaceId,
|
||||
});
|
||||
@ -82,13 +78,27 @@ export class UserRoleService {
|
||||
});
|
||||
}
|
||||
|
||||
public async getRoleForUserWorkspace(
|
||||
userWorkspaceId: string,
|
||||
): Promise<RoleEntity | null> {
|
||||
if (!isDefined(userWorkspaceId)) {
|
||||
return null;
|
||||
}
|
||||
public async unassignAllRolesFromUserWorkspace({
|
||||
userWorkspaceId,
|
||||
workspaceId,
|
||||
}: {
|
||||
userWorkspaceId: string;
|
||||
workspaceId: string;
|
||||
}): Promise<void> {
|
||||
await this.validatesUserWorkspaceIsNotLastAdminIfUnassigningAdminRoleOrThrow(
|
||||
userWorkspaceId,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
await this.userWorkspaceRoleRepository.delete({
|
||||
userWorkspaceId,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
public async getRolesForUserWorkspace(
|
||||
userWorkspaceId: string,
|
||||
): Promise<RoleDTO[] | []> {
|
||||
const userWorkspaceRole = await this.userWorkspaceRoleRepository.findOne({
|
||||
where: {
|
||||
userWorkspaceId,
|
||||
@ -96,14 +106,23 @@ export class UserRoleService {
|
||||
});
|
||||
|
||||
if (!isDefined(userWorkspaceRole)) {
|
||||
return null;
|
||||
return [];
|
||||
}
|
||||
|
||||
return await this.roleRepository.findOne({
|
||||
const role = await this.roleRepository.findOne({
|
||||
where: {
|
||||
id: userWorkspaceRole.roleId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!isDefined(role)) {
|
||||
throw new PermissionsException(
|
||||
'Role not found',
|
||||
PermissionsExceptionCode.ROLE_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
return [role];
|
||||
}
|
||||
|
||||
public async getWorkspaceMembersAssignedToRole(
|
||||
@ -145,4 +164,25 @@ export class UserRoleService {
|
||||
|
||||
return workspaceMembers;
|
||||
}
|
||||
|
||||
private async validatesUserWorkspaceIsNotLastAdminIfUnassigningAdminRoleOrThrow(
|
||||
userWorkspaceId: string,
|
||||
workspaceId: string,
|
||||
): Promise<void> {
|
||||
const roles = await this.getRolesForUserWorkspace(userWorkspaceId);
|
||||
|
||||
const adminRole = roles.find((role: RoleDTO) => role.isEditable === false);
|
||||
|
||||
if (isDefined(adminRole)) {
|
||||
const workspaceMembersWithAdminRole =
|
||||
await this.getWorkspaceMembersAssignedToRole(adminRole.id, workspaceId);
|
||||
|
||||
if (workspaceMembersWithAdminRole.length === 1) {
|
||||
throw new PermissionsException(
|
||||
`Cannot unassign admin role as there is only one admin in the workspace`,
|
||||
PermissionsExceptionCode.CANNOT_UNASSIGN_LAST_ADMIN,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user