diff --git a/packages/cli/config/schema.ts b/packages/cli/config/schema.ts index f8fc82fa2d..045f676a22 100644 --- a/packages/cli/config/schema.ts +++ b/packages/cli/config/schema.ts @@ -185,6 +185,12 @@ export const schema = { default: false, env: 'N8N_ONBOARDING_FLOW_DISABLED', }, + callerPolicyDefaultOption: { + doc: 'Default option for which workflows may call the current workflow', + format: ['any', 'none', 'workflowsFromAList'] as const, + default: 'any', + env: 'N8N_WORKFLOW_CALLER_POLICY_DEFAULT_OPTION', + }, }, executions: { diff --git a/packages/cli/src/Interfaces.ts b/packages/cli/src/Interfaces.ts index f300d2f2ff..3b092f2055 100644 --- a/packages/cli/src/Interfaces.ts +++ b/packages/cli/src/Interfaces.ts @@ -485,6 +485,7 @@ export interface IN8nUISettings { saveManualExecutions: boolean; executionTimeout: number; maxExecutionTimeout: number; + workflowCallerPolicyDefaultOption: 'any' | 'none' | 'workflowsFromAList'; oauthCallbackUrls: { oauth1: string; oauth2: string; diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 4eb5b28858..514eddde85 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -284,6 +284,7 @@ class App { saveManualExecutions: this.saveManualExecutions, executionTimeout: this.executionTimeout, maxExecutionTimeout: this.maxExecutionTimeout, + workflowCallerPolicyDefaultOption: config.getEnv('workflows.callerPolicyDefaultOption'), timezone: this.timezone, urlBaseWebhook, urlBaseEditor: instanceBaseUrl, diff --git a/packages/cli/src/WorkflowExecuteAdditionalData.ts b/packages/cli/src/WorkflowExecuteAdditionalData.ts index fc5ad0aad4..3439e289e2 100644 --- a/packages/cli/src/WorkflowExecuteAdditionalData.ts +++ b/packages/cli/src/WorkflowExecuteAdditionalData.ts @@ -747,9 +747,38 @@ export async function getRunData( workflowData: IWorkflowBase, userId: string, inputData?: INodeExecutionData[], + parentWorkflowId?: string, ): Promise { const mode = 'integrated'; + const policy = + workflowData.settings?.callerPolicy ?? config.getEnv('workflows.callerPolicyDefaultOption'); + + if (policy === 'none') { + throw new SubworkflowOperationError( + `Target workflow ID ${workflowData.id} may not be called by other workflows.`, + 'Please update the settings of the target workflow or ask its owner to do so.', + ); + } + + if ( + policy === 'workflowsFromAList' && + typeof workflowData.settings?.callerIds === 'string' && + parentWorkflowId !== undefined + ) { + const allowedCallerIds = workflowData.settings.callerIds + .split(',') + .map((id) => id.trim()) + .filter((id) => id !== ''); + + if (!allowedCallerIds.includes(parentWorkflowId)) { + throw new SubworkflowOperationError( + `Target workflow ID ${workflowData.id} may only be called by a list of workflows, which does not include current workflow ID ${parentWorkflowId}.`, + 'Please update the settings of the target workflow or ask its owner to do so.', + ); + } + } + const startingNode = findSubworkflowStart(workflowData.nodes); // Always start with empty data if no inputData got supplied diff --git a/packages/cli/src/WorkflowRunnerProcess.ts b/packages/cli/src/WorkflowRunnerProcess.ts index abf501b93d..8f0ff4a9b0 100644 --- a/packages/cli/src/WorkflowRunnerProcess.ts +++ b/packages/cli/src/WorkflowRunnerProcess.ts @@ -291,6 +291,7 @@ export class WorkflowRunnerProcess { workflowData, additionalData.userId, options?.inputData, + options?.parentWorkflowId, ); await sendToParentProcess('startExecution', { runData }); const executionId: string = await new Promise((resolve) => { diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index 32a151ab6a..efb1d6961a 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -719,12 +719,15 @@ export interface ITemplatesCategory { name: string; } +export type WorkflowCallerPolicyDefaultOption = 'any' | 'none' | 'workflowsFromAList'; + export interface IN8nUISettings { endpointWebhook: string; endpointWebhookTest: string; saveDataErrorExecution: string; saveDataSuccessExecution: string; saveManualExecutions: boolean; + workflowCallerPolicyDefaultOption: WorkflowCallerPolicyDefaultOption; timezone: string; executionTimeout: number; maxExecutionTimeout: number; @@ -768,6 +771,7 @@ export interface IN8nUISettings { deployment?: { type: string; }; + isWorkflowSharingEnabled: boolean; } export interface IWorkflowSettings extends IWorkflowSettingsWorkflow { @@ -777,6 +781,8 @@ export interface IWorkflowSettings extends IWorkflowSettingsWorkflow { saveManualExecutions?: boolean; timezone?: string; executionTimeout?: number; + callerIds?: string; + callerPolicy?: WorkflowCallerPolicyDefaultOption; } export interface ITimeoutHMS { diff --git a/packages/editor-ui/src/components/WorkflowSettings.vue b/packages/editor-ui/src/components/WorkflowSettings.vue index 6b710a2992..8db2a02381 100644 --- a/packages/editor-ui/src/components/WorkflowSettings.vue +++ b/packages/editor-ui/src/components/WorkflowSettings.vue @@ -123,6 +123,45 @@ +
+ + + {{ $locale.baseText('workflowSettings.callerPolicy') + ":" }} + +
+ +
+
+ + + + + + + +
+ + + {{ $locale.baseText('workflowSettings.callerIds') + ":" }} + +
+ +
+
+ + + +
+
{{ $locale.baseText('workflowSettings.timeoutWorkflow') + ":" }} @@ -216,6 +255,8 @@ export default mixins( saveManualExecutions: this.$locale.baseText('workflowSettings.helpTexts.saveManualExecutions'), executionTimeoutToggle: this.$locale.baseText('workflowSettings.helpTexts.executionTimeoutToggle'), executionTimeout: this.$locale.baseText('workflowSettings.helpTexts.executionTimeout'), + workflowCallerPolicy: this.$locale.baseText('workflowSettings.helpTexts.workflowCallerPolicy'), + workflowCallerIds: this.$locale.baseText('workflowSettings.helpTexts.workflowCallerIds'), }, defaultValues: { timezone: 'America/New_York', @@ -223,7 +264,9 @@ export default mixins( saveDataSuccessExecution: 'all', saveExecutionProgress: false, saveManualExecutions: false, + workflowCallerPolicy: '', }, + workflowCallerPolicyOptions: [] as Array<{ key: string, value: string }>, saveDataErrorExecutionOptions: [] as Array<{ key: string, value: string }>, saveDataSuccessExecutionOptions: [] as Array<{ key: string, value: string }>, saveExecutionProgressOptions: [] as Array<{ key: string | boolean, value: string }>, @@ -241,6 +284,9 @@ export default mixins( computed: { ...mapGetters(['workflowName', 'workflowId']), + isWorkflowSharingEnabled(): boolean { + return this.$store.getters['settings/isWorkflowSharingEnabled']; + }, }, async mounted () { @@ -259,6 +305,7 @@ export default mixins( this.defaultValues.saveDataSuccessExecution = this.$store.getters.saveDataSuccessExecution; this.defaultValues.saveManualExecutions = this.$store.getters.saveManualExecutions; this.defaultValues.timezone = this.$store.getters.timezone; + this.defaultValues.workflowCallerPolicy = this.$store.getters['settings/workflowCallerPolicyDefaultOption']; this.isLoading = true; const promises = []; @@ -268,6 +315,7 @@ export default mixins( promises.push(this.loadSaveExecutionProgressOptions()); promises.push(this.loadSaveManualOptions()); promises.push(this.loadTimezones()); + promises.push(this.loadWorkflowCallerPolicyOptions()); try { await Promise.all(promises); @@ -292,6 +340,9 @@ export default mixins( if (workflowSettings.saveManualExecutions === undefined) { workflowSettings.saveManualExecutions = 'DEFAULT'; } + if (workflowSettings.callerPolicy === undefined) { + workflowSettings.callerPolicy = this.defaultValues.workflowCallerPolicy; + } if (workflowSettings.executionTimeout === undefined) { workflowSettings.executionTimeout = this.$store.getters.executionTimeout; } @@ -307,6 +358,11 @@ export default mixins( this.$telemetry.track('User opened workflow settings', { workflow_id: this.$store.getters.workflowId }); }, methods: { + onCallerIdsInput(str: string) { + this.workflowSettings.callerIds = /^[0-9,\s]+$/.test(str) + ? str + : str.replace(/[^0-9,\s]/g, ''); + }, closeDialog () { this.modalBus.$emit('close'); this.$externalHooks().run('workflowSettings.dialogVisibleChanged', { dialogVisible: false }); @@ -319,6 +375,22 @@ export default mixins( [key]: time, }; }, + async loadWorkflowCallerPolicyOptions () { + this.workflowCallerPolicyOptions = [ + { + key: 'any', + value: this.$locale.baseText('workflowSettings.callerPolicy.options.any'), + }, + { + key: 'none', + value: this.$locale.baseText('workflowSettings.callerPolicy.options.none'), + }, + { + key: 'workflowsFromAList', + value: this.$locale.baseText('workflowSettings.callerPolicy.options.workflowsFromAList'), + }, + ]; + }, async loadSaveDataErrorExecutionOptions () { this.saveDataErrorExecutionOptions.length = 0; this.saveDataErrorExecutionOptions.push.apply( // eslint-disable-line no-useless-call diff --git a/packages/editor-ui/src/modules/settings.ts b/packages/editor-ui/src/modules/settings.ts index c3af2c413c..68ea3b578c 100644 --- a/packages/editor-ui/src/modules/settings.ts +++ b/packages/editor-ui/src/modules/settings.ts @@ -6,6 +6,7 @@ import { IN8nValueSurveyData, IRootState, ISettingsState, + WorkflowCallerPolicyDefaultOption, } from '../Interface'; import { getPromptsData, submitValueSurvey, submitContactInfo, getSettings } from '../api/settings'; import Vue from 'vue'; @@ -108,6 +109,12 @@ const module: Module = { isQueueModeEnabled: (state): boolean => { return state.settings.executionMode === 'queue'; }, + workflowCallerPolicyDefaultOption: (state): WorkflowCallerPolicyDefaultOption => { + return state.settings.workflowCallerPolicyDefaultOption; + }, + isWorkflowSharingEnabled: (state): boolean => { + return state.settings.isWorkflowSharingEnabled; + }, }, mutations: { setSettings(state: ISettingsState, settings: IN8nUISettings) { @@ -132,6 +139,12 @@ const module: Module = { setCommunityNodesFeatureEnabled(state: ISettingsState, isEnabled: boolean) { state.settings.communityNodesEnabled = isEnabled; }, + setIsWorkflowSharingEnabled(state: ISettingsState, enabled: boolean) { + state.settings.isWorkflowSharingEnabled = enabled; + }, + setWorkflowCallerPolicyDefaultOption(state: ISettingsState, defaultOption: WorkflowCallerPolicyDefaultOption) { + state.settings.workflowCallerPolicyDefaultOption = defaultOption; + }, setAllowedModules(state, allowedModules: { builtIn?: string, external?: string }) { state.settings.allowedModules = { ...(allowedModules.builtIn && { builtIn: allowedModules.builtIn.split(',') }), @@ -164,6 +177,8 @@ const module: Module = { context.commit('versions/setVersionNotificationSettings', settings.versionNotifications, {root: true}); context.commit('setCommunityNodesFeatureEnabled', settings.communityNodesEnabled === true); context.commit('settings/setAllowedModules', settings.allowedModules, {root: true}); + context.commit('settings/setWorkflowCallerPolicyDefaultOption', settings.workflowCallerPolicyDefaultOption, {root: true}); + context.commit('settings/setIsWorkflowSharingEnabled', settings.enterprise.workflowSharing, {root: true}); }, async fetchPromptsData(context: ActionContext) { if (!context.getters.isTelemetryEnabled) { diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index d4489f0753..cba1e8c9b4 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -1267,6 +1267,11 @@ "workflowRun.showError.title": "Problem running workflow", "workflowRun.showMessage.message": "Please fix them before executing", "workflowRun.showMessage.title": "Workflow has issues", + "workflowSettings.callerIds": "Caller IDs", + "workflowSettings.callerPolicy": "This workflow can be called by", + "workflowSettings.callerPolicy.options.any": "Any workflow", + "workflowSettings.callerPolicy.options.workflowsFromAList": "List of specific workflow IDs", + "workflowSettings.callerPolicy.options.none": "No workflows can call one", "workflowSettings.defaultTimezone": "Default - {defaultTimezoneValue}", "workflowSettings.defaultTimezoneNotValid": "Default Timezone not valid", "workflowSettings.errorWorkflow": "Error Workflow", @@ -1278,6 +1283,8 @@ "workflowSettings.helpTexts.saveExecutionProgress": "Whether to save data after each node execution. This allows you to resume from where execution stopped if there is an error, but may increase latency.", "workflowSettings.helpTexts.saveManualExecutions": "Whether to save data of executions that are started manually from the editor", "workflowSettings.helpTexts.timezone": "The timezone in which the workflow should run. Used by 'cron' node, for example.", + "workflowSettings.helpTexts.workflowCallerIds": "Comma-delimited list of IDs of workflows that are allowed to call this workflow", + "workflowSettings.helpTexts.workflowCallerPolicy": "Workflows that are allowed to call this workflow using the Execute Workflow node", "workflowSettings.hours": "hours", "workflowSettings.minutes": "minutes", "workflowSettings.noWorkflow": "- No Workflow -",