Permission checks on twentyORM global manager (#11477)

In this PR we are handling permissions when using
twentyORMGlobalManager,
and handling permissions for rest api and api key
This commit is contained in:
Marie
2025-04-23 17:57:48 +02:00
committed by GitHub
parent 28a1354928
commit 4257f30f12
54 changed files with 547 additions and 116 deletions

View File

@ -41,10 +41,19 @@ export class WorkspaceDataSource extends DataSource {
override getRepository<Entity extends ObjectLiteral>(
target: EntityTarget<Entity>,
shouldBypassPermissionChecks = false,
roleId?: string,
): WorkspaceRepository<Entity> {
if (shouldBypassPermissionChecks === true) {
return this.manager.getRepository(target, shouldBypassPermissionChecks);
}
if (roleId) {
return this.manager.getRepository(target, roleId);
return this.manager.getRepository(
target,
shouldBypassPermissionChecks,
roleId,
);
}
return this.manager.getRepository(target);
@ -64,11 +73,11 @@ export class WorkspaceDataSource extends DataSource {
this.permissionsPerRoleId = permissionsPerRoleId;
}
setFeatureFlagsMap(featureFlagMap: FeatureFlagMap) {
setFeatureFlagMap(featureFlagMap: FeatureFlagMap) {
this.featureFlagMap = featureFlagMap;
}
setFeatureFlagsMapVersion(featureFlagMapVersion: string) {
setFeatureFlagMapVersion(featureFlagMapVersion: string) {
this.featureFlagMapVersion = featureFlagMapVersion;
}
}

View File

@ -28,10 +28,17 @@ export class WorkspaceEntityManager extends EntityManager {
override getRepository<Entity extends ObjectLiteral>(
target: EntityTarget<Entity>,
shouldBypassPermissionChecks = false,
roleId?: string,
): WorkspaceRepository<Entity> {
const dataSource = this.connection as WorkspaceDataSource;
const repositoryKey = `${dataSource.getMetadata(target).name}_${roleId ?? 'default'}${dataSource.rolesPermissionsVersion ? `_${dataSource.rolesPermissionsVersion}` : ''}${dataSource.featureFlagMapVersion ? `_${dataSource.featureFlagMapVersion}` : ''}`;
const repositoryKey = this.getRepositoryKey({
target,
dataSource,
roleId,
shouldBypassPermissionChecks,
});
const repoFromMap = this.repositories.get(repositoryKey);
if (repoFromMap) {
@ -53,10 +60,36 @@ export class WorkspaceEntityManager extends EntityManager {
dataSource.featureFlagMap,
this.queryRunner,
objectPermissions,
shouldBypassPermissionChecks,
);
this.repositories.set(repositoryKey, newRepository);
return newRepository;
}
private getRepositoryKey({
target,
dataSource,
roleId,
shouldBypassPermissionChecks,
}: {
target: EntityTarget<any>;
dataSource: WorkspaceDataSource;
shouldBypassPermissionChecks: boolean;
roleId?: string;
}) {
const repositoryPrefix = dataSource.getMetadata(target).name;
const roleIdSuffix = roleId ? `_${roleId}` : '';
const rolesPermissionsVersionSuffix = dataSource.rolesPermissionsVersion
? `_${dataSource.rolesPermissionsVersion}`
: '';
const featureFlagMapVersionSuffix = dataSource.featureFlagMapVersion
? `_${dataSource.featureFlagMapVersion}`
: '';
return shouldBypassPermissionChecks
? `${repositoryPrefix}_bypass${featureFlagMapVersionSuffix}`
: `${repositoryPrefix}${roleIdSuffix}${rolesPermissionsVersionSuffix}${featureFlagMapVersionSuffix}`;
}
}

View File

@ -13,6 +13,7 @@ export class ScopedWorkspaceContextFactory {
workspaceId: string | null;
workspaceMetadataVersion: number | null;
userWorkspaceId: string | null;
isExecutedByApiKey: boolean;
} {
const workspaceId: string | undefined =
this.request?.['req']?.['workspaceId'] ||
@ -24,6 +25,7 @@ export class ScopedWorkspaceContextFactory {
workspaceId: workspaceId ?? null,
workspaceMetadataVersion: workspaceMetadataVersion ?? null,
userWorkspaceId: this.request?.['req']?.['userWorkspaceId'] ?? null,
isExecutedByApiKey: !!this.request?.['req']?.['apiKey'],
};
}
}

View File

@ -294,9 +294,9 @@ export class WorkspaceDatasourceFactory {
currentVersion: workspaceDataSource.featureFlagMapVersion,
newVersion: cachedFeatureFlagMapVersion,
newData: cachedFeatureFlagMap,
setData: (data) => workspaceDataSource.setFeatureFlagsMap(data),
setData: (data) => workspaceDataSource.setFeatureFlagMap(data),
setVersion: (version) =>
workspaceDataSource.setFeatureFlagsMapVersion(version),
workspaceDataSource.setFeatureFlagMapVersion(version),
});
}

View File

@ -6,6 +6,7 @@ import {
PermissionsExceptionCode,
PermissionsExceptionMessage,
} from 'src/engine/metadata-modules/permissions/permissions.exception';
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
const getTargetEntityAndOperationType = (expressionMap: QueryExpressionMap) => {
const mainEntity = expressionMap.aliases[0].metadata.name;
@ -20,10 +21,26 @@ const getTargetEntityAndOperationType = (expressionMap: QueryExpressionMap) => {
export const validateQueryIsPermittedOrThrow = (
expressionMap: QueryExpressionMap,
objectRecordsPermissions: ObjectRecordsPermissions,
objectMetadataMaps: ObjectMetadataMaps,
shouldBypassPermissionChecks: boolean,
) => {
if (shouldBypassPermissionChecks) {
return;
}
const { mainEntity, operationType } =
getTargetEntityAndOperationType(expressionMap);
const objectMetadataIdForEntity =
objectMetadataMaps.idByNameSingular[mainEntity];
const objectMetadataIsSystem =
objectMetadataMaps.byId[objectMetadataIdForEntity]?.isSystem === true;
if (objectMetadataIsSystem) {
return;
}
const permissionsForEntity = objectRecordsPermissions[mainEntity];
switch (operationType) {

View File

@ -1,6 +1,8 @@
import { ObjectRecordsPermissions } from 'twenty-shared/types';
import { ObjectLiteral, SelectQueryBuilder } from 'typeorm';
import { WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface';
import { WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder';
export class WorkspaceQueryBuilder<
@ -9,9 +11,15 @@ export class WorkspaceQueryBuilder<
constructor(
queryBuilder: SelectQueryBuilder<T>,
objectRecordsPermissions: ObjectRecordsPermissions,
internalContext: WorkspaceInternalContext,
shouldBypassPermissionChecks: boolean,
) {
super(queryBuilder, objectRecordsPermissions);
this.objectRecordsPermissions = objectRecordsPermissions;
super(
queryBuilder,
objectRecordsPermissions,
internalContext,
shouldBypassPermissionChecks,
);
}
override clone(): this {
@ -20,6 +28,8 @@ export class WorkspaceQueryBuilder<
return new WorkspaceQueryBuilder(
clonedQueryBuilder,
this.objectRecordsPermissions,
this.internalContext,
this.shouldBypassPermissionChecks,
) as this;
}
}

View File

@ -2,6 +2,8 @@ import { ObjectRecordsPermissions } from 'twenty-shared/types';
import { ObjectLiteral, SelectQueryBuilder, UpdateQueryBuilder } from 'typeorm';
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
import { WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface';
import { validateQueryIsPermittedOrThrow } from 'src/engine/twenty-orm/repository/permissions.util';
import { WorkspaceUpdateQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-update-query-builder';
@ -9,12 +11,18 @@ export class WorkspaceSelectQueryBuilder<
T extends ObjectLiteral,
> extends SelectQueryBuilder<T> {
objectRecordsPermissions: ObjectRecordsPermissions;
shouldBypassPermissionChecks: boolean;
internalContext: WorkspaceInternalContext;
constructor(
queryBuilder: SelectQueryBuilder<T>,
objectRecordsPermissions: ObjectRecordsPermissions,
internalContext: WorkspaceInternalContext,
shouldBypassPermissionChecks: boolean,
) {
super(queryBuilder);
this.objectRecordsPermissions = objectRecordsPermissions;
this.internalContext = internalContext;
this.shouldBypassPermissionChecks = shouldBypassPermissionChecks;
}
override update(): WorkspaceUpdateQueryBuilder<T>;
@ -33,6 +41,8 @@ export class WorkspaceSelectQueryBuilder<
return new WorkspaceUpdateQueryBuilder<T>(
updateQueryBuilder,
this.objectRecordsPermissions,
this.internalContext,
this.shouldBypassPermissionChecks,
);
}
@ -40,6 +50,8 @@ export class WorkspaceSelectQueryBuilder<
validateQueryIsPermittedOrThrow(
this.expressionMap,
this.objectRecordsPermissions,
this.internalContext.objectMetadataMaps,
this.shouldBypassPermissionChecks,
);
return super.execute();
@ -49,6 +61,8 @@ export class WorkspaceSelectQueryBuilder<
validateQueryIsPermittedOrThrow(
this.expressionMap,
this.objectRecordsPermissions,
this.internalContext.objectMetadataMaps,
this.shouldBypassPermissionChecks,
);
return super.getMany();

View File

@ -1,24 +1,34 @@
import { ObjectRecordsPermissions } from 'twenty-shared/types';
import { ObjectLiteral, UpdateQueryBuilder, UpdateResult } from 'typeorm';
import { WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface';
import { validateQueryIsPermittedOrThrow } from 'src/engine/twenty-orm/repository/permissions.util';
export class WorkspaceUpdateQueryBuilder<
Entity extends ObjectLiteral,
> extends UpdateQueryBuilder<Entity> {
private objectRecordsPermissions: ObjectRecordsPermissions;
private shouldBypassPermissionChecks: boolean;
private internalContext: WorkspaceInternalContext;
constructor(
queryBuilder: UpdateQueryBuilder<Entity>,
objectRecordsPermissions: ObjectRecordsPermissions,
internalContext: WorkspaceInternalContext,
shouldBypassPermissionChecks: boolean,
) {
super(queryBuilder);
this.objectRecordsPermissions = objectRecordsPermissions;
this.internalContext = internalContext;
this.shouldBypassPermissionChecks = shouldBypassPermissionChecks;
}
override execute(): Promise<UpdateResult> {
validateQueryIsPermittedOrThrow(
this.expressionMap,
this.objectRecordsPermissions,
this.internalContext.objectMetadataMaps,
this.shouldBypassPermissionChecks,
);
return super.execute();

View File

@ -36,9 +36,9 @@ export class WorkspaceRepository<
T extends ObjectLiteral,
> extends Repository<T> {
private readonly internalContext: WorkspaceInternalContext;
private shouldBypassPermissionChecks: boolean;
private featureFlagMap: FeatureFlagMap;
private objectRecordsPermissions?: ObjectRecordsPermissions;
constructor(
internalContext: WorkspaceInternalContext,
target: EntityTarget<T>,
@ -46,11 +46,13 @@ export class WorkspaceRepository<
featureFlagMap: FeatureFlagMap,
queryRunner?: QueryRunner,
objectRecordsPermissions?: ObjectRecordsPermissions,
shouldBypassPermissionChecks = false,
) {
super(target, manager, queryRunner);
this.internalContext = internalContext;
this.featureFlagMap = featureFlagMap;
this.objectRecordsPermissions = objectRecordsPermissions;
this.shouldBypassPermissionChecks = shouldBypassPermissionChecks;
}
override createQueryBuilder<U extends T>(
@ -74,6 +76,8 @@ export class WorkspaceRepository<
return new WorkspaceQueryBuilder(
queryBuilder,
this.objectRecordsPermissions,
this.internalContext,
this.shouldBypassPermissionChecks,
);
}
}

View File

@ -15,19 +15,31 @@ export class TwentyORMGlobalManager {
async getRepositoryForWorkspace<T extends ObjectLiteral>(
workspaceId: string,
workspaceEntity: Type<T>,
shouldFailIfMetadataNotFound?: boolean,
options?: {
shouldBypassPermissionChecks?: boolean;
shouldFailIfMetadataNotFound?: boolean;
},
): Promise<WorkspaceRepository<T>>;
async getRepositoryForWorkspace<T extends ObjectLiteral>(
workspaceId: string,
objectMetadataName: string,
shouldFailIfMetadataNotFound?: boolean,
options?: {
shouldBypassPermissionChecks?: boolean;
shouldFailIfMetadataNotFound?: boolean;
},
): Promise<WorkspaceRepository<T>>;
async getRepositoryForWorkspace<T extends ObjectLiteral>(
workspaceId: string,
workspaceEntityOrobjectMetadataName: Type<T> | string,
shouldFailIfMetadataNotFound = true,
options: {
shouldBypassPermissionChecks?: boolean;
shouldFailIfMetadataNotFound?: boolean;
} = {
shouldBypassPermissionChecks: false,
shouldFailIfMetadataNotFound: true,
},
): Promise<WorkspaceRepository<T>> {
let objectMetadataName: string;
@ -42,10 +54,13 @@ export class TwentyORMGlobalManager {
const workspaceDataSource = await this.workspaceDataSourceFactory.create(
workspaceId,
null,
shouldFailIfMetadataNotFound,
options.shouldFailIfMetadataNotFound,
);
const repository = workspaceDataSource.getRepository<T>(objectMetadataName);
const repository = workspaceDataSource.getRepository<T>(
objectMetadataName,
options.shouldBypassPermissionChecks,
);
return repository;
}

View File

@ -30,8 +30,12 @@ export class TwentyORMManager {
async getRepository<T extends ObjectLiteral>(
workspaceEntityOrobjectMetadataName: Type<T> | string,
): Promise<WorkspaceRepository<T>> {
const { workspaceId, workspaceMetadataVersion, userWorkspaceId } =
this.scopedWorkspaceContextFactory.create();
const {
workspaceId,
workspaceMetadataVersion,
userWorkspaceId,
isExecutedByApiKey,
} = this.scopedWorkspaceContextFactory.create();
let objectMetadataName: string;
@ -65,7 +69,13 @@ export class TwentyORMManager {
roleId = userWorkspaceRole?.roleId;
}
return workspaceDataSource.getRepository<T>(objectMetadataName, roleId);
const shouldBypassPermissionChecks = !!isExecutedByApiKey;
return workspaceDataSource.getRepository<T>(
objectMetadataName,
shouldBypassPermissionChecks,
roleId,
);
}
async getDatasource() {