-
Notifications
You must be signed in to change notification settings - Fork 3.1k
[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
base: main
Are you sure you want to change the base?
Conversation
🚀 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, { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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>( |
There was a problem hiding this comment.
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
There was a problem hiding this 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
.createQueryBuilder(undefined, undefined, undefined, { | ||
shouldBypassPermissionChecks: true, | ||
}) |
There was a problem hiding this comment.
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
.createQueryBuilder(undefined, undefined, undefined, { | ||
shouldBypassPermissionChecks: true, | ||
}) |
There was a problem hiding this comment.
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', |
There was a problem hiding this comment.
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.
operationType: 'update', | |
operationType: 'create', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is it expected?
if (options?.roleId) { | ||
const dataSource = this.connection as WorkspaceDataSource; | ||
const objectPermissionsByRoleId = dataSource.permissionsPerRoleId; |
There was a problem hiding this comment.
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> { |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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
const formatedEntity = await this.formatData(entity); | |
const formattedEntity = await this.formatData(entity); |
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); | ||
}); |
There was a problem hiding this comment.
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.
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'); | |
} | |
}); |
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); | ||
}); |
There was a problem hiding this comment.
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
There was a problem hiding this 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, { |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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) => { |
There was a problem hiding this comment.
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 = |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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 ?
There was a problem hiding this comment.
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>( |
There was a problem hiding this comment.
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', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is it expected?
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