Introduce userWorkspaceRoles and Roles + seed standard admin role at workspace creation (#9929)

Closes https://github.com/twentyhq/core-team-issues/issues/303
This commit is contained in:
Marie
2025-01-30 16:05:33 +01:00
committed by GitHub
parent e895aa27e6
commit 3a78e6f889
10 changed files with 279 additions and 2 deletions

View File

@ -94,6 +94,11 @@ export class EnvironmentVariables {
@IsBoolean()
TELEMETRY_ENABLED = true;
@CastToBoolean()
@IsOptional()
@IsBoolean()
PERMISSIONS_ENABLED = false;
@CastToBoolean()
@IsOptional()
@IsBoolean()

View File

@ -0,0 +1 @@
export const ADMIN_ROLE_LABEL = 'Admin';

View File

@ -0,0 +1,15 @@
import { CustomException } from 'src/utils/custom-exception';
export class PermissionsException extends CustomException {
code: PermissionsExceptionCode;
constructor(message: string, code: PermissionsExceptionCode) {
super(message, code);
}
}
export enum PermissionsExceptionCode {
ADMIN_ROLE_NOT_FOUND = 'ADMIN_ROLE_NOT_FOUND',
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',
}

View File

@ -0,0 +1,19 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
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';
@Module({
imports: [
TypeOrmModule.forFeature([RoleEntity, UserWorkspaceRoleEntity], 'metadata'),
FeatureFlagModule,
TypeOrmModule.forFeature([UserWorkspace], 'core'),
],
providers: [PermissionsService],
exports: [PermissionsService],
})
export class PermissionsModule {}

View File

@ -0,0 +1,74 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
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 { isDefined } from 'src/utils/is-defined';
@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,
) {}
public async createAdminRole({
workspaceId,
}: {
workspaceId: string;
}): Promise<RoleEntity> {
return this.roleRepository.save({
label: ADMIN_ROLE_LABEL,
description: 'Admin role',
canUpdateAllSettings: true,
isEditable: false,
workspaceId,
});
}
public async assignRoleToUserWorkspace({
workspaceId,
userWorkspaceId,
roleId,
}: {
workspaceId: string;
userWorkspaceId: string;
roleId: string;
}): Promise<void> {
const userWorkspace = await this.userWorkspaceRepository.findOne({
where: {
id: userWorkspaceId,
},
});
if (!isDefined(userWorkspace)) {
throw new PermissionsException(
'User workspace not found',
PermissionsExceptionCode.USER_WORKSPACE_NOT_FOUND,
);
}
await this.userWorkspaceRoleRepository.save({
roleId,
userWorkspaceId: userWorkspace.id,
workspaceId,
});
}
public async isPermissionsEnabled(): Promise<boolean> {
return this.environmentService.get('PERMISSIONS_ENABLED') === true;
}
}

View File

@ -0,0 +1,44 @@
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

@ -0,0 +1,39 @@
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

@ -1,8 +1,11 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
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 { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module';
import { SeederModule } from 'src/engine/seeder/seeder.module';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
@ -21,6 +24,8 @@ import { WorkspaceManagerService } from './workspace-manager.service';
WorkspaceSyncMetadataModule,
WorkspaceHealthModule,
FeatureFlagModule,
PermissionsModule,
TypeOrmModule.forFeature([UserWorkspace], 'core'),
],
exports: [WorkspaceManagerService],
providers: [WorkspaceManagerService],

View File

@ -1,9 +1,18 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import isEmpty from 'lodash.isempty';
import { Repository } from 'typeorm';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
import {
PermissionsException,
PermissionsExceptionCode,
} from 'src/engine/metadata-modules/permissions/permissions.exception';
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service';
import { PETS_DATA_SEEDS } from 'src/engine/seeder/data-seeds/pets-data-seeds';
import { SURVEY_RESULTS_DATA_SEEDS } from 'src/engine/seeder/data-seeds/survey-results-data-seeds';
@ -24,7 +33,9 @@ export class WorkspaceManagerService {
private readonly seederService: SeederService,
private readonly dataSourceService: DataSourceService,
private readonly workspaceSyncMetadataService: WorkspaceSyncMetadataService,
private readonly featureFlagService: FeatureFlagService,
private readonly permissionsService: PermissionsService,
@InjectRepository(UserWorkspace, 'core')
private readonly userWorkspaceRepository: Repository<UserWorkspace>,
) {}
/**
@ -49,6 +60,13 @@ export class WorkspaceManagerService {
dataSourceId: dataSourceMetadata.id,
});
const permissionsEnabled =
await this.permissionsService.isPermissionsEnabled();
if (permissionsEnabled === true) {
await this.initPermissions(workspaceId);
}
await this.prefillWorkspaceWithStandardObjects(
dataSourceMetadata,
workspaceId,
@ -168,4 +186,36 @@ export class WorkspaceManagerService {
// Delete schema
await this.workspaceDataSourceService.deleteWorkspaceDBSchema(workspaceId);
}
private async initPermissions(workspaceId: string) {
const adminRole = await this.permissionsService.createAdminRole({
workspaceId,
});
const userWorkspace = await this.userWorkspaceRepository.find({
where: {
workspaceId,
},
});
if (isEmpty(userWorkspace)) {
throw new PermissionsException(
'User workspace not found',
PermissionsExceptionCode.USER_WORKSPACE_NOT_FOUND,
);
}
if (userWorkspace.length > 1) {
throw new PermissionsException(
'Multiple user workspaces found, cannot tell which one should be admin',
PermissionsExceptionCode.TOO_MANY_ADMIN_CANDIDATES,
);
}
await this.permissionsService.assignRoleToUserWorkspace({
workspaceId,
userWorkspaceId: userWorkspace[0].id,
roleId: adminRole.id,
});
}
}