From 9b11735c57e615e7d1a9d5ac5579553e1ce38f2b Mon Sep 17 00:00:00 2001 From: Abdul Rahman <81605929+abdulrahmancodes@users.noreply.github.com> Date: Mon, 11 Aug 2025 16:44:45 +0530 Subject: [PATCH] Code first roles sync (#13667) --- .../src/generated-metadata/graphql.ts | 1 + .../twenty-front/src/generated/graphql.ts | 1 + ...sign-roles-to-existing-api-keys.command.ts | 4 +- .../1754480368129-addStandardIdToRole.ts | 23 ++++ ...update-member-role-can-access-all-tools.ts | 19 +++ .../core-modules/ai/services/mcp.service.ts | 4 +- .../flat-role/types/flat-role.type.ts | 18 +++ .../from-role-entity-to-flat-role.util.ts | 21 +++ ...ndard-role-definition-to-flat-role.util.ts | 16 +++ .../metadata-modules/role/dtos/role.dto.ts | 3 + .../metadata-modules/role/role.entity.ts | 3 + .../metadata-modules/role/role.service.ts | 23 +--- .../utils/fromRoleEntityToRoleDto.util.ts | 1 + .../user-role/user-role.service.ts | 14 +- .../dev-seeder-permissions.service.ts | 17 ++- .../workspace-manager.service.ts | 76 +++++------ .../comparators/index.ts | 2 + .../comparators/workspace-role.comparator.ts | 121 ++++++++++++++++++ .../factories/index.ts | 2 + .../factories/standard-role.factory.ts | 45 +++++++ .../services/workspace-sync-role.service.ts | 89 +++++++++++++ .../standard-roles/index.ts | 6 + .../standard-roles/roles/admin-role.ts | 15 +++ .../standard-role-definition.interface.ts | 8 ++ .../workspace-sync-metadata.module.ts | 2 + .../workspace-sync-metadata.service.ts | 13 ++ 26 files changed, 474 insertions(+), 73 deletions(-) create mode 100644 packages/twenty-server/src/database/typeorm/core/migrations/common/1754480368129-addStandardIdToRole.ts create mode 100644 packages/twenty-server/src/database/typeorm/core/migrations/common/update-member-role-can-access-all-tools.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-role/types/flat-role.type.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-role/utils/from-role-entity-to-flat-role.util.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/flat-role/utils/from-standard-role-definition-to-flat-role.util.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-role.comparator.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-role.factory.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-role.service.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-roles/index.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-roles/roles/admin-role.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-roles/types/standard-role-definition.interface.ts diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 978f4f3a1a..4fa739dcb7 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -2845,6 +2845,7 @@ export type Role = { label: Scalars['String']; objectPermissions?: Maybe>; permissionFlags?: Maybe>; + standardId?: Maybe; workspaceMembers: Array; }; diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts index cfb9990b13..07f9a92110 100644 --- a/packages/twenty-front/src/generated/graphql.ts +++ b/packages/twenty-front/src/generated/graphql.ts @@ -2691,6 +2691,7 @@ export type Role = { label: Scalars['String']; objectPermissions?: Maybe>; permissionFlags?: Maybe>; + standardId?: Maybe; workspaceMembers: Array; }; diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-3/1-3-assign-roles-to-existing-api-keys.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-3/1-3-assign-roles-to-existing-api-keys.command.ts index 37ad374270..acf83d17b5 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-3/1-3-assign-roles-to-existing-api-keys.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-3/1-3-assign-roles-to-existing-api-keys.command.ts @@ -12,12 +12,12 @@ import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/featu import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; 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 { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { WorkspaceFeatureFlagsMapCacheService } from 'src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.service'; import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { ADMIN_ROLE } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-roles/roles/admin-role'; @Command({ name: 'upgrade:1-3:assign-roles-to-existing-api-keys', @@ -217,7 +217,7 @@ export class AssignRolesToExistingApiKeysCommand extends ActiveOrSuspendedWorksp const adminRole = await this.roleRepository.findOne({ where: { workspaceId, - label: ADMIN_ROLE_LABEL, + standardId: ADMIN_ROLE.standardId, }, }); diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/common/1754480368129-addStandardIdToRole.ts b/packages/twenty-server/src/database/typeorm/core/migrations/common/1754480368129-addStandardIdToRole.ts new file mode 100644 index 0000000000..727682a29f --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/common/1754480368129-addStandardIdToRole.ts @@ -0,0 +1,23 @@ +import { type MigrationInterface, type QueryRunner } from 'typeorm'; + +import { ADMIN_ROLE } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-roles/roles/admin-role'; + +export class AddStandardIdToRole1754480368129 implements MigrationInterface { + name = 'AddStandardIdToRole1754480368129'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "core"."role" ADD "standardId" uuid`); + + await queryRunner.query( + `UPDATE "core"."role" SET "standardId" = '${ADMIN_ROLE.standardId}' WHERE "label" = 'Admin'`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`UPDATE "core"."role" SET "standardId" = NULL`); + + await queryRunner.query( + `ALTER TABLE "core"."role" DROP COLUMN "standardId"`, + ); + } +} diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/common/update-member-role-can-access-all-tools.ts b/packages/twenty-server/src/database/typeorm/core/migrations/common/update-member-role-can-access-all-tools.ts new file mode 100644 index 0000000000..a383cb9bc0 --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/common/update-member-role-can-access-all-tools.ts @@ -0,0 +1,19 @@ +import { type MigrationInterface, type QueryRunner } from 'typeorm'; + +export class UpdateMemberRoleCanAccessAllTools1753318977614 + implements MigrationInterface +{ + name = 'UpdateMemberRoleCanAccessAllTools1753318977614'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `UPDATE "core"."role" SET "canAccessAllTools" = true WHERE "label" = 'Member'`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `UPDATE "core"."role" SET "canAccessAllTools" = false WHERE "label" = 'Member'`, + ); + } +} diff --git a/packages/twenty-server/src/engine/core-modules/ai/services/mcp.service.ts b/packages/twenty-server/src/engine/core-modules/ai/services/mcp.service.ts index bf623b45e7..8f1b486e3b 100644 --- a/packages/twenty-server/src/engine/core-modules/ai/services/mcp.service.ts +++ b/packages/twenty-server/src/engine/core-modules/ai/services/mcp.service.ts @@ -11,9 +11,9 @@ import { wrapJsonRpcResponse } from 'src/engine/core-modules/ai/utils/wrap-jsonr import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { type 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 { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; +import { ADMIN_ROLE } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-roles/roles/admin-role'; @Injectable() export class McpService { @@ -60,7 +60,7 @@ export class McpService { const roles = await this.roleRepository.find({ where: { workspaceId, - label: ADMIN_ROLE_LABEL, + standardId: ADMIN_ROLE.standardId, }, }); diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-role/types/flat-role.type.ts b/packages/twenty-server/src/engine/metadata-modules/flat-role/types/flat-role.type.ts new file mode 100644 index 0000000000..9a6bc1a988 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-role/types/flat-role.type.ts @@ -0,0 +1,18 @@ +import { type RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; + +export const roleEntityRelationProperties = [ + 'roleTargets', + 'objectPermissions', + 'permissionFlags', + 'fieldPermissions', +] as const; + +export type RoleEntityRelationProperties = + (typeof roleEntityRelationProperties)[number]; + +export type FlatRole = Omit< + RoleEntity, + RoleEntityRelationProperties | 'createdAt' | 'updatedAt' +> & { + uniqueIdentifier: string; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-role/utils/from-role-entity-to-flat-role.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-role/utils/from-role-entity-to-flat-role.util.ts new file mode 100644 index 0000000000..4b91572557 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-role/utils/from-role-entity-to-flat-role.util.ts @@ -0,0 +1,21 @@ +import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type'; +import { type RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; + +export const fromRoleEntityToFlatRole = (role: RoleEntity): FlatRole => { + return { + id: role.id, + standardId: role.standardId, + label: role.label, + description: role.description, + icon: role.icon, + isEditable: role.isEditable, + canUpdateAllSettings: role.canUpdateAllSettings, + canAccessAllTools: role.canAccessAllTools, + canReadAllObjectRecords: role.canReadAllObjectRecords, + canUpdateAllObjectRecords: role.canUpdateAllObjectRecords, + canSoftDeleteAllObjectRecords: role.canSoftDeleteAllObjectRecords, + canDestroyAllObjectRecords: role.canDestroyAllObjectRecords, + workspaceId: role.workspaceId, + uniqueIdentifier: role.standardId || role.id, + }; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-role/utils/from-standard-role-definition-to-flat-role.util.ts b/packages/twenty-server/src/engine/metadata-modules/flat-role/utils/from-standard-role-definition-to-flat-role.util.ts new file mode 100644 index 0000000000..42c1013e7f --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/flat-role/utils/from-standard-role-definition-to-flat-role.util.ts @@ -0,0 +1,16 @@ +import { v4 } from 'uuid'; + +import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type'; +import { type StandardRoleDefinition } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-roles/types/standard-role-definition.interface'; + +export const fromStandardRoleDefinitionToFlatRole = ( + standardRoleDefinition: StandardRoleDefinition, + workspaceId: string, +): FlatRole => { + return { + ...standardRoleDefinition, + id: v4(), + workspaceId, + uniqueIdentifier: standardRoleDefinition.standardId || v4(), + }; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/role/dtos/role.dto.ts b/packages/twenty-server/src/engine/metadata-modules/role/dtos/role.dto.ts index c0dacf137a..3a17e108d3 100644 --- a/packages/twenty-server/src/engine/metadata-modules/role/dtos/role.dto.ts +++ b/packages/twenty-server/src/engine/metadata-modules/role/dtos/role.dto.ts @@ -14,6 +14,9 @@ export class RoleDTO { @Field(() => UUIDScalarType, { nullable: false }) id: string; + @Field(() => UUIDScalarType, { nullable: true }) + standardId?: string; + @Field({ nullable: false }) label: string; diff --git a/packages/twenty-server/src/engine/metadata-modules/role/role.entity.ts b/packages/twenty-server/src/engine/metadata-modules/role/role.entity.ts index 016fe1877b..81451c9e35 100644 --- a/packages/twenty-server/src/engine/metadata-modules/role/role.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/role/role.entity.ts @@ -20,6 +20,9 @@ export class RoleEntity { @PrimaryGeneratedColumn('uuid') id: string; + @Column({ nullable: true, type: 'uuid' }) + standardId?: string; + @Column({ nullable: false }) label: string; diff --git a/packages/twenty-server/src/engine/metadata-modules/role/role.service.ts b/packages/twenty-server/src/engine/metadata-modules/role/role.service.ts index af059d6fc7..97e60dfe47 100644 --- a/packages/twenty-server/src/engine/metadata-modules/role/role.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/role/role.service.ts @@ -5,7 +5,6 @@ import { isDefined } from 'twenty-shared/utils'; import { Repository } from 'typeorm'; 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 { PermissionsException, @@ -145,26 +144,6 @@ export class RoleService { return { ...existingRole, ...updatedRole }; } - public async createAdminRole({ - workspaceId, - }: { - workspaceId: string; - }): Promise { - return this.roleRepository.save({ - label: ADMIN_ROLE_LABEL, - description: 'Admin role', - icon: 'IconUserCog', - canUpdateAllSettings: true, - canAccessAllTools: true, - canReadAllObjectRecords: true, - canUpdateAllObjectRecords: true, - canSoftDeleteAllObjectRecords: true, - canDestroyAllObjectRecords: true, - isEditable: false, - workspaceId, - }); - } - public async deleteRole( roleId: string, workspaceId: string, @@ -226,7 +205,7 @@ export class RoleService { description: 'Member role', icon: 'IconUser', canUpdateAllSettings: false, - canAccessAllTools: false, + canAccessAllTools: true, canReadAllObjectRecords: true, canUpdateAllObjectRecords: true, canSoftDeleteAllObjectRecords: true, diff --git a/packages/twenty-server/src/engine/metadata-modules/role/utils/fromRoleEntityToRoleDto.util.ts b/packages/twenty-server/src/engine/metadata-modules/role/utils/fromRoleEntityToRoleDto.util.ts index 2b82e996e6..5769fc046d 100644 --- a/packages/twenty-server/src/engine/metadata-modules/role/utils/fromRoleEntityToRoleDto.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/role/utils/fromRoleEntityToRoleDto.util.ts @@ -4,6 +4,7 @@ import { type RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; export const fromRoleEntityToRoleDto = (role: RoleEntity): RoleDTO => { return { id: role.id, + standardId: role.standardId, label: role.label, canUpdateAllSettings: role.canUpdateAllSettings, canAccessAllTools: role.canAccessAllTools, 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 df5635d810..dd3524ee68 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 @@ -4,7 +4,6 @@ import { isDefined } from 'twenty-shared/utils'; import { In, Not, Repository } from 'typeorm'; 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, @@ -14,6 +13,7 @@ import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { ADMIN_ROLE } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-roles/roles/admin-role'; import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; export class UserRoleService { @@ -202,7 +202,10 @@ export class UserRoleService { ); } - if (roleOfUserWorkspace.label === ADMIN_ROLE_LABEL) { + if ( + isDefined(roleOfUserWorkspace) && + roleOfUserWorkspace.standardId === ADMIN_ROLE.standardId + ) { const adminRole = roleOfUserWorkspace; await this.validateMoreThanOneWorkspaceMemberHasAdminRoleOrThrow({ @@ -268,7 +271,12 @@ export class UserRoleService { }; } - if (!(currentRole?.label === ADMIN_ROLE_LABEL)) { + if ( + !( + isDefined(currentRole) && + currentRole.standardId === ADMIN_ROLE.standardId + ) + ) { return; } diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/services/dev-seeder-permissions.service.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/services/dev-seeder-permissions.service.ts index aaa02563e4..0b2f9c667e 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/services/dev-seeder-permissions.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/services/dev-seeder-permissions.service.ts @@ -9,6 +9,7 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { FieldPermissionService } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.service'; import { ObjectPermissionService } from 'src/engine/metadata-modules/object-permission/object-permission.service'; +import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { RoleService } from 'src/engine/metadata-modules/role/role.service'; import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; @@ -18,6 +19,7 @@ import { SEED_YCOMBINATOR_WORKSPACE_ID, } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-workspaces.util'; import { API_KEY_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/api-key-data-seeds.constant'; +import { ADMIN_ROLE } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-roles/roles/admin-role'; @Injectable() export class DevSeederPermissionsService { @@ -29,16 +31,27 @@ export class DevSeederPermissionsService { private readonly objectPermissionService: ObjectPermissionService, @InjectRepository(ObjectMetadataEntity, 'core') private readonly objectMetadataRepository: Repository, + @InjectRepository(RoleEntity, 'core') + private readonly roleRepository: Repository, private readonly typeORMService: TypeORMService, private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService, private readonly fieldPermissionService: FieldPermissionService, ) {} public async initPermissions(workspaceId: string) { - const adminRole = await this.roleService.createAdminRole({ - workspaceId, + const adminRole = await this.roleRepository.findOne({ + where: { + standardId: ADMIN_ROLE.standardId, + workspaceId, + }, }); + if (!adminRole) { + throw new Error( + 'Required roles not found. Make sure the permission sync has run.', + ); + } + const dataSource = this.typeORMService.getMainDataSource(); if (dataSource) { diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-manager.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-manager.service.ts index 999290fe52..0ce58ec0b6 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-manager.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-manager.service.ts @@ -20,6 +20,7 @@ import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; import { prefillCoreViews } from 'src/engine/workspace-manager/standard-objects-prefill-data/prefill-core-views'; import { standardObjectsPrefillData } from 'src/engine/workspace-manager/standard-objects-prefill-data/standard-objects-prefill-data'; +import { ADMIN_ROLE } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-roles/roles/admin-role'; import { WorkspaceSyncMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.service'; @Injectable() @@ -89,15 +90,7 @@ export class WorkspaceManagerService { `Metadata creation took ${dataSourceMetadataCreationEnd - dataSourceMetadataCreationStart}ms`, ); - const permissionsEnabledStart = performance.now(); - - await this.initPermissions({ workspaceId, userId }); - - const permissionsEnabledEnd = performance.now(); - - this.logger.log( - `Permissions enabled took ${permissionsEnabledEnd - permissionsEnabledStart}ms`, - ); + await this.setupDefaultRoles(workspaceId, userId); if (featureFlags[FeatureFlagKey.IS_AI_ENABLED]) { const defaultAgentEnabledStart = performance.now(); @@ -189,39 +182,6 @@ export class WorkspaceManagerService { this.logger.log(`workspace ${workspaceId} schema deleted`); } - private async initPermissions({ - workspaceId, - userId, - }: { - workspaceId: string; - userId: string; - }) { - const adminRole = await this.roleService.createAdminRole({ - workspaceId, - }); - - const userWorkspace = await this.userWorkspaceRepository.findOneOrFail({ - where: { - workspaceId, - userId, - }, - }); - - await this.userRoleService.assignRoleToUserWorkspace({ - workspaceId, - userWorkspaceId: userWorkspace.id, - roleId: adminRole.id, - }); - - const memberRole = await this.roleService.createMemberRole({ - workspaceId, - }); - - await this.workspaceRepository.update(workspaceId, { - defaultRoleId: memberRole.id, - }); - } - private async initDefaultAgent(workspaceId: string) { const agent = await this.agentService.createOneAgent( { @@ -239,4 +199,36 @@ export class WorkspaceManagerService { defaultAgentId: agent.id, }); } + + private async setupDefaultRoles( + workspaceId: string, + userId: string, + ): Promise { + const adminRole = await this.roleRepository.findOne({ + where: { + standardId: ADMIN_ROLE.standardId, + workspaceId, + }, + }); + + if (adminRole) { + const userWorkspace = await this.userWorkspaceRepository.findOneOrFail({ + where: { workspaceId, userId }, + }); + + await this.userRoleService.assignRoleToUserWorkspace({ + workspaceId, + userWorkspaceId: userWorkspace.id, + roleId: adminRole.id, + }); + } + + const memberRole = await this.roleService.createMemberRole({ + workspaceId, + }); + + await this.workspaceRepository.update(workspaceId, { + defaultRoleId: memberRole.id, + }); + } } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/comparators/index.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/comparators/index.ts index 0b21612a5a..0e3a5ca870 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/comparators/index.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/comparators/index.ts @@ -1,5 +1,6 @@ import { WorkspaceFieldRelationComparator } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-field-relation.comparator'; import { WorkspaceIndexComparator } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-index.comparator'; +import { WorkspaceRoleComparator } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-role.comparator'; import { WorkspaceFieldComparator } from './workspace-field.comparator'; import { WorkspaceObjectComparator } from './workspace-object.comparator'; @@ -9,4 +10,5 @@ export const workspaceSyncMetadataComparators = [ WorkspaceFieldRelationComparator, WorkspaceObjectComparator, WorkspaceIndexComparator, + WorkspaceRoleComparator, ]; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-role.comparator.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-role.comparator.ts new file mode 100644 index 0000000000..5aa0f40717 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-role.comparator.ts @@ -0,0 +1,121 @@ +import { Injectable } from '@nestjs/common'; + +import diff from 'microdiff'; +import { type FromTo } from 'twenty-shared/types'; + +import { ComparatorAction } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/comparator.interface'; + +import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type'; +import { type RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; +import { transformMetadataForComparison } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/utils/transform-metadata-for-comparison.util'; + +type RoleComparatorResult = + | { + action: ComparatorAction.CREATE; + fromFlatRole: null; + toFlatRole: FlatRole; + } + | { + action: ComparatorAction.UPDATE; + fromFlatRole: FlatRole; + toFlatRole: FlatRole; + } + | { + action: ComparatorAction.DELETE; + fromFlatRole: FlatRole; + toFlatRole: null; + }; + +type WorkspaceRoleComparatorArgs = FromTo; + +const rolePropertiesToIgnore = [ + 'id', + 'createdAt', + 'updatedAt', + 'workspaceId', + 'roleTargets', + 'permissionFlags', + 'objectPermissions', + 'fieldPermissions', +]; + +@Injectable() +export class WorkspaceRoleComparator { + compare({ + fromFlatRoles, + toFlatRoles, + }: WorkspaceRoleComparatorArgs): RoleComparatorResult[] { + const results: RoleComparatorResult[] = []; + + const keyFactory = (role: FlatRole | RoleEntity) => + role.standardId || role.id; + + const fromRoleMap = transformMetadataForComparison(fromFlatRoles, { + shouldIgnoreProperty: (property) => + rolePropertiesToIgnore.includes(property), + keyFactory, + }); + + const toRoleMap = transformMetadataForComparison(toFlatRoles, { + shouldIgnoreProperty: (property) => + rolePropertiesToIgnore.includes(property), + keyFactory, + }); + + const roleDifferences = diff(fromRoleMap, toRoleMap); + + for (const difference of roleDifferences) { + const uniqueIdentifier = difference.path[0] as string; + + switch (difference.type) { + case 'CREATE': { + const toRole = toFlatRoles.find( + (role) => keyFactory(role) === uniqueIdentifier, + ); + + if (toRole) { + results.push({ + action: ComparatorAction.CREATE, + fromFlatRole: null, + toFlatRole: toRole, + }); + } + break; + } + case 'CHANGE': { + const fromRole = fromFlatRoles.find( + (role) => keyFactory(role) === uniqueIdentifier, + ); + const toRole = toFlatRoles.find( + (role) => keyFactory(role) === uniqueIdentifier, + ); + + if (fromRole && toRole) { + results.push({ + action: ComparatorAction.UPDATE, + fromFlatRole: fromRole, + toFlatRole: toRole, + }); + } + break; + } + case 'REMOVE': { + const fromRole = fromFlatRoles.find( + (role) => keyFactory(role) === uniqueIdentifier, + ); + + if (fromRole && difference.path.length === 1) { + results.push({ + action: ComparatorAction.DELETE, + fromFlatRole: fromRole, + toFlatRole: null, + }); + } + break; + } + } + } + + return results; + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/index.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/index.ts index 5af894cafe..5da67109e4 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/index.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/index.ts @@ -1,4 +1,5 @@ import { StandardIndexFactory } from 'src/engine/workspace-manager/workspace-sync-metadata/factories/standard-index.factory'; +import { StandardRoleFactory } from 'src/engine/workspace-manager/workspace-sync-metadata/factories/standard-role.factory'; import { StandardFieldRelationFactory } from './standard-field-relation.factory'; import { StandardFieldFactory } from './standard-field.factory'; @@ -9,4 +10,5 @@ export const workspaceSyncMetadataFactories = [ StandardObjectFactory, StandardFieldRelationFactory, StandardIndexFactory, + StandardRoleFactory, ]; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-role.factory.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-role.factory.ts new file mode 100644 index 0000000000..d5fd1f9fb7 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-role.factory.ts @@ -0,0 +1,45 @@ +import { Injectable } from '@nestjs/common'; + +import { type WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface'; + +import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type'; +import { fromStandardRoleDefinitionToFlatRole } from 'src/engine/metadata-modules/flat-role/utils/from-standard-role-definition-to-flat-role.util'; +import { type RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; +import { type StandardRoleDefinition } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-roles/types/standard-role-definition.interface'; + +@Injectable() +export class StandardRoleFactory { + create( + roleDefinitions: StandardRoleDefinition[], + context: WorkspaceSyncContext, + existingRoles: RoleEntity[], + ): FlatRole[] { + const computedRoles: FlatRole[] = []; + + for (const roleDefinition of roleDefinitions) { + const existingRole = existingRoles.find( + (role) => role.standardId === roleDefinition.standardId, + ); + + const flatRole = fromStandardRoleDefinitionToFlatRole( + roleDefinition, + context.workspaceId, + ); + + if (existingRole) { + computedRoles.push({ + ...flatRole, + id: existingRole.id, + uniqueIdentifier: roleDefinition.standardId || existingRole.id, + }); + } else { + computedRoles.push({ + ...flatRole, + uniqueIdentifier: roleDefinition.standardId, + }); + } + } + + return computedRoles; + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-role.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-role.service.ts new file mode 100644 index 0000000000..f35dd21c64 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-role.service.ts @@ -0,0 +1,89 @@ +import { Injectable, Logger } from '@nestjs/common'; + +import { removePropertiesFromRecord } from 'twenty-shared/utils'; +import { IsNull, Not, type EntityManager } from 'typeorm'; + +import { ComparatorAction } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/comparator.interface'; +import { type WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface'; + +import { fromRoleEntityToFlatRole } from 'src/engine/metadata-modules/flat-role/utils/from-role-entity-to-flat-role.util'; +import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; +import { WorkspaceRoleComparator } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-role.comparator'; +import { StandardRoleFactory } from 'src/engine/workspace-manager/workspace-sync-metadata/factories/standard-role.factory'; +import { standardRoleDefinitions } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-roles'; + +@Injectable() +export class WorkspaceSyncRoleService { + private readonly logger = new Logger(WorkspaceSyncRoleService.name); + + constructor( + private readonly standardRoleFactory: StandardRoleFactory, + private readonly workspaceRoleComparator: WorkspaceRoleComparator, + ) {} + + async synchronize( + context: WorkspaceSyncContext, + manager: EntityManager, + ): Promise { + this.logger.log('Syncing standard role metadata'); + + const roleRepository = manager.getRepository(RoleEntity); + + const existingStandardRoleEntities = await roleRepository.find({ + where: { + workspaceId: context.workspaceId, + standardId: Not(IsNull()), + }, + }); + + const targetStandardRoles = this.standardRoleFactory.create( + standardRoleDefinitions, + context, + existingStandardRoleEntities, + ); + + const roleComparatorResults = this.workspaceRoleComparator.compare({ + fromFlatRoles: existingStandardRoleEntities.map(fromRoleEntityToFlatRole), + toFlatRoles: targetStandardRoles, + }); + + for (const roleComparatorResult of roleComparatorResults) { + switch (roleComparatorResult.action) { + case ComparatorAction.CREATE: { + const roleToCreate = roleComparatorResult.toFlatRole; + + const flatRoleData = removePropertiesFromRecord(roleToCreate, [ + 'uniqueIdentifier', + 'id', + ]); + + await roleRepository.save({ + ...flatRoleData, + workspaceId: context.workspaceId, + }); + break; + } + + case ComparatorAction.UPDATE: { + const roleToUpdate = roleComparatorResult.toFlatRole; + + const flatRoleData = removePropertiesFromRecord(roleToUpdate, [ + 'id', + 'uniqueIdentifier', + 'workspaceId', + ]); + + await roleRepository.update({ id: roleToUpdate.id }, flatRoleData); + break; + } + + case ComparatorAction.DELETE: { + const roleToDelete = roleComparatorResult.fromFlatRole; + + await roleRepository.delete({ id: roleToDelete.id }); + break; + } + } + } + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-roles/index.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-roles/index.ts new file mode 100644 index 0000000000..24a8d71f78 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-roles/index.ts @@ -0,0 +1,6 @@ +import { ADMIN_ROLE } from './roles/admin-role'; +import { type StandardRoleDefinition } from './types/standard-role-definition.interface'; + +export const standardRoleDefinitions = [ + ADMIN_ROLE, +] as const satisfies StandardRoleDefinition[]; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-roles/roles/admin-role.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-roles/roles/admin-role.ts new file mode 100644 index 0000000000..b791266d17 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-roles/roles/admin-role.ts @@ -0,0 +1,15 @@ +import { type StandardRoleDefinition } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-roles/types/standard-role-definition.interface'; + +export const ADMIN_ROLE: StandardRoleDefinition = { + standardId: '20202020-0001-0001-0001-000000000001', + label: 'Admin', + description: 'Admin role', + icon: 'IconUserCog', + isEditable: false, + canUpdateAllSettings: true, + canAccessAllTools: true, + canReadAllObjectRecords: true, + canUpdateAllObjectRecords: true, + canSoftDeleteAllObjectRecords: true, + canDestroyAllObjectRecords: true, +}; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-roles/types/standard-role-definition.interface.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-roles/types/standard-role-definition.interface.ts new file mode 100644 index 0000000000..692fb40ee5 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-roles/types/standard-role-definition.interface.ts @@ -0,0 +1,8 @@ +import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type'; + +export type StandardRoleDefinition = Omit< + FlatRole, + 'id' | 'workspaceId' | 'uniqueIdentifier' | 'standardId' +> & { + standardId: string; +}; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.module.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.module.ts index 105737e447..4d6e956fc9 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.module.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.module.ts @@ -21,6 +21,7 @@ import { WorkspaceSyncFieldMetadataService } from 'src/engine/workspace-manager/ import { WorkspaceSyncIndexMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-index-metadata.service'; import { WorkspaceSyncObjectMetadataIdentifiersService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-object-metadata-identifiers.service'; import { WorkspaceSyncObjectMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-object-metadata.service'; +import { WorkspaceSyncRoleService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-role.service'; import { WorkspaceSyncMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.service'; @Module({ @@ -46,6 +47,7 @@ import { WorkspaceSyncMetadataService } from 'src/engine/workspace-manager/works WorkspaceSyncFieldMetadataRelationService, WorkspaceSyncMetadataService, WorkspaceSyncIndexMetadataService, + WorkspaceSyncRoleService, SyncWorkspaceLoggerService, SyncWorkspaceMetadataCommand, ], diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.service.ts index acb5eb6340..3cfc50b445 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.service.ts @@ -16,6 +16,7 @@ import { WorkspaceSyncFieldMetadataService } from 'src/engine/workspace-manager/ import { WorkspaceSyncIndexMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-index-metadata.service'; import { WorkspaceSyncObjectMetadataIdentifiersService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-object-metadata-identifiers.service'; import { WorkspaceSyncObjectMetadataService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-object-metadata.service'; +import { WorkspaceSyncRoleService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-role.service'; import { WorkspaceSyncStorage } from 'src/engine/workspace-manager/workspace-sync-metadata/storage/workspace-sync.storage'; interface SynchronizeOptions { @@ -36,6 +37,7 @@ export class WorkspaceSyncMetadataService { private readonly workspaceSyncIndexMetadataService: WorkspaceSyncIndexMetadataService, private readonly workspaceSyncObjectMetadataIdentifiersService: WorkspaceSyncObjectMetadataIdentifiersService, private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService, + private readonly workspaceSyncRoleService: WorkspaceSyncRoleService, ) {} /** @@ -159,6 +161,17 @@ export class WorkspaceSyncMetadataService { `Workspace object metadata identifiers took ${workspaceObjectMetadataIdentifiersEnd - workspaceObjectMetadataIdentifiersStart}ms`, ); + // 6 - Sync standard roles + const workspaceRoleMigrationsStart = performance.now(); + + await this.workspaceSyncRoleService.synchronize(context, manager); + + const workspaceRoleMigrationsEnd = performance.now(); + + this.logger.log( + `Workspace role migrations took ${workspaceRoleMigrationsEnd - workspaceRoleMigrationsStart}ms`, + ); + const workspaceMigrationsSaveStart = performance.now(); // Save workspace migrations into the database