[Permissions] Implement getRoles (#9955)

In this PR

- introducing roles module to separate roles logic (assign a Role, get a
workspace's roles etc.) from permission logic (check if a user has a
permission)
- Introduces getRoles endpoint to fetch a workspace's roles
- introduces the first permission check: getRoles in only accessible to
users with permission on ROLE setting. Implemented
validatesUserHasWorkspaceSettingPermissionOrThrow
This commit is contained in:
Marie
2025-02-03 19:14:18 +01:00
committed by GitHub
parent caee5b1f89
commit 351e768038
18 changed files with 413 additions and 50 deletions

View File

@ -0,0 +1,30 @@
import { Field, HideField, ObjectType } from '@nestjs/graphql';
import { Relation } from 'typeorm';
import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto';
import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/role/user-workspace-role.entity';
@ObjectType()
export class RoleDTO {
@Field({ nullable: false })
id: string;
@Field({ nullable: false })
label: string;
@Field({ nullable: false })
canUpdateAllSettings: boolean;
@Field({ nullable: true })
description: string;
@Field({ nullable: false })
isEditable: boolean;
@HideField()
userWorkspaceRoles: Relation<UserWorkspaceRoleEntity[]>;
@Field(() => [WorkspaceMember], { nullable: true })
workspaceMembers?: WorkspaceMember[];
}

View File

@ -0,0 +1,44 @@
import {
Column,
CreateDateColumn,
Entity,
OneToMany,
PrimaryGeneratedColumn,
Relation,
UpdateDateColumn,
} from 'typeorm';
import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/role/user-workspace-role.entity';
@Entity('role')
export class RoleEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ nullable: false })
label: string;
@Column({ nullable: false, default: false })
canUpdateAllSettings: boolean;
@Column({ nullable: true, type: 'text' })
description: string;
@Column({ nullable: false, type: 'uuid' })
workspaceId: string;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt: Date;
@Column({ nullable: false, default: true })
isEditable: boolean;
@OneToMany(
() => UserWorkspaceRoleEntity,
(userWorkspaceRole: UserWorkspaceRoleEntity) => userWorkspaceRole.role,
)
userWorkspaceRoles: Relation<UserWorkspaceRoleEntity[]>;
}

View File

@ -0,0 +1,22 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
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';
@Module({
imports: [
TypeOrmModule.forFeature([RoleEntity, UserWorkspaceRoleEntity], 'metadata'),
TypeOrmModule.forFeature([UserWorkspace], 'core'),
UserRoleModule,
PermissionsModule,
],
providers: [RoleService, RoleResolver],
exports: [RoleService],
})
export class RoleModule {}

View File

@ -0,0 +1,65 @@
import { Parent, Query, ResolveField, Resolver } from '@nestjs/graphql';
import { SettingsFeatures } from 'twenty-shared';
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';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
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 { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@Resolver(() => RoleDTO)
export class RoleResolver {
constructor(
private readonly userRoleService: UserRoleService,
private readonly permissionsService: PermissionsService,
private readonly roleService: RoleService,
) {}
@Query(() => [RoleDTO])
async getRoles(
@AuthUserWorkspaceId() userWorkspaceId: string,
@AuthWorkspace() workspace: Workspace,
): Promise<RoleDTO[]> {
try {
await this.permissionsService.validateUserHasWorkspaceSettingPermissionOrThrow(
{
userWorkspaceId,
setting: SettingsFeatures.ROLES,
},
);
const roles = await this.roleService.getWorkspaceRoles(workspace.id);
return roles.map((role) => ({
id: role.id,
label: role.label,
canUpdateAllSettings: role.canUpdateAllSettings,
description: role.description,
workspaceId: role.workspaceId,
createdAt: role.createdAt,
updatedAt: role.updatedAt,
isEditable: role.isEditable,
userWorkspaceRoles: role.userWorkspaceRoles,
}));
} catch (error) {
return permissionsGraphqlApiExceptionHandler(error);
}
}
@ResolveField('workspaceMembers', () => [WorkspaceMember])
async getWorkspaceMembersAssignedToRole(
@Parent() role: RoleDTO,
@AuthWorkspace() workspace: Workspace,
): Promise<WorkspaceMemberWorkspaceEntity[]> {
return this.userRoleService.getWorkspaceMembersAssignedToRole(
role.id,
workspace.id,
);
}
}

View File

@ -0,0 +1,36 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ADMIN_ROLE_LABEL } from 'src/engine/metadata-modules/permissions/constants/admin-role-label.constants';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
export class RoleService {
constructor(
@InjectRepository(RoleEntity, 'metadata')
private readonly roleRepository: Repository<RoleEntity>,
) {}
public async getWorkspaceRoles(workspaceId: string): Promise<RoleEntity[]> {
return this.roleRepository.find({
where: {
workspaceId,
},
relations: ['userWorkspaceRoles'],
});
}
public async createAdminRole({
workspaceId,
}: {
workspaceId: string;
}): Promise<RoleEntity> {
return this.roleRepository.save({
label: ADMIN_ROLE_LABEL,
description: 'Admin role',
canUpdateAllSettings: true,
isEditable: false,
workspaceId,
});
}
}

View File

@ -0,0 +1,41 @@
import {
Column,
CreateDateColumn,
Entity,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
Relation,
Unique,
UpdateDateColumn,
} from 'typeorm';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
@Entity('userWorkspaceRole')
@Unique('IndexOnUserWorkspaceRoleUnique', ['userWorkspaceId', 'roleId'])
export class UserWorkspaceRoleEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ nullable: false, type: 'uuid' })
workspaceId: string;
@Column({ nullable: false, type: 'uuid' })
roleId: string;
@ManyToOne(() => RoleEntity, (role) => role.userWorkspaceRoles, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'roleId' })
role: Relation<RoleEntity>;
@Column({ nullable: false, type: 'uuid' })
userWorkspaceId: string;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt: Date;
}