mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-24 20:24:05 -08:00
feat(core, editor): introduce workflow caller policy (#4368)
* ✨ Create env `N8N_WORKFLOW_CALLER_POLICY_DEFAULT_OPTION` * 👕 Adjust BE settings interface * 👕 Adjust FE settings interface * ⚡ Send policy along with settings * ⚡ Enforce policy * ✨ Create `SubworkflowOperationError` * ⚡ Add policy to Vuex store * ⚡ Add setting to FE * ⚡ Trim caller IDs on BE * ⚡ Hide new UI behind `isWorkflowSharingEnabled` * ✏️ Copy updates * 👕 Fix lint
This commit is contained in:
parent
dd3c59677b
commit
e8935de3b2
|
@ -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: {
|
||||
|
|
|
@ -485,6 +485,7 @@ export interface IN8nUISettings {
|
|||
saveManualExecutions: boolean;
|
||||
executionTimeout: number;
|
||||
maxExecutionTimeout: number;
|
||||
workflowCallerPolicyDefaultOption: 'any' | 'none' | 'workflowsFromAList';
|
||||
oauthCallbackUrls: {
|
||||
oauth1: string;
|
||||
oauth2: string;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -747,9 +747,38 @@ export async function getRunData(
|
|||
workflowData: IWorkflowBase,
|
||||
userId: string,
|
||||
inputData?: INodeExecutionData[],
|
||||
parentWorkflowId?: string,
|
||||
): Promise<IWorkflowExecutionDataProcess> {
|
||||
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
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -123,6 +123,45 @@
|
|||
</n8n-select>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<div v-if="isWorkflowSharingEnabled">
|
||||
<el-row>
|
||||
<el-col :span="10" class="setting-name">
|
||||
{{ $locale.baseText('workflowSettings.callerPolicy') + ":" }}
|
||||
<n8n-tooltip class="setting-info" placement="top" >
|
||||
<div slot="content" v-text="helpTexts.workflowCallerPolicy"></div>
|
||||
<font-awesome-icon icon="question-circle" />
|
||||
</n8n-tooltip>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="14" class="ignore-key-press">
|
||||
<n8n-select v-model="workflowSettings.callerPolicy" :placeholder="$locale.baseText('workflowSettings.selectOption')" size="medium" filterable :limit-popper-width="true">
|
||||
<n8n-option
|
||||
v-for="option of workflowCallerPolicyOptions"
|
||||
:key="option.key"
|
||||
:label="option.value"
|
||||
:value="option.key">
|
||||
</n8n-option>
|
||||
</n8n-select>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row v-if="workflowSettings.callerPolicy === 'workflowsFromAList'">
|
||||
<el-col :span="10" class="setting-name">
|
||||
{{ $locale.baseText('workflowSettings.callerIds') + ":" }}
|
||||
<n8n-tooltip class="setting-info" placement="top" >
|
||||
<div slot="content" v-text="helpTexts.workflowCallerIds"></div>
|
||||
<font-awesome-icon icon="question-circle" />
|
||||
</n8n-tooltip>
|
||||
</el-col>
|
||||
<el-col :span="14">
|
||||
<n8n-input
|
||||
type="text"
|
||||
size="medium"
|
||||
v-model="workflowSettings.callerIds"
|
||||
@input="onCallerIdsInput"
|
||||
/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
<el-row>
|
||||
<el-col :span="10" class="setting-name">
|
||||
{{ $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
|
||||
|
|
|
@ -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<ISettingsState, IRootState> = {
|
|||
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<ISettingsState, IRootState> = {
|
|||
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<ISettingsState, IRootState> = {
|
|||
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<ISettingsState, IRootState>) {
|
||||
if (!context.getters.isTelemetryEnabled) {
|
||||
|
|
|
@ -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 -",
|
||||
|
|
Loading…
Reference in a new issue