Skip to content

[permissions] Add permissions check layer in entityManager #11818

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from

Conversation

ijreilly
Copy link
Collaborator

@ijreilly ijreilly commented Apr 30, 2025

First and main step of twentyhq/core-team-issues#747

We are implementing a permission check layer in our custom WorkspaceEntityManager by overriding all the db-executing methods (this PR only overrides some as a POC, the rest will be done in the next PR).
Our custom repositories call entity managers under the hood to interact with the db so this solves the repositories case too.
This is still behind the feature flag IsPermissionsV2Enabled.

In the next PR

  • finish overriding all the methods required in WorkspaceEntityManager
  • add tests

Copy link
Contributor

github-actions bot commented Apr 30, 2025

🚀 Preview Environment Ready!

Your preview environment is available at: http://bore.pub:47835

This environment will automatically shut down when the PR is closed or after 5 hours.

schemaName: string,
) => {
await entityManager
.createQueryBuilder()
.createQueryBuilder(undefined, undefined, undefined, {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks a bit ugly but needed if we want to override EntityManager's createQueryBuilder, + is just in the seeds

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be an object as an input to avoid that

this.featureFlagMap = featureFlagMap;
this.featureFlagMapVersion = featureFlagMapVersion;
// Recreate manager after internalContext has been initialized
this.manager = this.createEntityManager();
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

order matters, otherwise this.featureFlagMap is empty

@@ -68,6 +81,88 @@ export class WorkspaceEntityManager extends EntityManager {
return newRepository;
}

override createQueryBuilder<Entity extends ObjectLiteral>(
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is currently only used in seeds

@ijreilly ijreilly changed the title Perm repo [permissions] Add permissions check layer in entityManager May 2, 2025
@ijreilly ijreilly marked this pull request as ready for review May 2, 2025 12:54
Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Summary

This PR implements a comprehensive permission check layer in the WorkspaceEntityManager by overriding database-executing methods. Here are the key changes:

  • Replaced TypeORM's EntityManager with custom WorkspaceEntityManager across all database operations to enforce permission checks
  • Added shouldBypassPermissionChecks flag to allow seeding operations to bypass permission validation
  • Implemented permission validation utilities and query builders with proper checks before executing operations
  • Feature is controlled by IsPermissionsV2Enabled flag for gradual rollout

Key points to review:

  • Permission validation is only implemented for some methods as proof of concept, with more to come in next PR
  • Tests are deferred to a future PR which could be risky for such a fundamental change
  • Raw SQL queries in some repositories bypass the new permission layer
  • Transaction management changes could impact data consistency in some services

The PR appears to be incorrectly linked to issue #747 about probability picker UI, as this is a backend permissions infrastructure change.

59 file(s) reviewed, 8 comment(s)
Edit PR Review Bot Settings | Greptile

Comment on lines +21 to +23
.createQueryBuilder(undefined, undefined, undefined, {
shouldBypassPermissionChecks: true,
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: createQueryBuilder parameters are undefined but options are provided - consider documenting why these parameters are undefined or remove them if not needed

Comment on lines +17 to +19
.createQueryBuilder(undefined, undefined, undefined, {
shouldBypassPermissionChecks: true,
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: createQueryBuilder parameters are undefined but used in options object - consider documenting why these parameters are undefined or remove them if not needed


validateOperationIsPermittedOrThrow({
entityName: this.extractTargetNameSingularFromEntityTarget(target),
operationType: 'update',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: operationType is hardcoded as 'update' for both insert and upsert operations. This is incorrect and could lead to wrong permission checks.

Suggested change
operationType: 'update',
operationType: 'create',

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it expected?

Comment on lines +119 to +121
if (options?.roleId) {
const dataSource = this.connection as WorkspaceDataSource;
const objectPermissionsByRoleId = dataSource.permissionsPerRoleId;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Redundant type casting of this.connection to WorkspaceDataSource since it's already declared as that type on line 26

} = {
shouldBypassPermissionChecks: false,
},
): SelectQueryBuilder<Entity> | WorkspaceSelectQueryBuilder<Entity> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Return type union is unnecessary since WorkspaceSelectQueryBuilder extends SelectQueryBuilder

@@ -456,12 +459,15 @@ export class WorkspaceRepository<
*/
override async insert(
entity: QueryDeepPartialEntity<T> | QueryDeepPartialEntity<T>[],
entityManager?: EntityManager,
entityManager?: WorkspaceEntityManager,
): Promise<InsertResult> {
const manager = entityManager || this.manager;

const formatedEntity = await this.formatData(entity);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

syntax: inconsistent spelling: 'formatedEntity' vs 'formattedResult' used later

Suggested change
const formatedEntity = await this.formatData(entity);
const formattedEntity = await this.formatData(entity);

Comment on lines +54 to +73
describe('permissions V2 enabled', () => {
beforeAll(async () => {
const enablePermissionsQuery = updateFeatureFlagFactory(
SEED_APPLE_WORKSPACE_ID,
'IsPermissionsV2Enabled',
true,
);

await makeGraphqlAPIRequest(enablePermissionsQuery);
});

afterAll(async () => {
const disablePermissionsQuery = updateFeatureFlagFactory(
SEED_APPLE_WORKSPACE_ID,
'IsPermissionsV2Enabled',
false,
);

await makeGraphqlAPIRequest(disablePermissionsQuery);
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Consider adding error handling for feature flag updates in beforeAll/afterAll. If these fail, subsequent tests could run in an incorrect state.

Suggested change
describe('permissions V2 enabled', () => {
beforeAll(async () => {
const enablePermissionsQuery = updateFeatureFlagFactory(
SEED_APPLE_WORKSPACE_ID,
'IsPermissionsV2Enabled',
true,
);
await makeGraphqlAPIRequest(enablePermissionsQuery);
});
afterAll(async () => {
const disablePermissionsQuery = updateFeatureFlagFactory(
SEED_APPLE_WORKSPACE_ID,
'IsPermissionsV2Enabled',
false,
);
await makeGraphqlAPIRequest(disablePermissionsQuery);
});
describe('permissions V2 enabled', () => {
beforeAll(async () => {
const enablePermissionsQuery = updateFeatureFlagFactory(
SEED_APPLE_WORKSPACE_ID,
'IsPermissionsV2Enabled',
true,
);
const response = await makeGraphqlAPIRequest(enablePermissionsQuery);
if (!response.body.data) {
throw new Error('Failed to enable permissions V2 feature flag');
}
});
afterAll(async () => {
const disablePermissionsQuery = updateFeatureFlagFactory(
SEED_APPLE_WORKSPACE_ID,
'IsPermissionsV2Enabled',
false,
);
const response = await makeGraphqlAPIRequest(disablePermissionsQuery);
if (!response.body.data) {
throw new Error('Failed to disable permissions V2 feature flag');
}
});

Comment on lines +87 to +93
expect(response.body.data).toStrictEqual({ createPerson: null });
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toBe(
PermissionsExceptionMessage.PERMISSION_DENIED,
);
expect(response.body.errors[0].extensions.code).toBe(ErrorCode.FORBIDDEN);
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Test could be more specific by checking error array length is exactly 1 to ensure no unexpected additional errors

Copy link
Member

@Weiko Weiko left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great!! Good job @ijreilly 👏

schemaName: string,
) => {
await entityManager
.createQueryBuilder()
.createQueryBuilder(undefined, undefined, undefined, {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be an object as an input to avoid that

mode = 'master' as ReplicationMode,
): WorkspaceQueryRunner {
const queryRunner = this.driver.createQueryRunner(mode);
const manager = this.createEntityManager(queryRunner);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we use this.manager here?

@@ -28,7 +28,6 @@ export class FieldMetadataRelatedRecordsService {
public async updateRelatedViewGroups(
oldFieldMetadata: FieldMetadataEntity,
newFieldMetadata: FieldMetadataEntity,
transactionManager?: EntityManager,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we want to remove the transactionManager?

import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource';
import { validateOperationIsPermittedOrThrow } from 'src/engine/twenty-orm/repository/permissions.utils';
import { WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder';
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';

export class WorkspaceEntityManager extends EntityManager {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just realised this but could we rename the file accordingly?

@@ -103,202 +104,208 @@ export class GoogleAPIsService {

const scopes = getGoogleApisOauthScopes();

await workspaceDataSource.transaction(async (manager: EntityManager) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hard to review this one but I guess this is mostly indentation?

@@ -60,12 +61,13 @@ export class SeederService {
const schemaName =
this.workspaceDataSourceService.getSchemaName(workspaceId);

const workspaceDataSource =
const workspaceDataSource: DataSource =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not your fault and note for myself but I've just realised that this one is very misleading since it returns the main datasource from TypeORMService (which is not the workspace datasource (also why your type is DataSource here)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know, initially I had left two comments around workspaceDataSource and the below entityManager to raise attention around that, but thought that was maybe too much and added explicit types instead. wdyt ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be fine for now but I think our next goal should be:

  • Removing DataSource table, move schema name to Workspace table and we don't need the rest.
  • connectedToWorkspaceDataSourceAndReturnMetadata should be removed, this is only called during seeding or internal code and it's actually returning the mainDataSource as stated above. We should use typeorm service in those places directly and have a getMainDataSource() instead of connectToDataSource()

return queryRunner as any as WorkspaceQueryRunner;
}

override transaction<T>(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this override needed? Looks like the implementation from typeorm already with no particular changes or maybe I'm missing something?


validateOperationIsPermittedOrThrow({
entityName: this.extractTargetNameSingularFromEntityTarget(target),
operationType: 'update',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it expected?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants