diff --git a/packages/twenty-server/@types/jest.d.ts b/packages/twenty-server/@types/jest.d.ts index d74d9502d..4994338de 100644 --- a/packages/twenty-server/@types/jest.d.ts +++ b/packages/twenty-server/@types/jest.d.ts @@ -9,6 +9,7 @@ declare module '@jest/types' { INVALID_ACCESS_TOKEN: string; MEMBER_ACCESS_TOKEN: string; GUEST_ACCESS_TOKEN: string; + API_KEY_ACCESS_TOKEN: string; } } } @@ -20,6 +21,7 @@ declare global { const INVALID_ACCESS_TOKEN: string; const MEMBER_ACCESS_TOKEN: string; const GUEST_ACCESS_TOKEN: string; + const API_KEY_ACCESS_TOKEN: string; } export {}; diff --git a/packages/twenty-server/jest-integration.config.ts b/packages/twenty-server/jest-integration.config.ts index a8c7f9b39..0a8cf6e93 100644 --- a/packages/twenty-server/jest-integration.config.ts +++ b/packages/twenty-server/jest-integration.config.ts @@ -76,6 +76,8 @@ const jestConfig: JestConfigWithTsJest = { 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC0zOTU3LTQ5MDgtOWMzNi0yOTI5YTIzZjgzNTciLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtNzdkNS00Y2I2LWI2MGEtZjRhODM1YTg1ZDYxIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMzk1Ny00OTA4LTljMzYtMjkyOWEyM2Y4MzUzIiwiaWF0IjoxNzM5NDU5NTcwLCJleHAiOjMzMjk3MDU5NTcwfQ.Er7EEU4IP4YlGN79jCLR_6sUBqBfKx2M3G_qGiDpPRo', GUEST_ACCESS_TOKEN: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC03MTY5LTQyY2YtYmM0Ny0xY2ZlZjE1MjY0YjgiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtMTU1My00NWM2LWEwMjgtNWE5MDY0Y2NlMDdmIiwidXNlcldvcmtzcGFjZUlkIjoiMjAyMDIwMjAtNzE2OS00MmNmLWJjNDctMWNmZWYxNTI2NGIxIiwiaWF0IjoxNzM5ODg4NDcwLCJleHAiOjMzMjk3NDg4NDcwfQ.0NEu-AWGv3l77rs-56Z5Gt0UTU7HDl6qUTHUcMWNrCc', + API_KEY_ACCESS_TOKEN: + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC0xYzI1LTRkMDItYmYyNS02YWVjY2Y3ZWE0MTkiLCJ0eXBlIjoiQVBJX0tFWSIsIndvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWMyNS00ZDAyLWJmMjUtNmFlY2NmN2VhNDE5IiwiaWF0IjoxNzQ0OTgzNzUwLCJleHAiOjQ4OTg1ODM2OTMsImp0aSI6IjIwMjAyMDIwLWY0MDEtNGQ4YS1hNzMxLTY0ZDAwN2MyN2JhZCJ9.4xkkwz_uu2xzs_V8hJSaM15fGziT5zS3vq2lM48OHr0', }, }; diff --git a/packages/twenty-server/src/database/commands/data-seed-dev-workspace.command.ts b/packages/twenty-server/src/database/commands/data-seed-dev-workspace.command.ts index b3832fadb..1f724a703 100644 --- a/packages/twenty-server/src/database/commands/data-seed-dev-workspace.command.ts +++ b/packages/twenty-server/src/database/commands/data-seed-dev-workspace.command.ts @@ -12,6 +12,7 @@ import { getDevSeedCompanyCustomFields, getDevSeedPeopleCustomFields, } from 'src/database/typeorm-seeds/metadata/fieldsMetadata'; +import { seedApiKey } from 'src/database/typeorm-seeds/workspace/api-key'; import { seedCalendarChannels } from 'src/database/typeorm-seeds/workspace/calendar-channel'; import { seedCalendarChannelEventAssociations } from 'src/database/typeorm-seeds/workspace/calendar-channel-event-association'; import { seedCalendarEventParticipants } from 'src/database/typeorm-seeds/workspace/calendar-event-participants'; @@ -184,6 +185,7 @@ export class DataSeedWorkspaceCommand extends CommandRunner { ); if (dataSourceMetadata.workspaceId === SEED_APPLE_WORKSPACE_ID) { + await seedApiKey(entityManager, dataSourceMetadata.schema); await seedMessageThread(entityManager, dataSourceMetadata.schema); await seedConnectedAccount(entityManager, dataSourceMetadata.schema); diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/0-43/0-43-add-tasks-assigned-to-me-view.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/0-43/0-43-add-tasks-assigned-to-me-view.command.ts index bce4367b3..5ef72d3ce 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/0-43/0-43-add-tasks-assigned-to-me-view.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/0-43/0-43-add-tasks-assigned-to-me-view.command.ts @@ -88,7 +88,6 @@ export class AddTasksAssignedToMeViewCommand extends ActiveOrSuspendedWorkspaces await this.twentyORMGlobalManager.getRepositoryForWorkspace( workspaceId, 'view', - false, ); const existingView = await viewRepository.findOne({ @@ -126,7 +125,6 @@ export class AddTasksAssignedToMeViewCommand extends ActiveOrSuspendedWorkspaces await this.twentyORMGlobalManager.getRepositoryForWorkspace( workspaceId, 'viewField', - false, ); const viewFields = viewDefinition.fields.map((field) => ({ @@ -145,7 +143,6 @@ export class AddTasksAssignedToMeViewCommand extends ActiveOrSuspendedWorkspaces await this.twentyORMGlobalManager.getRepositoryForWorkspace( workspaceId, 'viewFilter', - false, ); const viewFilters = viewDefinition.filters.map((filter) => ({ @@ -202,7 +199,6 @@ export class AddTasksAssignedToMeViewCommand extends ActiveOrSuspendedWorkspaces await this.twentyORMGlobalManager.getRepositoryForWorkspace( workspaceId, 'viewGroup', - false, ); await viewGroupRepository.insert(viewGroups); diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/0-43/0-43-update-default-view-record-opening-on-workflow-objects.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/0-43/0-43-update-default-view-record-opening-on-workflow-objects.command.ts index f52680d67..f81757273 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/0-43/0-43-update-default-view-record-opening-on-workflow-objects.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/0-43/0-43-update-default-view-record-opening-on-workflow-objects.command.ts @@ -81,7 +81,9 @@ export class UpdateDefaultViewRecordOpeningOnWorkflowObjectsCommand extends Acti await this.twentyORMGlobalManager.getRepositoryForWorkspace( workspaceId, 'view', - failOnMetadataCacheMiss, + { + shouldFailIfMetadataNotFound: failOnMetadataCacheMiss, + }, ); await viewRepository.update( diff --git a/packages/twenty-server/src/database/typeorm-seeds/workspace/api-key.ts b/packages/twenty-server/src/database/typeorm-seeds/workspace/api-key.ts new file mode 100644 index 000000000..8176b240f --- /dev/null +++ b/packages/twenty-server/src/database/typeorm-seeds/workspace/api-key.ts @@ -0,0 +1,26 @@ +import { EntityManager } from 'typeorm'; + +const tableName = 'apiKey'; + +const API_KEY_ID = '20202020-f401-4d8a-a731-64d007c27bad'; + +export const seedApiKey = async ( + entityManager: EntityManager, + schemaName: string, +) => { + await entityManager + .createQueryBuilder() + .insert() + .into(`${schemaName}.${tableName}`, ['id', 'name', 'expiresAt']) + .orIgnore() + .values([ + { + id: API_KEY_ID, + name: 'My api key', + expiresAt: new Date( + new Date().getTime() + 1000 * 60 * 60 * 24 * 365 * 100, // In 100 years + ), + }, + ]) + .execute(); +}; diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations-v2.helper.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations-v2.helper.ts index 81881fd26..4e05631a3 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations-v2.helper.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations-v2.helper.ts @@ -42,6 +42,7 @@ export class ProcessNestedRelationsV2Helper { authContext, dataSource, roleId, + shouldBypassPermissionChecks, }: { objectMetadataMaps: ObjectMetadataMaps; parentObjectMetadataItem: ObjectMetadataItemWithFieldMaps; @@ -52,6 +53,7 @@ export class ProcessNestedRelationsV2Helper { limit: number; authContext: AuthContext; dataSource: WorkspaceDataSource; + shouldBypassPermissionChecks: boolean; roleId?: string; }): Promise { const processRelationTasks = Object.entries(relations).map( @@ -67,6 +69,7 @@ export class ProcessNestedRelationsV2Helper { limit, authContext, dataSource, + shouldBypassPermissionChecks, roleId, }), ); @@ -85,6 +88,7 @@ export class ProcessNestedRelationsV2Helper { limit, authContext, dataSource, + shouldBypassPermissionChecks, roleId, }: { objectMetadataMaps: ObjectMetadataMaps; @@ -97,6 +101,7 @@ export class ProcessNestedRelationsV2Helper { limit: number; authContext: AuthContext; dataSource: WorkspaceDataSource; + shouldBypassPermissionChecks: boolean; roleId?: string; }): Promise { const sourceFieldMetadata = @@ -129,6 +134,7 @@ export class ProcessNestedRelationsV2Helper { const targetObjectRepository = dataSource.getRepository( targetObjectMetadata.nameSingular, + shouldBypassPermissionChecks, roleId, ); @@ -199,6 +205,8 @@ export class ProcessNestedRelationsV2Helper { limit, authContext, dataSource, + shouldBypassPermissionChecks, + roleId, }); } } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper.ts index 4fc89db28..cc79a8ea9 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper.ts @@ -46,6 +46,7 @@ export class ProcessNestedRelationsHelper { authContext, dataSource, isNewRelationEnabled, + shouldBypassPermissionChecks, roleId, }: { objectMetadataMaps: ObjectMetadataMaps; @@ -58,6 +59,7 @@ export class ProcessNestedRelationsHelper { authContext: AuthContext; dataSource: WorkspaceDataSource; isNewRelationEnabled: boolean; + shouldBypassPermissionChecks: boolean; roleId?: string; }): Promise { if (isNewRelationEnabled) { @@ -71,6 +73,7 @@ export class ProcessNestedRelationsHelper { limit, authContext, dataSource, + shouldBypassPermissionChecks, roleId, }); } @@ -89,6 +92,7 @@ export class ProcessNestedRelationsHelper { authContext, dataSource, isNewRelationEnabled, + shouldBypassPermissionChecks, roleId, }), ); @@ -108,6 +112,7 @@ export class ProcessNestedRelationsHelper { authContext, dataSource, isNewRelationEnabled, + shouldBypassPermissionChecks, roleId, }: { objectMetadataMaps: ObjectMetadataMaps; @@ -118,9 +123,10 @@ export class ProcessNestedRelationsHelper { nestedRelations: any; aggregate: Record; limit: number; - authContext: any; + authContext: AuthContext; dataSource: DataSource; isNewRelationEnabled: boolean; + shouldBypassPermissionChecks: boolean; roleId?: string; }): Promise { const relationFieldMetadata = @@ -148,6 +154,7 @@ export class ProcessNestedRelationsHelper { authContext, dataSource, isNewRelationEnabled, + shouldBypassPermissionChecks, roleId, }); } @@ -164,6 +171,7 @@ export class ProcessNestedRelationsHelper { authContext, dataSource, isNewRelationEnabled, + shouldBypassPermissionChecks, roleId, }: { objectMetadataMaps: ObjectMetadataMaps; @@ -177,6 +185,7 @@ export class ProcessNestedRelationsHelper { authContext: AuthContext; dataSource: WorkspaceDataSource; isNewRelationEnabled: boolean; + shouldBypassPermissionChecks: boolean; roleId?: string; }): Promise { const { inverseRelationName, referenceObjectMetadata } = @@ -188,6 +197,7 @@ export class ProcessNestedRelationsHelper { const relationRepository = dataSource.getRepository( referenceObjectMetadata.nameSingular, + shouldBypassPermissionChecks, roleId, ); @@ -248,6 +258,8 @@ export class ProcessNestedRelationsHelper { authContext, dataSource, isNewRelationEnabled, + shouldBypassPermissionChecks, + roleId, }); } } @@ -264,6 +276,7 @@ export class ProcessNestedRelationsHelper { authContext, dataSource, isNewRelationEnabled, + shouldBypassPermissionChecks, roleId, }: { objectMetadataMaps: ObjectMetadataMaps; @@ -277,6 +290,7 @@ export class ProcessNestedRelationsHelper { authContext: any; dataSource: WorkspaceDataSource; isNewRelationEnabled: boolean; + shouldBypassPermissionChecks: boolean; roleId?: string; }): Promise { const { referenceObjectMetadata } = this.getRelationMetadata({ @@ -287,6 +301,7 @@ export class ProcessNestedRelationsHelper { const relationRepository = dataSource.getRepository( referenceObjectMetadata.nameSingular, + shouldBypassPermissionChecks, roleId, ); @@ -346,6 +361,8 @@ export class ProcessNestedRelationsHelper { authContext, dataSource, isNewRelationEnabled, + shouldBypassPermissionChecks, + roleId, }); } } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service.ts index fca01e495..285bba5b2 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/interfaces/base-resolver-service.ts @@ -45,6 +45,7 @@ export type GraphqlQueryResolverExecutionArgs = { repository: WorkspaceRepository; graphqlQueryParser: GraphqlQueryParser; graphqlQuerySelectedFieldsResult: GraphqlQuerySelectedFieldsResult; + isExecutedByApiKey: boolean; roleId?: string; }; @@ -123,8 +124,12 @@ export abstract class GraphqlQueryBaseResolverService< workspaceId: authContext.workspace.id, }); + const executedByApiKey = isDefined(authContext.apiKey); + const shouldBypassPermissionChecks = executedByApiKey; + const repository = dataSource.getRepository( objectMetadataItemWithFieldMaps.nameSingular, + shouldBypassPermissionChecks, roleId, ); @@ -150,6 +155,7 @@ export abstract class GraphqlQueryBaseResolverService< repository, graphqlQueryParser, graphqlQuerySelectedFieldsResult, + isExecutedByApiKey: executedByApiKey, roleId, }; diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service.ts index 83e5015b6..67503b2b1 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service.ts @@ -53,12 +53,15 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol objectMetadataItemWithFieldMaps, ); + const shouldBypassPermissionChecks = executionArgs.isExecutedByApiKey; + await this.processNestedRelationsIfNeeded( executionArgs, upsertedRecords, objectMetadataItemWithFieldMaps, objectMetadataMaps, featureFlagsMap, + shouldBypassPermissionChecks, roleId, ); @@ -329,6 +332,7 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps, objectMetadataMaps: ObjectMetadataMaps, featureFlagsMap: Record, + shouldBypassPermissionChecks: boolean, roleId?: string, ): Promise { if (!executionArgs.graphqlQuerySelectedFieldsResult.relations) { @@ -346,6 +350,7 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol isNewRelationEnabled: featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled], roleId, + shouldBypassPermissionChecks, }); } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-one-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-one-resolver.service.ts index 52476579e..f875dbff2 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-one-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-one-resolver.service.ts @@ -74,6 +74,7 @@ export class GraphqlQueryCreateOneResolverService extends GraphqlQueryBaseResolv isNewRelationEnabled: featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled], roleId, + shouldBypassPermissionChecks: executionArgs.isExecutedByApiKey, }); } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-many-resolver.service.ts index e99a50f18..48eb51603 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-many-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-many-resolver.service.ts @@ -75,6 +75,7 @@ export class GraphqlQueryDeleteManyResolverService extends GraphqlQueryBaseResol isNewRelationEnabled: featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled], roleId, + shouldBypassPermissionChecks: executionArgs.isExecutedByApiKey, }); } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-one-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-one-resolver.service.ts index 992912742..a681949f3 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-one-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-delete-one-resolver.service.ts @@ -77,6 +77,7 @@ export class GraphqlQueryDeleteOneResolverService extends GraphqlQueryBaseResolv isNewRelationEnabled: featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled], roleId, + shouldBypassPermissionChecks: executionArgs.isExecutedByApiKey, }); } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-many-resolver.service.ts index 8919e68c1..f02b31bd3 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-many-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-many-resolver.service.ts @@ -73,6 +73,7 @@ export class GraphqlQueryDestroyManyResolverService extends GraphqlQueryBaseReso isNewRelationEnabled: featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled], roleId, + shouldBypassPermissionChecks: executionArgs.isExecutedByApiKey, }); } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service.ts index 22575a62f..3db4c0a82 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service.ts @@ -73,6 +73,7 @@ export class GraphqlQueryDestroyOneResolverService extends GraphqlQueryBaseResol isNewRelationEnabled: featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled], roleId, + shouldBypassPermissionChecks: executionArgs.isExecutedByApiKey, }); } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service.ts index 0c62f5ea9..e78e33a94 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service.ts @@ -160,6 +160,7 @@ export class GraphqlQueryFindManyResolverService extends GraphqlQueryBaseResolve isNewRelationEnabled: featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled], roleId, + shouldBypassPermissionChecks: executionArgs.isExecutedByApiKey, }); } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service.ts index b124e6d76..6dec1bd57 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service.ts @@ -83,6 +83,7 @@ export class GraphqlQueryFindOneResolverService extends GraphqlQueryBaseResolver isNewRelationEnabled: featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled], roleId, + shouldBypassPermissionChecks: executionArgs.isExecutedByApiKey, }); } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-many-resolver.service.ts index df18d4e37..3491e0aec 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-many-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-many-resolver.service.ts @@ -75,6 +75,7 @@ export class GraphqlQueryRestoreManyResolverService extends GraphqlQueryBaseReso isNewRelationEnabled: featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled], roleId, + shouldBypassPermissionChecks: executionArgs.isExecutedByApiKey, }); } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-one-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-one-resolver.service.ts index 73c4d9002..58d8ae8a8 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-one-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-restore-one-resolver.service.ts @@ -77,6 +77,7 @@ export class GraphqlQueryRestoreOneResolverService extends GraphqlQueryBaseResol isNewRelationEnabled: featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled], roleId, + shouldBypassPermissionChecks: executionArgs.isExecutedByApiKey, }); } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service.ts index 37a302d88..10a71e86e 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service.ts @@ -113,6 +113,7 @@ export class GraphqlQueryUpdateManyResolverService extends GraphqlQueryBaseResol isNewRelationEnabled: featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled], roleId, + shouldBypassPermissionChecks: executionArgs.isExecutedByApiKey, }); } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-one-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-one-resolver.service.ts index 13fed1c5b..aee873ec1 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-one-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-one-resolver.service.ts @@ -107,6 +107,7 @@ export class GraphqlQueryUpdateOneResolverService extends GraphqlQueryBaseResolv isNewRelationEnabled: featureFlagsMap[FeatureFlagKey.IsNewRelationEnabled], roleId, + shouldBypassPermissionChecks: executionArgs.isExecutedByApiKey, }); } diff --git a/packages/twenty-server/src/engine/api/rest/core/rest-api-core-v2.service.ts b/packages/twenty-server/src/engine/api/rest/core/rest-api-core-v2.service.ts index 371797d8e..db1e6be92 100644 --- a/packages/twenty-server/src/engine/api/rest/core/rest-api-core-v2.service.ts +++ b/packages/twenty-server/src/engine/api/rest/core/rest-api-core-v2.service.ts @@ -1,16 +1,17 @@ import { BadRequestException, Injectable } from '@nestjs/common'; -import { capitalize, isDefined } from 'twenty-shared/utils'; import { Request } from 'express'; +import { capitalize, isDefined } from 'twenty-shared/utils'; import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; +import { ApiEventEmitterService } from 'src/engine/api/graphql/graphql-query-runner/services/api-event-emitter.service'; import { CoreQueryBuilderFactory } from 'src/engine/api/rest/core/query-builder/core-query-builder.factory'; import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; -import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service'; -import { ApiEventEmitterService } from 'src/engine/api/graphql/graphql-query-runner/services/api-event-emitter.service'; import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; +import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service'; +import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; @Injectable() export class RestApiCoreServiceV2 { @@ -19,6 +20,7 @@ export class RestApiCoreServiceV2 { private readonly twentyORMGlobalManager: TwentyORMGlobalManager, private readonly recordInputTransformerService: RecordInputTransformerService, protected readonly apiEventEmitterService: ApiEventEmitterService, + private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService, ) {} async delete(request: Request) { @@ -137,7 +139,7 @@ export class RestApiCoreServiceV2 { } private async getRepositoryAndMetadataOrFail(request: Request) { - const { workspace } = request; + const { workspace, apiKey, userWorkspaceId } = request; const { object: parsedObject } = parseCorePath(request); const objectMetadata = await this.coreQueryBuilderFactory.getObjectMetadata( @@ -153,13 +155,25 @@ export class RestApiCoreServiceV2 { throw new BadRequestException('Workspace not found'); } + const dataSource = + await this.twentyORMGlobalManager.getDataSourceForWorkspace(workspace.id); + const objectMetadataNameSingular = objectMetadata.objectMetadataMapItem.nameSingular; - const repository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspace.id, - objectMetadataNameSingular, - ); + + const shouldBypassPermissionChecks = !!apiKey; + + const roleId = + await this.workspacePermissionsCacheService.getRoleIdFromUserWorkspaceId({ + workspaceId: workspace.id, + userWorkspaceId, + }); + + const repository = dataSource.getRepository( + objectMetadataNameSingular, + shouldBypassPermissionChecks, + roleId, + ); return { objectMetadataNameSingular, diff --git a/packages/twenty-server/src/engine/api/rest/rest-api.module.ts b/packages/twenty-server/src/engine/api/rest/rest-api.module.ts index f6aab815b..72fcc6a69 100644 --- a/packages/twenty-server/src/engine/api/rest/rest-api.module.ts +++ b/packages/twenty-server/src/engine/api/rest/rest-api.module.ts @@ -1,6 +1,7 @@ import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; +import { ApiEventEmitterService } from 'src/engine/api/graphql/graphql-query-runner/services/api-event-emitter.service'; import { RestApiCoreBatchController } from 'src/engine/api/rest/core/controllers/rest-api-core-batch.controller'; import { RestApiCoreController } from 'src/engine/api/rest/core/controllers/rest-api-core.controller'; import { CoreQueryBuilderModule } from 'src/engine/api/rest/core/query-builder/core-query-builder.module'; @@ -15,9 +16,9 @@ import { RestApiMetadataService } from 'src/engine/api/rest/metadata/rest-api-me import { RestApiService } from 'src/engine/api/rest/rest-api.service'; import { AuthModule } from 'src/engine/core-modules/auth/auth.module'; import { RecordTransformerModule } from 'src/engine/core-modules/record-transformer/record-transformer.module'; +import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module'; import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module'; import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; -import { ApiEventEmitterService } from 'src/engine/api/graphql/graphql-query-runner/services/api-event-emitter.service'; @Module({ imports: [ @@ -28,6 +29,7 @@ import { ApiEventEmitterService } from 'src/engine/api/graphql/graphql-query-run HttpModule, TwentyORMModule, RecordTransformerModule, + WorkspacePermissionsCacheModule, ], controllers: [ RestApiMetadataController, diff --git a/packages/twenty-server/src/engine/core-modules/auth/token/services/access-token.service.ts b/packages/twenty-server/src/engine/core-modules/auth/token/services/access-token.service.ts index e0be07126..17df32f37 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/token/services/access-token.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/token/services/access-token.service.ts @@ -72,7 +72,6 @@ export class AccessTokenService { await this.twentyORMGlobalManager.getRepositoryForWorkspace( workspaceId, 'workspaceMember', - false, ); const workspaceMember = await workspaceMemberRepository.findOne({ diff --git a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.spec.ts b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.spec.ts index fa9b04cb6..aff325701 100644 --- a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.spec.ts @@ -5,6 +5,7 @@ import { Repository } from 'typeorm'; import { TypeORMService } from 'src/database/typeorm/typeorm.service'; import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; +import { USER_SIGNUP_EVENT_NAME } from 'src/engine/api/graphql/workspace-query-runner/constants/user-signup-event-name.constants'; import { AuthException } from 'src/engine/core-modules/auth/auth.exception'; import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; @@ -12,6 +13,7 @@ import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/use import { User } from 'src/engine/core-modules/user/user.entity'; import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { PermissionsException } from 'src/engine/metadata-modules/permissions/permissions.exception'; @@ -19,8 +21,6 @@ import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; -import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; -import { USER_SIGNUP_EVENT_NAME } from 'src/engine/api/graphql/workspace-query-runner/constants/user-signup-event-name.constants'; describe('UserWorkspaceService', () => { let service: UserWorkspaceService; diff --git a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts index 77aa87b7b..44f625e14 100644 --- a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts +++ b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts @@ -20,6 +20,7 @@ import { User } from 'src/engine/core-modules/user/user.entity'; import { userValidator } from 'src/engine/core-modules/user/user.validate'; import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { @@ -32,7 +33,6 @@ import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global. import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; import { assert } from 'src/utils/assert'; -import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate'; export class UserWorkspaceService extends TypeOrmQueryService { constructor( diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.module.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.module.ts index c14cfbb9a..cb7e32b72 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.module.ts @@ -31,6 +31,7 @@ import { RemoteTableRelationsModule } from 'src/engine/metadata-modules/remote-s import { SearchVectorModule } from 'src/engine/metadata-modules/search-vector/search-vector.module'; import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module'; import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module'; +import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module'; import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module'; import { ObjectMetadataEntity } from './object-metadata.entity'; @@ -59,6 +60,7 @@ import { UpdateObjectPayload } from './dtos/update-object.input'; IndexMetadataModule, FeatureFlagModule, PermissionsModule, + WorkspacePermissionsCacheModule, ], services: [ ObjectMetadataService, diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts index 3e0150605..a5aa07749 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts @@ -40,6 +40,7 @@ import { RemoteTableRelationsService } from 'src/engine/metadata-modules/remote- import { SearchVectorService } from 'src/engine/metadata-modules/search-vector/search-vector.service'; import { validateNameAndLabelAreSyncOrThrow } from 'src/engine/metadata-modules/utils/validate-name-and-label-are-sync-or-throw.util'; import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service'; +import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service'; import { CUSTOM_OBJECT_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; @@ -68,6 +69,7 @@ export class ObjectMetadataService extends TypeOrmQueryService { - const isPermissionsV2Enabled = - await this.featureFlagService.isFeatureEnabled( - FeatureFlagKey.IsPermissionsV2Enabled, - workspaceId, - ); + if (!ignoreLock) { + const isAlreadyCaching = + await this.workspacePermissionsCacheStorageService.getRolesPermissionsOngoingCachingLock( + workspaceId, + ); - const isAlreadyCaching = - await this.workspacePermissionsCacheStorageService.getRolesPermissionsOngoingCachingLock( - workspaceId, - ); - - if (!ignoreLock && isAlreadyCaching) { - return; + if (isAlreadyCaching) { + return; + } } await this.workspacePermissionsCacheStorageService.addRolesPermissionsOngoingCachingLock( @@ -80,6 +77,12 @@ export class WorkspacePermissionsCacheService { ); } + const isPermissionsV2Enabled = + await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IsPermissionsV2Enabled, + workspaceId, + ); + const recomputedRolesPermissions = await this.getObjectRecordPermissionsForRoles({ workspaceId, @@ -109,13 +112,15 @@ export class WorkspacePermissionsCacheService { workspaceId: string; ignoreLock?: boolean; }): Promise { - const isAlreadyCaching = - await this.workspacePermissionsCacheStorageService.getUserWorkspaceRoleMapOngoingCachingLock( - workspaceId, - ); + if (!ignoreLock) { + const isAlreadyCaching = + await this.workspacePermissionsCacheStorageService.getUserWorkspaceRoleMapOngoingCachingLock( + workspaceId, + ); - if (!ignoreLock && isAlreadyCaching) { - return; + if (isAlreadyCaching) { + return; + } } await this.workspacePermissionsCacheStorageService.addUserWorkspaceRoleMapOngoingCachingLock( @@ -183,6 +188,24 @@ export class WorkspacePermissionsCacheService { }); } + async getRoleIdFromUserWorkspaceId({ + workspaceId, + userWorkspaceId, + }: { + workspaceId: string; + userWorkspaceId?: string; + }): Promise { + if (!isDefined(userWorkspaceId)) { + return; + } + + const userWorkspaceRoleMap = await this.getUserWorkspaceRoleMapFromCache({ + workspaceId, + }); + + return userWorkspaceRoleMap[userWorkspaceId]; + } + private async getObjectRecordPermissionsForRoles({ workspaceId, isPermissionsV2Enabled, diff --git a/packages/twenty-server/src/engine/twenty-orm/datasource/workspace.datasource.ts b/packages/twenty-server/src/engine/twenty-orm/datasource/workspace.datasource.ts index 7612b26f2..3d4907121 100644 --- a/packages/twenty-server/src/engine/twenty-orm/datasource/workspace.datasource.ts +++ b/packages/twenty-server/src/engine/twenty-orm/datasource/workspace.datasource.ts @@ -41,10 +41,19 @@ export class WorkspaceDataSource extends DataSource { override getRepository( target: EntityTarget, + shouldBypassPermissionChecks = false, roleId?: string, ): WorkspaceRepository { + if (shouldBypassPermissionChecks === true) { + return this.manager.getRepository(target, shouldBypassPermissionChecks); + } + if (roleId) { - return this.manager.getRepository(target, roleId); + return this.manager.getRepository( + target, + shouldBypassPermissionChecks, + roleId, + ); } return this.manager.getRepository(target); @@ -64,11 +73,11 @@ export class WorkspaceDataSource extends DataSource { this.permissionsPerRoleId = permissionsPerRoleId; } - setFeatureFlagsMap(featureFlagMap: FeatureFlagMap) { + setFeatureFlagMap(featureFlagMap: FeatureFlagMap) { this.featureFlagMap = featureFlagMap; } - setFeatureFlagsMapVersion(featureFlagMapVersion: string) { + setFeatureFlagMapVersion(featureFlagMapVersion: string) { this.featureFlagMapVersion = featureFlagMapVersion; } } diff --git a/packages/twenty-server/src/engine/twenty-orm/entity-manager/entity.manager.ts b/packages/twenty-server/src/engine/twenty-orm/entity-manager/entity.manager.ts index 5cadf9792..6584b53d6 100644 --- a/packages/twenty-server/src/engine/twenty-orm/entity-manager/entity.manager.ts +++ b/packages/twenty-server/src/engine/twenty-orm/entity-manager/entity.manager.ts @@ -28,10 +28,17 @@ export class WorkspaceEntityManager extends EntityManager { override getRepository( target: EntityTarget, + shouldBypassPermissionChecks = false, roleId?: string, ): WorkspaceRepository { const dataSource = this.connection as WorkspaceDataSource; - const repositoryKey = `${dataSource.getMetadata(target).name}_${roleId ?? 'default'}${dataSource.rolesPermissionsVersion ? `_${dataSource.rolesPermissionsVersion}` : ''}${dataSource.featureFlagMapVersion ? `_${dataSource.featureFlagMapVersion}` : ''}`; + + const repositoryKey = this.getRepositoryKey({ + target, + dataSource, + roleId, + shouldBypassPermissionChecks, + }); const repoFromMap = this.repositories.get(repositoryKey); if (repoFromMap) { @@ -53,10 +60,36 @@ export class WorkspaceEntityManager extends EntityManager { dataSource.featureFlagMap, this.queryRunner, objectPermissions, + shouldBypassPermissionChecks, ); this.repositories.set(repositoryKey, newRepository); return newRepository; } + + private getRepositoryKey({ + target, + dataSource, + roleId, + shouldBypassPermissionChecks, + }: { + target: EntityTarget; + dataSource: WorkspaceDataSource; + shouldBypassPermissionChecks: boolean; + roleId?: string; + }) { + const repositoryPrefix = dataSource.getMetadata(target).name; + const roleIdSuffix = roleId ? `_${roleId}` : ''; + const rolesPermissionsVersionSuffix = dataSource.rolesPermissionsVersion + ? `_${dataSource.rolesPermissionsVersion}` + : ''; + const featureFlagMapVersionSuffix = dataSource.featureFlagMapVersion + ? `_${dataSource.featureFlagMapVersion}` + : ''; + + return shouldBypassPermissionChecks + ? `${repositoryPrefix}_bypass${featureFlagMapVersionSuffix}` + : `${repositoryPrefix}${roleIdSuffix}${rolesPermissionsVersionSuffix}${featureFlagMapVersionSuffix}`; + } } diff --git a/packages/twenty-server/src/engine/twenty-orm/factories/scoped-workspace-context.factory.ts b/packages/twenty-server/src/engine/twenty-orm/factories/scoped-workspace-context.factory.ts index 50e8db2eb..8e2e8f476 100644 --- a/packages/twenty-server/src/engine/twenty-orm/factories/scoped-workspace-context.factory.ts +++ b/packages/twenty-server/src/engine/twenty-orm/factories/scoped-workspace-context.factory.ts @@ -13,6 +13,7 @@ export class ScopedWorkspaceContextFactory { workspaceId: string | null; workspaceMetadataVersion: number | null; userWorkspaceId: string | null; + isExecutedByApiKey: boolean; } { const workspaceId: string | undefined = this.request?.['req']?.['workspaceId'] || @@ -24,6 +25,7 @@ export class ScopedWorkspaceContextFactory { workspaceId: workspaceId ?? null, workspaceMetadataVersion: workspaceMetadataVersion ?? null, userWorkspaceId: this.request?.['req']?.['userWorkspaceId'] ?? null, + isExecutedByApiKey: !!this.request?.['req']?.['apiKey'], }; } } diff --git a/packages/twenty-server/src/engine/twenty-orm/factories/workspace-datasource.factory.ts b/packages/twenty-server/src/engine/twenty-orm/factories/workspace-datasource.factory.ts index 0a22d1efb..6c1efbb80 100644 --- a/packages/twenty-server/src/engine/twenty-orm/factories/workspace-datasource.factory.ts +++ b/packages/twenty-server/src/engine/twenty-orm/factories/workspace-datasource.factory.ts @@ -294,9 +294,9 @@ export class WorkspaceDatasourceFactory { currentVersion: workspaceDataSource.featureFlagMapVersion, newVersion: cachedFeatureFlagMapVersion, newData: cachedFeatureFlagMap, - setData: (data) => workspaceDataSource.setFeatureFlagsMap(data), + setData: (data) => workspaceDataSource.setFeatureFlagMap(data), setVersion: (version) => - workspaceDataSource.setFeatureFlagsMapVersion(version), + workspaceDataSource.setFeatureFlagMapVersion(version), }); } diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/permissions.util.ts b/packages/twenty-server/src/engine/twenty-orm/repository/permissions.util.ts index 896c596ba..65bd23c66 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/permissions.util.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/permissions.util.ts @@ -6,6 +6,7 @@ import { PermissionsExceptionCode, PermissionsExceptionMessage, } from 'src/engine/metadata-modules/permissions/permissions.exception'; +import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; const getTargetEntityAndOperationType = (expressionMap: QueryExpressionMap) => { const mainEntity = expressionMap.aliases[0].metadata.name; @@ -20,10 +21,26 @@ const getTargetEntityAndOperationType = (expressionMap: QueryExpressionMap) => { export const validateQueryIsPermittedOrThrow = ( expressionMap: QueryExpressionMap, objectRecordsPermissions: ObjectRecordsPermissions, + objectMetadataMaps: ObjectMetadataMaps, + shouldBypassPermissionChecks: boolean, ) => { + if (shouldBypassPermissionChecks) { + return; + } + const { mainEntity, operationType } = getTargetEntityAndOperationType(expressionMap); + const objectMetadataIdForEntity = + objectMetadataMaps.idByNameSingular[mainEntity]; + + const objectMetadataIsSystem = + objectMetadataMaps.byId[objectMetadataIdForEntity]?.isSystem === true; + + if (objectMetadataIsSystem) { + return; + } + const permissionsForEntity = objectRecordsPermissions[mainEntity]; switch (operationType) { diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-query-builder.ts b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-query-builder.ts index e4594d786..166d0f5d3 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-query-builder.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-query-builder.ts @@ -1,6 +1,8 @@ import { ObjectRecordsPermissions } from 'twenty-shared/types'; import { ObjectLiteral, SelectQueryBuilder } from 'typeorm'; +import { WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface'; + import { WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder'; export class WorkspaceQueryBuilder< @@ -9,9 +11,15 @@ export class WorkspaceQueryBuilder< constructor( queryBuilder: SelectQueryBuilder, objectRecordsPermissions: ObjectRecordsPermissions, + internalContext: WorkspaceInternalContext, + shouldBypassPermissionChecks: boolean, ) { - super(queryBuilder, objectRecordsPermissions); - this.objectRecordsPermissions = objectRecordsPermissions; + super( + queryBuilder, + objectRecordsPermissions, + internalContext, + shouldBypassPermissionChecks, + ); } override clone(): this { @@ -20,6 +28,8 @@ export class WorkspaceQueryBuilder< return new WorkspaceQueryBuilder( clonedQueryBuilder, this.objectRecordsPermissions, + this.internalContext, + this.shouldBypassPermissionChecks, ) as this; } } diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-select-query-builder.ts b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-select-query-builder.ts index c8d94774f..65392d284 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-select-query-builder.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-select-query-builder.ts @@ -2,6 +2,8 @@ import { ObjectRecordsPermissions } from 'twenty-shared/types'; import { ObjectLiteral, SelectQueryBuilder, UpdateQueryBuilder } from 'typeorm'; import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity'; +import { WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface'; + import { validateQueryIsPermittedOrThrow } from 'src/engine/twenty-orm/repository/permissions.util'; import { WorkspaceUpdateQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-update-query-builder'; @@ -9,12 +11,18 @@ export class WorkspaceSelectQueryBuilder< T extends ObjectLiteral, > extends SelectQueryBuilder { objectRecordsPermissions: ObjectRecordsPermissions; + shouldBypassPermissionChecks: boolean; + internalContext: WorkspaceInternalContext; constructor( queryBuilder: SelectQueryBuilder, objectRecordsPermissions: ObjectRecordsPermissions, + internalContext: WorkspaceInternalContext, + shouldBypassPermissionChecks: boolean, ) { super(queryBuilder); this.objectRecordsPermissions = objectRecordsPermissions; + this.internalContext = internalContext; + this.shouldBypassPermissionChecks = shouldBypassPermissionChecks; } override update(): WorkspaceUpdateQueryBuilder; @@ -33,6 +41,8 @@ export class WorkspaceSelectQueryBuilder< return new WorkspaceUpdateQueryBuilder( updateQueryBuilder, this.objectRecordsPermissions, + this.internalContext, + this.shouldBypassPermissionChecks, ); } @@ -40,6 +50,8 @@ export class WorkspaceSelectQueryBuilder< validateQueryIsPermittedOrThrow( this.expressionMap, this.objectRecordsPermissions, + this.internalContext.objectMetadataMaps, + this.shouldBypassPermissionChecks, ); return super.execute(); @@ -49,6 +61,8 @@ export class WorkspaceSelectQueryBuilder< validateQueryIsPermittedOrThrow( this.expressionMap, this.objectRecordsPermissions, + this.internalContext.objectMetadataMaps, + this.shouldBypassPermissionChecks, ); return super.getMany(); diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-update-query-builder.ts b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-update-query-builder.ts index fd0ed37e0..ac127e64f 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/workspace-update-query-builder.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/workspace-update-query-builder.ts @@ -1,24 +1,34 @@ import { ObjectRecordsPermissions } from 'twenty-shared/types'; import { ObjectLiteral, UpdateQueryBuilder, UpdateResult } from 'typeorm'; +import { WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface'; + import { validateQueryIsPermittedOrThrow } from 'src/engine/twenty-orm/repository/permissions.util'; export class WorkspaceUpdateQueryBuilder< Entity extends ObjectLiteral, > extends UpdateQueryBuilder { private objectRecordsPermissions: ObjectRecordsPermissions; + private shouldBypassPermissionChecks: boolean; + private internalContext: WorkspaceInternalContext; constructor( queryBuilder: UpdateQueryBuilder, objectRecordsPermissions: ObjectRecordsPermissions, + internalContext: WorkspaceInternalContext, + shouldBypassPermissionChecks: boolean, ) { super(queryBuilder); this.objectRecordsPermissions = objectRecordsPermissions; + this.internalContext = internalContext; + this.shouldBypassPermissionChecks = shouldBypassPermissionChecks; } override execute(): Promise { validateQueryIsPermittedOrThrow( this.expressionMap, this.objectRecordsPermissions, + this.internalContext.objectMetadataMaps, + this.shouldBypassPermissionChecks, ); return super.execute(); diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts b/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts index dc45a7f95..69abd972b 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts @@ -36,9 +36,9 @@ export class WorkspaceRepository< T extends ObjectLiteral, > extends Repository { private readonly internalContext: WorkspaceInternalContext; + private shouldBypassPermissionChecks: boolean; private featureFlagMap: FeatureFlagMap; private objectRecordsPermissions?: ObjectRecordsPermissions; - constructor( internalContext: WorkspaceInternalContext, target: EntityTarget, @@ -46,11 +46,13 @@ export class WorkspaceRepository< featureFlagMap: FeatureFlagMap, queryRunner?: QueryRunner, objectRecordsPermissions?: ObjectRecordsPermissions, + shouldBypassPermissionChecks = false, ) { super(target, manager, queryRunner); this.internalContext = internalContext; this.featureFlagMap = featureFlagMap; this.objectRecordsPermissions = objectRecordsPermissions; + this.shouldBypassPermissionChecks = shouldBypassPermissionChecks; } override createQueryBuilder( @@ -74,6 +76,8 @@ export class WorkspaceRepository< return new WorkspaceQueryBuilder( queryBuilder, this.objectRecordsPermissions, + this.internalContext, + this.shouldBypassPermissionChecks, ); } } diff --git a/packages/twenty-server/src/engine/twenty-orm/twenty-orm-global.manager.ts b/packages/twenty-server/src/engine/twenty-orm/twenty-orm-global.manager.ts index abb0708d4..1a6b485eb 100644 --- a/packages/twenty-server/src/engine/twenty-orm/twenty-orm-global.manager.ts +++ b/packages/twenty-server/src/engine/twenty-orm/twenty-orm-global.manager.ts @@ -15,19 +15,31 @@ export class TwentyORMGlobalManager { async getRepositoryForWorkspace( workspaceId: string, workspaceEntity: Type, - shouldFailIfMetadataNotFound?: boolean, + options?: { + shouldBypassPermissionChecks?: boolean; + shouldFailIfMetadataNotFound?: boolean; + }, ): Promise>; async getRepositoryForWorkspace( workspaceId: string, objectMetadataName: string, - shouldFailIfMetadataNotFound?: boolean, + options?: { + shouldBypassPermissionChecks?: boolean; + shouldFailIfMetadataNotFound?: boolean; + }, ): Promise>; async getRepositoryForWorkspace( workspaceId: string, workspaceEntityOrobjectMetadataName: Type | string, - shouldFailIfMetadataNotFound = true, + options: { + shouldBypassPermissionChecks?: boolean; + shouldFailIfMetadataNotFound?: boolean; + } = { + shouldBypassPermissionChecks: false, + shouldFailIfMetadataNotFound: true, + }, ): Promise> { let objectMetadataName: string; @@ -42,10 +54,13 @@ export class TwentyORMGlobalManager { const workspaceDataSource = await this.workspaceDataSourceFactory.create( workspaceId, null, - shouldFailIfMetadataNotFound, + options.shouldFailIfMetadataNotFound, ); - const repository = workspaceDataSource.getRepository(objectMetadataName); + const repository = workspaceDataSource.getRepository( + objectMetadataName, + options.shouldBypassPermissionChecks, + ); return repository; } diff --git a/packages/twenty-server/src/engine/twenty-orm/twenty-orm.manager.ts b/packages/twenty-server/src/engine/twenty-orm/twenty-orm.manager.ts index fc18a2ffe..fdc1319ab 100644 --- a/packages/twenty-server/src/engine/twenty-orm/twenty-orm.manager.ts +++ b/packages/twenty-server/src/engine/twenty-orm/twenty-orm.manager.ts @@ -30,8 +30,12 @@ export class TwentyORMManager { async getRepository( workspaceEntityOrobjectMetadataName: Type | string, ): Promise> { - const { workspaceId, workspaceMetadataVersion, userWorkspaceId } = - this.scopedWorkspaceContextFactory.create(); + const { + workspaceId, + workspaceMetadataVersion, + userWorkspaceId, + isExecutedByApiKey, + } = this.scopedWorkspaceContextFactory.create(); let objectMetadataName: string; @@ -65,7 +69,13 @@ export class TwentyORMManager { roleId = userWorkspaceRole?.roleId; } - return workspaceDataSource.getRepository(objectMetadataName, roleId); + const shouldBypassPermissionChecks = !!isExecutedByApiKey; + + return workspaceDataSource.getRepository( + objectMetadataName, + shouldBypassPermissionChecks, + roleId, + ); } async getDatasource() { diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/crons/jobs/calendar-event-list-fetch.cron.job.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/crons/jobs/calendar-event-list-fetch.cron.job.ts index a00a970a1..687ffa23d 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/crons/jobs/calendar-event-list-fetch.cron.job.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/crons/jobs/calendar-event-list-fetch.cron.job.ts @@ -1,7 +1,7 @@ import { InjectRepository } from '@nestjs/typeorm'; -import { Any, Repository } from 'typeorm'; import { WorkspaceActivationStatus } from 'twenty-shared/workspace'; +import { Any, Repository } from 'typeorm'; import { SentryCronMonitor } from 'src/engine/core-modules/cron/sentry-cron-monitor.decorator'; import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service'; diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/crons/jobs/calendar-events-import.cron.job.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/crons/jobs/calendar-events-import.cron.job.ts index 511da51c7..8f2e0b6d4 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/crons/jobs/calendar-events-import.cron.job.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/crons/jobs/calendar-events-import.cron.job.ts @@ -1,7 +1,7 @@ import { InjectRepository } from '@nestjs/typeorm'; -import { Equal, Repository } from 'typeorm'; import { WorkspaceActivationStatus } from 'twenty-shared/workspace'; +import { Equal, Repository } from 'typeorm'; import { SentryCronMonitor } from 'src/engine/core-modules/cron/sentry-cron-monitor.decorator'; import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service'; diff --git a/packages/twenty-server/src/modules/connected-account/listeners/connected-account.listener.ts b/packages/twenty-server/src/modules/connected-account/listeners/connected-account.listener.ts index 323bc8edd..dbeb7126a 100644 --- a/packages/twenty-server/src/modules/connected-account/listeners/connected-account.listener.ts +++ b/packages/twenty-server/src/modules/connected-account/listeners/connected-account.listener.ts @@ -1,13 +1,13 @@ import { Injectable } from '@nestjs/common'; +import { OnDatabaseBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-batch-event.decorator'; +import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; import { ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event.type'; import { AccountsToReconnectService } from 'src/modules/connected-account/services/accounts-to-reconnect.service'; import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; -import { OnDatabaseBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-batch-event.decorator'; -import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; @Injectable() export class ConnectedAccountListener { diff --git a/packages/twenty-server/src/modules/contact-creation-manager/services/create-company-and-contact.service.ts b/packages/twenty-server/src/modules/contact-creation-manager/services/create-company-and-contact.service.ts index 3a9488a7b..2b3abe1ae 100644 --- a/packages/twenty-server/src/modules/contact-creation-manager/services/create-company-and-contact.service.ts +++ b/packages/twenty-server/src/modules/contact-creation-manager/services/create-company-and-contact.service.ts @@ -55,6 +55,9 @@ export class CreateCompanyAndContactService { await this.twentyORMGlobalManager.getRepositoryForWorkspace( workspaceId, PersonWorkspaceEntity, + { + shouldBypassPermissionChecks: true, + }, ); const workspaceMembers = diff --git a/packages/twenty-server/src/modules/contact-creation-manager/services/create-company.service.ts b/packages/twenty-server/src/modules/contact-creation-manager/services/create-company.service.ts index 4503ae3e8..cf4ce4d8f 100644 --- a/packages/twenty-server/src/modules/contact-creation-manager/services/create-company.service.ts +++ b/packages/twenty-server/src/modules/contact-creation-manager/services/create-company.service.ts @@ -2,9 +2,9 @@ import { Injectable } from '@nestjs/common'; import axios, { AxiosInstance } from 'axios'; import uniqBy from 'lodash.uniqby'; -import { DeepPartial, EntityManager, ILike } from 'typeorm'; -import { ConnectedAccountProvider } from 'twenty-shared/types'; import { TWENTY_COMPANIES_BASE_URL } from 'twenty-shared/constants'; +import { ConnectedAccountProvider } from 'twenty-shared/types'; +import { DeepPartial, EntityManager, ILike } from 'typeorm'; import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; @@ -49,6 +49,9 @@ export class CreateCompanyService { await this.twentyORMGlobalManager.getRepositoryForWorkspace( workspaceId, CompanyWorkspaceEntity, + { + shouldBypassPermissionChecks: true, + }, ); // Avoid creating duplicate companies diff --git a/packages/twenty-server/src/modules/contact-creation-manager/services/create-contact.service.ts b/packages/twenty-server/src/modules/contact-creation-manager/services/create-contact.service.ts index 3258b8aca..280333e51 100644 --- a/packages/twenty-server/src/modules/contact-creation-manager/services/create-contact.service.ts +++ b/packages/twenty-server/src/modules/contact-creation-manager/services/create-contact.service.ts @@ -1,8 +1,8 @@ import { Injectable } from '@nestjs/common'; +import { ConnectedAccountProvider } from 'twenty-shared/types'; import { DeepPartial, EntityManager } from 'typeorm'; import { v4 } from 'uuid'; -import { ConnectedAccountProvider } from 'twenty-shared/types'; import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; @@ -82,6 +82,9 @@ export class CreateContactService { await this.twentyORMGlobalManager.getRepositoryForWorkspace( workspaceId, PersonWorkspaceEntity, + { + shouldBypassPermissionChecks: true, + }, ); const lastPersonPosition = await this.getLastPersonPosition( diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/crons/jobs/messaging-message-list-fetch.cron.job.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/crons/jobs/messaging-message-list-fetch.cron.job.ts index 22a9d3579..2375aa209 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/crons/jobs/messaging-message-list-fetch.cron.job.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/crons/jobs/messaging-message-list-fetch.cron.job.ts @@ -1,7 +1,7 @@ import { InjectRepository } from '@nestjs/typeorm'; -import { In, Repository } from 'typeorm'; import { WorkspaceActivationStatus } from 'twenty-shared/workspace'; +import { In, Repository } from 'typeorm'; import { SentryCronMonitor } from 'src/engine/core-modules/cron/sentry-cron-monitor.decorator'; import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service'; diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/crons/jobs/messaging-messages-import.cron.job.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/crons/jobs/messaging-messages-import.cron.job.ts index 67dc48fcb..71ac47a68 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/crons/jobs/messaging-messages-import.cron.job.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/crons/jobs/messaging-messages-import.cron.job.ts @@ -1,7 +1,7 @@ import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; import { WorkspaceActivationStatus } from 'twenty-shared/workspace'; +import { Repository } from 'typeorm'; import { SentryCronMonitor } from 'src/engine/core-modules/cron/sentry-cron-monitor.decorator'; import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service'; diff --git a/packages/twenty-server/src/modules/messaging/monitoring/crons/jobs/messaging-message-channel-sync-status-monitoring.cron.job.ts b/packages/twenty-server/src/modules/messaging/monitoring/crons/jobs/messaging-message-channel-sync-status-monitoring.cron.job.ts index d9cc44245..f48f1d603 100644 --- a/packages/twenty-server/src/modules/messaging/monitoring/crons/jobs/messaging-message-channel-sync-status-monitoring.cron.job.ts +++ b/packages/twenty-server/src/modules/messaging/monitoring/crons/jobs/messaging-message-channel-sync-status-monitoring.cron.job.ts @@ -2,8 +2,8 @@ import { Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import snakeCase from 'lodash.snakecase'; -import { Repository } from 'typeorm'; import { WorkspaceActivationStatus } from 'twenty-shared/workspace'; +import { Repository } from 'typeorm'; import { SentryCronMonitor } from 'src/engine/core-modules/cron/sentry-cron-monitor.decorator'; import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service'; diff --git a/packages/twenty-server/src/modules/view/pre-hooks/view-delete-one.pre-query.hook.ts b/packages/twenty-server/src/modules/view/pre-hooks/view-delete-one.pre-query.hook.ts index e508d7a0d..6d0798de8 100644 --- a/packages/twenty-server/src/modules/view/pre-hooks/view-delete-one.pre-query.hook.ts +++ b/packages/twenty-server/src/modules/view/pre-hooks/view-delete-one.pre-query.hook.ts @@ -8,7 +8,6 @@ import { import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator'; import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; - @WorkspaceQueryHook(`view.deleteOne`) export class ViewDeleteOnePreQueryHook implements WorkspaceQueryHookInstance { constructor( @@ -21,7 +20,6 @@ export class ViewDeleteOnePreQueryHook implements WorkspaceQueryHookInstance { payload: DeleteOneResolverArgs, ): Promise { const targettedViewId = payload.id; - const viewRepository = await this.twentyORMGlobalManager.getRepositoryForWorkspace( authContext.workspace.id, diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/mail-sender/send-email.workflow-action.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/mail-sender/send-email.workflow-action.ts index ec52c80da..20a4533f4 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/mail-sender/send-email.workflow-action.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/mail-sender/send-email.workflow-action.ts @@ -2,8 +2,8 @@ import { Injectable, Logger } from '@nestjs/common'; import DOMPurify from 'dompurify'; import { JSDOM } from 'jsdom'; -import { z } from 'zod'; import { isDefined, isValidUuid } from 'twenty-shared/utils'; +import { z } from 'zod'; import { WorkflowExecutor } from 'src/modules/workflow/workflow-executor/interfaces/workflow-executor.interface'; diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/database-event-trigger/listeners/database-event-trigger.listener.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/database-event-trigger/listeners/database-event-trigger.listener.ts index ea6646514..36f78a194 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-trigger/database-event-trigger/listeners/database-event-trigger.listener.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/database-event-trigger/listeners/database-event-trigger.listener.ts @@ -1,5 +1,7 @@ import { Injectable, Logger } from '@nestjs/common'; +import { OnDatabaseBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-batch-event.decorator'; +import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event'; import { ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event'; import { ObjectRecordDestroyEvent } from 'src/engine/core-modules/event-emitter/types/object-record-destroy.event'; @@ -16,8 +18,6 @@ import { WorkflowTriggerJob, WorkflowTriggerJobData, } from 'src/modules/workflow/workflow-trigger/jobs/workflow-trigger.job'; -import { OnDatabaseBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-batch-event.decorator'; -import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; @Injectable() export class DatabaseEventTriggerListener { diff --git a/packages/twenty-server/test/integration/graphql/suites/object-records-permissions/update-one-object-records-permissions.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-records-permissions/update-one-object-records-permissions.integration-spec.ts index 31bd91bbb..fda37a11e 100644 --- a/packages/twenty-server/test/integration/graphql/suites/object-records-permissions/update-one-object-records-permissions.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-records-permissions/update-one-object-records-permissions.integration-spec.ts @@ -2,67 +2,219 @@ import { randomUUID } from 'node:crypto'; import { PERSON_GQL_FIELDS } from 'test/integration/constants/person-gql-fields.constants'; import { createOneOperationFactory } from 'test/integration/graphql/utils/create-one-operation-factory.util'; +import { findOneOperationFactory } from 'test/integration/graphql/utils/find-one-operation-factory.util'; +import { makeGraphqlAPIRequestWithApiKey } from 'test/integration/graphql/utils/make-graphql-api-request-with-api-key.util'; import { makeGraphqlAPIRequestWithGuestRole } from 'test/integration/graphql/utils/make-graphql-api-request-with-guest-role.util'; import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util'; +import { updateFeatureFlagFactory } from 'test/integration/graphql/utils/update-feature-flag-factory.util'; import { updateOneOperationFactory } from 'test/integration/graphql/utils/update-one-operation-factory.util'; +import { SEED_APPLE_WORKSPACE_ID } from 'src/database/typeorm-seeds/core/workspaces'; import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; import { PermissionsExceptionMessage } from 'src/engine/metadata-modules/permissions/permissions.exception'; describe('updateOneObjectRecordsPermissions', () => { - const personId = randomUUID(); - - beforeAll(async () => { - const createPersonOperation = createOneOperationFactory({ - objectMetadataSingularName: 'person', - gqlFields: PERSON_GQL_FIELDS, - data: { - id: personId, - jobTitle: 'Software Engineer', - }, - }); - - await makeGraphqlAPIRequest(createPersonOperation); - }); - - it('should throw a permission error when user does not have permission (guest role)', async () => { + describe('permissions V2 disabled', () => { const personId = randomUUID(); - const graphqlOperation = updateOneOperationFactory({ - objectMetadataSingularName: 'person', - gqlFields: PERSON_GQL_FIELDS, - recordId: personId, - data: { - jobTitle: 'Senior Software Engineer', - }, + + beforeAll(async () => { + const createPersonOperation = createOneOperationFactory({ + objectMetadataSingularName: 'person', + gqlFields: PERSON_GQL_FIELDS, + data: { + id: personId, + jobTitle: 'Software Engineer', + }, + }); + + await makeGraphqlAPIRequest(createPersonOperation); }); - const response = await makeGraphqlAPIRequestWithGuestRole(graphqlOperation); + it('should throw a permission error when user does not have permission (guest role)', async () => { + const graphqlOperation = updateOneOperationFactory({ + objectMetadataSingularName: 'person', + gqlFields: PERSON_GQL_FIELDS, + recordId: personId, + data: { + jobTitle: 'Senior Software Engineer', + }, + }); - expect(response.body.data).toStrictEqual({ updatePerson: 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); + const response = + await makeGraphqlAPIRequestWithGuestRole(graphqlOperation); + + expect(response.body.data).toStrictEqual({ updatePerson: 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); + }); + + it('should update an object record when user has permission (admin role)', async () => { + const graphqlOperation = updateOneOperationFactory({ + objectMetadataSingularName: 'person', + gqlFields: PERSON_GQL_FIELDS, + recordId: personId, + data: { + jobTitle: 'Senior Software Engineer', + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data).toBeDefined(); + expect(response.body.data.updatePerson).toBeDefined(); + expect(response.body.data.updatePerson.id).toBe(personId); + expect(response.body.data.updatePerson.jobTitle).toBe( + 'Senior Software Engineer', + ); + }); }); - it('should update an object record when user has permission (admin role)', async () => { - const graphqlOperation = updateOneOperationFactory({ - objectMetadataSingularName: 'person', - gqlFields: PERSON_GQL_FIELDS, - recordId: personId, - data: { - jobTitle: 'Senior Software Engineer', - }, + describe('permissions V2 enabled', () => { + const personId = randomUUID(); + let allPetsViewId: string; + + beforeAll(async () => { + const createPersonOperation = createOneOperationFactory({ + objectMetadataSingularName: 'person', + gqlFields: PERSON_GQL_FIELDS, + data: { + id: personId, + jobTitle: 'Software Engineer', + }, + }); + + await makeGraphqlAPIRequest(createPersonOperation); + + const enablePermissionsQuery = updateFeatureFlagFactory( + SEED_APPLE_WORKSPACE_ID, + 'IsPermissionsV2Enabled', + true, + ); + + await makeGraphqlAPIRequest(enablePermissionsQuery); + + const findAllPetsViewOperation = findOneOperationFactory({ + objectMetadataSingularName: 'view', + gqlFields: 'id', + filter: { + icon: { + eq: 'IconCat', + }, + }, + }); + + const findAllPetsViewResponse = await makeGraphqlAPIRequest( + findAllPetsViewOperation, + ); + + allPetsViewId = findAllPetsViewResponse.body.data.view.id; }); - const response = await makeGraphqlAPIRequest(graphqlOperation); + afterAll(async () => { + const disablePermissionsQuery = updateFeatureFlagFactory( + SEED_APPLE_WORKSPACE_ID, + 'IsPermissionsV2Enabled', + false, + ); - expect(response.body.data).toBeDefined(); - expect(response.body.data.updatePerson).toBeDefined(); - expect(response.body.data.updatePerson.id).toBe(personId); - expect(response.body.data.updatePerson.jobTitle).toBe( - 'Senior Software Engineer', - ); + await makeGraphqlAPIRequest(disablePermissionsQuery); + + const updateViewOperation = updateOneOperationFactory({ + objectMetadataSingularName: 'view', + gqlFields: 'id', + recordId: allPetsViewId, + data: { + icon: 'IconCat', + }, + }); + + await makeGraphqlAPIRequest(updateViewOperation); + }); + + it('should throw a permission error when user does not have permission (guest role)', async () => { + const graphqlOperation = updateOneOperationFactory({ + objectMetadataSingularName: 'person', + gqlFields: PERSON_GQL_FIELDS, + recordId: personId, + data: { + jobTitle: 'Senior Software Engineer', + }, + }); + + const response = + await makeGraphqlAPIRequestWithGuestRole(graphqlOperation); + + expect(response.body.data).toStrictEqual({ updatePerson: 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); + }); + + it('should allow to update a system object record even without update permission (guest role)', async () => { + const graphqlOperation = updateOneOperationFactory({ + objectMetadataSingularName: 'view', + gqlFields: ` + id + icon + `, + recordId: allPetsViewId, + data: { + icon: 'IconDog', + }, + }); + + const response = + await makeGraphqlAPIRequestWithGuestRole(graphqlOperation); + + expect(response.body.data).toBeDefined(); + expect(response.body.data.updateView).toBeDefined(); + expect(response.body.data.updateView.id).toBe(allPetsViewId); + expect(response.body.data.updateView.icon).toBe('IconDog'); + }); + + it('should update an object record when user has permission (admin role)', async () => { + const graphqlOperation = updateOneOperationFactory({ + objectMetadataSingularName: 'person', + gqlFields: PERSON_GQL_FIELDS, + recordId: personId, + data: { + jobTitle: 'Senior Software Engineer', + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data).toBeDefined(); + expect(response.body.data.updatePerson).toBeDefined(); + expect(response.body.data.updatePerson.id).toBe(personId); + expect(response.body.data.updatePerson.jobTitle).toBe( + 'Senior Software Engineer', + ); + }); + + it('should update an object record when executed by api key', async () => { + const graphqlOperation = updateOneOperationFactory({ + objectMetadataSingularName: 'person', + gqlFields: PERSON_GQL_FIELDS, + recordId: personId, + data: { + jobTitle: 'Senior Software Engineer', + }, + }); + + const response = await makeGraphqlAPIRequestWithApiKey(graphqlOperation); + + expect(response.body.data).toBeDefined(); + expect(response.body.data.updatePerson).toBeDefined(); + expect(response.body.data.updatePerson.id).toBe(personId); + expect(response.body.data.updatePerson.jobTitle).toBe( + 'Senior Software Engineer', + ); + }); }); }); diff --git a/packages/twenty-server/test/integration/graphql/utils/make-graphql-api-request-with-api-key.util.ts b/packages/twenty-server/test/integration/graphql/utils/make-graphql-api-request-with-api-key.util.ts new file mode 100644 index 000000000..c71c600b6 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/utils/make-graphql-api-request-with-api-key.util.ts @@ -0,0 +1,21 @@ +import { ASTNode, print } from 'graphql'; +import request from 'supertest'; + +type GraphqlOperation = { + query: ASTNode; + variables?: Record; +}; + +export const makeGraphqlAPIRequestWithApiKey = ( + graphqlOperation: GraphqlOperation, +) => { + const client = request(`http://localhost:${APP_PORT}`); + + return client + .post('/graphql') + .set('Authorization', `Bearer ${API_KEY_ACCESS_TOKEN}`) + .send({ + query: print(graphqlOperation.query), + variables: graphqlOperation.variables || {}, + }); +};