[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:
Marie
2025-06-02 18:37:23 +02:00
committed by GitHub
parent 5ea3a3c887
commit e1a7fa3e5d
19 changed files with 129 additions and 37 deletions

View File

@ -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,

View File

@ -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>(

View File

@ -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,