-
Notifications
You must be signed in to change notification settings - Fork 3.8k
[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
Conversation
🚀 Preview Environment Ready! Your preview environment is available at: http://bore.pub:17301 This environment will automatically shut down when the PR is closed or after 5 hours. |
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
packages/twenty-server/src/database/typeorm-seeds/workspace/message-channels.ts
Show resolved
Hide resolved
packages/twenty-server/src/database/typeorm-seeds/workspace/connected-account.ts
Show resolved
Hide resolved
|
||
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?
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.
oops no, this should not be hardcoded indeed ! (didn't matter too much here as insert and update are behind the same permission gate, canUpdate, but definitely required for what's next)
packages/twenty-server/src/engine/twenty-orm/entity-manager/entity.manager.ts
Outdated
Show resolved
Hide resolved
packages/twenty-server/src/engine/twenty-orm/entity-manager/entity.manager.ts
Outdated
Show resolved
Hide resolved
packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts
Outdated
Show resolved
Hide resolved
.../suites/object-records-permissions/create-one-object-records-permissions.integration-spec.ts
Show resolved
Hide resolved
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 👏
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?
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 think we need to pass our queryRunner to keep the same across different operations led by different managers
...rc/engine/metadata-modules/field-metadata/services/field-metadata-related-records.service.ts
Show resolved
Hide resolved
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?
packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.ts
Show resolved
Hide resolved
@@ -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?
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.
Can't remember why i did it in the first place indeed, doesn't look necessary yes, removing it !
|
||
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?
}, | ||
transactionManager, | ||
); | ||
const existingCompanies = await companyRepository.find({ |
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.
@@ -14,7 +12,6 @@ export class BlocklistRepository { | |||
public async getById( | |||
id: string, | |||
workspaceId: string, | |||
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.
checked - was never used
@@ -45,7 +45,6 @@ export class CreateCompanyAndContactService { | |||
contactsToCreate: Contact[], | |||
workspaceId: string, | |||
source: FieldActorSource, | |||
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.
checked - was not used
@@ -74,7 +74,6 @@ export class CreateContactService { | |||
public async createPeople( | |||
contactsToCreate: ContactToCreate[], | |||
workspaceId: string, | |||
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.
checked - was not used
@@ -159,7 +157,6 @@ export class TimelineActivityRepository { | |||
linkedObjectMetadataId: string | undefined; | |||
}[], | |||
workspaceId: string, | |||
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.
checked - was not used
https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/modules/timeline/repositiories/timeline-activity.repository.ts#L121
and
https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/listeners/calendar-event-participant.listener.ts#L59
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.
LGTM
@@ -144,7 +144,7 @@ export class GraphqlQueryFindManyResolverService extends GraphqlQueryBaseResolve | |||
} | |||
|
|||
const parentObjectRecordsAggregatedValues = | |||
await aggregateQueryBuilder.getRawOne(); | |||
await aggregateQueryBuilder.getRawOne<any>(); |
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.
<Record<string, any>>
might be a little bit more accurate here but that's probably the best we can do in terms of typing for now
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.
Actually I've just realised you have this typing error everywhere, maybe it's better to implement a default type in the class itself (see my other comment)
@@ -124,6 +124,7 @@ export class WorkspaceDataSourceService { | |||
workspaceId: string, | |||
transactionManager?: EntityManager, | |||
): Promise<any> { | |||
// TODO |
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 more explicit here if you want to leave a TODO?
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.
oops, that was just something for me to think of but i will remove it for now
return super.getRawOne(); | ||
} | ||
|
||
override getRawMany<U>(): Promise<U[]> { |
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.
override getRawMany<U>(): Promise<U[]> { | |
override getRawMany<U = any>(): Promise<U[]> { |
to avoid having to pass any everywhere anyway (or casting)
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.
yes right ! actually that's what they do in typeORM's SelectQueryBuilder. thanks
Suspect IssuesThis pull request was deployed and Sentry observed the following issues:
Did you find this useful? React with a 👍 or 👎 |
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