[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:
@ -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',
|
||||
}
|
||||
|
||||
@ -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],
|
||||
|
||||
@ -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> {
|
||||
|
||||
@ -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[]>;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user