[permissions] Backfill command to prepare workspaces (#10581)

Closes https://github.com/twentyhq/core-team-issues/issues/317

---------

Co-authored-by: Weiko <corentin@twenty.com>
This commit is contained in:
Marie
2025-02-28 15:46:51 +01:00
committed by GitHub
parent fba63d9cb7
commit 122a6a7801
3 changed files with 298 additions and 4 deletions

View File

@ -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<Workspace>,
@InjectRepository(UserWorkspace, 'core')
protected readonly userWorkspaceRepository: Repository<UserWorkspace>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly roleService: RoleService,
private readonly userRoleService: UserRoleService,
) {
super(workspaceRepository, twentyORMGlobalManager);
}
async runMigrationCommandOnMaintainedWorkspaces(
_passedParam: string[],
options: MaintainedWorkspacesMigrationCommandOptions,
workspaceIds: string[],
): Promise<void> {
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<void> {
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,
});
}
}
}

View File

@ -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],
}),
],
})

View File

@ -35,12 +35,16 @@ export class UserRoleService {
userWorkspaceId: string;
roleId: string;
}): Promise<void> {
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)) {