import { Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { ObjectRecordsPermissions, ObjectRecordsPermissionsByRoleId, } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; 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 { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; 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'; import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids'; type CacheResult = { version: T; data: U; }; export const USER_WORKSPACE_ROLE_MAP = 'User workspace role map'; export const ROLES_PERMISSIONS = 'Roles permissions'; @Injectable() export class WorkspacePermissionsCacheService { logger = new Logger(WorkspacePermissionsCacheService.name); constructor( @InjectRepository(ObjectMetadataEntity, 'core') private readonly objectMetadataRepository: Repository, @InjectRepository(RoleEntity, 'core') private readonly roleRepository: Repository, @InjectRepository(UserWorkspaceRoleEntity, 'core') 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 isAlreadyCaching = await this.workspacePermissionsCacheStorageService.getRolesPermissionsOngoingCachingLock( workspaceId, ); if (isAlreadyCaching) { if (ignoreLock) { this.logger.warn( `RolesPermissions data is already being cached (workspace ${workspaceId}), ignoring lock`, ); } else { this.logger.warn( `RolesPermissions data is already being cached (workspace ${workspaceId}), respecting lock and returning no data`, ); return; } } await this.workspacePermissionsCacheStorageService.addRolesPermissionsOngoingCachingLock( workspaceId, ); try { let currentRolesPermissions: ObjectRecordsPermissionsByRoleId | undefined; if (roleIds) { currentRolesPermissions = await this.workspacePermissionsCacheStorageService.getRolesPermissions( workspaceId, ); } const isPermissionsV2Enabled = await this.featureFlagService.isFeatureEnabled( FeatureFlagKey.IS_PERMISSIONS_V2_ENABLED, workspaceId, ); const recomputedRolesPermissions = await this.getObjectRecordPermissionsForRoles({ workspaceId, isPermissionsV2Enabled, roleIds, }); const freshObjectRecordsPermissionsByRoleId = roleIds ? { ...currentRolesPermissions, ...recomputedRolesPermissions } : recomputedRolesPermissions; await this.workspacePermissionsCacheStorageService.setRolesPermissions( workspaceId, freshObjectRecordsPermissionsByRoleId, ); } finally { await this.workspacePermissionsCacheStorageService.removeRolesPermissionsOngoingCachingLock( workspaceId, ); } } async recomputeUserWorkspaceRoleMapCache({ workspaceId, ignoreLock = false, }: { workspaceId: string; ignoreLock?: boolean; }): Promise { try { const isAlreadyCaching = await this.workspacePermissionsCacheStorageService.getUserWorkspaceRoleMapOngoingCachingLock( workspaceId, ); if (isAlreadyCaching) { if (ignoreLock) { this.logger.warn( `UserWorkspaceRoleMap data is already being cached (workspace ${workspaceId}), ignoring lock`, ); } else { this.logger.warn( `UserWorkspaceRoleMap data is already being cached (workspace ${workspaceId}), respecting lock and returning no data`, ); return; } } await this.workspacePermissionsCacheStorageService.addUserWorkspaceRoleMapOngoingCachingLock( workspaceId, ); try { const freshUserWorkspaceRoleMap = await this.getUserWorkspaceRoleMapFromDatabase({ workspaceId, }); await this.workspacePermissionsCacheStorageService.setUserWorkspaceRoleMap( workspaceId, freshUserWorkspaceRoleMap, ); } finally { await this.workspacePermissionsCacheStorageService.removeUserWorkspaceRoleMapOngoingCachingLock( workspaceId, ); } } catch (error) { // Flush stale userWorkspaceRoleMap await this.workspacePermissionsCacheStorageService.removeUserWorkspaceRoleMap( 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, logger: this.logger, }); } async getUserWorkspaceRoleMapFromCache({ workspaceId, }: { workspaceId: string; }): Promise> { return getFromCacheWithRecompute({ workspaceId, getCacheData: () => this.workspacePermissionsCacheStorageService.getUserWorkspaceRoleMap( workspaceId, ), recomputeCache: (params) => this.recomputeUserWorkspaceRoleMapCache(params), cachedEntityName: USER_WORKSPACE_ROLE_MAP, exceptionCode: TwentyORMExceptionCode.USER_WORKSPACE_ROLE_MAP_VERSION_NOT_FOUND, logger: this.logger, }); } async getRoleIdFromUserWorkspaceId({ workspaceId, userWorkspaceId, }: { workspaceId: string; userWorkspaceId?: string; }): Promise { if (!isDefined(userWorkspaceId)) { return; } const userWorkspaceRoleMap = await this.getUserWorkspaceRoleMapFromCache({ workspaceId, }); // @ts-expect-error legacy noImplicitAny return userWorkspaceRoleMap[userWorkspaceId]; } 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', 'settingPermissions'], }); const workspaceObjectMetadataCollection = await this.getWorkspaceObjectMetadataCollection(workspaceId); const permissionsByRoleId: ObjectRecordsPermissionsByRoleId = {}; for (const role of roles) { const objectRecordsPermissions: ObjectRecordsPermissions = {}; for (const objectMetadata of workspaceObjectMetadataCollection) { const { id: objectMetadataId, isSystem, standardId } = objectMetadata; let canRead = role.canReadAllObjectRecords; let canUpdate = role.canUpdateAllObjectRecords; let canSoftDelete = role.canSoftDeleteAllObjectRecords; let canDestroy = role.canDestroyAllObjectRecords; if (isPermissionsV2Enabled) { if ( standardId && [ STANDARD_OBJECT_IDS.workflow, STANDARD_OBJECT_IDS.workflowRun, STANDARD_OBJECT_IDS.workflowVersion, ].includes(standardId) ) { const hasWorkflowsPermissions = this.hasWorkflowsPermissions(role); canRead = hasWorkflowsPermissions; canUpdate = hasWorkflowsPermissions; canSoftDelete = hasWorkflowsPermissions; canDestroy = hasWorkflowsPermissions; } else { const objectRecordPermissionsOverride = role.objectPermissions.find( (objectPermission) => objectPermission.objectMetadataId === objectMetadataId, ); const getPermissionValue = ( overrideValue: boolean | undefined, defaultValue: boolean, ) => (isSystem ? true : (overrideValue ?? defaultValue)); canRead = getPermissionValue( objectRecordPermissionsOverride?.canReadObjectRecords, canRead, ); canUpdate = getPermissionValue( objectRecordPermissionsOverride?.canUpdateObjectRecords, canUpdate, ); canSoftDelete = getPermissionValue( objectRecordPermissionsOverride?.canSoftDeleteObjectRecords, canSoftDelete, ); canDestroy = getPermissionValue( objectRecordPermissionsOverride?.canDestroyObjectRecords, canDestroy, ); } } objectRecordsPermissions[objectMetadataId] = { canRead, canUpdate, canSoftDelete, canDestroy, }; permissionsByRoleId[role.id] = objectRecordsPermissions; } } return permissionsByRoleId; } private async getWorkspaceObjectMetadataCollection( workspaceId: string, ): Promise { const workspaceObjectMetadata = await this.objectMetadataRepository.find({ where: { workspaceId, }, select: ['id', 'isSystem', 'standardId'], }); return workspaceObjectMetadata; } 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); } private hasWorkflowsPermissions(role: RoleEntity): boolean { const hasWorkflowsPermissionFromRole = role.canUpdateAllSettings; const hasWorkflowsPermissionsFromSettingPermissions = isDefined( role.settingPermissions.find( (settingPermission) => settingPermission.setting === SettingPermissionType.WORKFLOWS, ), ); return ( hasWorkflowsPermissionFromRole || hasWorkflowsPermissionsFromSettingPermissions ); } }