From 4d78f5f97f670c892a7997185c2460f143773372 Mon Sep 17 00:00:00 2001 From: Marie <51697796+ijreilly@users.noreply.github.com> Date: Wed, 16 Apr 2025 17:07:43 +0200 Subject: [PATCH] [permissions] Improve performances using a cache for userWorkspaces roles (#11587) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In this PR we are - introducing a cached map `{ userworkspaceId: roleId } `to reduce calls to get a userWorkspace's role (we were having N+1 around that with combinedFindMany queries and generally having a lot of avoidable queries) - using the roles permissions cache (`{ roleId: { objectNameSingular: { canRead: bool, canUpdate: bool, ...} } `) in Permissions V1's userHasObjectPermission, in order to 1) improve performances to avoid calls to get roles 2) start using our permissions cache --- .../object-permission.module.ts | 4 +- .../object-permission.service.ts | 7 +- .../permissions/permissions.module.ts | 2 + .../permissions/permissions.service.ts | 88 +++-- .../metadata-modules/role/role.module.ts | 4 +- .../metadata-modules/role/role.service.ts | 59 ++-- .../user-role/user-role.module.ts | 2 + .../user-role/user-role.service.ts | 49 ++- ...rkspace-feature-flags-map-cache.service.ts | 4 +- .../types/user-workspace-role-map.type.ts | 1 + ...space-permissions-cache-storage.service.ts | 156 +++++++++ .../workspace-permissions-cache.module.ts | 33 ++ .../workspace-permissions-cache.service.ts | 318 ++++++++++++++++++ ...orkspace-roles-permissions-cache.module.ts | 20 -- ...rkspace-roles-permissions-cache.service.ts | 128 ------- .../datasource/workspace.datasource.ts | 8 +- .../exceptions/twenty-orm.exception.ts | 1 + .../factories/workspace-datasource.factory.ts | 53 +-- .../engine/twenty-orm/twenty-orm.module.ts | 4 +- .../workspace-cache-storage.service.ts | 101 ++---- 20 files changed, 692 insertions(+), 350 deletions(-) create mode 100644 packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/types/user-workspace-role-map.type.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache-storage.service.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service.ts delete mode 100644 packages/twenty-server/src/engine/metadata-modules/workspace-roles-permissions-cache/workspace-roles-permissions-cache.module.ts delete mode 100644 packages/twenty-server/src/engine/metadata-modules/workspace-roles-permissions-cache/workspace-roles-permissions-cache.service.ts diff --git a/packages/twenty-server/src/engine/metadata-modules/object-permission/object-permission.module.ts b/packages/twenty-server/src/engine/metadata-modules/object-permission/object-permission.module.ts index f7139d051..c6fd88839 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-permission/object-permission.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-permission/object-permission.module.ts @@ -5,7 +5,7 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity'; import { ObjectPermissionService } from 'src/engine/metadata-modules/object-permission/object-permission.service'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; -import { WorkspaceRolesPermissionsCacheModule } from 'src/engine/metadata-modules/workspace-roles-permissions-cache/workspace-roles-permissions-cache.module'; +import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module'; @Module({ imports: [ @@ -13,7 +13,7 @@ import { WorkspaceRolesPermissionsCacheModule } from 'src/engine/metadata-module [ObjectPermissionEntity, RoleEntity, ObjectMetadataEntity], 'metadata', ), - WorkspaceRolesPermissionsCacheModule, + WorkspacePermissionsCacheModule, ], providers: [ObjectPermissionService], exports: [ObjectPermissionService], diff --git a/packages/twenty-server/src/engine/metadata-modules/object-permission/object-permission.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-permission/object-permission.service.ts index 2920f10d2..a023b2fa0 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-permission/object-permission.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-permission/object-permission.service.ts @@ -12,7 +12,7 @@ import { PermissionsExceptionMessage, } from 'src/engine/metadata-modules/permissions/permissions.exception'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; -import { WorkspaceRolesPermissionsCacheService } from 'src/engine/metadata-modules/workspace-roles-permissions-cache/workspace-roles-permissions-cache.service'; +import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; export class ObjectPermissionService { constructor( @@ -22,7 +22,7 @@ export class ObjectPermissionService { private readonly roleRepository: Repository, @InjectRepository(ObjectMetadataEntity, 'metadata') private readonly objectMetadataRepository: Repository, - private readonly workspaceRolesPermissionsCacheService: WorkspaceRolesPermissionsCacheService, + private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService, ) {} public async upsertObjectPermission({ @@ -54,9 +54,10 @@ export class ObjectPermissionService { throw new Error('Failed to upsert object permission'); } - await this.workspaceRolesPermissionsCacheService.recomputeRolesPermissionsCache( + await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache( { workspaceId, + roleIds: [input.roleId], }, ); diff --git a/packages/twenty-server/src/engine/metadata-modules/permissions/permissions.module.ts b/packages/twenty-server/src/engine/metadata-modules/permissions/permissions.module.ts index 12a865acb..36ebab16b 100644 --- a/packages/twenty-server/src/engine/metadata-modules/permissions/permissions.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/permissions/permissions.module.ts @@ -7,6 +7,7 @@ import { PermissionsService } from 'src/engine/metadata-modules/permissions/perm 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/user-role/user-role.module'; +import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module'; @Module({ imports: [ @@ -14,6 +15,7 @@ import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role. FeatureFlagModule, TypeOrmModule.forFeature([UserWorkspace], 'core'), UserRoleModule, + WorkspacePermissionsCacheModule, ], providers: [PermissionsService], exports: [PermissionsService], diff --git a/packages/twenty-server/src/engine/metadata-modules/permissions/permissions.service.ts b/packages/twenty-server/src/engine/metadata-modules/permissions/permissions.service.ts index 840436bbf..ec9aacf0c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/permissions/permissions.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/permissions/permissions.service.ts @@ -7,18 +7,24 @@ import { AuthException, AuthExceptionCode, } from 'src/engine/core-modules/auth/auth.exception'; +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 { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; import { PermissionsException, PermissionsExceptionCode, PermissionsExceptionMessage, } from 'src/engine/metadata-modules/permissions/permissions.exception'; -import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; 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'; @Injectable() export class PermissionsService { - constructor(private readonly userRoleService: UserRoleService) {} + constructor( + private readonly userRoleService: UserRoleService, + private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService, + private readonly featureFlagService: FeatureFlagService, + ) {} public async getUserWorkspacePermissions({ userWorkspaceId, @@ -124,11 +130,21 @@ export class PermissionsService { return true; } - const settingPermissions = roleOfUserWorkspace.settingPermissions ?? []; + const isPermissionsV2Enabled = + await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IsPermissionsV2Enabled, + workspaceId, + ); - return settingPermissions.some( - (settingPermission) => settingPermission.setting === setting, - ); + if (isPermissionsV2Enabled) { + const settingPermissions = roleOfUserWorkspace.settingPermissions ?? []; + + return settingPermissions.some( + (settingPermission) => settingPermission.setting === setting, + ); + } else { + return false; + } } public async userHasObjectRecordsPermission({ @@ -142,6 +158,18 @@ export class PermissionsService { requiredPermission: PermissionsOnAllObjectRecords; isExecutedByApiKey: boolean; }): Promise { + const isPermissionsV2Enabled = + await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IsPermissionsV2Enabled, + workspaceId, + ); + + if (isPermissionsV2Enabled) { + throw new Error( + 'This should not be called once Permissions V2 is enabled', + ); + } + if (isExecutedByApiKey) { return true; } @@ -153,31 +181,51 @@ export class PermissionsService { ); } - const [roleOfUserWorkspace] = await this.userRoleService - .getRolesByUserWorkspaces({ - userWorkspaceIds: [userWorkspaceId], + const roleIdOfUserWorkspace = + await this.userRoleService.getRoleIdForUserWorkspace({ + userWorkspaceId, workspaceId, - }) - .then((roles) => roles?.get(userWorkspaceId) ?? []); + }); - const roleColumn = - this.getRoleColumnForRequiredPermission(requiredPermission); + if (!isDefined(roleIdOfUserWorkspace)) { + throw new PermissionsException( + PermissionsExceptionMessage.NO_ROLE_FOUND_FOR_USER_WORKSPACE, + PermissionsExceptionCode.NO_ROLE_FOUND_FOR_USER_WORKSPACE, + ); + } - return roleOfUserWorkspace?.[roleColumn] === true; + const { data: rolesPermissions } = + await this.workspacePermissionsCacheService.getRolesPermissionsFromCache({ + workspaceId, + }); + + const rolePermissionsForUserWorkspaceRole = + rolesPermissions[roleIdOfUserWorkspace]; + + const objectPermissionKey = + this.getObjectPermissionKeyForRequiredPermission(requiredPermission); + + // until permissions V2 is enabled all objects have the same permission values deriving from role, ex role.canReadAllObjectRecords + const objectPermissionValue = + rolePermissionsForUserWorkspaceRole[ + Object.keys(rolePermissionsForUserWorkspaceRole)[0] + ]?.[objectPermissionKey]; + + return objectPermissionValue === true; } - private getRoleColumnForRequiredPermission( + private getObjectPermissionKeyForRequiredPermission( requiredPermission: PermissionsOnAllObjectRecords, - ): keyof RoleEntity { + ) { switch (requiredPermission) { case PermissionsOnAllObjectRecords.READ_ALL_OBJECT_RECORDS: - return 'canReadAllObjectRecords'; + return 'canRead'; case PermissionsOnAllObjectRecords.UPDATE_ALL_OBJECT_RECORDS: - return 'canUpdateAllObjectRecords'; + return 'canUpdate'; case PermissionsOnAllObjectRecords.SOFT_DELETE_ALL_OBJECT_RECORDS: - return 'canSoftDeleteAllObjectRecords'; + return 'canSoftDelete'; case PermissionsOnAllObjectRecords.DESTROY_ALL_OBJECT_RECORDS: - return 'canDestroyAllObjectRecords'; + return 'canDestroy'; default: throw new PermissionsException( PermissionsExceptionMessage.UNKNOWN_REQUIRED_PERMISSION, diff --git a/packages/twenty-server/src/engine/metadata-modules/role/role.module.ts b/packages/twenty-server/src/engine/metadata-modules/role/role.module.ts index e84fdeec0..740908b0f 100644 --- a/packages/twenty-server/src/engine/metadata-modules/role/role.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/role/role.module.ts @@ -13,7 +13,7 @@ import { RoleService } from 'src/engine/metadata-modules/role/role.service'; import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/role/user-workspace-role.entity'; import { SettingPermissionModule } from 'src/engine/metadata-modules/setting-permission/setting-permission.module'; import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module'; -import { WorkspaceRolesPermissionsCacheModule } from 'src/engine/metadata-modules/workspace-roles-permissions-cache/workspace-roles-permissions-cache.module'; +import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module'; @Module({ imports: [ @@ -25,7 +25,7 @@ import { WorkspaceRolesPermissionsCacheModule } from 'src/engine/metadata-module FeatureFlagModule, ObjectPermissionModule, SettingPermissionModule, - WorkspaceRolesPermissionsCacheModule, + WorkspacePermissionsCacheModule, ], providers: [RoleService, RoleResolver], exports: [RoleService], 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 351be8189..33f93fd61 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 @@ -20,7 +20,7 @@ import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/role/user-workspace-role.entity'; import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; import { isArgDefinedIfProvidedOrThrow } from 'src/engine/metadata-modules/utils/is-arg-defined-if-provided-or-throw.util'; -import { WorkspaceRolesPermissionsCacheService } from 'src/engine/metadata-modules/workspace-roles-permissions-cache/workspace-roles-permissions-cache.service'; +import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; export class RoleService { constructor( @@ -31,7 +31,7 @@ export class RoleService { @InjectRepository(UserWorkspaceRoleEntity, 'metadata') private readonly userWorkspaceRoleRepository: Repository, private readonly userRoleService: UserRoleService, - private readonly workspaceRolesPermissionsCacheService: WorkspaceRolesPermissionsCacheService, + private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService, ) {} public async getWorkspaceRoles(workspaceId: string): Promise { @@ -69,7 +69,7 @@ export class RoleService { }): Promise { await this.validateRoleInput({ input, workspaceId }); - const role = this.roleRepository.save({ + const role = await this.roleRepository.save({ label: input.label, description: input.description, icon: input.icon, @@ -82,11 +82,10 @@ export class RoleService { workspaceId, }); - await this.workspaceRolesPermissionsCacheService.recomputeRolesPermissionsCache( - { - workspaceId, - }, - ); + await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache({ + workspaceId, + roleIds: [role.id], + }); return role; } @@ -128,11 +127,10 @@ export class RoleService { ...input.update, }); - await this.workspaceRolesPermissionsCacheService.recomputeRolesPermissionsCache( - { - workspaceId, - }, - ); + await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache({ + workspaceId, + roleIds: [input.id], + }); return { ...existingRole, ...updatedRole }; } @@ -196,11 +194,9 @@ export class RoleService { workspaceId, }); - await this.workspaceRolesPermissionsCacheService.recomputeRolesPermissionsCache( - { - workspaceId, - }, - ); + await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache({ + workspaceId, + }); return roleId; } @@ -317,10 +313,11 @@ export class RoleService { workspaceId: string; defaultRoleId: string; }): Promise { - const userWorkspaceIds = await this.getUserWorkspaceIdsForRole( - roleId, - workspaceId, - ); + const userWorkspaceIds = + await this.userRoleService.getUserWorkspaceIdsAssignedToRole( + roleId, + workspaceId, + ); await Promise.all( userWorkspaceIds.map((userWorkspaceId) => @@ -333,24 +330,6 @@ export class RoleService { ); } - private async getUserWorkspaceIdsForRole( - roleId: string, - workspaceId: string, - ): Promise { - return this.userWorkspaceRoleRepository - .find({ - where: { - roleId: roleId, - workspaceId, - }, - }) - .then((userWorkspaceRoles) => - userWorkspaceRoles.map( - (userWorkspaceRole) => userWorkspaceRole.userWorkspaceId, - ), - ); - } - private async getRole( roleId: string, workspaceId: string, diff --git a/packages/twenty-server/src/engine/metadata-modules/user-role/user-role.module.ts b/packages/twenty-server/src/engine/metadata-modules/user-role/user-role.module.ts index 9c9533226..ed8e43811 100644 --- a/packages/twenty-server/src/engine/metadata-modules/user-role/user-role.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/user-role/user-role.module.ts @@ -5,11 +5,13 @@ import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-works import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/role/user-workspace-role.entity'; import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; +import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module'; @Module({ imports: [ TypeOrmModule.forFeature([RoleEntity, UserWorkspaceRoleEntity], 'metadata'), TypeOrmModule.forFeature([UserWorkspace], 'core'), + WorkspacePermissionsCacheModule, ], providers: [UserRoleService], exports: [UserRoleService], 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 34eeb5786..299d2a8ab 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 @@ -12,6 +12,7 @@ import { } from 'src/engine/metadata-modules/permissions/permissions.exception'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/role/user-workspace-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 { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; @@ -24,6 +25,7 @@ export class UserRoleService { @InjectRepository(UserWorkspace, 'core') private readonly userWorkspaceRepository: Repository, private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService, ) {} public async assignRoleToUserWorkspace({ @@ -56,6 +58,12 @@ export class UserRoleService { workspaceId, id: Not(newUserWorkspaceRole.id), }); + + await this.workspacePermissionsCacheService.recomputeUserWorkspaceRoleMapCache( + { + workspaceId, + }, + ); } public async getRoleIdForUserWorkspace({ @@ -69,11 +77,14 @@ export class UserRoleService { return; } - const userWorkspaceRole = await this.userWorkspaceRoleRepository.findOne({ - where: { userWorkspaceId, workspaceId }, - }); + const userWorkspaceRoleMap = + await this.workspacePermissionsCacheService.getUserWorkspaceRoleMapFromCache( + { + workspaceId, + }, + ); - return userWorkspaceRole?.roleId; + return userWorkspaceRoleMap.data[userWorkspaceId]; } public async getRolesByUserWorkspaces({ @@ -125,21 +136,13 @@ export class UserRoleService { roleId: string, workspaceId: string, ): Promise { - const userWorkspaceRoles = await this.userWorkspaceRoleRepository.find({ - where: { - roleId, - workspaceId, - }, - }); + const userWorkspaceIdsWithRole = + await this.getUserWorkspaceIdsAssignedToRole(roleId, workspaceId); const userIds = await this.userWorkspaceRepository .find({ where: { - id: In( - userWorkspaceRoles.map( - (userWorkspaceRole) => userWorkspaceRole.userWorkspaceId, - ), - ), + id: In(userWorkspaceIdsWithRole), }, }) .then((userWorkspaces) => @@ -161,6 +164,22 @@ export class UserRoleService { return workspaceMembers; } + public async getUserWorkspaceIdsAssignedToRole( + roleId: string, + workspaceId: string, + ): Promise { + const userWorkspaceRoleMap = + await this.workspacePermissionsCacheService.getUserWorkspaceRoleMapFromCache( + { + workspaceId, + }, + ); + + return Object.entries(userWorkspaceRoleMap.data) + .filter(([_, roleIdFromMap]) => roleIdFromMap === roleId) + .map(([userWorkspaceId]) => userWorkspaceId); + } + public async validateUserWorkspaceIsNotUniqueAdminOrThrow({ userWorkspaceId, workspaceId, diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.service.ts index 100699da3..740dd711e 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.service.ts @@ -10,6 +10,8 @@ import { TwentyORMExceptionCode } from 'src/engine/twenty-orm/exceptions/twenty- import { getFromCacheWithRecompute } from 'src/engine/utils/get-data-from-cache-with-recompute.util'; import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; +const FEATURE_FLAG_MAP = 'Feature flag map'; + @Injectable() export class WorkspaceFeatureFlagsMapCacheService { logger = new Logger(WorkspaceFeatureFlagsMapCacheService.name); @@ -45,7 +47,7 @@ export class WorkspaceFeatureFlagsMapCacheService { workspaceId, ), recomputeCache: (params) => this.recomputeFeatureFlagsMapCache(params), - cachedEntityName: 'Feature flag map', + cachedEntityName: FEATURE_FLAG_MAP, exceptionCode: TwentyORMExceptionCode.FEATURE_FLAG_MAP_VERSION_NOT_FOUND, }); } diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/types/user-workspace-role-map.type.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/types/user-workspace-role-map.type.ts new file mode 100644 index 000000000..ea1b5684f --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/types/user-workspace-role-map.type.ts @@ -0,0 +1 @@ +export type UserWorkspaceRoleMap = Record; diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache-storage.service.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache-storage.service.ts new file mode 100644 index 000000000..1d05c15eb --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache-storage.service.ts @@ -0,0 +1,156 @@ +import { Injectable, Logger } from '@nestjs/common'; + +import { ObjectRecordsPermissionsByRoleId } from 'twenty-shared/types'; +import { v4 } from 'uuid'; + +import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator'; +import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service'; +import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum'; +import { UserWorkspaceRoleMap } from 'src/engine/metadata-modules/workspace-permissions-cache/types/user-workspace-role-map.type'; +import { WorkspaceCacheKeys } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; + +const TTL_INFINITE = 0; + +@Injectable() +export class WorkspacePermissionsCacheStorageService { + logger = new Logger(WorkspacePermissionsCacheStorageService.name); + + constructor( + @InjectCacheStorage(CacheStorageNamespace.EngineWorkspace) + private readonly cacheStorageService: CacheStorageService, + ) {} + + async setRolesPermissionsVersion(workspaceId: string): Promise { + const rolesPermissionsVersion = v4(); + + await this.cacheStorageService.set( + `${WorkspaceCacheKeys.MetadataPermissionsRolesPermissionsVersion}:${workspaceId}`, + rolesPermissionsVersion, + TTL_INFINITE, + ); + + return rolesPermissionsVersion; + } + + async setRolesPermissions( + workspaceId: string, + permissions: ObjectRecordsPermissionsByRoleId, + ): Promise<{ + newRolesPermissionsVersion: string; + }> { + const [, newRolesPermissionsVersion] = await Promise.all([ + this.cacheStorageService.set( + `${WorkspaceCacheKeys.MetadataPermissionsRolesPermissions}:${workspaceId}`, + permissions, + TTL_INFINITE, + ), + this.setRolesPermissionsVersion(workspaceId), + ]); + + return { newRolesPermissionsVersion }; + } + + getRolesPermissions( + workspaceId: string, + ): Promise { + return this.cacheStorageService.get( + `${WorkspaceCacheKeys.MetadataPermissionsRolesPermissions}:${workspaceId}`, + ); + } + + getRolesPermissionsVersion(workspaceId: string): Promise { + return this.cacheStorageService.get( + `${WorkspaceCacheKeys.MetadataPermissionsRolesPermissionsVersion}:${workspaceId}`, + ); + } + + addRolesPermissionsOngoingCachingLock(workspaceId: string) { + return this.cacheStorageService.set( + `${WorkspaceCacheKeys.MetadataPermissionsRolesPermissionsOngoingCachingLock}:${workspaceId}`, + true, + 1_000 * 60, // 1 minute + ); + } + + removeRolesPermissionsOngoingCachingLock(workspaceId: string) { + return this.cacheStorageService.del( + `${WorkspaceCacheKeys.MetadataPermissionsRolesPermissionsOngoingCachingLock}:${workspaceId}`, + ); + } + + getRolesPermissionsOngoingCachingLock( + workspaceId: string, + ): Promise { + return this.cacheStorageService.get( + `${WorkspaceCacheKeys.MetadataPermissionsRolesPermissionsOngoingCachingLock}:${workspaceId}`, + ); + } + + async setUserWorkspaceRoleMap( + workspaceId: string, + userWorkspaceRoleMap: UserWorkspaceRoleMap, + ): Promise<{ + newUserWorkspaceRoleMapVersion: string; + }> { + const [, newUserWorkspaceRoleMapVersion] = await Promise.all([ + this.cacheStorageService.set( + `${WorkspaceCacheKeys.MetadataPermissionsUserWorkspaceRoleMap}:${workspaceId}`, + userWorkspaceRoleMap, + TTL_INFINITE, + ), + this.setUserWorkspaceRoleMapVersion(workspaceId), + ]); + + return { newUserWorkspaceRoleMapVersion }; + } + + async setUserWorkspaceRoleMapVersion(workspaceId: string) { + const userWorkspaceRoleMapVersion = v4(); + + await this.cacheStorageService.set( + `${WorkspaceCacheKeys.MetadataPermissionsUserWorkspaceRoleMapVersion}:${workspaceId}`, + userWorkspaceRoleMapVersion, + TTL_INFINITE, + ); + + return userWorkspaceRoleMapVersion; + } + + getUserWorkspaceRoleMap( + workspaceId: string, + ): Promise | undefined> { + return this.cacheStorageService.get>( + `${WorkspaceCacheKeys.MetadataPermissionsUserWorkspaceRoleMap}:${workspaceId}`, + ); + } + + getUserWorkspaceRoleMapVersion( + workspaceId: string, + ): Promise { + return this.cacheStorageService.get( + `${WorkspaceCacheKeys.MetadataPermissionsUserWorkspaceRoleMapVersion}:${workspaceId}`, + ); + } + + addUserWorkspaceRoleMapOngoingCachingLock(workspaceId: string) { + return this.cacheStorageService.set( + `${WorkspaceCacheKeys.MetadataPermissionsUserWorkspaceRoleMapOngoingCachingLock}:${workspaceId}`, + true, + 1_000 * 60, // 1 minute + ); + } + + removeUserWorkspaceRoleMapOngoingCachingLock(workspaceId: string) { + return this.cacheStorageService.del( + `${WorkspaceCacheKeys.MetadataPermissionsUserWorkspaceRoleMapOngoingCachingLock}:${workspaceId}`, + ); + } + + getUserWorkspaceRoleMapOngoingCachingLock( + workspaceId: string, + ): Promise { + return this.cacheStorageService.get( + `${WorkspaceCacheKeys.MetadataPermissionsUserWorkspaceRoleMapOngoingCachingLock}:${workspaceId}`, + ); + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module.ts new file mode 100644 index 000000000..b4144782e --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module.ts @@ -0,0 +1,33 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; +import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/role/user-workspace-role.entity'; +import { WorkspacePermissionsCacheStorageService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache-storage.service'; +import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; + +import { WorkspacePermissionsCacheService } from './workspace-permissions-cache.service'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([Workspace], 'core'), + TypeOrmModule.forFeature( + [ObjectMetadataEntity, RoleEntity, UserWorkspaceRoleEntity], + 'metadata', + ), + WorkspaceCacheStorageModule, + FeatureFlagModule, + ], + providers: [ + WorkspacePermissionsCacheService, + WorkspacePermissionsCacheStorageService, + ], + exports: [ + WorkspacePermissionsCacheService, + WorkspacePermissionsCacheStorageService, + ], +}) +export class WorkspacePermissionsCacheModule {} diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service.ts new file mode 100644 index 000000000..d53581133 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service.ts @@ -0,0 +1,318 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { + ObjectRecordsPermissions, + ObjectRecordsPermissionsByRoleId, +} from 'twenty-shared/types'; +import { In, Repository } from 'typeorm'; + +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 { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; +import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/role/user-workspace-role.entity'; +import { UserWorkspaceRoleMap } from 'src/engine/metadata-modules/workspace-permissions-cache/types/user-workspace-role-map.type'; +import { WorkspacePermissionsCacheStorageService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache-storage.service'; +import { TwentyORMExceptionCode } from 'src/engine/twenty-orm/exceptions/twenty-orm.exception'; +import { getFromCacheWithRecompute } from 'src/engine/utils/get-data-from-cache-with-recompute.util'; +import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; + +type CacheResult = { + version: T; + data: U; +}; + +const USER_WORKSPACE_ROLE_MAP = 'User workspace role map'; +const ROLES_PERMISSIONS = 'Roles permissions'; + +@Injectable() +export class WorkspacePermissionsCacheService { + logger = new Logger(WorkspacePermissionsCacheService.name); + + constructor( + @InjectRepository(ObjectMetadataEntity, 'metadata') + private readonly objectMetadataRepository: Repository, + @InjectRepository(RoleEntity, 'metadata') + private readonly roleRepository: Repository, + @InjectRepository(UserWorkspaceRoleEntity, 'metadata') + private readonly userWorkspaceRoleRepository: Repository, + private readonly workspacePermissionsCacheStorageService: WorkspacePermissionsCacheStorageService, + private readonly workspaceCacheStorageService: WorkspaceCacheStorageService, + private readonly featureFlagService: FeatureFlagService, + ) {} + + async recomputeRolesPermissionsCache({ + workspaceId, + ignoreLock = false, + roleIds, + }: { + workspaceId: string; + ignoreLock?: boolean; + roleIds?: string[]; + }): Promise { + const isPermissionsV2Enabled = + await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IsPermissionsV2Enabled, + workspaceId, + ); + + const isAlreadyCaching = + await this.workspacePermissionsCacheStorageService.getRolesPermissionsOngoingCachingLock( + workspaceId, + ); + + if (!ignoreLock && isAlreadyCaching) { + return; + } + + await this.workspacePermissionsCacheStorageService.addRolesPermissionsOngoingCachingLock( + workspaceId, + ); + + let currentRolesPermissions: ObjectRecordsPermissionsByRoleId | undefined = + undefined; + + if (roleIds) { + currentRolesPermissions = + await this.workspacePermissionsCacheStorageService.getRolesPermissions( + workspaceId, + ); + } + + const recomputedRolesPermissions = + await this.getObjectRecordPermissionsForRoles({ + workspaceId, + isPermissionsV2Enabled, + roleIds, + }); + + const freshObjectRecordsPermissionsByRoleId = roleIds + ? { ...currentRolesPermissions, ...recomputedRolesPermissions } + : recomputedRolesPermissions; + + await this.workspacePermissionsCacheStorageService.setRolesPermissions( + workspaceId, + freshObjectRecordsPermissionsByRoleId, + ); + + await this.workspacePermissionsCacheStorageService.removeRolesPermissionsOngoingCachingLock( + workspaceId, + ); + } + + async recomputeUserWorkspaceRoleMapCache({ + workspaceId, + ignoreLock = false, + }: { + workspaceId: string; + ignoreLock?: boolean; + }): Promise { + const isAlreadyCaching = + await this.workspacePermissionsCacheStorageService.getUserWorkspaceRoleMapOngoingCachingLock( + workspaceId, + ); + + if (!ignoreLock && isAlreadyCaching) { + return; + } + + await this.workspacePermissionsCacheStorageService.addUserWorkspaceRoleMapOngoingCachingLock( + workspaceId, + ); + + const freshUserWorkspaceRoleMap = + await this.getUserWorkspaceRoleMapFromDatabase({ + workspaceId, + }); + + await this.workspacePermissionsCacheStorageService.setUserWorkspaceRoleMap( + workspaceId, + freshUserWorkspaceRoleMap, + ); + + await this.workspacePermissionsCacheStorageService.removeUserWorkspaceRoleMapOngoingCachingLock( + workspaceId, + ); + } + + async getRolesPermissionsFromCache({ + workspaceId, + }: { + workspaceId: string; + }): Promise> { + return getFromCacheWithRecompute({ + workspaceId, + getCacheData: () => + this.workspacePermissionsCacheStorageService.getRolesPermissions( + workspaceId, + ), + getCacheVersion: () => + this.workspacePermissionsCacheStorageService.getRolesPermissionsVersion( + workspaceId, + ), + recomputeCache: (params) => this.recomputeRolesPermissionsCache(params), + cachedEntityName: ROLES_PERMISSIONS, + exceptionCode: TwentyORMExceptionCode.ROLES_PERMISSIONS_VERSION_NOT_FOUND, + }); + } + + async getUserWorkspaceRoleMapFromCache({ + workspaceId, + }: { + workspaceId: string; + }): Promise> { + return getFromCacheWithRecompute({ + workspaceId, + getCacheData: () => + this.workspacePermissionsCacheStorageService.getUserWorkspaceRoleMap( + workspaceId, + ), + getCacheVersion: () => + this.workspacePermissionsCacheStorageService.getUserWorkspaceRoleMapVersion( + workspaceId, + ), + recomputeCache: (params) => + this.recomputeUserWorkspaceRoleMapCache(params), + cachedEntityName: USER_WORKSPACE_ROLE_MAP, + exceptionCode: + TwentyORMExceptionCode.USER_WORKSPACE_ROLE_MAP_VERSION_NOT_FOUND, + }); + } + + private async getObjectRecordPermissionsForRoles({ + workspaceId, + isPermissionsV2Enabled, + roleIds, + }: { + workspaceId: string; + isPermissionsV2Enabled: boolean; + roleIds?: string[]; + }): Promise { + let roles: RoleEntity[] = []; + + roles = await this.roleRepository.find({ + where: { + workspaceId, + ...(roleIds ? { id: In(roleIds) } : {}), + }, + relations: ['objectPermissions'], + }); + + const workspaceObjectMetadataNameIdMap = + await this.getWorkspaceObjectMetadataNameIdMap(workspaceId); + + const permissionsByRoleId: ObjectRecordsPermissionsByRoleId = {}; + + for (const role of roles) { + const objectRecordsPermissions: ObjectRecordsPermissions = {}; + + for (const objectMetadataNameSingular of Object.keys( + workspaceObjectMetadataNameIdMap, + )) { + let canRead = role.canReadAllObjectRecords; + let canUpdate = role.canUpdateAllObjectRecords; + let canSoftDelete = role.canSoftDeleteAllObjectRecords; + let canDestroy = role.canDestroyAllObjectRecords; + + if (isPermissionsV2Enabled) { + const objectRecordPermissionsOverride = role.objectPermissions.find( + (objectPermission) => + objectPermission.objectMetadataId === + workspaceObjectMetadataNameIdMap[objectMetadataNameSingular], + ); + + canRead = + objectRecordPermissionsOverride?.canReadObjectRecords ?? canRead; + canUpdate = + objectRecordPermissionsOverride?.canUpdateObjectRecords ?? + canUpdate; + canSoftDelete = + objectRecordPermissionsOverride?.canSoftDeleteObjectRecords ?? + canSoftDelete; + canDestroy = + objectRecordPermissionsOverride?.canDestroyObjectRecords ?? + canDestroy; + } + + objectRecordsPermissions[objectMetadataNameSingular] = { + canRead, + canUpdate, + canSoftDelete, + canDestroy, + }; + } + + permissionsByRoleId[role.id] = objectRecordsPermissions; + } + + return permissionsByRoleId; + } + + private async getWorkspaceObjectMetadataNameIdMap( + workspaceId: string, + ): Promise> { + let workspaceObjectMetadataMap: Record = {}; + const metadataVersion = + await this.workspaceCacheStorageService.getMetadataVersion(workspaceId); + + if (metadataVersion) { + const objectMetadataMaps = + await this.workspaceCacheStorageService.getObjectMetadataMaps( + workspaceId, + metadataVersion, + ); + + workspaceObjectMetadataMap = Object.values( + objectMetadataMaps?.byId ?? {}, + ).reduce( + (acc, objectMetadata) => { + acc[objectMetadata.nameSingular] = objectMetadata.id; + + return acc; + }, + {} as Record, + ); + } + + if ( + !metadataVersion || + Object.keys(workspaceObjectMetadataMap).length === 0 + ) { + const workspaceObjectMetadata = await this.objectMetadataRepository.find({ + where: { + workspaceId, + }, + }); + + workspaceObjectMetadataMap = workspaceObjectMetadata.reduce( + (acc, objectMetadata) => { + acc[objectMetadata.nameSingular] = objectMetadata.id; + + return acc; + }, + {} as Record, + ); + } + + return workspaceObjectMetadataMap; + } + + private async getUserWorkspaceRoleMapFromDatabase({ + workspaceId, + }: { + workspaceId: string; + }): Promise { + const userWorkspaceRoleMap = await this.userWorkspaceRoleRepository.find({ + where: { + workspaceId, + }, + }); + + return userWorkspaceRoleMap.reduce((acc, userWorkspaceRole) => { + acc[userWorkspaceRole.userWorkspaceId] = userWorkspaceRole.roleId; + + return acc; + }, {} as UserWorkspaceRoleMap); + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-roles-permissions-cache/workspace-roles-permissions-cache.module.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-roles-permissions-cache/workspace-roles-permissions-cache.module.ts deleted file mode 100644 index 051c369f8..000000000 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-roles-permissions-cache/workspace-roles-permissions-cache.module.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; - -import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; -import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; - -import { WorkspaceRolesPermissionsCacheService } from './workspace-roles-permissions-cache.service'; - -@Module({ - imports: [ - TypeOrmModule.forFeature([Workspace], 'core'), - TypeOrmModule.forFeature([ObjectMetadataEntity, RoleEntity], 'metadata'), - WorkspaceCacheStorageModule, - ], - providers: [WorkspaceRolesPermissionsCacheService], - exports: [WorkspaceRolesPermissionsCacheService], -}) -export class WorkspaceRolesPermissionsCacheModule {} diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-roles-permissions-cache/workspace-roles-permissions-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-roles-permissions-cache/workspace-roles-permissions-cache.service.ts deleted file mode 100644 index 4d6ce7d49..000000000 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-roles-permissions-cache/workspace-roles-permissions-cache.service.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; - -import { - ObjectRecordsPermissions, - ObjectRecordsPermissionsByRoleId, -} from 'twenty-shared/types'; -import { Repository } from 'typeorm'; - -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; -import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; - -@Injectable() -export class WorkspaceRolesPermissionsCacheService { - logger = new Logger(WorkspaceRolesPermissionsCacheService.name); - - constructor( - @InjectRepository(ObjectMetadataEntity, 'metadata') - private readonly objectMetadataRepository: Repository, - @InjectRepository(RoleEntity, 'metadata') - private readonly roleRepository: Repository, - private readonly workspaceCacheStorageService: WorkspaceCacheStorageService, - ) {} - - async recomputeRolesPermissionsCache({ - workspaceId, - ignoreLock = false, - }: { - workspaceId: string; - ignoreLock?: boolean; - }): Promise { - const isAlreadyCaching = - await this.workspaceCacheStorageService.getRolesPermissionsOngoingCachingLock( - workspaceId, - ); - - if (!ignoreLock && isAlreadyCaching) { - return; - } - - await this.workspaceCacheStorageService.addRolesPermissionsOngoingCachingLock( - workspaceId, - ); - - const freshObjectRecordsPermissionsByRoleId = - await this.getObjectRecordPermissionsForRoles({ - workspaceId, - }); - - await this.workspaceCacheStorageService.setRolesPermissions( - workspaceId, - freshObjectRecordsPermissionsByRoleId, - ); - - await this.workspaceCacheStorageService.removeRolesPermissionsOngoingCachingLock( - workspaceId, - ); - } - - private async getObjectRecordPermissionsForRoles({ - workspaceId, - }: { - workspaceId: string; - }): Promise { - const roles = await this.roleRepository.find({ - where: { - workspaceId, - }, - }); - - const workspaceObjectMetadataNames = - await this.getWorkspaceObjectMetadataNames(workspaceId); - - const permissionsByRoleId: ObjectRecordsPermissionsByRoleId = {}; - - for (const role of roles) { - const objectRecordsPermissions: ObjectRecordsPermissions = {}; - - for (const objectMetadataNameSingular of workspaceObjectMetadataNames) { - objectRecordsPermissions[objectMetadataNameSingular] = { - canRead: role.canReadAllObjectRecords, - canUpdate: role.canUpdateAllObjectRecords, - canSoftDelete: role.canSoftDeleteAllObjectRecords, - canDestroy: role.canDestroyAllObjectRecords, - }; - } - - permissionsByRoleId[role.id] = objectRecordsPermissions; - } - - return permissionsByRoleId; - } - - private async getWorkspaceObjectMetadataNames( - workspaceId: string, - ): Promise { - let workspaceObjectMetadataNames: string[] = []; - const metadataVersion = - await this.workspaceCacheStorageService.getMetadataVersion(workspaceId); - - if (metadataVersion) { - const objectMetadataMaps = - await this.workspaceCacheStorageService.getObjectMetadataMaps( - workspaceId, - metadataVersion, - ); - - workspaceObjectMetadataNames = Object.values( - objectMetadataMaps?.byId ?? {}, - ).map((objectMetadata) => objectMetadata.nameSingular); - } - - if (!metadataVersion || workspaceObjectMetadataNames.length === 0) { - const workspaceObjectMetadata = await this.objectMetadataRepository.find({ - where: { - workspaceId, - }, - }); - - workspaceObjectMetadataNames = workspaceObjectMetadata.map( - (objectMetadata) => objectMetadata.nameSingular, - ); - } - - return workspaceObjectMetadataNames; - } -} diff --git a/packages/twenty-server/src/engine/twenty-orm/datasource/workspace.datasource.ts b/packages/twenty-server/src/engine/twenty-orm/datasource/workspace.datasource.ts index e75f49a64..7612b26f2 100644 --- a/packages/twenty-server/src/engine/twenty-orm/datasource/workspace.datasource.ts +++ b/packages/twenty-server/src/engine/twenty-orm/datasource/workspace.datasource.ts @@ -18,16 +18,16 @@ export class WorkspaceDataSource extends DataSource { readonly manager: WorkspaceEntityManager; featureFlagMapVersion: string; featureFlagMap: FeatureFlagMap; - rolesPermissionsVersion?: string; - permissionsPerRoleId?: ObjectRecordsPermissionsByRoleId; + rolesPermissionsVersion: string; + permissionsPerRoleId: ObjectRecordsPermissionsByRoleId; constructor( internalContext: WorkspaceInternalContext, options: DataSourceOptions, featureFlagMapVersion: string, featureFlagMap: FeatureFlagMap, - rolesPermissionsVersion?: string, - permissionsPerRoleId?: ObjectRecordsPermissionsByRoleId, + rolesPermissionsVersion: string, + permissionsPerRoleId: ObjectRecordsPermissionsByRoleId, ) { super(options); this.internalContext = internalContext; diff --git a/packages/twenty-server/src/engine/twenty-orm/exceptions/twenty-orm.exception.ts b/packages/twenty-server/src/engine/twenty-orm/exceptions/twenty-orm.exception.ts index 32ba899a1..c71f1d395 100644 --- a/packages/twenty-server/src/engine/twenty-orm/exceptions/twenty-orm.exception.ts +++ b/packages/twenty-server/src/engine/twenty-orm/exceptions/twenty-orm.exception.ts @@ -13,4 +13,5 @@ export enum TwentyORMExceptionCode { WORKSPACE_SCHEMA_NOT_FOUND = 'WORKSPACE_SCHEMA_NOT_FOUND', ROLES_PERMISSIONS_VERSION_NOT_FOUND = 'ROLES_PERMISSIONS_VERSION_NOT_FOUND', FEATURE_FLAG_MAP_VERSION_NOT_FOUND = 'FEATURE_FLAG_MAP_VERSION_NOT_FOUND', + USER_WORKSPACE_ROLE_MAP_VERSION_NOT_FOUND = 'USER_WORKSPACE_ROLE_MAP_VERSION_NOT_FOUND', } diff --git a/packages/twenty-server/src/engine/twenty-orm/factories/workspace-datasource.factory.ts b/packages/twenty-server/src/engine/twenty-orm/factories/workspace-datasource.factory.ts index 5d3a2ad8f..774187cff 100644 --- a/packages/twenty-server/src/engine/twenty-orm/factories/workspace-datasource.factory.ts +++ b/packages/twenty-server/src/engine/twenty-orm/factories/workspace-datasource.factory.ts @@ -7,12 +7,12 @@ import { EntitySchema } from 'typeorm'; import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface'; import { NodeEnvironment } from 'src/engine/core-modules/twenty-config/interfaces/node-environment.interface'; -import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { WorkspaceFeatureFlagsMapCacheService } from 'src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.service'; import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service'; -import { WorkspaceRolesPermissionsCacheService } from 'src/engine/metadata-modules/workspace-roles-permissions-cache/workspace-roles-permissions-cache.service'; +import { WorkspacePermissionsCacheStorageService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache-storage.service'; +import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; import { TwentyORMException, @@ -40,7 +40,8 @@ export class WorkspaceDatasourceFactory { private readonly workspaceCacheStorageService: WorkspaceCacheStorageService, private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService, private readonly entitySchemaFactory: EntitySchemaFactory, - private readonly workspaceRolesPermissionsCacheService: WorkspaceRolesPermissionsCacheService, + private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService, + private readonly workspacePermissionsCacheStorageService: WorkspacePermissionsCacheStorageService, private readonly workspaceFeatureFlagsMapCacheService: WorkspaceFeatureFlagsMapCacheService, ) {} @@ -60,15 +61,11 @@ export class WorkspaceDatasourceFactory { { workspaceId }, ); - const isPermissionsV2Enabled = - cachedFeatureFlagMap[FeatureFlagKey.IsPermissionsV2Enabled]; - const { data: cachedRolesPermissions, version: cachedRolesPermissionsVersion, } = await this.getRolesPermissionsFromCache({ workspaceId, - isPermissionsV2Enabled, }); if ( @@ -196,13 +193,11 @@ export class WorkspaceDatasourceFactory { throw new Error(`Failed to create WorkspaceDataSource for ${cacheKey}`); } - if (isPermissionsV2Enabled) { - await this.updateWorkspaceDataSourceRolesPermissionsIfNeeded({ - workspaceDataSource, - cachedRolesPermissionsVersion, - cachedRolesPermissions, - }); - } + await this.updateWorkspaceDataSourceRolesPermissionsIfNeeded({ + workspaceDataSource, + cachedRolesPermissionsVersion, + cachedRolesPermissions, + }); await this.updateWorkspaceDataSourceFeatureFlagsMapIfNeeded({ workspaceDataSource, @@ -215,33 +210,21 @@ export class WorkspaceDatasourceFactory { private async getRolesPermissionsFromCache({ workspaceId, - isPermissionsV2Enabled, }: { workspaceId: string; - isPermissionsV2Enabled?: boolean; - }): Promise< - CacheResult< - string | undefined, - ObjectRecordsPermissionsByRoleId | undefined - > - > { - if (!isPermissionsV2Enabled) { - return { version: undefined, data: undefined }; - } - - return getFromCacheWithRecompute< - string | undefined, - ObjectRecordsPermissionsByRoleId | undefined - >({ + }): Promise> { + return getFromCacheWithRecompute({ workspaceId, getCacheData: () => - this.workspaceCacheStorageService.getRolesPermissions(workspaceId), + this.workspacePermissionsCacheStorageService.getRolesPermissions( + workspaceId, + ), getCacheVersion: () => - this.workspaceCacheStorageService.getRolesPermissionsVersionFromCache( + this.workspacePermissionsCacheStorageService.getRolesPermissionsVersion( workspaceId, ), recomputeCache: (params) => - this.workspaceRolesPermissionsCacheService.recomputeRolesPermissionsCache( + this.workspacePermissionsCacheService.recomputeRolesPermissionsCache( params, ), cachedEntityName: 'Roles permissions', @@ -281,8 +264,8 @@ export class WorkspaceDatasourceFactory { cachedRolesPermissions, }: { workspaceDataSource: WorkspaceDataSource; - cachedRolesPermissionsVersion: string | undefined; - cachedRolesPermissions: ObjectRecordsPermissionsByRoleId | undefined; + cachedRolesPermissionsVersion: string; + cachedRolesPermissions: ObjectRecordsPermissionsByRoleId; }): Promise { this.updateWorkspaceDataSourceIfNeeded({ workspaceDataSource, diff --git a/packages/twenty-server/src/engine/twenty-orm/twenty-orm.module.ts b/packages/twenty-server/src/engine/twenty-orm/twenty-orm.module.ts index d936a7c91..d0a08e3c4 100644 --- a/packages/twenty-server/src/engine/twenty-orm/twenty-orm.module.ts +++ b/packages/twenty-server/src/engine/twenty-orm/twenty-orm.module.ts @@ -8,7 +8,7 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/role/user-workspace-role.entity'; import { WorkspaceFeatureFlagsMapCacheModule } from 'src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.module'; import { WorkspaceMetadataCacheModule } from 'src/engine/metadata-modules/workspace-metadata-cache/workspace-metadata-cache.module'; -import { WorkspaceRolesPermissionsCacheModule } from 'src/engine/metadata-modules/workspace-roles-permissions-cache/workspace-roles-permissions-cache.module'; +import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module'; import { entitySchemaFactories } from 'src/engine/twenty-orm/factories'; import { EntitySchemaFactory } from 'src/engine/twenty-orm/factories/entity-schema.factory'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; @@ -26,8 +26,8 @@ import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/ WorkspaceCacheStorageModule, WorkspaceMetadataCacheModule, PermissionsModule, - WorkspaceRolesPermissionsCacheModule, WorkspaceFeatureFlagsMapCacheModule, + WorkspacePermissionsCacheModule, FeatureFlagModule, ], providers: [ diff --git a/packages/twenty-server/src/engine/workspace-cache-storage/workspace-cache-storage.service.ts b/packages/twenty-server/src/engine/workspace-cache-storage/workspace-cache-storage.service.ts index c8a53a837..74d33e148 100644 --- a/packages/twenty-server/src/engine/workspace-cache-storage/workspace-cache-storage.service.ts +++ b/packages/twenty-server/src/engine/workspace-cache-storage/workspace-cache-storage.service.ts @@ -2,9 +2,7 @@ import { Injectable } from '@nestjs/common'; import crypto from 'crypto'; -import { ObjectRecordsPermissionsByRoleId } from 'twenty-shared/types'; import { EntitySchemaOptions } from 'typeorm'; -import { v4 } from 'uuid'; import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface'; @@ -23,12 +21,15 @@ export enum WorkspaceCacheKeys { MetadataObjectMetadataMaps = 'metadata:object-metadata-maps', MetadataObjectMetadataOngoingCachingLock = 'metadata:object-metadata-ongoing-caching-lock', MetadataVersion = 'metadata:workspace-metadata-version', - MetadataRolesPermissions = 'metadata:roles-permissions', - MetadataRolesPermissionsVersion = 'metadata:roles-permissions-version', - MetadataRolesPermissionsOngoingCachingLock = 'metadata:roles-permissions-ongoing-caching-lock', - FeatureFlagMap = 'feature-flag-map', - FeatureFlagMapVersion = 'feature-flag-map-version', + FeatureFlagMap = 'feature-flag:feature-flag-map', + FeatureFlagMapVersion = 'feature-flag:feature-flag-map-version', FeatureFlagMapOngoingCachingLock = 'feature-flag-map-ongoing-caching-lock', + MetadataPermissionsRolesPermissions = 'metadata:permissions:roles-permissions', + MetadataPermissionsRolesPermissionsVersion = 'metadata:permissions:roles-permissions-version', + MetadataPermissionsRolesPermissionsOngoingCachingLock = 'metadata:permissions:roles-permissions-ongoing-caching-lock', + MetadataPermissionsUserWorkspaceRoleMap = 'metadata:permissions:user-workspace-role-map', + MetadataPermissionsUserWorkspaceRoleMapVersion = 'metadata:permissions:user-workspace-role-map-version', + MetadataPermissionsUserWorkspaceRoleMapOngoingCachingLock = 'metadata:permissions:user-workspace-role-map-ongoing-caching-lock', } const TTL_INFINITE = 0; @@ -186,74 +187,6 @@ export class WorkspaceCacheStorageService { ); } - getRolesPermissionsVersionFromCache( - workspaceId: string, - ): Promise { - return this.cacheStorageService.get( - `${WorkspaceCacheKeys.MetadataRolesPermissionsVersion}:${workspaceId}`, - ); - } - - async setRolesPermissionsVersion(workspaceId: string): Promise { - const rolesPermissionsVersion = v4(); - - await this.cacheStorageService.set( - `${WorkspaceCacheKeys.MetadataRolesPermissionsVersion}:${workspaceId}`, - rolesPermissionsVersion, - TTL_INFINITE, - ); - - return rolesPermissionsVersion; - } - - async setRolesPermissions( - workspaceId: string, - permissions: ObjectRecordsPermissionsByRoleId, - ): Promise<{ - newRolesPermissionsVersion: string; - }> { - const [, newRolesPermissionsVersion] = await Promise.all([ - this.cacheStorageService.set( - `${WorkspaceCacheKeys.MetadataRolesPermissions}:${workspaceId}`, - permissions, - TTL_INFINITE, - ), - this.setRolesPermissionsVersion(workspaceId), - ]); - - return { newRolesPermissionsVersion }; - } - - getRolesPermissions( - workspaceId: string, - ): Promise { - return this.cacheStorageService.get( - `${WorkspaceCacheKeys.MetadataRolesPermissions}:${workspaceId}`, - ); - } - - addRolesPermissionsOngoingCachingLock(workspaceId: string) { - return this.cacheStorageService.set( - `${WorkspaceCacheKeys.MetadataRolesPermissionsOngoingCachingLock}:${workspaceId}`, - true, - 1_000 * 60, // 1 minute - ); - } - - removeRolesPermissionsOngoingCachingLock(workspaceId: string) { - return this.cacheStorageService.del( - `${WorkspaceCacheKeys.MetadataRolesPermissionsOngoingCachingLock}:${workspaceId}`, - ); - } - - getRolesPermissionsOngoingCachingLock( - workspaceId: string, - ): Promise { - return this.cacheStorageService.get( - `${WorkspaceCacheKeys.MetadataRolesPermissionsOngoingCachingLock}:${workspaceId}`, - ); - } - getFeatureFlagsMapVersionFromCache( workspaceId: string, ): Promise { @@ -341,15 +274,27 @@ export class WorkspaceCacheStorageService { ); await this.cacheStorageService.del( - `${WorkspaceCacheKeys.MetadataRolesPermissions}:${workspaceId}`, + `${WorkspaceCacheKeys.MetadataPermissionsRolesPermissions}:${workspaceId}`, ); await this.cacheStorageService.del( - `${WorkspaceCacheKeys.MetadataRolesPermissionsVersion}:${workspaceId}`, + `${WorkspaceCacheKeys.MetadataPermissionsRolesPermissionsVersion}:${workspaceId}`, ); await this.cacheStorageService.del( - `${WorkspaceCacheKeys.MetadataRolesPermissionsOngoingCachingLock}:${workspaceId}`, + `${WorkspaceCacheKeys.MetadataPermissionsRolesPermissionsOngoingCachingLock}:${workspaceId}`, + ); + + await this.cacheStorageService.del( + `${WorkspaceCacheKeys.MetadataPermissionsUserWorkspaceRoleMap}:${workspaceId}`, + ); + + await this.cacheStorageService.del( + `${WorkspaceCacheKeys.MetadataPermissionsUserWorkspaceRoleMapVersion}:${workspaceId}`, + ); + + await this.cacheStorageService.del( + `${WorkspaceCacheKeys.MetadataPermissionsUserWorkspaceRoleMapOngoingCachingLock}:${workspaceId}`, ); await this.cacheStorageService.del(