From 122a6a7801e668ee714a28d83b36d9d69662dc8c Mon Sep 17 00:00:00 2001 From: Marie <51697796+ijreilly@users.noreply.github.com> Date: Fri, 28 Feb 2025 15:46:51 +0100 Subject: [PATCH] [permissions] Backfill command to prepare workspaces (#10581) Closes https://github.com/twentyhq/core-team-issues/issues/317 --------- Co-authored-by: Weiko --- .../0-44-initialize-permissions.command.ts | 276 ++++++++++++++++++ .../0-44/0-44-upgrade-version.module.ts | 16 +- .../user-role/user-role.service.ts | 10 +- 3 files changed, 298 insertions(+), 4 deletions(-) create mode 100644 packages/twenty-server/src/database/commands/upgrade-version/0-44/0-44-initialize-permissions.command.ts diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-44/0-44-initialize-permissions.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-44/0-44-initialize-permissions.command.ts new file mode 100644 index 000000000..7dd3669f6 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-44/0-44-initialize-permissions.command.ts @@ -0,0 +1,276 @@ +import { InjectRepository } from '@nestjs/typeorm'; + +import chalk from 'chalk'; +import { isDefined } from 'twenty-shared'; +import { IsNull, Repository } from 'typeorm'; + +import { MigrationCommand } from 'src/database/commands/migration-command/decorators/migration-command.decorator'; +import { + MaintainedWorkspacesMigrationCommandOptions, + MaintainedWorkspacesMigrationCommandRunner, +} from 'src/database/commands/migration-command/maintained-workspaces-migration-command.runner'; +import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; +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 { RoleService } from 'src/engine/metadata-modules/role/role.service'; +import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; + +@MigrationCommand({ + name: 'initialize-permissions', + description: 'Initialize permissions', + version: '0.44', +}) +export class InitializePermissionsCommand extends MaintainedWorkspacesMigrationCommandRunner { + private options: MaintainedWorkspacesMigrationCommandOptions; + constructor( + @InjectRepository(Workspace, 'core') + protected readonly workspaceRepository: Repository, + @InjectRepository(UserWorkspace, 'core') + protected readonly userWorkspaceRepository: Repository, + protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly roleService: RoleService, + private readonly userRoleService: UserRoleService, + ) { + super(workspaceRepository, twentyORMGlobalManager); + } + + async runMigrationCommandOnMaintainedWorkspaces( + _passedParam: string[], + options: MaintainedWorkspacesMigrationCommandOptions, + workspaceIds: string[], + ): Promise { + this.logger.log(chalk.green('Running command to initialize permissions')); + + this.options = options; + for (const [index, workspaceId] of workspaceIds.entries()) { + await this.processWorkspace(workspaceId, index, workspaceIds.length); + } + + this.logger.log(chalk.green('Command completed!')); + } + + private async processWorkspace( + workspaceId: string, + index: number, + total: number, + ): Promise { + try { + this.logger.log( + `Running command for workspace ${workspaceId} ${index + 1}/${total}`, + ); + + let adminRoleId: string | undefined; + + const workspaceRoles = + await this.roleService.getWorkspaceRoles(workspaceId); + + adminRoleId = workspaceRoles.find( + (role) => role.label === ADMIN_ROLE_LABEL, + )?.id; + + if (!isDefined(adminRoleId)) { + adminRoleId = await this.createAdminRole({ workspaceId }); + } + + await this.assignAdminRole({ + workspaceId, + adminRoleId, + }); + + let memberRoleId: string | undefined; + + memberRoleId = workspaceRoles.find( + (role) => role.label === MEMBER_ROLE_LABEL, + )?.id; + + if (!isDefined(memberRoleId)) { + memberRoleId = await this.createMemberRole({ + workspaceId, + }); + } + + await this.setMemberRoleAsDefaultRole({ + workspaceId, + memberRoleId, + }); + + await this.assignMemberRoleToUserWorkspacesWithoutRole({ + workspaceId, + memberRoleId, + }); + } catch (error) { + this.logger.log( + chalk.red(`Error in workspace ${workspaceId} - ${error.message}`), + ); + } + } + + private async createAdminRole({ workspaceId }: { workspaceId: string }) { + this.logger.log( + chalk.green( + `Creating admin role ${this.options.dryRun ? '(dry run)' : ''}`, + ), + ); + + if (this.options.dryRun) { + return ''; + } + + const adminRole = await this.roleService.createAdminRole({ + workspaceId, + }); + + return adminRole.id; + } + + private async createMemberRole({ workspaceId }: { workspaceId: string }) { + this.logger.log( + chalk.green( + `Creating member role ${this.options.dryRun ? '(dry run)' : ''}`, + ), + ); + + if (this.options.dryRun) { + return ''; + } + + const memberRole = await this.roleService.createMemberRole({ + workspaceId, + }); + + return memberRole.id; + } + + private async setMemberRoleAsDefaultRole({ + workspaceId, + memberRoleId, + }: { + workspaceId: string; + memberRoleId: string; + }) { + const workspaceDefaultRole = await this.workspaceRepository.findOne({ + where: { + id: workspaceId, + }, + }); + + if (!isDefined(workspaceDefaultRole?.defaultRoleId)) { + this.logger.log( + chalk.green( + `Setting member role as default role ${this.options.dryRun ? '(dry run)' : ''}`, + ), + ); + + if (this.options.dryRun) { + return; + } + + await this.workspaceRepository.update(workspaceId, { + defaultRoleId: memberRoleId, + }); + } + } + + private async assignAdminRole({ + workspaceId, + adminRoleId, + }: { + workspaceId: string; + adminRoleId: string; + }) { + const oldestUserWorkspace = await this.userWorkspaceRepository.findOne({ + where: { + workspaceId, + deletedAt: IsNull(), + }, + relations: { + user: true, + }, + order: { + user: { + createdAt: 'ASC', + }, + }, + }); + + if (!oldestUserWorkspace) { + throw new Error('No user workspace found'); + } + + this.logger.log( + chalk.green( + `Assigning admin role to user ${oldestUserWorkspace.id} ${this.options.dryRun ? '(dry run)' : ''}`, + ), + ); + + if (this.options.dryRun) { + return; + } + + await this.userRoleService.assignRoleToUserWorkspace({ + roleId: adminRoleId, + userWorkspaceId: oldestUserWorkspace.id, + workspaceId, + }); + } + + private async assignMemberRoleToUserWorkspacesWithoutRole({ + workspaceId, + memberRoleId, + }: { + workspaceId: string; + memberRoleId: string; + }) { + const userWorkspaces = await this.userWorkspaceRepository.find({ + where: { + workspaceId, + }, + relations: { + user: true, + }, + }); + + const rolesByUserWorkspace = + await this.userRoleService.getRolesByUserWorkspaces({ + userWorkspaceIds: userWorkspaces.map( + (userWorkspace) => userWorkspace.id, + ), + workspaceId, + }); + + for (const userWorkspace of userWorkspaces) { + // If userWorkspace has a role, do nothing + if ( + rolesByUserWorkspace + .get(userWorkspace.id) + ?.some((role) => isDefined(role)) + ) { + this.logger.log( + chalk.green( + `User workspace ${userWorkspace.id} already has a role. Skipping member role assignation`, + ), + ); + continue; + } + + this.logger.log( + chalk.green( + `Assigning member role to user workspace ${userWorkspace.id} ${this.options.dryRun ? '(dry run)' : ''}`, + ), + ); + + if (this.options.dryRun) { + continue; + } + + // Otherwise, assign member role to userWorkspace + await this.userRoleService.assignRoleToUserWorkspace({ + roleId: memberRoleId, + userWorkspaceId: userWorkspace.id, + workspaceId, + }); + } + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-44/0-44-upgrade-version.module.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-44/0-44-upgrade-version.module.ts index fff8cf08f..225e0b822 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version/0-44/0-44-upgrade-version.module.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-44/0-44-upgrade-version.module.ts @@ -2,11 +2,15 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { MigrationCommandModule } from 'src/database/commands/migration-command/migration-command.module'; +import { InitializePermissionsCommand } from 'src/database/commands/upgrade-version/0-44/0-44-initialize-permissions.command'; import { MigrateRelationsToFieldMetadataCommand } from 'src/database/commands/upgrade-version/0-44/0-44-migrate-relations-to-field-metadata.command'; import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; +import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { RoleModule } from 'src/engine/metadata-modules/role/role.module'; +import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module'; import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module'; import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module'; import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module'; @@ -15,7 +19,10 @@ import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/wor imports: [ MigrationCommandModule.register('0.44', { imports: [ - TypeOrmModule.forFeature([Workspace, FeatureFlag], 'core'), + TypeOrmModule.forFeature( + [Workspace, FeatureFlag, UserWorkspace], + 'core', + ), TypeOrmModule.forFeature( [ObjectMetadataEntity, FieldMetadataEntity], 'metadata', @@ -23,8 +30,13 @@ import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/wor WorkspaceMigrationRunnerModule, WorkspaceMigrationModule, WorkspaceMetadataVersionModule, + UserRoleModule, + RoleModule, + ], + providers: [ + MigrateRelationsToFieldMetadataCommand, + InitializePermissionsCommand, ], - providers: [MigrateRelationsToFieldMetadataCommand], }), ], }) diff --git a/packages/twenty-server/src/engine/metadata-modules/user-role/user-role.service.ts b/packages/twenty-server/src/engine/metadata-modules/user-role/user-role.service.ts index 3d2ddcf82..3ce4f3c82 100644 --- a/packages/twenty-server/src/engine/metadata-modules/user-role/user-role.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/user-role/user-role.service.ts @@ -35,12 +35,16 @@ export class UserRoleService { userWorkspaceId: string; roleId: string; }): Promise { - await this.validateAssignRoleInput({ + const validationResult = await this.validateAssignRoleInput({ userWorkspaceId, workspaceId, roleId, }); + if (validationResult?.roleToAssignIsSameAsCurrentRole) { + return; + } + const newUserWorkspaceRole = await this.userWorkspaceRoleRepository.save({ roleId, userWorkspaceId, @@ -209,7 +213,9 @@ export class UserRoleService { const currentRole = roles.get(userWorkspace.id)?.[0]; if (currentRole?.id === roleId) { - return; + return { + roleToAssignIsSameAsCurrentRole: true, + }; } if (!(currentRole?.label === ADMIN_ROLE_LABEL)) {