[permissions] Implement object-records permissions in query builders (#11458)
In this PR we are - (if permissionsV2 is enabled) executing permission checks at query builder level. To do so we want to override the query builders methods that are performing db calls (.execute(), .getMany(), ... etc.) For now I have just overriden some of the query builders methods for the poc. To do so I created custom query builder classes that extend typeorm's query builder (selectQueryBuilder and updateQueryBuilder, for now and later I will tackle softDeleteQueryBuilder, etc.). - adding a notion of roles permissions version and roles permissions object to datasources. We will now use one datasource per roleId and rolePermissionVersion. Both rolesPermissionsVersion and rolesPermissions objects are stored in redis and recomputed at role update or if queried and found empty. Unlike for metadata version we don't need to store a version in the db that stands for the source of truth. We also don't need to destroy and recreate the datasource if the rolesPermissions version changes, but only to update the value for rolesPermissions and rolesPermissionsVersions on the existing datasource. What this PR misses - computing of roles permissions should take into account objectPermissions table (for now it only looks at what's on the roles table) - pursue extension of query builder classes and overriding of their db calling-methods - what should the behaviour be for calls from twentyOrmGlobalManager that don't have a roleId?
This commit is contained in:
@ -12,6 +12,7 @@ export class ScopedWorkspaceContextFactory {
|
||||
public create(): {
|
||||
workspaceId: string | null;
|
||||
workspaceMetadataVersion: number | null;
|
||||
userWorkspaceId: string | null;
|
||||
} {
|
||||
const workspaceId: string | undefined =
|
||||
this.request?.['req']?.['workspaceId'] ||
|
||||
@ -22,6 +23,7 @@ export class ScopedWorkspaceContextFactory {
|
||||
return {
|
||||
workspaceId: workspaceId ?? null,
|
||||
workspaceMetadataVersion: workspaceMetadataVersion ?? null,
|
||||
userWorkspaceId: this.request?.['req']?.['userWorkspaceId'] ?? null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,12 +1,18 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { ObjectRecordsPermissionsByRoleId } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
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 { WorkspaceFeatureFlagMapCacheService } from 'src/engine/metadata-modules/workspace-feature-flag-map-cache.service.ts/workspace-feature-flag-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 { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource';
|
||||
import {
|
||||
TwentyORMException,
|
||||
@ -17,6 +23,11 @@ import { PromiseMemoizer } from 'src/engine/twenty-orm/storage/promise-memoizer.
|
||||
import { CacheKey } from 'src/engine/twenty-orm/storage/types/cache-key.type';
|
||||
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||
|
||||
type CacheResult<T, U> = {
|
||||
version: T;
|
||||
data: U;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceDatasourceFactory {
|
||||
private readonly logger = new Logger(WorkspaceDatasourceFactory.name);
|
||||
@ -28,19 +39,35 @@ export class WorkspaceDatasourceFactory {
|
||||
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
|
||||
private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService,
|
||||
private readonly entitySchemaFactory: EntitySchemaFactory,
|
||||
private readonly workspaceRolesPermissionsCacheService: WorkspaceRolesPermissionsCacheService,
|
||||
private readonly workspaceFeatureFlagMapCacheService: WorkspaceFeatureFlagMapCacheService,
|
||||
) {}
|
||||
|
||||
public async create(
|
||||
workspaceId: string,
|
||||
workspaceMetadataVersion: number | null,
|
||||
failOnMetadataCacheMiss = true,
|
||||
shouldFailIfMetadataNotFound = true,
|
||||
): Promise<WorkspaceDataSource> {
|
||||
const cachedWorkspaceMetadataVersion =
|
||||
await this.getWorkspaceMetadataVersionFromCache(
|
||||
workspaceId,
|
||||
failOnMetadataCacheMiss,
|
||||
shouldFailIfMetadataNotFound,
|
||||
);
|
||||
|
||||
const { data: cachedFeatureFlagMap, version: cachedFeatureFlagMapVersion } =
|
||||
await this.getFeatureFlagMapFromCache({ workspaceId });
|
||||
|
||||
const isPermissionsV2Enabled =
|
||||
cachedFeatureFlagMap[FeatureFlagKey.IsPermissionsV2Enabled];
|
||||
|
||||
const {
|
||||
data: cachedRolesPermissions,
|
||||
version: cachedRolesPermissionsVersion,
|
||||
} = await this.getRolesPermissionsFromCache({
|
||||
workspaceId,
|
||||
isPermissionsV2Enabled,
|
||||
});
|
||||
|
||||
if (
|
||||
workspaceMetadataVersion !== null &&
|
||||
cachedWorkspaceMetadataVersion !== workspaceMetadataVersion
|
||||
@ -139,6 +166,10 @@ export class WorkspaceDatasourceFactory {
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
cachedFeatureFlagMapVersion,
|
||||
cachedFeatureFlagMap,
|
||||
cachedRolesPermissionsVersion,
|
||||
cachedRolesPermissions,
|
||||
);
|
||||
|
||||
await workspaceDataSource.initialize();
|
||||
@ -162,28 +193,206 @@ export class WorkspaceDatasourceFactory {
|
||||
throw new Error(`Failed to create WorkspaceDataSource for ${cacheKey}`);
|
||||
}
|
||||
|
||||
if (isPermissionsV2Enabled) {
|
||||
await this.updateWorkspaceDataSourceRolesPermissionsIfNeeded({
|
||||
workspaceDataSource,
|
||||
cachedRolesPermissionsVersion,
|
||||
cachedRolesPermissions,
|
||||
});
|
||||
}
|
||||
|
||||
await this.updateWorkspaceDataSourceFeatureFlagMapIfNeeded({
|
||||
workspaceDataSource,
|
||||
cachedFeatureFlagMapVersion,
|
||||
cachedFeatureFlagMap,
|
||||
});
|
||||
|
||||
return workspaceDataSource;
|
||||
}
|
||||
|
||||
private async getFromCacheWithRecompute<T, U>({
|
||||
workspaceId,
|
||||
getCacheData,
|
||||
getCacheVersion,
|
||||
recomputeCache,
|
||||
cachedEntityName,
|
||||
exceptionCode,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
getCacheData: (workspaceId: string) => Promise<U | undefined>;
|
||||
getCacheVersion: (workspaceId: string) => Promise<T | undefined>;
|
||||
recomputeCache: (params: { workspaceId: string }) => Promise<void>;
|
||||
cachedEntityName: string;
|
||||
exceptionCode: TwentyORMExceptionCode;
|
||||
}): Promise<CacheResult<T, U>> {
|
||||
let cachedVersion: T | undefined;
|
||||
let cachedData: U | undefined;
|
||||
|
||||
cachedVersion = await getCacheVersion(workspaceId);
|
||||
cachedData = await getCacheData(workspaceId);
|
||||
|
||||
if (!isDefined(cachedData) || !isDefined(cachedVersion)) {
|
||||
await recomputeCache({ workspaceId });
|
||||
|
||||
cachedData = await getCacheData(workspaceId);
|
||||
cachedVersion = await getCacheVersion(workspaceId);
|
||||
|
||||
if (!isDefined(cachedData) || !isDefined(cachedVersion)) {
|
||||
throw new TwentyORMException(
|
||||
`${cachedEntityName} not found after recompute for workspace ${workspaceId}`,
|
||||
exceptionCode,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
version: cachedVersion,
|
||||
data: cachedData,
|
||||
};
|
||||
}
|
||||
|
||||
private async getRolesPermissionsFromCache({
|
||||
workspaceId,
|
||||
isPermissionsV2Enabled,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
isPermissionsV2Enabled?: boolean;
|
||||
}): Promise<
|
||||
CacheResult<
|
||||
string | undefined,
|
||||
ObjectRecordsPermissionsByRoleId | undefined
|
||||
>
|
||||
> {
|
||||
if (!isPermissionsV2Enabled) {
|
||||
return { version: undefined, data: undefined };
|
||||
}
|
||||
|
||||
return this.getFromCacheWithRecompute<
|
||||
string | undefined,
|
||||
ObjectRecordsPermissionsByRoleId | undefined
|
||||
>({
|
||||
workspaceId,
|
||||
getCacheData: () =>
|
||||
this.workspaceCacheStorageService.getRolesPermissions(workspaceId),
|
||||
getCacheVersion: () =>
|
||||
this.workspaceCacheStorageService.getRolesPermissionsVersionFromCache(
|
||||
workspaceId,
|
||||
),
|
||||
recomputeCache: (params) =>
|
||||
this.workspaceRolesPermissionsCacheService.recomputeRolesPermissionsCache(
|
||||
params,
|
||||
),
|
||||
cachedEntityName: 'Roles permissions',
|
||||
exceptionCode: TwentyORMExceptionCode.ROLES_PERMISSIONS_VERSION_NOT_FOUND,
|
||||
});
|
||||
}
|
||||
|
||||
private async getFeatureFlagMapFromCache({
|
||||
workspaceId,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
}): Promise<CacheResult<string, FeatureFlagMap>> {
|
||||
return this.getFromCacheWithRecompute<string, FeatureFlagMap>({
|
||||
workspaceId,
|
||||
getCacheData: () =>
|
||||
this.workspaceCacheStorageService.getFeatureFlagMap(workspaceId),
|
||||
getCacheVersion: () =>
|
||||
this.workspaceCacheStorageService.getFeatureFlagMapVersionFromCache(
|
||||
workspaceId,
|
||||
),
|
||||
recomputeCache: (params) =>
|
||||
this.workspaceFeatureFlagMapCacheService.recomputeFeatureFlagMapCache(
|
||||
params,
|
||||
),
|
||||
cachedEntityName: 'Feature flag map',
|
||||
exceptionCode: TwentyORMExceptionCode.FEATURE_FLAG_MAP_VERSION_NOT_FOUND,
|
||||
});
|
||||
}
|
||||
|
||||
private updateWorkspaceDataSourceIfNeeded<T>({
|
||||
workspaceDataSource,
|
||||
currentVersion,
|
||||
newVersion,
|
||||
newData,
|
||||
setData,
|
||||
setVersion,
|
||||
}: {
|
||||
workspaceDataSource: WorkspaceDataSource;
|
||||
currentVersion: string | undefined;
|
||||
newVersion: string | undefined;
|
||||
newData: T | undefined;
|
||||
setData: (data: T) => void;
|
||||
setVersion: (version: string) => void;
|
||||
}): void {
|
||||
if (
|
||||
isDefined(newVersion) &&
|
||||
isDefined(newData) &&
|
||||
currentVersion !== newVersion
|
||||
) {
|
||||
workspaceDataSource.manager.repositories.clear();
|
||||
setData(newData);
|
||||
setVersion(newVersion);
|
||||
}
|
||||
}
|
||||
|
||||
private async updateWorkspaceDataSourceRolesPermissionsIfNeeded({
|
||||
workspaceDataSource,
|
||||
cachedRolesPermissionsVersion,
|
||||
cachedRolesPermissions,
|
||||
}: {
|
||||
workspaceDataSource: WorkspaceDataSource;
|
||||
cachedRolesPermissionsVersion: string | undefined;
|
||||
cachedRolesPermissions: ObjectRecordsPermissionsByRoleId | undefined;
|
||||
}): Promise<void> {
|
||||
this.updateWorkspaceDataSourceIfNeeded({
|
||||
workspaceDataSource,
|
||||
currentVersion: workspaceDataSource.rolesPermissionsVersion,
|
||||
newVersion: cachedRolesPermissionsVersion,
|
||||
newData: cachedRolesPermissions,
|
||||
setData: (data) => workspaceDataSource.setRolesPermissions(data),
|
||||
setVersion: (version) =>
|
||||
workspaceDataSource.setRolesPermissionsVersion(version),
|
||||
});
|
||||
}
|
||||
|
||||
private async updateWorkspaceDataSourceFeatureFlagMapIfNeeded({
|
||||
workspaceDataSource,
|
||||
cachedFeatureFlagMapVersion,
|
||||
cachedFeatureFlagMap,
|
||||
}: {
|
||||
workspaceDataSource: WorkspaceDataSource;
|
||||
cachedFeatureFlagMapVersion: string | undefined;
|
||||
cachedFeatureFlagMap: FeatureFlagMap | undefined;
|
||||
}): Promise<void> {
|
||||
this.updateWorkspaceDataSourceIfNeeded({
|
||||
workspaceDataSource,
|
||||
currentVersion: workspaceDataSource.featureFlagMapVersion,
|
||||
newVersion: cachedFeatureFlagMapVersion,
|
||||
newData: cachedFeatureFlagMap,
|
||||
setData: (data) => workspaceDataSource.setFeatureFlagMap(data),
|
||||
setVersion: (version) =>
|
||||
workspaceDataSource.setFeatureFlagMapVersion(version),
|
||||
});
|
||||
}
|
||||
|
||||
private async getWorkspaceMetadataVersionFromCache(
|
||||
workspaceId: string,
|
||||
failOnMetadataCacheMiss = true,
|
||||
shouldFailIfMetadataNotFound = true,
|
||||
): Promise<number> {
|
||||
let latestWorkspaceMetadataVersion =
|
||||
await this.workspaceCacheStorageService.getMetadataVersion(workspaceId);
|
||||
|
||||
if (latestWorkspaceMetadataVersion === undefined) {
|
||||
await this.workspaceMetadataCacheService.recomputeMetadataCache({
|
||||
workspaceId,
|
||||
ignoreLock: !failOnMetadataCacheMiss,
|
||||
});
|
||||
|
||||
if (failOnMetadataCacheMiss) {
|
||||
if (shouldFailIfMetadataNotFound) {
|
||||
throw new TwentyORMException(
|
||||
`Metadata version not found for workspace ${workspaceId}`,
|
||||
TwentyORMExceptionCode.METADATA_VERSION_NOT_FOUND,
|
||||
);
|
||||
} else {
|
||||
await this.workspaceMetadataCacheService.recomputeMetadataCache({
|
||||
workspaceId,
|
||||
ignoreLock: !shouldFailIfMetadataNotFound,
|
||||
});
|
||||
latestWorkspaceMetadataVersion =
|
||||
await this.workspaceCacheStorageService.getMetadataVersion(
|
||||
workspaceId,
|
||||
|
||||
Reference in New Issue
Block a user