[permissions] Improve performances using a cache for userWorkspaces roles (#11587)
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
This commit is contained in:
@ -0,0 +1 @@
|
||||
export type UserWorkspaceRoleMap = Record<string, string>;
|
||||
@ -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<string> {
|
||||
const rolesPermissionsVersion = v4();
|
||||
|
||||
await this.cacheStorageService.set<string>(
|
||||
`${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<ObjectRecordsPermissionsByRoleId>(
|
||||
`${WorkspaceCacheKeys.MetadataPermissionsRolesPermissions}:${workspaceId}`,
|
||||
permissions,
|
||||
TTL_INFINITE,
|
||||
),
|
||||
this.setRolesPermissionsVersion(workspaceId),
|
||||
]);
|
||||
|
||||
return { newRolesPermissionsVersion };
|
||||
}
|
||||
|
||||
getRolesPermissions(
|
||||
workspaceId: string,
|
||||
): Promise<ObjectRecordsPermissionsByRoleId | undefined> {
|
||||
return this.cacheStorageService.get<ObjectRecordsPermissionsByRoleId>(
|
||||
`${WorkspaceCacheKeys.MetadataPermissionsRolesPermissions}:${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
getRolesPermissionsVersion(workspaceId: string): Promise<string | undefined> {
|
||||
return this.cacheStorageService.get<string>(
|
||||
`${WorkspaceCacheKeys.MetadataPermissionsRolesPermissionsVersion}:${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
addRolesPermissionsOngoingCachingLock(workspaceId: string) {
|
||||
return this.cacheStorageService.set<boolean>(
|
||||
`${WorkspaceCacheKeys.MetadataPermissionsRolesPermissionsOngoingCachingLock}:${workspaceId}`,
|
||||
true,
|
||||
1_000 * 60, // 1 minute
|
||||
);
|
||||
}
|
||||
|
||||
removeRolesPermissionsOngoingCachingLock(workspaceId: string) {
|
||||
return this.cacheStorageService.del(
|
||||
`${WorkspaceCacheKeys.MetadataPermissionsRolesPermissionsOngoingCachingLock}:${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
getRolesPermissionsOngoingCachingLock(
|
||||
workspaceId: string,
|
||||
): Promise<boolean | undefined> {
|
||||
return this.cacheStorageService.get<boolean>(
|
||||
`${WorkspaceCacheKeys.MetadataPermissionsRolesPermissionsOngoingCachingLock}:${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
async setUserWorkspaceRoleMap(
|
||||
workspaceId: string,
|
||||
userWorkspaceRoleMap: UserWorkspaceRoleMap,
|
||||
): Promise<{
|
||||
newUserWorkspaceRoleMapVersion: string;
|
||||
}> {
|
||||
const [, newUserWorkspaceRoleMapVersion] = await Promise.all([
|
||||
this.cacheStorageService.set<UserWorkspaceRoleMap>(
|
||||
`${WorkspaceCacheKeys.MetadataPermissionsUserWorkspaceRoleMap}:${workspaceId}`,
|
||||
userWorkspaceRoleMap,
|
||||
TTL_INFINITE,
|
||||
),
|
||||
this.setUserWorkspaceRoleMapVersion(workspaceId),
|
||||
]);
|
||||
|
||||
return { newUserWorkspaceRoleMapVersion };
|
||||
}
|
||||
|
||||
async setUserWorkspaceRoleMapVersion(workspaceId: string) {
|
||||
const userWorkspaceRoleMapVersion = v4();
|
||||
|
||||
await this.cacheStorageService.set<string>(
|
||||
`${WorkspaceCacheKeys.MetadataPermissionsUserWorkspaceRoleMapVersion}:${workspaceId}`,
|
||||
userWorkspaceRoleMapVersion,
|
||||
TTL_INFINITE,
|
||||
);
|
||||
|
||||
return userWorkspaceRoleMapVersion;
|
||||
}
|
||||
|
||||
getUserWorkspaceRoleMap(
|
||||
workspaceId: string,
|
||||
): Promise<Record<string, string> | undefined> {
|
||||
return this.cacheStorageService.get<Record<string, string>>(
|
||||
`${WorkspaceCacheKeys.MetadataPermissionsUserWorkspaceRoleMap}:${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
getUserWorkspaceRoleMapVersion(
|
||||
workspaceId: string,
|
||||
): Promise<string | undefined> {
|
||||
return this.cacheStorageService.get<string>(
|
||||
`${WorkspaceCacheKeys.MetadataPermissionsUserWorkspaceRoleMapVersion}:${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
addUserWorkspaceRoleMapOngoingCachingLock(workspaceId: string) {
|
||||
return this.cacheStorageService.set<boolean>(
|
||||
`${WorkspaceCacheKeys.MetadataPermissionsUserWorkspaceRoleMapOngoingCachingLock}:${workspaceId}`,
|
||||
true,
|
||||
1_000 * 60, // 1 minute
|
||||
);
|
||||
}
|
||||
|
||||
removeUserWorkspaceRoleMapOngoingCachingLock(workspaceId: string) {
|
||||
return this.cacheStorageService.del(
|
||||
`${WorkspaceCacheKeys.MetadataPermissionsUserWorkspaceRoleMapOngoingCachingLock}:${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
getUserWorkspaceRoleMapOngoingCachingLock(
|
||||
workspaceId: string,
|
||||
): Promise<boolean | undefined> {
|
||||
return this.cacheStorageService.get<boolean>(
|
||||
`${WorkspaceCacheKeys.MetadataPermissionsUserWorkspaceRoleMapOngoingCachingLock}:${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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 {}
|
||||
@ -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<T, U> = {
|
||||
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<ObjectMetadataEntity>,
|
||||
@InjectRepository(RoleEntity, 'metadata')
|
||||
private readonly roleRepository: Repository<RoleEntity>,
|
||||
@InjectRepository(UserWorkspaceRoleEntity, 'metadata')
|
||||
private readonly userWorkspaceRoleRepository: Repository<UserWorkspaceRoleEntity>,
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<CacheResult<string, ObjectRecordsPermissionsByRoleId>> {
|
||||
return getFromCacheWithRecompute<string, ObjectRecordsPermissionsByRoleId>({
|
||||
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<CacheResult<string, UserWorkspaceRoleMap>> {
|
||||
return getFromCacheWithRecompute<string, UserWorkspaceRoleMap>({
|
||||
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<ObjectRecordsPermissionsByRoleId> {
|
||||
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<Record<string, string>> {
|
||||
let workspaceObjectMetadataMap: Record<string, string> = {};
|
||||
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<string, string>,
|
||||
);
|
||||
}
|
||||
|
||||
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<string, string>,
|
||||
);
|
||||
}
|
||||
|
||||
return workspaceObjectMetadataMap;
|
||||
}
|
||||
|
||||
private async getUserWorkspaceRoleMapFromDatabase({
|
||||
workspaceId,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
}): Promise<UserWorkspaceRoleMap> {
|
||||
const userWorkspaceRoleMap = await this.userWorkspaceRoleRepository.find({
|
||||
where: {
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
return userWorkspaceRoleMap.reduce((acc, userWorkspaceRole) => {
|
||||
acc[userWorkspaceRole.userWorkspaceId] = userWorkspaceRole.roleId;
|
||||
|
||||
return acc;
|
||||
}, {} as UserWorkspaceRoleMap);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user