Restrict workflow object permissions (#14290)

- workflows should not edit system objects or workflow related objects
- system fields should be usable within variables for reading
This commit is contained in:
Thomas Trompette
2025-09-04 10:39:51 +02:00
committed by GitHub
parent 6e570ba6c8
commit d6ba6a66a4
10 changed files with 109 additions and 32 deletions
@@ -3,6 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm';
import { isDefined } from 'class-validator';
import { resolveInput } from 'twenty-shared/utils';
import { canObjectBeManagedByWorkflow } from 'twenty-shared/workflow';
import { Repository } from 'typeorm';
import { type WorkflowAction } from 'src/modules/workflow/workflow-executor/interfaces/workflow-action.interface';
@@ -66,31 +67,30 @@ export class CreateRecordWorkflowAction implements WorkflowAction {
{ shouldBypassPermissionChecks: true },
);
const objectMetadata = await this.objectMetadataRepository.findOne({
where: {
nameSingular: workflowActionInput.objectName,
},
});
const { objectMetadataItemWithFieldsMaps } =
await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps(
workflowActionInput.objectName,
workspaceId,
);
if (!objectMetadata) {
if (
!canObjectBeManagedByWorkflow({
nameSingular: objectMetadataItemWithFieldsMaps.nameSingular,
isSystem: objectMetadataItemWithFieldsMaps.isSystem,
})
) {
throw new RecordCRUDActionException(
'Failed to create: Object metadata not found',
'Failed to create: Object cannot be created by workflow',
RecordCRUDActionExceptionCode.INVALID_REQUEST,
);
}
const position = await this.recordPositionService.buildRecordPosition({
value: 'first',
objectMetadata,
objectMetadata: objectMetadataItemWithFieldsMaps,
workspaceId,
});
const { objectMetadataItemWithFieldsMaps } =
await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps(
workflowActionInput.objectName,
workspaceId,
);
const validObjectRecord = Object.fromEntries(
Object.entries(workflowActionInput.objectRecord).filter(([key]) =>
isDefined(objectMetadataItemWithFieldsMaps.fieldIdByName[key]),
@@ -2,11 +2,13 @@ import { Injectable } from '@nestjs/common';
import { isDefined } from 'class-validator';
import { isValidUuid, resolveInput } from 'twenty-shared/utils';
import { canObjectBeManagedByWorkflow } from 'twenty-shared/workflow';
import { type WorkflowAction } from 'src/modules/workflow/workflow-executor/interfaces/workflow-action.interface';
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service';
import {
WorkflowStepExecutorException,
WorkflowStepExecutorExceptionCode,
@@ -25,6 +27,7 @@ import { type WorkflowDeleteRecordActionInput } from 'src/modules/workflow/workf
export class DeleteRecordWorkflowAction implements WorkflowAction {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService,
private readonly scopedWorkspaceContextFactory: ScopedWorkspaceContextFactory,
) {}
@@ -77,6 +80,24 @@ export class DeleteRecordWorkflowAction implements WorkflowAction {
{ shouldBypassPermissionChecks: true },
);
const { objectMetadataItemWithFieldsMaps } =
await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps(
workflowActionInput.objectName,
workspaceId,
);
if (
!canObjectBeManagedByWorkflow({
nameSingular: objectMetadataItemWithFieldsMaps.nameSingular,
isSystem: objectMetadataItemWithFieldsMaps.isSystem,
})
) {
throw new RecordCRUDActionException(
'Failed to delete: Object cannot be deleted by workflow',
RecordCRUDActionExceptionCode.INVALID_REQUEST,
);
}
const objectRecord = await repository.findOne({
where: {
id: workflowActionInput.objectRecordId,
@@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
import deepEqual from 'deep-equal';
import { isDefined, isValidUuid, resolveInput } from 'twenty-shared/utils';
import { canObjectBeManagedByWorkflow } from 'twenty-shared/workflow';
import { type WorkflowAction } from 'src/modules/workflow/workflow-executor/interfaces/workflow-action.interface';
@@ -106,6 +107,18 @@ export class UpdateRecordWorkflowAction implements WorkflowAction {
workspaceId,
);
if (
!canObjectBeManagedByWorkflow({
nameSingular: objectMetadataItemWithFieldsMaps.nameSingular,
isSystem: objectMetadataItemWithFieldsMaps.isSystem,
})
) {
throw new RecordCRUDActionException(
'Failed to update: Object cannot be updated by workflow',
RecordCRUDActionExceptionCode.INVALID_REQUEST,
);
}
const objectRecordWithFilteredFields = Object.keys(
workflowActionInput.objectRecord,
).reduce((acc, key) => {