import { InjectRepository } from '@nestjs/typeorm'; import { isDefined } from 'twenty-shared/utils'; import { Repository } from 'typeorm'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { ADMIN_ROLE_LABEL } from 'src/engine/metadata-modules/permissions/constants/admin-role-label.constants'; import { MEMBER_ROLE_LABEL } from 'src/engine/metadata-modules/permissions/constants/member-role-label.constants'; import { PermissionsException, PermissionsExceptionCode, PermissionsExceptionMessage, } from 'src/engine/metadata-modules/permissions/permissions.exception'; import { CreateRoleInput } from 'src/engine/metadata-modules/role/dtos/create-role-input.dto'; import { UpdateRoleInput, UpdateRolePayload, } from 'src/engine/metadata-modules/role/dtos/update-role-input.dto'; 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/user-role/user-role.service'; import { isArgDefinedIfProvidedOrThrow } from 'src/engine/metadata-modules/utils/is-arg-defined-if-provided-or-throw.util'; import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; export class RoleService { constructor( @InjectRepository(Workspace, 'core') private readonly workspaceRepository: Repository, @InjectRepository(RoleEntity, 'metadata') private readonly roleRepository: Repository, @InjectRepository(UserWorkspaceRoleEntity, 'metadata') private readonly userWorkspaceRoleRepository: Repository, private readonly userRoleService: UserRoleService, private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService, ) {} public async getWorkspaceRoles(workspaceId: string): Promise { return this.roleRepository.find({ where: { workspaceId, }, relations: [ 'userWorkspaceRoles', 'settingPermissions', 'objectPermissions', ], }); } public async getRoleById( id: string, workspaceId: string, ): Promise { return this.roleRepository.findOne({ where: { id, workspaceId, }, relations: ['userWorkspaceRoles', 'settingPermissions'], }); } public async createRole({ input, workspaceId, }: { input: CreateRoleInput; workspaceId: string; }): Promise { await this.validateRoleInput({ input, workspaceId }); const role = await this.roleRepository.save({ label: input.label, description: input.description, icon: input.icon, canUpdateAllSettings: input.canUpdateAllSettings, canReadAllObjectRecords: input.canReadAllObjectRecords, canUpdateAllObjectRecords: input.canUpdateAllObjectRecords, canSoftDeleteAllObjectRecords: input.canSoftDeleteAllObjectRecords, canDestroyAllObjectRecords: input.canDestroyAllObjectRecords, isEditable: true, workspaceId, }); await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache({ workspaceId, roleIds: [role.id], ignoreLock: true, }); return role; } public async updateRole({ input, workspaceId, }: { input: UpdateRoleInput; workspaceId: string; }): Promise { await this.validateRoleIsEditableOrThrow({ roleId: input.id, workspaceId, }); const existingRole = await this.roleRepository.findOne({ where: { id: input.id, workspaceId, }, }); if (!isDefined(existingRole)) { throw new PermissionsException( PermissionsExceptionMessage.ROLE_NOT_FOUND, PermissionsExceptionCode.ROLE_NOT_FOUND, ); } await this.validateRoleInput({ input: input.update, workspaceId, roleId: input.id, }); const updatedRole = await this.roleRepository.save({ id: input.id, ...input.update, }); await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache({ workspaceId, roleIds: [input.id], ignoreLock: true, }); return { ...existingRole, ...updatedRole }; } public async createAdminRole({ workspaceId, }: { workspaceId: string; }): Promise { return this.roleRepository.save({ label: ADMIN_ROLE_LABEL, description: 'Admin role', icon: 'IconUserCog', canUpdateAllSettings: true, canReadAllObjectRecords: true, canUpdateAllObjectRecords: true, canSoftDeleteAllObjectRecords: true, canDestroyAllObjectRecords: true, isEditable: false, workspaceId, }); } public async deleteRole( roleId: string, workspaceId: string, ): Promise { await this.validateRoleIsEditableOrThrow({ roleId, workspaceId, }); const defaultRole = await this.workspaceRepository.findOne({ where: { id: workspaceId, }, }); const defaultRoleId = defaultRole?.defaultRoleId; if (!isDefined(defaultRoleId)) { throw new PermissionsException( PermissionsExceptionMessage.DEFAULT_ROLE_NOT_FOUND, PermissionsExceptionCode.DEFAULT_ROLE_NOT_FOUND, ); } await this.validateRoleIsNotDefaultRoleOrThrow({ roleId, defaultRoleId, }); await this.assignDefaultRoleToMembersWithRoleToDelete({ roleId, workspaceId, defaultRoleId, }); await this.roleRepository.delete({ id: roleId, workspaceId, }); await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache({ workspaceId, ignoreLock: true, }); return roleId; } public async createMemberRole({ workspaceId, }: { workspaceId: string; }): Promise { return this.roleRepository.save({ label: MEMBER_ROLE_LABEL, description: 'Member role', icon: 'IconUser', canUpdateAllSettings: false, canReadAllObjectRecords: true, canUpdateAllObjectRecords: true, canSoftDeleteAllObjectRecords: true, canDestroyAllObjectRecords: true, isEditable: false, workspaceId, }); } // Only used for dev seeding and testing public async createGuestRole({ workspaceId, }: { workspaceId: string; }): Promise { return this.roleRepository.save({ label: 'Guest', description: 'Guest role', icon: 'IconUser', canUpdateAllSettings: false, canReadAllObjectRecords: true, canUpdateAllObjectRecords: false, canSoftDeleteAllObjectRecords: false, canDestroyAllObjectRecords: false, isEditable: false, workspaceId, }); } private async validateRoleInput({ input, workspaceId, roleId, }: { input: CreateRoleInput | UpdateRolePayload; workspaceId: string; roleId?: string; }): Promise { const keysToValidate = [ 'label', 'canUpdateAllSettings', 'canReadAllObjectRecords', 'canUpdateAllObjectRecords', 'canSoftDeleteAllObjectRecords', 'canDestroyAllObjectRecords', ]; for (const key of keysToValidate) { try { isArgDefinedIfProvidedOrThrow({ input, key, // @ts-expect-error legacy noImplicitAny value: input[key], }); } catch (error) { throw new PermissionsException( error.message, PermissionsExceptionCode.INVALID_ARG, ); } } if (isDefined(input.label)) { let workspaceRoles = await this.getWorkspaceRoles(workspaceId); if (isDefined(roleId)) { workspaceRoles = workspaceRoles.filter((role) => role.id !== roleId); } if (workspaceRoles.some((role) => role.label === input.label)) { throw new PermissionsException( PermissionsExceptionMessage.ROLE_LABEL_ALREADY_EXISTS, PermissionsExceptionCode.ROLE_LABEL_ALREADY_EXISTS, ); } } } private async validateRoleIsNotDefaultRoleOrThrow({ roleId, defaultRoleId, }: { roleId: string; defaultRoleId: string; }): Promise { if (defaultRoleId === roleId) { throw new PermissionsException( PermissionsExceptionMessage.DEFAULT_ROLE_CANNOT_BE_DELETED, PermissionsExceptionCode.DEFAULT_ROLE_CANNOT_BE_DELETED, ); } } private async assignDefaultRoleToMembersWithRoleToDelete({ roleId, workspaceId, defaultRoleId, }: { roleId: string; workspaceId: string; defaultRoleId: string; }): Promise { const userWorkspaceIds = await this.userRoleService.getUserWorkspaceIdsAssignedToRole( roleId, workspaceId, ); await Promise.all( userWorkspaceIds.map((userWorkspaceId) => this.userRoleService.assignRoleToUserWorkspace({ userWorkspaceId, roleId: defaultRoleId, workspaceId, }), ), ); } private async getRole( roleId: string, workspaceId: string, ): Promise { return this.roleRepository.findOne({ where: { id: roleId, workspaceId, }, }); } private async validateRoleIsEditableOrThrow({ roleId, workspaceId, }: { roleId: string; workspaceId: string; }) { const role = await this.getRole(roleId, workspaceId); if (!role?.isEditable) { throw new PermissionsException( PermissionsExceptionMessage.ROLE_NOT_EDITABLE, PermissionsExceptionCode.ROLE_NOT_EDITABLE, ); } } }