[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

@ -12,4 +12,6 @@ export enum PermissionsExceptionCode {
USER_WORKSPACE_NOT_FOUND = 'USER_WORKSPACE_NOT_FOUND',
WORKSPACE_ID_ROLE_USER_WORKSPACE_MISMATCH = 'WORKSPACE_ID_ROLE_USER_WORKSPACE_MISMATCH',
TOO_MANY_ADMIN_CANDIDATES = 'TOO_MANY_ADMIN_CANDIDATES',
USER_WORKSPACE_ALREADY_HAS_ROLE = 'USER_WORKSPACE_ALREADY_HAS_ROLE',
PERMISSION_DENIED = 'PERMISSION_DENIED',
}

View File

@ -1,17 +1,21 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { EnvironmentModule } from 'src/engine/core-modules/environment/environment.module';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
import { RoleEntity } from 'src/engine/metadata-modules/permissions/role.entity';
import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/permissions/user-workspace-role.entity';
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';
@Module({
imports: [
TypeOrmModule.forFeature([RoleEntity, UserWorkspaceRoleEntity], 'metadata'),
FeatureFlagModule,
TypeOrmModule.forFeature([UserWorkspace], 'core'),
EnvironmentModule,
UserRoleModule,
],
providers: [PermissionsService],
exports: [PermissionsService],

View File

@ -1,71 +1,62 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { isDefined } from 'twenty-shared';
import { SettingsFeatures } from 'twenty-shared';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { ADMIN_ROLE_LABEL } from 'src/engine/metadata-modules/permissions/constants/admin-role-label.constants';
import {
PermissionsException,
PermissionsExceptionCode,
} from 'src/engine/metadata-modules/permissions/permissions.exception';
import { RoleEntity } from 'src/engine/metadata-modules/permissions/role.entity';
import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/permissions/user-workspace-role.entity';
import { UserRoleService } from 'src/engine/metadata-modules/userRole/userRole.service';
@Injectable()
export class PermissionsService {
constructor(
@InjectRepository(RoleEntity, 'metadata')
private readonly roleRepository: Repository<RoleEntity>,
@InjectRepository(UserWorkspaceRoleEntity, 'metadata')
private readonly userWorkspaceRoleRepository: Repository<UserWorkspaceRoleEntity>,
@InjectRepository(UserWorkspace, 'core')
private readonly userWorkspaceRepository: Repository<UserWorkspace>,
private readonly environmentService: EnvironmentService,
private readonly userRoleService: UserRoleService,
) {}
public async createAdminRole({
workspaceId,
public async getUserWorkspaceSettingsPermissions({
userWorkspaceId,
}: {
workspaceId: string;
}): Promise<RoleEntity> {
return this.roleRepository.save({
label: ADMIN_ROLE_LABEL,
description: 'Admin role',
canUpdateAllSettings: true,
isEditable: false,
workspaceId,
});
userWorkspaceId: string;
}): Promise<Record<SettingsFeatures, boolean>> {
const roleOfUserWorkspace =
await this.userRoleService.getRoleForUserWorkspace(userWorkspaceId);
let hasPermissionOnSettingFeature = false;
if (roleOfUserWorkspace?.canUpdateAllSettings === true) {
hasPermissionOnSettingFeature = true;
}
return Object.keys(SettingsFeatures).reduce(
(acc, feature) => ({
...acc,
[feature]: hasPermissionOnSettingFeature,
}),
{} as Record<SettingsFeatures, boolean>,
);
}
public async assignRoleToUserWorkspace({
workspaceId,
public async validateUserHasWorkspaceSettingPermissionOrThrow({
userWorkspaceId,
roleId,
setting,
}: {
workspaceId: string;
userWorkspaceId: string;
roleId: string;
setting: SettingsFeatures;
}): Promise<void> {
const userWorkspace = await this.userWorkspaceRepository.findOne({
where: {
id: userWorkspaceId,
},
});
const userWorkspaceRole =
await this.userRoleService.getRoleForUserWorkspace(userWorkspaceId);
if (!isDefined(userWorkspace)) {
throw new PermissionsException(
'User workspace not found',
PermissionsExceptionCode.USER_WORKSPACE_NOT_FOUND,
);
if (userWorkspaceRole?.canUpdateAllSettings === true) {
return;
}
await this.userWorkspaceRoleRepository.save({
roleId,
userWorkspaceId: userWorkspace.id,
workspaceId,
});
throw new PermissionsException(
`User does not have permission to update this setting: ${setting}`,
PermissionsExceptionCode.PERMISSION_DENIED,
);
}
public async isPermissionsEnabled(): Promise<boolean> {

View File

@ -1,44 +0,0 @@
import {
Column,
CreateDateColumn,
Entity,
OneToMany,
PrimaryGeneratedColumn,
Relation,
UpdateDateColumn,
} from 'typeorm';
import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/permissions/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

@ -1,39 +0,0 @@
import {
Column,
CreateDateColumn,
Entity,
ManyToOne,
PrimaryGeneratedColumn,
Relation,
Unique,
UpdateDateColumn,
} from 'typeorm';
import { RoleEntity } from 'src/engine/metadata-modules/permissions/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',
})
role: Relation<RoleEntity>;
@Column({ nullable: false, type: 'uuid' })
userWorkspaceId: string;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt: Date;
}

View File

@ -0,0 +1,21 @@
import {
ForbiddenError,
InternalServerError,
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import {
PermissionsException,
PermissionsExceptionCode,
} from 'src/engine/metadata-modules/permissions/permissions.exception';
export const permissionsGraphqlApiExceptionHandler = (error: Error) => {
if (error instanceof PermissionsException) {
switch (error.code) {
case PermissionsExceptionCode.PERMISSION_DENIED:
throw new ForbiddenError(error.message);
default:
throw new InternalServerError(error.message);
}
}
throw error;
};