diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/constants/system-objects-permissions-requirements.constant.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/constants/objects-with-settings-permissions-requirements.ts similarity index 79% rename from packages/twenty-server/src/engine/api/graphql/graphql-query-runner/constants/system-objects-permissions-requirements.constant.ts rename to packages/twenty-server/src/engine/api/graphql/graphql-query-runner/constants/objects-with-settings-permissions-requirements.ts index 27b0bab71..603fad32d 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/constants/system-objects-permissions-requirements.constant.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/constants/objects-with-settings-permissions-requirements.ts @@ -1,6 +1,6 @@ import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; -export const SYSTEM_OBJECTS_PERMISSIONS_REQUIREMENTS = { +export const OBJECTS_WITH_SETTINGS_PERMISSIONS_REQUIREMENTS = { apiKey: SettingPermissionType.API_KEYS_AND_WEBHOOKS, webhook: SettingPermissionType.API_KEYS_AND_WEBHOOKS, } as const; 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 e1dfd4e90..b6b885fe3 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 @@ -15,7 +15,7 @@ import { WorkspaceResolverBuilderMethodNames, } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; -import { SYSTEM_OBJECTS_PERMISSIONS_REQUIREMENTS } from 'src/engine/api/graphql/graphql-query-runner/constants/system-objects-permissions-requirements.constant'; +import { OBJECTS_WITH_SETTINGS_PERMISSIONS_REQUIREMENTS } from 'src/engine/api/graphql/graphql-query-runner/constants/objects-with-settings-permissions-requirements'; import { GraphqlQuerySelectedFieldsResult } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser'; import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser'; import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper'; @@ -97,7 +97,7 @@ export abstract class GraphqlQueryBaseResolverService< featureFlagsMap[FeatureFlagKey.IS_PERMISSIONS_V2_ENABLED]; if (objectMetadataItemWithFieldMaps.isSystem === true) { - await this.validateSystemObjectPermissionsOrThrow(options); + await this.validateSettingsPermissionsOnObjectOrThrow(options); } else { if (!isPermissionsV2Enabled) await this.validateObjectRecordPermissionsOrThrow({ @@ -186,19 +186,19 @@ export abstract class GraphqlQueryBaseResolverService< } } - private async validateSystemObjectPermissionsOrThrow( + private async validateSettingsPermissionsOnObjectOrThrow( options: WorkspaceQueryRunnerOptions, ) { const { authContext, objectMetadataItemWithFieldMaps } = options; if ( - Object.keys(SYSTEM_OBJECTS_PERMISSIONS_REQUIREMENTS).includes( + Object.keys(OBJECTS_WITH_SETTINGS_PERMISSIONS_REQUIREMENTS).includes( objectMetadataItemWithFieldMaps.nameSingular, ) ) { const permissionRequired: SettingPermissionType = // @ts-expect-error legacy noImplicitAny - SYSTEM_OBJECTS_PERMISSIONS_REQUIREMENTS[ + OBJECTS_WITH_SETTINGS_PERMISSIONS_REQUIREMENTS[ objectMetadataItemWithFieldMaps.nameSingular ]; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.explorer.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.explorer.ts index f16d0c0eb..e56a17403 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.explorer.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.explorer.ts @@ -162,6 +162,10 @@ export class WorkspaceQueryHookExplorer implements OnModuleInit { { req: { workspaceId: executeParams?.[0].workspace.id, + userWorkspaceId: executeParams?.[0].userWorkspaceId, + apiKey: executeParams?.[0].apiKey, + workspaceMemberId: executeParams?.[0].workspaceMemberId, + user: executeParams?.[0].user, }, }, contextId, diff --git a/packages/twenty-server/src/engine/core-modules/workflow/controllers/workflow-trigger.controller.ts b/packages/twenty-server/src/engine/core-modules/workflow/controllers/workflow-trigger.controller.ts index 01664cdbe..b0f664cce 100644 --- a/packages/twenty-server/src/engine/core-modules/workflow/controllers/workflow-trigger.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/workflow/controllers/workflow-trigger.controller.ts @@ -14,7 +14,8 @@ import { isDefined } from 'twenty-shared/utils'; import { WorkflowTriggerRestApiExceptionFilter } from 'src/engine/core-modules/workflow/filters/workflow-trigger-rest-api-exception.filter'; import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard'; import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; -import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; +import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { WorkflowVersionStatus, WorkflowVersionWorkspaceEntity, @@ -28,10 +29,13 @@ import { WorkflowTriggerType } from 'src/modules/workflow/workflow-trigger/types import { WorkflowTriggerWorkspaceService } from 'src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service'; @Controller('webhooks') -@UseFilters(WorkflowTriggerRestApiExceptionFilter) +@UseFilters( + WorkflowTriggerRestApiExceptionFilter, + PermissionsGraphqlApiExceptionFilter, +) export class WorkflowTriggerController { constructor( - private readonly twentyORMManager: TwentyORMManager, + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, private readonly workflowTriggerWorkspaceService: WorkflowTriggerWorkspaceService, ) {} @@ -68,8 +72,10 @@ export class WorkflowTriggerController { workspaceId: string; }) { const workflowRepository = - await this.twentyORMManager.getRepository( + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, 'workflow', + { shouldBypassPermissionChecks: true }, ); const workflow = await workflowRepository.findOne({ @@ -94,8 +100,10 @@ export class WorkflowTriggerController { } const workflowVersionRepository = - await this.twentyORMManager.getRepository( + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, 'workflowVersion', + { shouldBypassPermissionChecks: true }, ); const workflowVersion = await workflowVersionRepository.findOne({ where: { id: workflow.lastPublishedVersionId }, diff --git a/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-builder.resolver.ts b/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-builder.resolver.ts index 958e70895..ac9766493 100644 --- a/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-builder.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-builder.resolver.ts @@ -7,14 +7,24 @@ import { ComputeStepOutputSchemaInput } from 'src/engine/core-modules/workflow/d import { WorkflowTriggerGraphqlApiExceptionFilter } from 'src/engine/core-modules/workflow/filters/workflow-trigger-graphql-api-exception.filter'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; +import { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions.guard'; import { UserAuthGuard } from 'src/engine/guards/user-auth.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; +import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; +import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter'; import { OutputSchema } from 'src/modules/workflow/workflow-builder/workflow-schema/types/output-schema.type'; import { WorkflowSchemaWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service'; @Resolver() -@UseGuards(WorkspaceAuthGuard, UserAuthGuard) -@UseFilters(WorkflowTriggerGraphqlApiExceptionFilter) +@UseGuards( + WorkspaceAuthGuard, + UserAuthGuard, + SettingsPermissionsGuard(SettingPermissionType.WORKFLOWS), +) +@UseFilters( + WorkflowTriggerGraphqlApiExceptionFilter, + PermissionsGraphqlApiExceptionFilter, +) export class WorkflowBuilderResolver { constructor( private readonly workflowSchemaWorkspaceService: WorkflowSchemaWorkspaceService, diff --git a/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-step.resolver.ts b/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-step.resolver.ts index 33b1f1f37..d64747046 100644 --- a/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-step.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-step.resolver.ts @@ -1,4 +1,4 @@ -import { UseGuards } from '@nestjs/common'; +import { UseFilters, UseGuards } from '@nestjs/common'; import { Args, Mutation, Resolver } from '@nestjs/graphql'; import { CreateWorkflowVersionStepInput } from 'src/engine/core-modules/workflow/dtos/create-workflow-version-step-input.dto'; @@ -9,13 +9,21 @@ import { UpdateWorkflowVersionStepInput } from 'src/engine/core-modules/workflow import { WorkflowActionDTO } from 'src/engine/core-modules/workflow/dtos/workflow-step.dto'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; +import { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions.guard'; import { UserAuthGuard } from 'src/engine/guards/user-auth.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; +import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; +import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter'; import { WorkflowVersionStepWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-step/workflow-version-step.workspace-service'; import { WorkflowRunWorkspaceService } from 'src/modules/workflow/workflow-runner/workflow-run/workflow-run.workspace-service'; @Resolver() -@UseGuards(WorkspaceAuthGuard, UserAuthGuard) +@UseGuards( + WorkspaceAuthGuard, + UserAuthGuard, + SettingsPermissionsGuard(SettingPermissionType.WORKFLOWS), +) +@UseFilters(PermissionsGraphqlApiExceptionFilter) export class WorkflowStepResolver { constructor( private readonly workflowVersionStepWorkspaceService: WorkflowVersionStepWorkspaceService, @@ -78,10 +86,12 @@ export class WorkflowStepResolver { @Mutation(() => WorkflowActionDTO) async updateWorkflowRunStep( + @AuthWorkspace() { id: workspaceId }: Workspace, @Args('input') { workflowRunId, step }: UpdateWorkflowRunStepInput, ): Promise { await this.workflowRunWorkspaceService.updateWorkflowRunStep({ + workspaceId, workflowRunId, step, }); diff --git a/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-trigger.resolver.ts b/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-trigger.resolver.ts index 50bf9010d..f988a08ba 100644 --- a/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-trigger.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-trigger.resolver.ts @@ -1,20 +1,30 @@ import { UseFilters, UseGuards } from '@nestjs/common'; import { Args, Mutation, Resolver } from '@nestjs/graphql'; +import { buildCreatedByFromFullNameMetadata } from 'src/engine/core-modules/actor/utils/build-created-by-from-full-name-metadata.util'; import { User } from 'src/engine/core-modules/user/user.entity'; import { RunWorkflowVersionInput } from 'src/engine/core-modules/workflow/dtos/run-workflow-version-input.dto'; import { WorkflowRunDTO } from 'src/engine/core-modules/workflow/dtos/workflow-run.dto'; import { WorkflowTriggerGraphqlApiExceptionFilter } from 'src/engine/core-modules/workflow/filters/workflow-trigger-graphql-api-exception.filter'; import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator'; import { AuthWorkspaceMemberId } from 'src/engine/decorators/auth/auth-workspace-member-id.decorator'; +import { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions.guard'; import { UserAuthGuard } from 'src/engine/guards/user-auth.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; +import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; +import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter'; import { WorkflowTriggerWorkspaceService } from 'src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service'; -import { buildCreatedByFromFullNameMetadata } from 'src/engine/core-modules/actor/utils/build-created-by-from-full-name-metadata.util'; @Resolver() -@UseGuards(WorkspaceAuthGuard, UserAuthGuard) -@UseFilters(WorkflowTriggerGraphqlApiExceptionFilter) +@UseGuards( + WorkspaceAuthGuard, + UserAuthGuard, + SettingsPermissionsGuard(SettingPermissionType.WORKFLOWS), +) +@UseFilters( + WorkflowTriggerGraphqlApiExceptionFilter, + PermissionsGraphqlApiExceptionFilter, +) export class WorkflowTriggerResolver { constructor( private readonly workflowTriggerWorkspaceService: WorkflowTriggerWorkspaceService, diff --git a/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-version.resolver.ts b/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-version.resolver.ts index c19093777..c35d3fff9 100644 --- a/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-version.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-version.resolver.ts @@ -1,16 +1,24 @@ -import { UseGuards } from '@nestjs/common'; +import { UseFilters, UseGuards } from '@nestjs/common'; import { Args, Mutation, Resolver } from '@nestjs/graphql'; import { CreateDraftFromWorkflowVersionInput } from 'src/engine/core-modules/workflow/dtos/create-draft-from-workflow-version-input'; import { WorkflowVersionDTO } from 'src/engine/core-modules/workflow/dtos/workflow-version.dto'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; +import { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions.guard'; import { UserAuthGuard } from 'src/engine/guards/user-auth.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; +import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; +import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter'; import { WorkflowVersionWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version/workflow-version.workspace-service'; @Resolver() -@UseGuards(WorkspaceAuthGuard, UserAuthGuard) +@UseGuards( + WorkspaceAuthGuard, + UserAuthGuard, + SettingsPermissionsGuard(SettingPermissionType.WORKFLOWS), +) +@UseFilters(PermissionsGraphqlApiExceptionFilter) export class WorkflowVersionResolver { constructor( private readonly workflowVersionWorkspaceService: WorkflowVersionWorkspaceService, diff --git a/packages/twenty-server/src/engine/core-modules/workflow/workflow-api.module.ts b/packages/twenty-server/src/engine/core-modules/workflow/workflow-api.module.ts index 401dada11..db43eeea5 100644 --- a/packages/twenty-server/src/engine/core-modules/workflow/workflow-api.module.ts +++ b/packages/twenty-server/src/engine/core-modules/workflow/workflow-api.module.ts @@ -5,6 +5,7 @@ import { WorkflowBuilderResolver } from 'src/engine/core-modules/workflow/resolv import { WorkflowStepResolver } from 'src/engine/core-modules/workflow/resolvers/workflow-step.resolver'; import { WorkflowTriggerResolver } from 'src/engine/core-modules/workflow/resolvers/workflow-trigger.resolver'; import { WorkflowVersionResolver } from 'src/engine/core-modules/workflow/resolvers/workflow-version.resolver'; +import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module'; import { WorkflowBuilderModule } from 'src/modules/workflow/workflow-builder/workflow-builder.module'; import { WorkflowVersionModule } from 'src/modules/workflow/workflow-builder/workflow-version/workflow-version.module'; @@ -18,6 +19,7 @@ import { WorkflowTriggerModule } from 'src/modules/workflow/workflow-trigger/wor WorkflowCommonModule, WorkflowVersionModule, WorkflowRunModule, + PermissionsModule, ], controllers: [WorkflowTriggerController], providers: [ diff --git a/packages/twenty-server/src/engine/metadata-modules/permissions/constants/setting-permission-type.constants.ts b/packages/twenty-server/src/engine/metadata-modules/permissions/constants/setting-permission-type.constants.ts index 415e34dac..2368d8675 100644 --- a/packages/twenty-server/src/engine/metadata-modules/permissions/constants/setting-permission-type.constants.ts +++ b/packages/twenty-server/src/engine/metadata-modules/permissions/constants/setting-permission-type.constants.ts @@ -6,4 +6,5 @@ export enum SettingPermissionType { DATA_MODEL = 'DATA_MODEL', ADMIN_PANEL = 'ADMIN_PANEL', SECURITY = 'SECURITY', + WORKFLOWS = 'WORKFLOWS', } diff --git a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.service.ts b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.service.ts index b43ed48e8..a63b9e143 100644 --- a/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/remote-server/remote-server.service.ts @@ -84,7 +84,7 @@ export class RemoteServerService { const createdRemoteServer = entityManager.create( RemoteServerEntity, remoteServerToCreate, - ); + ) as RemoteServerEntity; const foreignDataWrapperQuery = this.foreignDataWrapperServerQueryFactory.createForeignDataWrapperServer( diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service.ts index e4a10cf44..b777daa38 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service.ts @@ -11,6 +11,7 @@ import { In, Repository } from 'typeorm'; import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/role/user-workspace-role.entity'; import { UserWorkspaceRoleMap } from 'src/engine/metadata-modules/workspace-permissions-cache/types/user-workspace-role-map.type'; @@ -18,6 +19,7 @@ import { WorkspacePermissionsCacheStorageService } from 'src/engine/metadata-mod import { TwentyORMExceptionCode } from 'src/engine/twenty-orm/exceptions/twenty-orm.exception'; import { getFromCacheWithRecompute } from 'src/engine/utils/get-data-from-cache-with-recompute.util'; import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; +import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids'; type CacheResult = { version: T; @@ -244,7 +246,7 @@ export class WorkspacePermissionsCacheService { workspaceId, ...(roleIds ? { id: In(roleIds) } : {}), }, - relations: ['objectPermissions'], + relations: ['objectPermissions', 'settingPermissions'], }); const workspaceObjectMetadataCollection = @@ -256,7 +258,7 @@ export class WorkspacePermissionsCacheService { const objectRecordsPermissions: ObjectRecordsPermissions = {}; for (const objectMetadata of workspaceObjectMetadataCollection) { - const { id: objectMetadataId, isSystem } = objectMetadata; + const { id: objectMetadataId, isSystem, standardId } = objectMetadata; let canRead = role.canReadAllObjectRecords; let canUpdate = role.canUpdateAllObjectRecords; @@ -264,32 +266,48 @@ export class WorkspacePermissionsCacheService { let canDestroy = role.canDestroyAllObjectRecords; if (isPermissionsV2Enabled) { - const objectRecordPermissionsOverride = role.objectPermissions.find( - (objectPermission) => - objectPermission.objectMetadataId === objectMetadataId, - ); + if ( + standardId && + [ + STANDARD_OBJECT_IDS.workflow, + STANDARD_OBJECT_IDS.workflowRun, + STANDARD_OBJECT_IDS.workflowVersion, + ].includes(standardId) + ) { + const hasWorkflowsPermissions = this.hasWorkflowsPermissions(role); - const getPermissionValue = ( - overrideValue: boolean | undefined, - defaultValue: boolean, - ) => (isSystem ? true : (overrideValue ?? defaultValue)); + canRead = hasWorkflowsPermissions; + canUpdate = hasWorkflowsPermissions; + canSoftDelete = hasWorkflowsPermissions; + canDestroy = hasWorkflowsPermissions; + } else { + const objectRecordPermissionsOverride = role.objectPermissions.find( + (objectPermission) => + objectPermission.objectMetadataId === objectMetadataId, + ); - canRead = getPermissionValue( - objectRecordPermissionsOverride?.canReadObjectRecords, - canRead, - ); - canUpdate = getPermissionValue( - objectRecordPermissionsOverride?.canUpdateObjectRecords, - canUpdate, - ); - canSoftDelete = getPermissionValue( - objectRecordPermissionsOverride?.canSoftDeleteObjectRecords, - canSoftDelete, - ); - canDestroy = getPermissionValue( - objectRecordPermissionsOverride?.canDestroyObjectRecords, - canDestroy, - ); + const getPermissionValue = ( + overrideValue: boolean | undefined, + defaultValue: boolean, + ) => (isSystem ? true : (overrideValue ?? defaultValue)); + + canRead = getPermissionValue( + objectRecordPermissionsOverride?.canReadObjectRecords, + canRead, + ); + canUpdate = getPermissionValue( + objectRecordPermissionsOverride?.canUpdateObjectRecords, + canUpdate, + ); + canSoftDelete = getPermissionValue( + objectRecordPermissionsOverride?.canSoftDeleteObjectRecords, + canSoftDelete, + ); + canDestroy = getPermissionValue( + objectRecordPermissionsOverride?.canDestroyObjectRecords, + canDestroy, + ); + } } objectRecordsPermissions[objectMetadataId] = { @@ -298,9 +316,9 @@ export class WorkspacePermissionsCacheService { canSoftDelete, canDestroy, }; - } - permissionsByRoleId[role.id] = objectRecordsPermissions; + permissionsByRoleId[role.id] = objectRecordsPermissions; + } } return permissionsByRoleId; @@ -313,7 +331,7 @@ export class WorkspacePermissionsCacheService { where: { workspaceId, }, - select: ['id', 'isSystem'], + select: ['id', 'isSystem', 'standardId'], }); return workspaceObjectMetadata; @@ -336,4 +354,19 @@ export class WorkspacePermissionsCacheService { return acc; }, {} as UserWorkspaceRoleMap); } + + private hasWorkflowsPermissions(role: RoleEntity): boolean { + const hasWorkflowsPermissionFromRole = role.canUpdateAllSettings; + const hasWorkflowsPermissionsFromSettingPermissions = isDefined( + role.settingPermissions.find( + (settingPermission) => + settingPermission.setting === SettingPermissionType.WORKFLOWS, + ), + ); + + return ( + hasWorkflowsPermissionFromRole || + hasWorkflowsPermissionsFromSettingPermissions + ); + } } diff --git a/packages/twenty-server/src/modules/workflow/common/query-hooks/workflow-version-delete-one.pre-query.hook.ts b/packages/twenty-server/src/modules/workflow/common/query-hooks/workflow-version-delete-one.pre-query.hook.ts index db24ddcf8..af67f09df 100644 --- a/packages/twenty-server/src/modules/workflow/common/query-hooks/workflow-version-delete-one.pre-query.hook.ts +++ b/packages/twenty-server/src/modules/workflow/common/query-hooks/workflow-version-delete-one.pre-query.hook.ts @@ -14,11 +14,14 @@ export class WorkflowVersionDeleteOnePreQueryHook ) {} async execute( - _authContext: AuthContext, + authContext: AuthContext, _objectName: string, payload: DeleteOneResolverArgs, ): Promise { + const { workspace } = authContext; + await this.workflowVersionValidationWorkspaceService.validateWorkflowVersionForDeleteOne( + workspace.id, payload, ); diff --git a/packages/twenty-server/src/modules/workflow/common/query-hooks/workflow-version-update-one.pre-query.hook.ts b/packages/twenty-server/src/modules/workflow/common/query-hooks/workflow-version-update-one.pre-query.hook.ts index acd50865a..0eb63902f 100644 --- a/packages/twenty-server/src/modules/workflow/common/query-hooks/workflow-version-update-one.pre-query.hook.ts +++ b/packages/twenty-server/src/modules/workflow/common/query-hooks/workflow-version-update-one.pre-query.hook.ts @@ -15,12 +15,17 @@ export class WorkflowVersionUpdateOnePreQueryHook ) {} async execute( - _authContext: AuthContext, + authContext: AuthContext, _objectName: string, payload: UpdateOneResolverArgs, ): Promise> { + const { workspace } = authContext; + await this.workflowVersionValidationWorkspaceService.validateWorkflowVersionForUpdateOne( - payload, + { + workspaceId: workspace.id, + payload, + }, ); return payload; diff --git a/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-common.workspace-service.ts b/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-common.workspace-service.ts index 8d90c86b9..7b7fa534a 100644 --- a/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-common.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-common.workspace-service.ts @@ -1,10 +1,11 @@ import { Injectable } from '@nestjs/common'; +import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service'; import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util'; import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; -import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; import { WorkflowCommonException, @@ -18,7 +19,6 @@ import { WorkflowTriggerException, WorkflowTriggerExceptionCode, } from 'src/modules/workflow/workflow-trigger/exceptions/workflow-trigger.exception'; -import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service'; export type ObjectMetadataInfo = { objectMetadataItemWithFieldsMaps: ObjectMetadataItemWithFieldMaps; @@ -28,14 +28,18 @@ export type ObjectMetadataInfo = { @Injectable() export class WorkflowCommonWorkspaceService { constructor( - private readonly twentyORMManager: TwentyORMManager, + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, private readonly serverlessFunctionService: ServerlessFunctionService, private readonly workspaceCacheStorageService: WorkspaceCacheStorageService, ) {} - async getWorkflowVersionOrFail( - workflowVersionId: string, - ): Promise { + async getWorkflowVersionOrFail({ + workspaceId, + workflowVersionId, + }: { + workspaceId: string; + workflowVersionId: string; + }): Promise { if (!workflowVersionId) { throw new WorkflowTriggerException( 'Workflow version ID is required', @@ -44,8 +48,10 @@ export class WorkflowCommonWorkspaceService { } const workflowVersionRepository = - await this.twentyORMManager.getRepository( + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, 'workflowVersion', + { shouldBypassPermissionChecks: true }, // settings permissions are checked at resolver-level ); const workflowVersion = await workflowVersionRepository.findOne({ @@ -124,18 +130,24 @@ export class WorkflowCommonWorkspaceService { operation: 'restore' | 'delete' | 'destroy'; }): Promise { const workflowVersionRepository = - await this.twentyORMManager.getRepository( + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, 'workflowVersion', + { shouldBypassPermissionChecks: true }, // settings permissions are checked at resolver-level ); const workflowRunRepository = - await this.twentyORMManager.getRepository( + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, 'workflowRun', + { shouldBypassPermissionChecks: true }, // settings permissions are checked at resolver-level ); const workflowAutomatedTriggerRepository = - await this.twentyORMManager.getRepository( + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, 'workflowAutomatedTrigger', + { shouldBypassPermissionChecks: true }, // settings permissions are checked at resolver-level ); for (const workflowId of workflowIds) { diff --git a/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-version-validation.workspace-service.ts b/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-version-validation.workspace-service.ts index 94b4291db..9d005dd2f 100644 --- a/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-version-validation.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-version-validation.workspace-service.ts @@ -8,7 +8,7 @@ import { UpdateOneResolverArgs, } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; -import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { WorkflowQueryValidationException, WorkflowQueryValidationExceptionCode, @@ -24,10 +24,11 @@ import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/work export class WorkflowVersionValidationWorkspaceService { constructor( private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService, - private readonly twentyORMManager: TwentyORMManager, + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, ) {} async validateWorkflowVersionForCreateOne( + workspaceId: string, payload: CreateOneResolverArgs, ) { if ( @@ -41,8 +42,10 @@ export class WorkflowVersionValidationWorkspaceService { } const workflowVersionRepository = - await this.twentyORMManager.getRepository( + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, 'workflowVersion', + { shouldBypassPermissionChecks: true }, // settings permissions are checked at resolver-level ); const workflowAlreadyHasDraftVersion = @@ -63,13 +66,18 @@ export class WorkflowVersionValidationWorkspaceService { } } - async validateWorkflowVersionForUpdateOne( - payload: UpdateOneResolverArgs, - ) { + async validateWorkflowVersionForUpdateOne({ + workspaceId, + payload, + }: { + workspaceId: string; + payload: UpdateOneResolverArgs; + }) { const workflowVersion = - await this.workflowCommonWorkspaceService.getWorkflowVersionOrFail( - payload.id, - ); + await this.workflowCommonWorkspaceService.getWorkflowVersionOrFail({ + workspaceId, + workflowVersionId: payload.id, + }); // If the only field updated is the name, we can update the workflow version // Otherwise, we need to assert that the workflow version is a draft @@ -93,17 +101,23 @@ export class WorkflowVersionValidationWorkspaceService { } } - async validateWorkflowVersionForDeleteOne(payload: DeleteOneResolverArgs) { + async validateWorkflowVersionForDeleteOne( + workspaceId: string, + payload: DeleteOneResolverArgs, + ) { const workflowVersion = - await this.workflowCommonWorkspaceService.getWorkflowVersionOrFail( - payload.id, - ); + await this.workflowCommonWorkspaceService.getWorkflowVersionOrFail({ + workspaceId, + workflowVersionId: payload.id, + }); assertWorkflowVersionIsDraft(workflowVersion); const workflowVersionRepository = - await this.twentyORMManager.getRepository( + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, 'workflowVersion', + { shouldBypassPermissionChecks: true }, // settings permissions are checked at resolver-level ); const otherWorkflowVersionsExist = await workflowVersionRepository.exists({ diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-step/workflow-version-step.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-step/workflow-version-step.workspace-service.ts index 01c3b1a47..6a9d70229 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-step/workflow-version-step.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-step/workflow-version-step.workspace-service.ts @@ -10,7 +10,7 @@ import { CreateWorkflowVersionStepInput } from 'src/engine/core-modules/workflow import { WorkflowActionDTO } from 'src/engine/core-modules/workflow/dtos/workflow-step.dto'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service'; -import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { WorkflowVersionStepException, WorkflowVersionStepExceptionCode, @@ -47,7 +47,7 @@ const BASE_STEP_DEFINITION: BaseWorkflowActionSettings = { @Injectable() export class WorkflowVersionStepWorkspaceService { constructor( - private readonly twentyORMManager: TwentyORMManager, + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, private readonly workflowSchemaWorkspaceService: WorkflowSchemaWorkspaceService, private readonly serverlessFunctionService: ServerlessFunctionService, @InjectRepository(ObjectMetadataEntity, 'core') @@ -74,7 +74,8 @@ export class WorkflowVersionStepWorkspaceService { workspaceId, }); const workflowVersionRepository = - await this.twentyORMManager.getRepository( + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, 'workflowVersion', ); @@ -118,8 +119,10 @@ export class WorkflowVersionStepWorkspaceService { step: WorkflowAction; }): Promise { const workflowVersionRepository = - await this.twentyORMManager.getRepository( + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, 'workflowVersion', + { shouldBypassPermissionChecks: true }, ); const workflowVersion = await workflowVersionRepository.findOne({ @@ -174,8 +177,10 @@ export class WorkflowVersionStepWorkspaceService { stepIdToDelete: string; }): Promise { const workflowVersionRepository = - await this.twentyORMManager.getRepository( + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, 'workflowVersion', + { shouldBypassPermissionChecks: true }, ); const workflowVersion = await workflowVersionRepository.findOne({ @@ -279,9 +284,10 @@ export class WorkflowVersionStepWorkspaceService { response: object; }) { const workflowRun = - await this.workflowRunWorkspaceService.getWorkflowRunOrFail( + await this.workflowRunWorkspaceService.getWorkflowRunOrFail({ workflowRunId, - ); + workspaceId, + }); const step = workflowRun.output?.flow?.steps?.find( (step) => step.id === stepId, @@ -302,6 +308,7 @@ export class WorkflowVersionStepWorkspaceService { } const enrichedResponse = await this.enrichFormStepResponse({ + workspaceId, step, response, }); @@ -319,6 +326,7 @@ export class WorkflowVersionStepWorkspaceService { }; await this.workflowRunWorkspaceService.saveWorkflowRunState({ + workspaceId, workflowRunId, stepOutput: newStepOutput, context: updatedContext, @@ -554,9 +562,11 @@ export class WorkflowVersionStepWorkspaceService { } private async enrichFormStepResponse({ + workspaceId, step, response, }: { + workspaceId: string; step: WorkflowFormAction; response: object; }) { @@ -580,9 +590,11 @@ export class WorkflowVersionStepWorkspaceService { // @ts-expect-error legacy noImplicitAny isValidUuid(response[key].id) ) { - const repository = await this.twentyORMManager.getRepository( - field.settings.objectName, - ); + const repository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, + field.settings.objectName, + ); const record = await repository.findOne({ // @ts-expect-error legacy noImplicitAny diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version/workflow-version.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version/workflow-version.workspace-service.ts index b6e09645d..0c23af39c 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version/workflow-version.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version/workflow-version.workspace-service.ts @@ -7,7 +7,7 @@ import { Repository } from 'typeorm'; import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; import { RecordPositionService } from 'src/engine/core-modules/record-position/services/record-position.service'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; import { WorkflowVersionStepException, @@ -26,7 +26,7 @@ import { WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow- @Injectable() export class WorkflowVersionWorkspaceService { constructor( - private readonly twentyORMManager: TwentyORMManager, + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, private readonly workflowVersionStepWorkspaceService: WorkflowVersionStepWorkspaceService, @InjectRepository(ObjectMetadataEntity, 'core') private readonly objectMetadataRepository: Repository, @@ -44,8 +44,10 @@ export class WorkflowVersionWorkspaceService { workflowVersionIdToCopy: string; }) { const workflowVersionRepository = - await this.twentyORMManager.getRepository( + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, 'workflowVersion', + { shouldBypassPermissionChecks: true }, ); const workflowVersionToCopy = await workflowVersionRepository.findOne({ diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/create-record.workflow-action.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/create-record.workflow-action.ts index d23b53d18..137011409 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/create-record.workflow-action.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/create-record.workflow-action.ts @@ -11,7 +11,7 @@ import { RecordInputTransformerService } from 'src/engine/core-modules/record-tr import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory'; -import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; import { @@ -31,7 +31,7 @@ import { WorkflowCreateRecordActionInput } from 'src/modules/workflow/workflow-e @Injectable() export class CreateRecordWorkflowAction implements WorkflowExecutor { constructor( - private readonly twentyORMManager: TwentyORMManager, + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, @InjectRepository(ObjectMetadataEntity, 'core') private readonly objectMetadataRepository: Repository, private readonly workspaceEventEmitter: WorkspaceEventEmitter, @@ -76,9 +76,12 @@ export class CreateRecordWorkflowAction implements WorkflowExecutor { context, ) as WorkflowCreateRecordActionInput; - const repository = await this.twentyORMManager.getRepository( - workflowActionInput.objectName, - ); + const repository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, + workflowActionInput.objectName, + { shouldBypassPermissionChecks: true }, + ); const objectMetadata = await this.objectMetadataRepository.findOne({ where: { diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/delete-record.workflow-action.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/delete-record.workflow-action.ts index 5c14e5409..904d651e0 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/delete-record.workflow-action.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/delete-record.workflow-action.ts @@ -10,7 +10,7 @@ import { WorkflowExecutor } from 'src/modules/workflow/workflow-executor/interfa import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory'; -import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; import { WorkflowStepExecutorException, @@ -29,7 +29,7 @@ import { WorkflowDeleteRecordActionInput } from 'src/modules/workflow/workflow-e @Injectable() export class DeleteRecordWorkflowAction implements WorkflowExecutor { constructor( - private readonly twentyORMManager: TwentyORMManager, + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, @InjectRepository(ObjectMetadataEntity, 'core') private readonly objectMetadataRepository: Repository, private readonly workspaceEventEmitter: WorkspaceEventEmitter, @@ -72,10 +72,6 @@ export class DeleteRecordWorkflowAction implements WorkflowExecutor { ); } - const repository = await this.twentyORMManager.getRepository( - workflowActionInput.objectName, - ); - const workspaceId = this.scopedWorkspaceContextFactory.create().workspaceId; if (!workspaceId) { @@ -85,6 +81,13 @@ export class DeleteRecordWorkflowAction implements WorkflowExecutor { ); } + const repository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, + workflowActionInput.objectName, + { shouldBypassPermissionChecks: true }, + ); + const objectMetadata = await this.objectMetadataRepository.findOne({ where: { nameSingular: workflowActionInput.objectName, diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/find-records.workflow-action.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/find-records.workflow-action.ts index 71e7bf003..1b9cec496 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/find-records.workflow-action.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/find-records.workflow-action.ts @@ -16,7 +16,7 @@ import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/typ import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory'; import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; -import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; import { @@ -36,7 +36,7 @@ import { WorkflowFindRecordsActionInput } from 'src/modules/workflow/workflow-ex @Injectable() export class FindRecordsWorkflowAction implements WorkflowExecutor { constructor( - private readonly twentyORMManager: TwentyORMManager, + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, private readonly scopedWorkspaceContextFactory: ScopedWorkspaceContextFactory, private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService, ) {} @@ -67,10 +67,6 @@ export class FindRecordsWorkflowAction implements WorkflowExecutor { context, ) as WorkflowFindRecordsActionInput; - const repository = await this.twentyORMManager.getRepository( - workflowActionInput.objectName, - ); - const workspaceId = this.scopedWorkspaceContextFactory.create().workspaceId; if (!workspaceId) { @@ -80,6 +76,13 @@ export class FindRecordsWorkflowAction implements WorkflowExecutor { ); } + const repository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, + workflowActionInput.objectName, + { shouldBypassPermissionChecks: true }, + ); + const { objectMetadataItemWithFieldsMaps, objectMetadataMaps } = await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps( workflowActionInput.objectName, diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/update-record.workflow-action.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/update-record.workflow-action.ts index 116cfe382..eb5a9fee6 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/update-record.workflow-action.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/update-record.workflow-action.ts @@ -12,7 +12,7 @@ import { objectRecordChangedValues } from 'src/engine/core-modules/event-emitter import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory'; -import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { formatData } from 'src/engine/twenty-orm/utils/format-data.util'; import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; @@ -33,7 +33,7 @@ import { WorkflowUpdateRecordActionInput } from 'src/modules/workflow/workflow-e @Injectable() export class UpdateRecordWorkflowAction implements WorkflowExecutor { constructor( - private readonly twentyORMManager: TwentyORMManager, + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, private readonly scopedWorkspaceContextFactory: ScopedWorkspaceContextFactory, @InjectRepository(ObjectMetadataEntity, 'core') private readonly objectMetadataRepository: Repository, @@ -79,10 +79,6 @@ export class UpdateRecordWorkflowAction implements WorkflowExecutor { ); } - const repository = await this.twentyORMManager.getRepository( - workflowActionInput.objectName, - ); - const workspaceId = this.scopedWorkspaceContextFactory.create().workspaceId; if (!workspaceId) { @@ -92,6 +88,13 @@ export class UpdateRecordWorkflowAction implements WorkflowExecutor { ); } + const repository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, + workflowActionInput.objectName, + { shouldBypassPermissionChecks: true }, + ); + const objectMetadata = await this.objectMetadataRepository.findOne({ where: { nameSingular: workflowActionInput.objectName, diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workspace-services/__tests__/workflow-executor.workspace-service.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workspace-services/__tests__/workflow-executor.workspace-service.spec.ts index 5ddad1d8a..86f356051 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workspace-services/__tests__/workflow-executor.workspace-service.spec.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workspace-services/__tests__/workflow-executor.workspace-service.spec.ts @@ -18,7 +18,6 @@ describe('WorkflowExecutorWorkspaceService', () => { let service: WorkflowExecutorWorkspaceService; let workflowExecutorFactory: WorkflowExecutorFactory; let workspaceEventEmitter: WorkspaceEventEmitter; - let scopedWorkspaceContextFactory: ScopedWorkspaceContextFactory; let workflowRunWorkspaceService: WorkflowRunWorkspaceService; const mockWorkflowExecutor = { @@ -86,9 +85,6 @@ describe('WorkflowExecutorWorkspaceService', () => { workspaceEventEmitter = module.get( WorkspaceEventEmitter, ); - scopedWorkspaceContextFactory = module.get( - ScopedWorkspaceContextFactory, - ); workflowRunWorkspaceService = module.get( WorkflowRunWorkspaceService, ); @@ -185,6 +181,7 @@ describe('WorkflowExecutorWorkspaceService', () => { data: 'some-data', 'step-1': { stepOutput: 'success' }, }, + workspaceId: 'workspace-id', }); expect(result).toEqual({ result: { success: true } }); @@ -221,6 +218,7 @@ describe('WorkflowExecutorWorkspaceService', () => { }, }, context: mockContext, + workspaceId: 'workspace-id', }); }); @@ -248,6 +246,7 @@ describe('WorkflowExecutorWorkspaceService', () => { output: mockPendingEvent, }, context: mockContext, + workspaceId: 'workspace-id', }); // No recursive call to execute should happen @@ -304,6 +303,7 @@ describe('WorkflowExecutorWorkspaceService', () => { }, }, context: mockContext, + workspaceId: 'workspace-id', }); expect(result).toEqual({ result: { success: true } }); @@ -387,6 +387,7 @@ describe('WorkflowExecutorWorkspaceService', () => { output: errorOutput, }, context: mockContext, + workspaceId: 'workspace-id', }); expect(result).toEqual(errorOutput); }); @@ -414,6 +415,7 @@ describe('WorkflowExecutorWorkspaceService', () => { }, }, context: mockContext, + workspaceId: 'workspace-id', }); expect(result).toEqual({ error: BILLING_WORKFLOW_EXECUTION_ERROR_MESSAGE, @@ -423,9 +425,8 @@ describe('WorkflowExecutorWorkspaceService', () => { describe('sendWorkflowNodeRunEvent', () => { it('should emit a billing event', () => { - service['sendWorkflowNodeRunEvent'](); + service['sendWorkflowNodeRunEvent']('workspace-id'); - expect(scopedWorkspaceContextFactory.create).toHaveBeenCalled(); expect(workspaceEventEmitter.emitCustomBatchEvent).toHaveBeenCalledWith( BILLING_FEATURE_USED, [ @@ -437,24 +438,5 @@ describe('WorkflowExecutorWorkspaceService', () => { 'workspace-id', ); }); - - it('should handle missing workspace ID', () => { - mockScopedWorkspaceContextFactory.create.mockReturnValueOnce({ - workspaceId: null, - }); - - service['sendWorkflowNodeRunEvent'](); - - expect(workspaceEventEmitter.emitCustomBatchEvent).toHaveBeenCalledWith( - BILLING_FEATURE_USED, - [ - { - eventName: BillingMeterEventName.WORKFLOW_NODE_RUN, - value: 1, - }, - ], - '', - ); - }); }); }); diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workspace-services/workflow-executor.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workspace-services/workflow-executor.workspace-service.ts index e3d2edbac..ef5e781dd 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workspace-services/workflow-executor.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workspace-services/workflow-executor.workspace-service.ts @@ -21,6 +21,10 @@ import { WorkflowExecutorFactory } from 'src/modules/workflow/workflow-executor/ import { WorkflowExecutorInput } from 'src/modules/workflow/workflow-executor/types/workflow-executor-input'; import { WorkflowExecutorOutput } from 'src/modules/workflow/workflow-executor/types/workflow-executor-output.type'; import { WorkflowRunWorkspaceService } from 'src/modules/workflow/workflow-runner/workflow-run/workflow-run.workspace-service'; +import { + WorkflowTriggerException, + WorkflowTriggerExceptionCode, +} from 'src/modules/workflow/workflow-trigger/exceptions/workflow-trigger.exception'; const MAX_RETRIES_ON_FAILURE = 3; @@ -58,15 +62,25 @@ export class WorkflowExecutorWorkspaceService implements WorkflowExecutor { let actionOutput: WorkflowExecutorOutput; + const { workspaceId } = this.scopedWorkspaceContextFactory.create(); + + if (!workspaceId) { + throw new WorkflowTriggerException( + 'No workspace id found', + WorkflowTriggerExceptionCode.INTERNAL_ERROR, + ); + } + if ( this.billingService.isBillingEnabled() && - !(await this.canBillWorkflowNodeExecution()) + !(await this.canBillWorkflowNodeExecution(workspaceId)) ) { const billingOutput = { error: BILLING_WORKFLOW_EXECUTION_ERROR_MESSAGE, }; await this.workflowRunWorkspaceService.saveWorkflowRunState({ + workspaceId, workflowRunId, stepOutput: { id: step.id, @@ -93,7 +107,7 @@ export class WorkflowExecutorWorkspaceService implements WorkflowExecutor { } if (!actionOutput.error) { - this.sendWorkflowNodeRunEvent(); + this.sendWorkflowNodeRunEvent(workspaceId); } const stepOutput: StepOutput = { @@ -106,6 +120,7 @@ export class WorkflowExecutorWorkspaceService implements WorkflowExecutor { workflowRunId, stepOutput, context, + workspaceId, }); return actionOutput; @@ -127,6 +142,7 @@ export class WorkflowExecutorWorkspaceService implements WorkflowExecutor { workflowRunId, stepOutput, context: updatedContext, + workspaceId, }); if (!isDefined(step.nextStepIds?.[0])) { @@ -159,15 +175,13 @@ export class WorkflowExecutorWorkspaceService implements WorkflowExecutor { workflowRunId, stepOutput, context, + workspaceId, }); return actionOutput; } - private sendWorkflowNodeRunEvent() { - const workspaceId = - this.scopedWorkspaceContextFactory.create().workspaceId ?? ''; - + private sendWorkflowNodeRunEvent(workspaceId: string) { this.workspaceEventEmitter.emitCustomBatchEvent( BILLING_FEATURE_USED, [ @@ -180,10 +194,7 @@ export class WorkflowExecutorWorkspaceService implements WorkflowExecutor { ); } - private async canBillWorkflowNodeExecution() { - const workspaceId = - this.scopedWorkspaceContextFactory.create().workspaceId ?? ''; - + private async canBillWorkflowNodeExecution(workspaceId: string) { return this.billingService.canBillMeteredProduct( workspaceId, BillingProductKey.WORKFLOW_NODE_EXECUTION, diff --git a/packages/twenty-server/src/modules/workflow/workflow-runner/jobs/run-workflow.job.ts b/packages/twenty-server/src/modules/workflow/workflow-runner/jobs/run-workflow.job.ts index 3bbe7fb88..fd60c96e8 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-runner/jobs/run-workflow.job.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-runner/jobs/run-workflow.job.ts @@ -37,21 +37,25 @@ export class RunWorkflowJob { workflowRunId, payload, lastExecutedStepId, + workspaceId, }: RunWorkflowJobData): Promise { try { if (lastExecutedStepId) { await this.resumeWorkflowExecution({ + workspaceId, workflowRunId, lastExecutedStepId, }); } else { await this.startWorkflowExecution({ workflowRunId, + workspaceId, payload: payload ?? {}, }); } } catch (error) { await this.workflowRunWorkspaceService.endWorkflowRun({ + workspaceId, workflowRunId, status: WorkflowRunStatus.FAILED, error: error.message, @@ -61,9 +65,11 @@ export class RunWorkflowJob { private async startWorkflowExecution({ workflowRunId, + workspaceId, payload, }: { workflowRunId: string; + workspaceId: string; payload: object; }): Promise { const context = { @@ -71,14 +77,16 @@ export class RunWorkflowJob { }; const workflowRun = - await this.workflowRunWorkspaceService.getWorkflowRunOrFail( + await this.workflowRunWorkspaceService.getWorkflowRunOrFail({ workflowRunId, - ); + workspaceId, + }); const workflowVersion = - await this.workflowCommonWorkspaceService.getWorkflowVersionOrFail( - workflowRun.workflowVersionId, - ); + await this.workflowCommonWorkspaceService.getWorkflowVersionOrFail({ + workspaceId, + workflowVersionId: workflowRun.workflowVersionId, + }); if (!workflowVersion.trigger || !workflowVersion.steps) { throw new WorkflowRunException( @@ -89,6 +97,7 @@ export class RunWorkflowJob { await this.workflowRunWorkspaceService.startWorkflowRun({ workflowRunId, + workspaceId, context, output: { flow: { @@ -110,20 +119,24 @@ export class RunWorkflowJob { currentStepId: workflowVersion.steps[0].id, steps: workflowVersion.steps, context, + workspaceId, }); } private async resumeWorkflowExecution({ workflowRunId, lastExecutedStepId, + workspaceId, }: { workflowRunId: string; lastExecutedStepId: string; + workspaceId: string; }): Promise { const workflowRun = - await this.workflowRunWorkspaceService.getWorkflowRunOrFail( + await this.workflowRunWorkspaceService.getWorkflowRunOrFail({ workflowRunId, - ); + workspaceId, + }); if (workflowRun.status !== WorkflowRunStatus.RUNNING) { throw new WorkflowRunException( @@ -148,6 +161,7 @@ export class RunWorkflowJob { if (!nextStepId) { await this.workflowRunWorkspaceService.endWorkflowRun({ workflowRunId, + workspaceId, status: WorkflowRunStatus.COMPLETED, }); @@ -159,6 +173,7 @@ export class RunWorkflowJob { currentStepId: nextStepId, steps: workflowRun.output?.flow?.steps ?? [], context: workflowRun.context ?? {}, + workspaceId, }); } @@ -167,12 +182,14 @@ export class RunWorkflowJob { currentStepId, steps, context, + workspaceId, }: { workflowRunId: string; currentStepId: string; steps: WorkflowAction[]; // eslint-disable-next-line @typescript-eslint/no-explicit-any context: Record; + workspaceId: string; }) { const { error, pendingEvent } = await this.workflowExecutorWorkspaceService.execute({ @@ -188,6 +205,7 @@ export class RunWorkflowJob { await this.workflowRunWorkspaceService.endWorkflowRun({ workflowRunId, + workspaceId, status: error ? WorkflowRunStatus.FAILED : WorkflowRunStatus.COMPLETED, error, }); diff --git a/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-run/workflow-run.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-run/workflow-run.workspace-service.ts index e54a3f4f1..dab4205e7 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-run/workflow-run.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-runner/workflow-run/workflow-run.workspace-service.ts @@ -9,7 +9,7 @@ import { RecordPositionService } from 'src/engine/core-modules/record-position/s import { ActorMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory'; -import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; import { StepOutput, @@ -17,7 +17,6 @@ import { WorkflowRunStatus, WorkflowRunWorkspaceEntity, } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity'; -import { WorkflowWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity'; import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; import { WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type'; import { @@ -28,7 +27,7 @@ import { @Injectable() export class WorkflowRunWorkspaceService { constructor( - private readonly twentyORMManager: TwentyORMManager, + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService, private readonly scopedWorkspaceContextFactory: ScopedWorkspaceContextFactory, private readonly workspaceEventEmitter: WorkspaceEventEmitter, @@ -44,19 +43,34 @@ export class WorkflowRunWorkspaceService { workflowVersionId: string; createdBy: ActorMetadata; }) { + const workspaceId = + this.scopedWorkspaceContextFactory.create()?.workspaceId; + + if (!workspaceId) { + throw new WorkflowRunException( + 'Workspace id is invalid', + WorkflowRunExceptionCode.WORKFLOW_RUN_INVALID, + ); + } + const workflowRunRepository = - await this.twentyORMManager.getRepository( + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, 'workflowRun', + { shouldBypassPermissionChecks: true }, ); const workflowVersion = - await this.workflowCommonWorkspaceService.getWorkflowVersionOrFail( + await this.workflowCommonWorkspaceService.getWorkflowVersionOrFail({ + workspaceId, workflowVersionId, - ); + }); const workflowRepository = - await this.twentyORMManager.getRepository( + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, 'workflow', + { shouldBypassPermissionChecks: true }, ); const workflow = await workflowRepository.findOne({ @@ -78,16 +92,6 @@ export class WorkflowRunWorkspaceService { }, }); - const workspaceId = - this.scopedWorkspaceContextFactory.create()?.workspaceId; - - if (!workspaceId) { - throw new WorkflowRunException( - 'Workspace id is invalid', - WorkflowRunExceptionCode.WORKFLOW_RUN_INVALID, - ); - } - const position = await this.recordPositionService.buildRecordPosition({ value: 'first', objectMetadata: { @@ -111,17 +115,21 @@ export class WorkflowRunWorkspaceService { async startWorkflowRun({ workflowRunId, + workspaceId, context, output, }: { workflowRunId: string; + workspaceId: string; // eslint-disable-next-line @typescript-eslint/no-explicit-any context: Record; output: WorkflowRunOutput; }) { const workflowRunRepository = - await this.twentyORMManager.getRepository( + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, 'workflowRun', + { shouldBypassPermissionChecks: true }, ); const workflowRunToUpdate = await workflowRunRepository.findOneBy({ @@ -159,16 +167,20 @@ export class WorkflowRunWorkspaceService { async endWorkflowRun({ workflowRunId, + workspaceId, status, error, }: { workflowRunId: string; + workspaceId: string; status: WorkflowRunStatus; error?: string; }) { const workflowRunRepository = - await this.twentyORMManager.getRepository( + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, 'workflowRun', + { shouldBypassPermissionChecks: true }, ); const workflowRunToUpdate = await workflowRunRepository.findOneBy({ @@ -202,16 +214,20 @@ export class WorkflowRunWorkspaceService { async saveWorkflowRunState({ workflowRunId, stepOutput, + workspaceId, context, }: { workflowRunId: string; stepOutput: StepOutput; + workspaceId: string; // eslint-disable-next-line @typescript-eslint/no-explicit-any context: Record; }) { const workflowRunRepository = - await this.twentyORMManager.getRepository( + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, 'workflowRun', + { shouldBypassPermissionChecks: true }, ); const workflowRunToUpdate = await workflowRunRepository.findOneBy({ @@ -250,13 +266,17 @@ export class WorkflowRunWorkspaceService { async updateWorkflowRunStep({ workflowRunId, step, + workspaceId, }: { workflowRunId: string; step: WorkflowAction; + workspaceId: string; }) { const workflowRunRepository = - await this.twentyORMManager.getRepository( + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, 'workflowRun', + { shouldBypassPermissionChecks: true }, ); const workflowRunToUpdate = await workflowRunRepository.findOneBy({ @@ -302,12 +322,18 @@ export class WorkflowRunWorkspaceService { }); } - async getWorkflowRunOrFail( - workflowRunId: string, - ): Promise { + async getWorkflowRunOrFail({ + workflowRunId, + workspaceId, + }: { + workflowRunId: string; + workspaceId: string; + }): Promise { const workflowRunRepository = - await this.twentyORMManager.getRepository( + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, 'workflowRun', + { shouldBypassPermissionChecks: true }, ); const workflowRun = await workflowRunRepository.findOne({ @@ -353,8 +379,10 @@ export class WorkflowRunWorkspaceService { } const workflowRunRepository = - await this.twentyORMManager.getRepository( + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, 'workflowRun', + { shouldBypassPermissionChecks: true }, ); const workflowRunAfter = await workflowRunRepository.findOneBy({ diff --git a/packages/twenty-server/src/modules/workflow/workflow-status/jobs/__tests__/workflow-statuses-update.job.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-status/jobs/__tests__/workflow-statuses-update.job.spec.ts index 28f44ab48..1b5ae934e 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-status/jobs/__tests__/workflow-statuses-update.job.spec.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-status/jobs/__tests__/workflow-statuses-update.job.spec.ts @@ -4,7 +4,7 @@ import { getRepositoryToken } from '@nestjs/typeorm'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service'; -import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; import { WorkflowVersionStatus } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity'; import { WorkflowStatus } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity'; @@ -28,17 +28,25 @@ describe('WorkflowStatusesUpdate', () => { update: jest.fn(), }; - const mockTwentyORMManager = { - getRepository: jest.fn().mockImplementation((entity) => { - if (entity === 'workflow') { - return Promise.resolve(mockWorkflowRepository); - } - if (entity === 'workflowVersion') { - return Promise.resolve(mockWorkflowVersionRepository); - } + const mockTwentyORMGlobalManager = { + getRepositoryForWorkspace: jest + .fn() + .mockImplementation((_workspaceId, entity, options) => { + if (!options?.shouldBypassPermissionChecks) { + throw new Error( + 'Permission check will fail because job runners dont have permissions', + ); + } - return Promise.resolve(null); - }), + if (entity === 'workflow') { + return Promise.resolve(mockWorkflowRepository); + } + if (entity === 'workflowVersion') { + return Promise.resolve(mockWorkflowVersionRepository); + } + + return Promise.resolve(null); + }), }; const mockServerlessFunctionService = { @@ -55,8 +63,8 @@ describe('WorkflowStatusesUpdate', () => { providers: [ WorkflowStatusesUpdateJob, { - provide: TwentyORMManager, - useValue: mockTwentyORMManager, + provide: TwentyORMGlobalManager, + useValue: mockTwentyORMGlobalManager, }, { provide: ServerlessFunctionService, diff --git a/packages/twenty-server/src/modules/workflow/workflow-status/jobs/workflow-statuses-update.job.ts b/packages/twenty-server/src/modules/workflow/workflow-status/jobs/workflow-statuses-update.job.ts index 58e4e6871..44a1b55a7 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-status/jobs/workflow-statuses-update.job.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-status/jobs/workflow-statuses-update.job.ts @@ -14,7 +14,7 @@ import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless import { ServerlessFunctionExceptionCode } from 'src/engine/metadata-modules/serverless-function/serverless-function.exception'; import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service'; import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; -import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; import { WorkflowVersionStatus, @@ -71,7 +71,7 @@ export class WorkflowStatusesUpdateJob { protected readonly logger = new Logger(WorkflowStatusesUpdateJob.name); constructor( - private readonly twentyORMManager: TwentyORMManager, + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, private readonly serverlessFunctionService: ServerlessFunctionService, private readonly workspaceEventEmitter: WorkspaceEventEmitter, @InjectRepository(ObjectMetadataEntity, 'core') @@ -128,13 +128,17 @@ export class WorkflowStatusesUpdateJob { workspaceId: string; }): Promise { const workflowRepository = - await this.twentyORMManager.getRepository( + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, 'workflow', + { shouldBypassPermissionChecks: true }, ); const workflowVersionRepository = - await this.twentyORMManager.getRepository( + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, 'workflowVersion', + { shouldBypassPermissionChecks: true }, ); const newWorkflowStatuses = await this.getWorkflowStatuses({ @@ -248,13 +252,17 @@ export class WorkflowStatusesUpdateJob { workspaceId: string; }): Promise { const workflowRepository = - await this.twentyORMManager.getRepository( + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, 'workflow', + { shouldBypassPermissionChecks: true }, ); const workflowVersionRepository = - await this.twentyORMManager.getRepository( + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, 'workflowVersion', + { shouldBypassPermissionChecks: true }, ); const workflow = await workflowRepository.findOneOrFail({ diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/listeners/database-event-trigger.listener.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/listeners/database-event-trigger.listener.ts index 5b09608ed..be4055aeb 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/listeners/database-event-trigger.listener.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/automated-trigger/listeners/database-event-trigger.listener.ts @@ -206,6 +206,7 @@ export class DatabaseEventTriggerListener { await this.twentyORMGlobalManager.getRepositoryForWorkspace( workspaceId, relatedObjectMetadataNameSingular, + { shouldBypassPermissionChecks: true }, ); record[joinField.name] = await relatedObjectRepository.findOne({ @@ -254,6 +255,7 @@ export class DatabaseEventTriggerListener { await this.twentyORMGlobalManager.getRepositoryForWorkspace( workspaceId, automatedTriggerTableName, + { shouldBypassPermissionChecks: true }, ); const eventListeners = await workflowAutomatedTriggerRepository.find({ diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/jobs/workflow-trigger.job.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/jobs/workflow-trigger.job.ts index 816246931..53d7ec1d8 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-trigger/jobs/workflow-trigger.job.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/jobs/workflow-trigger.job.ts @@ -10,7 +10,7 @@ import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queu import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; import { handleWorkflowTriggerException } from 'src/engine/core-modules/workflow/filters/workflow-trigger-graphql-api-exception.filter'; import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; -import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { WorkflowVersionStatus, WorkflowVersionWorkspaceEntity, @@ -33,7 +33,7 @@ const DEFAULT_WORKFLOW_NAME = 'Workflow'; @Processor({ queueName: MessageQueue.workflowQueue, scope: Scope.REQUEST }) export class WorkflowTriggerJob { constructor( - private readonly twentyORMManager: TwentyORMManager, + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, private readonly workflowRunnerWorkspaceService: WorkflowRunnerWorkspaceService, @InjectMessageQueue(MessageQueue.workflowQueue) private readonly messageQueueService: MessageQueueService, @@ -43,8 +43,10 @@ export class WorkflowTriggerJob { async handle(data: WorkflowTriggerJobData): Promise { try { const workflowRepository = - await this.twentyORMManager.getRepository( + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + data.workspaceId, 'workflow', + { shouldBypassPermissionChecks: true }, ); const workflow = await workflowRepository.findOneBy({ @@ -66,8 +68,10 @@ export class WorkflowTriggerJob { } const workflowVersionRepository = - await this.twentyORMManager.getRepository( + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + data.workspaceId, 'workflowVersion', + { shouldBypassPermissionChecks: true }, ); const workflowVersion = await workflowVersionRepository.findOneBy({ diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service.ts index 680f219b1..bc66d5a05 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service.ts @@ -9,7 +9,7 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat import { WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory'; import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; -import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; import { AutomatedTriggerType } from 'src/modules/workflow/common/standard-objects/workflow-automated-trigger.workspace-entity'; import { @@ -36,7 +36,7 @@ import { assertNever } from 'src/utils/assert'; @Injectable() export class WorkflowTriggerWorkspaceService { constructor( - private readonly twentyORMManager: TwentyORMManager, + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService, private readonly scopedWorkspaceContextFactory: ScopedWorkspaceContextFactory, private readonly workflowRunnerWorkspaceService: WorkflowRunnerWorkspaceService, @@ -68,9 +68,10 @@ export class WorkflowTriggerWorkspaceService { payload: object; createdBy: ActorMetadata; }) { - await this.workflowCommonWorkspaceService.getWorkflowVersionOrFail( + await this.workflowCommonWorkspaceService.getWorkflowVersionOrFail({ workflowVersionId, - ); + workspaceId: this.getWorkspaceId(), + }); return this.workflowRunnerWorkspaceService.run({ workspaceId: this.getWorkspaceId(), @@ -81,9 +82,12 @@ export class WorkflowTriggerWorkspaceService { } async activateWorkflowVersion(workflowVersionId: string) { + const workspaceId = this.getWorkspaceId(); const workflowVersionRepository = - await this.twentyORMManager.getRepository( + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + this.getWorkspaceId(), 'workflowVersion', + { shouldBypassPermissionChecks: true }, // settings permissions are checked at resolver-level ); const workflowVersionNullable = await workflowVersionRepository.findOne({ @@ -96,8 +100,10 @@ export class WorkflowTriggerWorkspaceService { ); const workflowRepository = - await this.twentyORMManager.getRepository( + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, 'workflow', + { shouldBypassPermissionChecks: true }, // settings permissions are checked at resolver-level ); const workflow = await workflowRepository.findOne({ @@ -113,7 +119,10 @@ export class WorkflowTriggerWorkspaceService { assertVersionCanBeActivated(workflowVersion, workflow); - const workspaceDataSource = await this.twentyORMManager.getDatasource(); + const workspaceDataSource = + await this.twentyORMGlobalManager.getDataSourceForWorkspace({ + workspaceId: this.getWorkspaceId(), + }); const queryRunner = workspaceDataSource.createQueryRunner(); await queryRunner.connect(); @@ -142,7 +151,10 @@ export class WorkflowTriggerWorkspaceService { } async deactivateWorkflowVersion(workflowVersionId: string) { - const workspaceDataSource = await this.twentyORMManager.getDatasource(); + const workspaceDataSource = + await this.twentyORMGlobalManager.getDataSourceForWorkspace({ + workspaceId: this.getWorkspaceId(), + }); const queryRunner = workspaceDataSource.createQueryRunner(); await queryRunner.connect(); @@ -150,8 +162,10 @@ export class WorkflowTriggerWorkspaceService { try { const workflowVersionRepository = - await this.twentyORMManager.getRepository( + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + this.getWorkspaceId(), 'workflowVersion', + { shouldBypassPermissionChecks: true }, ); await this.performDeactivationSteps( diff --git a/packages/twenty-server/test/integration/constants/workflow-gql-fields.constants.ts b/packages/twenty-server/test/integration/constants/workflow-gql-fields.constants.ts new file mode 100644 index 000000000..93c955ab6 --- /dev/null +++ b/packages/twenty-server/test/integration/constants/workflow-gql-fields.constants.ts @@ -0,0 +1,15 @@ +export const WORKFLOW_GQL_FIELDS = ` + id + name + lastPublishedVersionId + statuses + position + createdBy { + source + workspaceMemberId + name + } + createdAt + updatedAt + deletedAt +`; diff --git a/packages/twenty-server/test/integration/graphql/suites/settings-permissions/granular-settings-permissions.ts b/packages/twenty-server/test/integration/graphql/suites/settings-permissions/granular-settings-permissions.integration-spec.ts similarity index 91% rename from packages/twenty-server/test/integration/graphql/suites/settings-permissions/granular-settings-permissions.ts rename to packages/twenty-server/test/integration/graphql/suites/settings-permissions/granular-settings-permissions.integration-spec.ts index f4d0a1150..0af829aef 100644 --- a/packages/twenty-server/test/integration/graphql/suites/settings-permissions/granular-settings-permissions.ts +++ b/packages/twenty-server/test/integration/graphql/suites/settings-permissions/granular-settings-permissions.integration-spec.ts @@ -1,6 +1,7 @@ import { print } from 'graphql'; import request from 'supertest'; import { deleteOneRoleOperationFactory } from 'test/integration/graphql/utils/delete-one-role-operation-factory.util'; +import { destroyOneOperationFactory } from 'test/integration/graphql/utils/destroy-one-operation-factory.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 { updateWorkspaceMemberRole } from 'test/integration/graphql/utils/update-workspace-member-role.util'; @@ -84,7 +85,7 @@ describe('Granular settings permissions', () => { mutation UpsertSettingPermissions { upsertSettingPermissions(upsertSettingPermissionsInput: { roleId: "${customRoleId}" - settingPermissionKeys: [${SettingPermissionType.DATA_MODEL}, ${SettingPermissionType.WORKSPACE}] + settingPermissionKeys: [${SettingPermissionType.DATA_MODEL}, ${SettingPermissionType.WORKSPACE}, ${SettingPermissionType.WORKFLOWS}] }) { id setting @@ -243,6 +244,48 @@ describe('Granular settings permissions', () => { }); }); + describe('Workflows Permissions', () => { + it('should allow access to workflows operations when user has WORKFLOWS setting permission', async () => { + // Test creating a workflow (requires WORKFLOWS permission) + const createWorkflowQuery = { + query: ` + mutation CreateWorkflow { + createWorkflow(data: { + name: "Test Workflow" + }) { + id + name + } + } + `, + }; + + const response = await client + .post('/graphql') + .set('Authorization', `Bearer ${MEMBER_ACCESS_TOKEN}`) + .send(createWorkflowQuery); + + expect(response.status).toBe(200); + expect(response.body.errors).toBeUndefined(); + expect(response.body.data.createWorkflow).toBeDefined(); + expect(response.body.data.createWorkflow.name).toBe('Test Workflow'); + + // Clean up - delete the created workflow + const graphqlOperation = destroyOneOperationFactory({ + objectMetadataSingularName: 'workflow', + gqlFields: ` + id + `, + recordId: response.body.data.createWorkflow.id, + }); + + await client + .post('/graphql') + .set('Authorization', `Bearer ${ADMIN_ACCESS_TOKEN}`) + .send(graphqlOperation); + }); + }); + describe('Denied Permissions', () => { it('should deny access to roles operations when user does not have ROLES setting permission', async () => { // Test creating a role (requires ROLES permission, which our custom role doesn't have) @@ -354,7 +397,7 @@ describe('Granular settings permissions', () => { expect(customRole).toBeDefined(); expect(customRole.canUpdateAllSettings).toBe(false); - expect(customRole.settingPermissions).toHaveLength(2); + expect(customRole.settingPermissions).toHaveLength(3); expect( customRole.settingPermissions.map((p: any) => p.setting), ).toContain(SettingPermissionType.DATA_MODEL); diff --git a/packages/twenty-server/test/integration/graphql/suites/settings-permissions/workflows.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/settings-permissions/workflows.integration-spec.ts new file mode 100644 index 000000000..e07afe837 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/settings-permissions/workflows.integration-spec.ts @@ -0,0 +1,370 @@ +import { randomUUID } from 'node:crypto'; + +import { WORKFLOW_GQL_FIELDS } from 'test/integration/constants/workflow-gql-fields.constants'; +import { createOneOperationFactory } from 'test/integration/graphql/utils/create-one-operation-factory.util'; +import { destroyOneOperationFactory } from 'test/integration/graphql/utils/destroy-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 { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; +import { PermissionsExceptionMessage } from 'src/engine/metadata-modules/permissions/permissions.exception'; +import { SEED_APPLE_WORKSPACE_ID } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-workspaces.util'; + +describe('workflowsPermissions', () => { + describe('createOne workflow', () => { + describe('permissions V2 disabled', () => { + it('should throw a permission error when user does not have permission (guest role)', async () => { + const workflowId = randomUUID(); + const graphqlOperation = createOneOperationFactory({ + objectMetadataSingularName: 'workflow', + gqlFields: WORKFLOW_GQL_FIELDS, + data: { + id: workflowId, + name: 'Test Workflow', + }, + }); + + const response = + await makeGraphqlAPIRequestWithGuestRole(graphqlOperation); + + expect(response.body.data).toStrictEqual({ createWorkflow: 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 create a workflow when user has permission (admin role)', async () => { + const workflowId = randomUUID(); + const graphqlOperation = createOneOperationFactory({ + objectMetadataSingularName: 'workflow', + gqlFields: WORKFLOW_GQL_FIELDS, + data: { + id: workflowId, + name: 'Test Workflow Admin', + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data).toBeDefined(); + expect(response.body.data.createWorkflow).toBeDefined(); + expect(response.body.data.createWorkflow.id).toBe(workflowId); + expect(response.body.data.createWorkflow.name).toBe( + 'Test Workflow Admin', + ); + + // Clean up - delete the created workflow + const destroyWorkflowOperation = destroyOneOperationFactory({ + objectMetadataSingularName: 'workflow', + gqlFields: ` + id + `, + recordId: response.body.data.createWorkflow.id, + }); + + await makeGraphqlAPIRequest(destroyWorkflowOperation); + }); + }); + + describe('permissions V2 enabled', () => { + beforeAll(async () => { + const enablePermissionsQuery = updateFeatureFlagFactory( + SEED_APPLE_WORKSPACE_ID, + 'IS_PERMISSIONS_V2_ENABLED', + true, + ); + + await makeGraphqlAPIRequest(enablePermissionsQuery); + }); + + afterAll(async () => { + const disablePermissionsQuery = updateFeatureFlagFactory( + SEED_APPLE_WORKSPACE_ID, + 'IS_PERMISSIONS_V2_ENABLED', + false, + ); + + await makeGraphqlAPIRequest(disablePermissionsQuery); + }); + + it('should throw a permission error when user does not have permission (guest role)', async () => { + const workflowId = randomUUID(); + const graphqlOperation = createOneOperationFactory({ + objectMetadataSingularName: 'workflow', + gqlFields: WORKFLOW_GQL_FIELDS, + data: { + id: workflowId, + name: 'Test Workflow V2', + }, + }); + + const response = + await makeGraphqlAPIRequestWithGuestRole(graphqlOperation); + + expect(response.body.data).toStrictEqual({ createWorkflow: 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 create a workflow when user has permission (admin role)', async () => { + const workflowId = randomUUID(); + const graphqlOperation = createOneOperationFactory({ + objectMetadataSingularName: 'workflow', + gqlFields: WORKFLOW_GQL_FIELDS, + data: { + id: workflowId, + name: 'Test Workflow Admin', + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data).toBeDefined(); + expect(response.body.data.createWorkflow).toBeDefined(); + expect(response.body.data.createWorkflow.id).toBe(workflowId); + expect(response.body.data.createWorkflow.name).toBe( + 'Test Workflow Admin', + ); + + // Clean up - delete the created workflow + const destroyWorkflowOperation = destroyOneOperationFactory({ + objectMetadataSingularName: 'workflow', + gqlFields: ` + id + `, + recordId: response.body.data.createWorkflow.id, + }); + + await makeGraphqlAPIRequest(destroyWorkflowOperation); + }); + + it('should create a workflow when executed by api key', async () => { + const workflowId = randomUUID(); + const graphqlOperation = createOneOperationFactory({ + objectMetadataSingularName: 'workflow', + gqlFields: WORKFLOW_GQL_FIELDS, + data: { + id: workflowId, + name: 'Test Workflow API Key', + }, + }); + + const response = + await makeGraphqlAPIRequestWithApiKey(graphqlOperation); + + expect(response.body.data).toBeDefined(); + expect(response.body.data.createWorkflow).toBeDefined(); + expect(response.body.data.createWorkflow.id).toBe(workflowId); + expect(response.body.data.createWorkflow.name).toBe( + 'Test Workflow API Key', + ); + + // Clean up - delete the created workflow + const destroyWorkflowOperation = destroyOneOperationFactory({ + objectMetadataSingularName: 'workflow', + gqlFields: ` + id + `, + recordId: response.body.data.createWorkflow.id, + }); + + await makeGraphqlAPIRequest(destroyWorkflowOperation); + }); + }); + }); + + describe('updateOne workflow', () => { + describe('permissions V2 disabled', () => { + const workflowId = randomUUID(); + + beforeAll(async () => { + const createWorkflowOperation = createOneOperationFactory({ + objectMetadataSingularName: 'workflow', + gqlFields: WORKFLOW_GQL_FIELDS, + data: { + id: workflowId, + name: 'Original Workflow Name', + }, + }); + + await makeGraphqlAPIRequest(createWorkflowOperation); + }); + + afterAll(async () => { + const destroyWorkflowOperation = destroyOneOperationFactory({ + objectMetadataSingularName: 'workflow', + gqlFields: ` + id + `, + recordId: workflowId, + }); + + await makeGraphqlAPIRequest(destroyWorkflowOperation); + }); + + it('should throw a permission error when user does not have permission (guest role)', async () => { + const graphqlOperation = updateOneOperationFactory({ + objectMetadataSingularName: 'workflow', + gqlFields: WORKFLOW_GQL_FIELDS, + recordId: workflowId, + data: { + name: 'Updated Workflow Name Guest', + }, + }); + + const response = + await makeGraphqlAPIRequestWithGuestRole(graphqlOperation); + + expect(response.body.data).toStrictEqual({ updateWorkflow: 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 a workflow when user has permission (admin role)', async () => { + const graphqlOperation = updateOneOperationFactory({ + objectMetadataSingularName: 'workflow', + gqlFields: WORKFLOW_GQL_FIELDS, + recordId: workflowId, + data: { + name: 'Updated Workflow Name Admin', + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data).toBeDefined(); + expect(response.body.data.updateWorkflow).toBeDefined(); + expect(response.body.data.updateWorkflow.id).toBe(workflowId); + expect(response.body.data.updateWorkflow.name).toBe( + 'Updated Workflow Name Admin', + ); + }); + }); + + describe('permissions V2 enabled', () => { + const workflowId = randomUUID(); + + beforeAll(async () => { + const createWorkflowOperation = createOneOperationFactory({ + objectMetadataSingularName: 'workflow', + gqlFields: WORKFLOW_GQL_FIELDS, + data: { + id: workflowId, + name: 'Original Workflow V2', + }, + }); + + await makeGraphqlAPIRequest(createWorkflowOperation); + + const enablePermissionsQuery = updateFeatureFlagFactory( + SEED_APPLE_WORKSPACE_ID, + 'IS_PERMISSIONS_V2_ENABLED', + true, + ); + + await makeGraphqlAPIRequest(enablePermissionsQuery); + }); + + afterAll(async () => { + const destroyWorkflowOperation = destroyOneOperationFactory({ + objectMetadataSingularName: 'workflow', + gqlFields: ` + id + `, + recordId: workflowId, + }); + + await makeGraphqlAPIRequest(destroyWorkflowOperation); + + const disablePermissionsQuery = updateFeatureFlagFactory( + SEED_APPLE_WORKSPACE_ID, + 'IS_PERMISSIONS_V2_ENABLED', + false, + ); + + await makeGraphqlAPIRequest(disablePermissionsQuery); + }); + + it('should throw a permission error when user does not have permission (guest role)', async () => { + const graphqlOperation = updateOneOperationFactory({ + objectMetadataSingularName: 'workflow', + gqlFields: WORKFLOW_GQL_FIELDS, + recordId: workflowId, + data: { + name: 'Updated Workflow V2 Guest', + }, + }); + + const response = + await makeGraphqlAPIRequestWithGuestRole(graphqlOperation); + + expect(response.body.data).toStrictEqual({ updateWorkflow: 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 a workflow when user has permission (admin role)', async () => { + const graphqlOperation = updateOneOperationFactory({ + objectMetadataSingularName: 'workflow', + gqlFields: WORKFLOW_GQL_FIELDS, + recordId: workflowId, + data: { + name: 'Updated Workflow V2 Admin', + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data).toBeDefined(); + expect(response.body.data.updateWorkflow).toBeDefined(); + expect(response.body.data.updateWorkflow.id).toBe(workflowId); + expect(response.body.data.updateWorkflow.name).toBe( + 'Updated Workflow V2 Admin', + ); + }); + + it('should update a workflow when executed by api key', async () => { + const graphqlOperation = updateOneOperationFactory({ + objectMetadataSingularName: 'workflow', + gqlFields: WORKFLOW_GQL_FIELDS, + recordId: workflowId, + data: { + name: 'Updated Workflow API Key', + }, + }); + + const response = + await makeGraphqlAPIRequestWithApiKey(graphqlOperation); + + expect(response.body.data).toBeDefined(); + expect(response.body.data.updateWorkflow).toBeDefined(); + expect(response.body.data.updateWorkflow.id).toBe(workflowId); + expect(response.body.data.updateWorkflow.name).toBe( + 'Updated Workflow API Key', + ); + }); + }); + }); +});