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:
+13
-5
@@ -15,6 +15,7 @@ import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { canObjectBeManagedByWorkflow } from 'twenty-shared/workflow';
|
||||
import { HorizontalSeparator, useIcons } from 'twenty-ui/display';
|
||||
import { type SelectOption } from 'twenty-ui/input';
|
||||
import { type JsonValue } from 'type-fest';
|
||||
@@ -73,11 +74,18 @@ export const WorkflowEditActionCreateRecord = ({
|
||||
useFilteredObjectMetadataItems();
|
||||
|
||||
const availableMetadata: Array<SelectOption<string>> =
|
||||
activeNonSystemObjectMetadataItems.map((item) => ({
|
||||
Icon: getIcon(item.icon),
|
||||
label: item.labelPlural,
|
||||
value: item.nameSingular,
|
||||
}));
|
||||
activeNonSystemObjectMetadataItems
|
||||
.filter((objectMetadataItem) =>
|
||||
canObjectBeManagedByWorkflow({
|
||||
nameSingular: objectMetadataItem.nameSingular,
|
||||
isSystem: objectMetadataItem.isSystem,
|
||||
}),
|
||||
)
|
||||
.map((item) => ({
|
||||
Icon: getIcon(item.icon),
|
||||
label: item.labelPlural,
|
||||
value: item.nameSingular,
|
||||
}));
|
||||
|
||||
const [formData, setFormData] = useState<CreateRecordFormData>({
|
||||
objectName: action.settings.input.objectName,
|
||||
|
||||
+13
-5
@@ -11,6 +11,7 @@ import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-acti
|
||||
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { canObjectBeManagedByWorkflow } from 'twenty-shared/workflow';
|
||||
import { HorizontalSeparator, useIcons } from 'twenty-ui/display';
|
||||
import { type SelectOption } from 'twenty-ui/input';
|
||||
import { type JsonValue } from 'type-fest';
|
||||
@@ -45,11 +46,18 @@ export const WorkflowEditActionDeleteRecord = ({
|
||||
useFilteredObjectMetadataItems();
|
||||
|
||||
const availableMetadata: Array<SelectOption<string>> =
|
||||
activeNonSystemObjectMetadataItems.map((item) => ({
|
||||
Icon: getIcon(item.icon),
|
||||
label: item.labelPlural,
|
||||
value: item.nameSingular,
|
||||
}));
|
||||
activeNonSystemObjectMetadataItems
|
||||
.filter((objectMetadataItem) =>
|
||||
canObjectBeManagedByWorkflow({
|
||||
nameSingular: objectMetadataItem.nameSingular,
|
||||
isSystem: objectMetadataItem.isSystem,
|
||||
}),
|
||||
)
|
||||
.map((item) => ({
|
||||
Icon: getIcon(item.icon),
|
||||
label: item.labelPlural,
|
||||
value: item.nameSingular,
|
||||
}));
|
||||
|
||||
const [formData, setFormData] = useState<DeleteRecordFormData>({
|
||||
objectNameSingular: action.settings.input.objectName,
|
||||
|
||||
+13
-5
@@ -16,6 +16,7 @@ import { shouldDisplayFormField } from '@/workflow/workflow-steps/workflow-actio
|
||||
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { canObjectBeManagedByWorkflow } from 'twenty-shared/workflow';
|
||||
import { HorizontalSeparator, useIcons } from 'twenty-ui/display';
|
||||
import { type SelectOption } from 'twenty-ui/input';
|
||||
import { type JsonValue } from 'type-fest';
|
||||
@@ -53,11 +54,18 @@ export const WorkflowEditActionUpdateRecord = ({
|
||||
useFilteredObjectMetadataItems();
|
||||
|
||||
const availableMetadata: Array<SelectOption<string>> =
|
||||
activeNonSystemObjectMetadataItems.map((item) => ({
|
||||
Icon: getIcon(item.icon),
|
||||
label: item.labelPlural,
|
||||
value: item.nameSingular,
|
||||
}));
|
||||
activeNonSystemObjectMetadataItems
|
||||
.filter((objectMetadataItem) =>
|
||||
canObjectBeManagedByWorkflow({
|
||||
nameSingular: objectMetadataItem.nameSingular,
|
||||
isSystem: objectMetadataItem.isSystem,
|
||||
}),
|
||||
)
|
||||
.map((item) => ({
|
||||
Icon: getIcon(item.icon),
|
||||
label: item.labelPlural,
|
||||
value: item.nameSingular,
|
||||
}));
|
||||
|
||||
const [formData, setFormData] = useState<UpdateRecordFormData>({
|
||||
objectNameSingular: action.settings.input.objectName,
|
||||
|
||||
+2
-2
@@ -61,7 +61,7 @@ describe('shouldGenerateFieldFakeValue', () => {
|
||||
expect(shouldGenerateFieldFakeValue(field)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for system fields (except id)', () => {
|
||||
it('should return true for system fields', () => {
|
||||
const field = getMockFieldMetadataEntity({
|
||||
workspaceId: '20202020-0000-0000-0000-000000000000',
|
||||
objectMetadataId: '20202020-0000-0000-0000-000000000001',
|
||||
@@ -77,7 +77,7 @@ describe('shouldGenerateFieldFakeValue', () => {
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
expect(shouldGenerateFieldFakeValue(field)).toBe(false);
|
||||
expect(shouldGenerateFieldFakeValue(field)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for relation fields', () => {
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ export const shouldGenerateFieldFakeValue = <T extends FieldMetadataType>(
|
||||
) => {
|
||||
return (
|
||||
field.isActive &&
|
||||
(!field.isSystem || field.name === 'id' || field.name === 'userEmail') &&
|
||||
field.name !== 'searchVector' &&
|
||||
(field.type !== FieldMetadataType.RELATION ||
|
||||
isManyToOneRelationField(field as unknown as FieldMetadataEntity))
|
||||
);
|
||||
|
||||
+14
-14
@@ -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]),
|
||||
|
||||
+21
@@ -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,
|
||||
|
||||
+13
@@ -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) => {
|
||||
|
||||
@@ -53,4 +53,5 @@ export type {
|
||||
WorkflowRunStepInfos,
|
||||
} from './types/WorkflowRunStateStepInfos';
|
||||
export { StepStatus } from './types/WorkflowRunStateStepInfos';
|
||||
export { canObjectBeManagedByWorkflow } from './utils/canObjectBeManagedByWorkflow';
|
||||
export { getWorkflowRunContext } from './utils/getWorkflowRunContext';
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
export const canObjectBeManagedByWorkflow = ({
|
||||
nameSingular,
|
||||
isSystem,
|
||||
}: {
|
||||
nameSingular: string;
|
||||
isSystem: boolean;
|
||||
}) => {
|
||||
const excludedNonSystemObjectMetadataItemNames = [
|
||||
'workflow',
|
||||
'workflowVersion',
|
||||
'workflowRun',
|
||||
'dashboard',
|
||||
];
|
||||
|
||||
return !excludedNonSystemObjectMetadataItemNames.includes(
|
||||
nameSingular,
|
||||
) && !isSystem;
|
||||
};
|
||||
Reference in New Issue
Block a user