[permissions] Override workspaceDatasource.createQueryBuilder (#12415)
In the frame of https://github.com/twentyhq/core-team-issues/issues/924 - Rename dataSource -> workspaceDataSource when relevant to ease understandability - override workspaceDataSource.createQueryBuilder, because we don't want developers to use it directly since it does not run permission checks at this level. Indeed, we cannot do so because 1) datasources are shared between roles so we would need to re-think its implementation to make that possible, while for now we never call workspaceDatasource.createQueryBuilder in our codebase 2) workspaceEntityManager.createQueryBuilder, that we have overriden with permission checks, then performs a call to workspaceDataSource.createQueryBuilder so that would make two permission checks.
This commit is contained in:
@ -37,7 +37,7 @@ export class ProcessNestedRelationsV2Helper {
|
||||
aggregate = {},
|
||||
limit,
|
||||
authContext,
|
||||
dataSource,
|
||||
workspaceDataSource,
|
||||
roleId,
|
||||
shouldBypassPermissionChecks,
|
||||
}: {
|
||||
@ -50,7 +50,7 @@ export class ProcessNestedRelationsV2Helper {
|
||||
aggregate?: Record<string, AggregationField>;
|
||||
limit: number;
|
||||
authContext: AuthContext;
|
||||
dataSource: WorkspaceDataSource;
|
||||
workspaceDataSource: WorkspaceDataSource;
|
||||
shouldBypassPermissionChecks: boolean;
|
||||
roleId?: string;
|
||||
}): Promise<void> {
|
||||
@ -66,7 +66,7 @@ export class ProcessNestedRelationsV2Helper {
|
||||
aggregate,
|
||||
limit,
|
||||
authContext,
|
||||
dataSource,
|
||||
workspaceDataSource,
|
||||
shouldBypassPermissionChecks,
|
||||
roleId,
|
||||
}),
|
||||
@ -85,7 +85,7 @@ export class ProcessNestedRelationsV2Helper {
|
||||
aggregate,
|
||||
limit,
|
||||
authContext,
|
||||
dataSource,
|
||||
workspaceDataSource,
|
||||
shouldBypassPermissionChecks,
|
||||
roleId,
|
||||
}: {
|
||||
@ -99,7 +99,7 @@ export class ProcessNestedRelationsV2Helper {
|
||||
aggregate: Record<string, AggregationField>;
|
||||
limit: number;
|
||||
authContext: AuthContext;
|
||||
dataSource: WorkspaceDataSource;
|
||||
workspaceDataSource: WorkspaceDataSource;
|
||||
shouldBypassPermissionChecks: boolean;
|
||||
roleId?: string;
|
||||
}): Promise<void> {
|
||||
@ -131,7 +131,7 @@ export class ProcessNestedRelationsV2Helper {
|
||||
sourceFieldName,
|
||||
});
|
||||
|
||||
const targetObjectRepository = dataSource.getRepository(
|
||||
const targetObjectRepository = workspaceDataSource.getRepository(
|
||||
targetObjectMetadata.nameSingular,
|
||||
shouldBypassPermissionChecks,
|
||||
roleId,
|
||||
@ -203,7 +203,7 @@ export class ProcessNestedRelationsV2Helper {
|
||||
aggregate,
|
||||
limit,
|
||||
authContext,
|
||||
dataSource,
|
||||
workspaceDataSource,
|
||||
shouldBypassPermissionChecks,
|
||||
roleId,
|
||||
});
|
||||
|
||||
@ -4,7 +4,6 @@ import { FindOptionsRelations, ObjectLiteral } from 'typeorm';
|
||||
|
||||
import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
|
||||
import { ProcessAggregateHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper';
|
||||
import { ProcessNestedRelationsV2Helper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations-v2.helper';
|
||||
import { AggregationField } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-available-aggregations-from-object-fields.util';
|
||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
@ -16,7 +15,6 @@ import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.
|
||||
export class ProcessNestedRelationsHelper {
|
||||
constructor(
|
||||
private readonly processNestedRelationsV2Helper: ProcessNestedRelationsV2Helper,
|
||||
private readonly processAggregateHelper: ProcessAggregateHelper,
|
||||
) {}
|
||||
|
||||
public async processNestedRelations<T extends ObjectRecord = ObjectRecord>({
|
||||
@ -28,7 +26,7 @@ export class ProcessNestedRelationsHelper {
|
||||
aggregate = {},
|
||||
limit,
|
||||
authContext,
|
||||
dataSource,
|
||||
workspaceDataSource,
|
||||
shouldBypassPermissionChecks,
|
||||
roleId,
|
||||
}: {
|
||||
@ -41,7 +39,7 @@ export class ProcessNestedRelationsHelper {
|
||||
aggregate?: Record<string, AggregationField>;
|
||||
limit: number;
|
||||
authContext: AuthContext;
|
||||
dataSource: WorkspaceDataSource;
|
||||
workspaceDataSource: WorkspaceDataSource;
|
||||
shouldBypassPermissionChecks: boolean;
|
||||
roleId?: string;
|
||||
}): Promise<void> {
|
||||
@ -54,7 +52,7 @@ export class ProcessNestedRelationsHelper {
|
||||
aggregate,
|
||||
limit,
|
||||
authContext,
|
||||
dataSource,
|
||||
workspaceDataSource,
|
||||
shouldBypassPermissionChecks,
|
||||
roleId,
|
||||
});
|
||||
|
||||
@ -41,7 +41,7 @@ import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.
|
||||
export type GraphqlQueryResolverExecutionArgs<Input extends ResolverArgs> = {
|
||||
args: Input;
|
||||
options: WorkspaceQueryRunnerOptions;
|
||||
dataSource: WorkspaceDataSource;
|
||||
workspaceDataSource: WorkspaceDataSource;
|
||||
repository: WorkspaceRepository<ObjectLiteral>;
|
||||
graphqlQueryParser: GraphqlQueryParser;
|
||||
graphqlQuerySelectedFieldsResult: GraphqlQuerySelectedFieldsResult;
|
||||
@ -152,7 +152,7 @@ export abstract class GraphqlQueryBaseResolverService<
|
||||
const graphqlQueryResolverExecutionArgs = {
|
||||
args: computedArgs,
|
||||
options,
|
||||
dataSource: workspaceDataSource,
|
||||
workspaceDataSource,
|
||||
repository,
|
||||
graphqlQueryParser,
|
||||
graphqlQuerySelectedFieldsResult,
|
||||
|
||||
@ -415,7 +415,7 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
|
||||
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
|
||||
limit: QUERY_MAX_RECORDS,
|
||||
authContext: executionArgs.options.authContext,
|
||||
dataSource: executionArgs.dataSource,
|
||||
workspaceDataSource: executionArgs.workspaceDataSource,
|
||||
roleId,
|
||||
shouldBypassPermissionChecks,
|
||||
});
|
||||
|
||||
@ -67,7 +67,7 @@ export class GraphqlQueryCreateOneResolverService extends GraphqlQueryBaseResolv
|
||||
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
|
||||
limit: QUERY_MAX_RECORDS,
|
||||
authContext,
|
||||
dataSource: executionArgs.dataSource,
|
||||
workspaceDataSource: executionArgs.workspaceDataSource,
|
||||
roleId,
|
||||
shouldBypassPermissionChecks: executionArgs.isExecutedByApiKey,
|
||||
});
|
||||
|
||||
@ -69,7 +69,7 @@ export class GraphqlQueryDeleteManyResolverService extends GraphqlQueryBaseResol
|
||||
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
|
||||
limit: QUERY_MAX_RECORDS,
|
||||
authContext,
|
||||
dataSource: executionArgs.dataSource,
|
||||
workspaceDataSource: executionArgs.workspaceDataSource,
|
||||
roleId,
|
||||
shouldBypassPermissionChecks: executionArgs.isExecutedByApiKey,
|
||||
});
|
||||
|
||||
@ -71,7 +71,7 @@ export class GraphqlQueryDeleteOneResolverService extends GraphqlQueryBaseResolv
|
||||
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
|
||||
limit: QUERY_MAX_RECORDS,
|
||||
authContext,
|
||||
dataSource: executionArgs.dataSource,
|
||||
workspaceDataSource: executionArgs.workspaceDataSource,
|
||||
roleId,
|
||||
shouldBypassPermissionChecks: executionArgs.isExecutedByApiKey,
|
||||
});
|
||||
|
||||
@ -67,7 +67,7 @@ export class GraphqlQueryDestroyManyResolverService extends GraphqlQueryBaseReso
|
||||
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
|
||||
limit: QUERY_MAX_RECORDS,
|
||||
authContext,
|
||||
dataSource: executionArgs.dataSource,
|
||||
workspaceDataSource: executionArgs.workspaceDataSource,
|
||||
roleId,
|
||||
shouldBypassPermissionChecks: executionArgs.isExecutedByApiKey,
|
||||
});
|
||||
|
||||
@ -67,7 +67,7 @@ export class GraphqlQueryDestroyOneResolverService extends GraphqlQueryBaseResol
|
||||
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
|
||||
limit: QUERY_MAX_RECORDS,
|
||||
authContext,
|
||||
dataSource: executionArgs.dataSource,
|
||||
workspaceDataSource: executionArgs.workspaceDataSource,
|
||||
roleId,
|
||||
shouldBypassPermissionChecks: executionArgs.isExecutedByApiKey,
|
||||
});
|
||||
|
||||
@ -153,7 +153,7 @@ export class GraphqlQueryFindManyResolverService extends GraphqlQueryBaseResolve
|
||||
aggregate: executionArgs.graphqlQuerySelectedFieldsResult.aggregate,
|
||||
limit: QUERY_MAX_RECORDS,
|
||||
authContext,
|
||||
dataSource: executionArgs.dataSource,
|
||||
workspaceDataSource: executionArgs.workspaceDataSource,
|
||||
roleId,
|
||||
shouldBypassPermissionChecks: executionArgs.isExecutedByApiKey,
|
||||
});
|
||||
|
||||
@ -77,7 +77,7 @@ export class GraphqlQueryFindOneResolverService extends GraphqlQueryBaseResolver
|
||||
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
|
||||
limit: QUERY_MAX_RECORDS,
|
||||
authContext,
|
||||
dataSource: executionArgs.dataSource,
|
||||
workspaceDataSource: executionArgs.workspaceDataSource,
|
||||
roleId,
|
||||
shouldBypassPermissionChecks: executionArgs.isExecutedByApiKey,
|
||||
});
|
||||
|
||||
@ -69,7 +69,7 @@ export class GraphqlQueryRestoreManyResolverService extends GraphqlQueryBaseReso
|
||||
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
|
||||
limit: QUERY_MAX_RECORDS,
|
||||
authContext,
|
||||
dataSource: executionArgs.dataSource,
|
||||
workspaceDataSource: executionArgs.workspaceDataSource,
|
||||
roleId,
|
||||
shouldBypassPermissionChecks: executionArgs.isExecutedByApiKey,
|
||||
});
|
||||
|
||||
@ -71,7 +71,7 @@ export class GraphqlQueryRestoreOneResolverService extends GraphqlQueryBaseResol
|
||||
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
|
||||
limit: QUERY_MAX_RECORDS,
|
||||
authContext,
|
||||
dataSource: executionArgs.dataSource,
|
||||
workspaceDataSource: executionArgs.workspaceDataSource,
|
||||
roleId,
|
||||
shouldBypassPermissionChecks: executionArgs.isExecutedByApiKey,
|
||||
});
|
||||
|
||||
@ -108,7 +108,7 @@ export class GraphqlQueryUpdateManyResolverService extends GraphqlQueryBaseResol
|
||||
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
|
||||
limit: QUERY_MAX_RECORDS,
|
||||
authContext,
|
||||
dataSource: executionArgs.dataSource,
|
||||
workspaceDataSource: executionArgs.workspaceDataSource,
|
||||
roleId,
|
||||
shouldBypassPermissionChecks: executionArgs.isExecutedByApiKey,
|
||||
});
|
||||
|
||||
@ -100,7 +100,7 @@ export class GraphqlQueryUpdateOneResolverService extends GraphqlQueryBaseResolv
|
||||
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
|
||||
limit: QUERY_MAX_RECORDS,
|
||||
authContext,
|
||||
dataSource: executionArgs.dataSource,
|
||||
workspaceDataSource: executionArgs.workspaceDataSource,
|
||||
roleId,
|
||||
shouldBypassPermissionChecks: executionArgs.isExecutedByApiKey,
|
||||
});
|
||||
|
||||
@ -104,7 +104,7 @@ export abstract class RestApiBaseHandler {
|
||||
throw new BadRequestException('Workspace not found');
|
||||
}
|
||||
|
||||
const dataSource =
|
||||
const workspaceDataSource =
|
||||
await this.twentyORMGlobalManager.getDataSourceForWorkspace({
|
||||
workspaceId: workspace.id,
|
||||
shouldFailIfMetadataNotFound: false,
|
||||
@ -125,7 +125,7 @@ export abstract class RestApiBaseHandler {
|
||||
);
|
||||
}
|
||||
|
||||
const shouldBypassPermissionChecks = !!apiKey;
|
||||
const shouldBypassPermissionChecks = isDefined(apiKey);
|
||||
|
||||
const roleId =
|
||||
await this.workspacePermissionsCacheService.getRoleIdFromUserWorkspaceId({
|
||||
@ -133,7 +133,7 @@ export abstract class RestApiBaseHandler {
|
||||
userWorkspaceId,
|
||||
});
|
||||
|
||||
const repository = dataSource.getRepository<ObjectRecord>(
|
||||
const repository = workspaceDataSource.getRepository<ObjectRecord>(
|
||||
objectMetadataNameSingular,
|
||||
shouldBypassPermissionChecks,
|
||||
roleId,
|
||||
@ -142,7 +142,7 @@ export abstract class RestApiBaseHandler {
|
||||
return {
|
||||
objectMetadata,
|
||||
repository,
|
||||
dataSource,
|
||||
workspaceDataSource,
|
||||
objectMetadataItemWithFieldsMaps,
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { isDefined } from 'class-validator';
|
||||
import { ObjectRecordsPermissionsByRoleId } from 'twenty-shared/types';
|
||||
import {
|
||||
DataSource,
|
||||
@ -6,11 +7,13 @@ import {
|
||||
ObjectLiteral,
|
||||
QueryRunner,
|
||||
ReplicationMode,
|
||||
SelectQueryBuilder,
|
||||
} from 'typeorm';
|
||||
|
||||
import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface';
|
||||
import { WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface';
|
||||
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import {
|
||||
PermissionsException,
|
||||
PermissionsExceptionCode,
|
||||
@ -19,6 +22,10 @@ import { WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/wor
|
||||
import { WorkspaceQueryRunner } from 'src/engine/twenty-orm/query-runner/workspace-query-runner';
|
||||
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
|
||||
|
||||
type CreateQueryBuilderOptions = {
|
||||
calledByWorkspaceEntityManager?: boolean;
|
||||
};
|
||||
|
||||
export class WorkspaceDataSource extends DataSource {
|
||||
readonly internalContext: WorkspaceInternalContext;
|
||||
readonly manager: WorkspaceEntityManager;
|
||||
@ -83,6 +90,69 @@ export class WorkspaceDataSource extends DataSource {
|
||||
return queryRunner as any as WorkspaceQueryRunner;
|
||||
}
|
||||
|
||||
override createQueryBuilder<Entity extends ObjectLiteral>(
|
||||
entityClass: EntityTarget<Entity>,
|
||||
alias: string,
|
||||
queryRunner?: QueryRunner,
|
||||
options?: CreateQueryBuilderOptions,
|
||||
): SelectQueryBuilder<Entity>;
|
||||
|
||||
override createQueryBuilder(
|
||||
queryRunner?: QueryRunner,
|
||||
options?: CreateQueryBuilderOptions, // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
): SelectQueryBuilder<any>;
|
||||
|
||||
// Only callable from workspaceEntityManager to guarantee a permission check was run
|
||||
override createQueryBuilder(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
queryRunnerOrEntityClass?: QueryRunner | EntityTarget<any>,
|
||||
aliasOrOptions?: string | CreateQueryBuilderOptions,
|
||||
queryRunner?: QueryRunner,
|
||||
options?: CreateQueryBuilderOptions,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
): SelectQueryBuilder<any> {
|
||||
let calledByWorkspaceEntityManager;
|
||||
|
||||
const isPermissionsV2Enabled =
|
||||
this.featureFlagMap[FeatureFlagKey.IS_PERMISSIONS_V2_ENABLED];
|
||||
|
||||
const isCalledWithEntityTarget =
|
||||
isDefined(aliasOrOptions) && typeof aliasOrOptions === 'string';
|
||||
|
||||
if (isPermissionsV2Enabled) {
|
||||
if (isCalledWithEntityTarget) {
|
||||
calledByWorkspaceEntityManager =
|
||||
options?.calledByWorkspaceEntityManager;
|
||||
} else {
|
||||
calledByWorkspaceEntityManager = (
|
||||
aliasOrOptions as CreateQueryBuilderOptions
|
||||
)?.calledByWorkspaceEntityManager;
|
||||
}
|
||||
|
||||
if (!(calledByWorkspaceEntityManager === true)) {
|
||||
throw new PermissionsException(
|
||||
'Method not allowed because permissions are not implemented at datasource level.',
|
||||
PermissionsExceptionCode.METHOD_NOT_ALLOWED,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (isCalledWithEntityTarget) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const entityClass = queryRunnerOrEntityClass as EntityTarget<any>;
|
||||
|
||||
return super.createQueryBuilder(
|
||||
entityClass,
|
||||
aliasOrOptions as string,
|
||||
queryRunner,
|
||||
);
|
||||
} else {
|
||||
const queryRunner = queryRunnerOrEntityClass as QueryRunner;
|
||||
|
||||
return super.createQueryBuilder(queryRunner);
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
override query<T = any>(
|
||||
query: string,
|
||||
|
||||
@ -145,14 +145,20 @@ export class WorkspaceEntityManager extends EntityManager {
|
||||
let queryBuilder: SelectQueryBuilder<Entity>;
|
||||
|
||||
if (alias) {
|
||||
queryBuilder = super.createQueryBuilder(
|
||||
queryBuilder = this.connection.createQueryBuilder(
|
||||
entityClassOrQueryRunner as EntityTarget<Entity>,
|
||||
alias as string,
|
||||
queryRunner as QueryRunner | undefined,
|
||||
{
|
||||
calledByWorkspaceEntityManager: true,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
queryBuilder = super.createQueryBuilder(
|
||||
queryBuilder = this.connection.createQueryBuilder(
|
||||
entityClassOrQueryRunner as QueryRunner,
|
||||
{
|
||||
calledByWorkspaceEntityManager: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -912,7 +918,10 @@ export class WorkspaceEntityManager extends EntityManager {
|
||||
permissionOptions,
|
||||
)
|
||||
.setFindOptions(options || {})
|
||||
.getExists();
|
||||
.select('1')
|
||||
.limit(1)
|
||||
.getRawOne()
|
||||
.then((result) => isDefined(result));
|
||||
}
|
||||
|
||||
override existsBy<Entity extends ObjectLiteral>(
|
||||
@ -929,7 +938,10 @@ export class WorkspaceEntityManager extends EntityManager {
|
||||
permissionOptions,
|
||||
)
|
||||
.setFindOptions({ where })
|
||||
.getExists();
|
||||
.select('1')
|
||||
.limit(1)
|
||||
.getRawOne()
|
||||
.then((result) => isDefined(result));
|
||||
}
|
||||
|
||||
override count<Entity extends ObjectLiteral>(
|
||||
|
||||
@ -4,6 +4,10 @@ import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity
|
||||
|
||||
import { WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface';
|
||||
|
||||
import {
|
||||
PermissionsException,
|
||||
PermissionsExceptionCode,
|
||||
} from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||
import { validateQueryIsPermittedOrThrow } from 'src/engine/twenty-orm/repository/permissions.utils';
|
||||
import { WorkspaceDeleteQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-delete-query-builder';
|
||||
import { WorkspaceSoftDeleteQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-soft-delete-query-builder';
|
||||
@ -83,9 +87,10 @@ export class WorkspaceSelectQueryBuilder<
|
||||
}
|
||||
|
||||
override getExists(): Promise<boolean> {
|
||||
this.validatePermissions();
|
||||
|
||||
return super.getExists();
|
||||
throw new PermissionsException(
|
||||
'getExists is not supported because it calls dataSource.createQueryBuilder()',
|
||||
PermissionsExceptionCode.METHOD_NOT_ALLOWED,
|
||||
);
|
||||
}
|
||||
|
||||
override getManyAndCount(): Promise<[T[], number]> {
|
||||
@ -148,6 +153,13 @@ export class WorkspaceSelectQueryBuilder<
|
||||
);
|
||||
}
|
||||
|
||||
override executeExistsQuery(): Promise<boolean> {
|
||||
throw new PermissionsException(
|
||||
'executeExistsQuery is not supported because it calls dataSource.createQueryBuilder()',
|
||||
PermissionsExceptionCode.METHOD_NOT_ALLOWED,
|
||||
);
|
||||
}
|
||||
|
||||
private validatePermissions(): void {
|
||||
validateQueryIsPermittedOrThrow(
|
||||
this.expressionMap,
|
||||
|
||||
Reference in New Issue
Block a user