mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-24 20:24:05 -08:00
feat: Add support for $env in the js task runner (no-changelog) (#11177)
Some checks are pending
Test Master / install-and-build (push) Waiting to run
Test Master / Unit tests (18.x) (push) Blocked by required conditions
Test Master / Unit tests (20.x) (push) Blocked by required conditions
Test Master / Unit tests (22.4) (push) Blocked by required conditions
Test Master / Lint (push) Blocked by required conditions
Test Master / Notify Slack on failure (push) Blocked by required conditions
Benchmark Docker Image CI / build (push) Waiting to run
Some checks are pending
Test Master / install-and-build (push) Waiting to run
Test Master / Unit tests (18.x) (push) Blocked by required conditions
Test Master / Unit tests (20.x) (push) Blocked by required conditions
Test Master / Unit tests (22.4) (push) Blocked by required conditions
Test Master / Lint (push) Blocked by required conditions
Test Master / Notify Slack on failure (push) Blocked by required conditions
Benchmark Docker Image CI / build (push) Waiting to run
This commit is contained in:
parent
939c0dfc4c
commit
e94cda3837
|
@ -26,5 +26,8 @@
|
||||||
"n8n-core": "workspace:*",
|
"n8n-core": "workspace:*",
|
||||||
"nanoid": "^3.3.6",
|
"nanoid": "^3.3.6",
|
||||||
"ws": "^8.18.0"
|
"ws": "^8.18.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"luxon": "catalog:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
import type { CodeExecutionMode, IDataObject } from 'n8n-workflow';
|
import type { CodeExecutionMode, IDataObject } from 'n8n-workflow';
|
||||||
|
|
||||||
import { ValidationError } from '@/js-task-runner/errors/validation-error';
|
import { ValidationError } from '@/js-task-runner/errors/validation-error';
|
||||||
|
@ -30,6 +31,36 @@ describe('JsTaskRunner', () => {
|
||||||
jest.restoreAllMocks();
|
jest.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const executeForAllItems = async ({
|
||||||
|
code,
|
||||||
|
inputItems,
|
||||||
|
settings,
|
||||||
|
}: { code: string; inputItems: IDataObject[]; settings?: Partial<JSExecSettings> }) => {
|
||||||
|
return await execTaskWithParams({
|
||||||
|
task: newTaskWithSettings({
|
||||||
|
code,
|
||||||
|
nodeMode: 'runOnceForAllItems',
|
||||||
|
...settings,
|
||||||
|
}),
|
||||||
|
taskData: newAllCodeTaskData(inputItems.map(wrapIntoJson)),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const executeForEachItem = async ({
|
||||||
|
code,
|
||||||
|
inputItems,
|
||||||
|
settings,
|
||||||
|
}: { code: string; inputItems: IDataObject[]; settings?: Partial<JSExecSettings> }) => {
|
||||||
|
return await execTaskWithParams({
|
||||||
|
task: newTaskWithSettings({
|
||||||
|
code,
|
||||||
|
nodeMode: 'runOnceForEachItem',
|
||||||
|
...settings,
|
||||||
|
}),
|
||||||
|
taskData: newAllCodeTaskData(inputItems.map(wrapIntoJson)),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
describe('console', () => {
|
describe('console', () => {
|
||||||
test.each<[CodeExecutionMode]>([['runOnceForAllItems'], ['runOnceForEachItem']])(
|
test.each<[CodeExecutionMode]>([['runOnceForAllItems'], ['runOnceForEachItem']])(
|
||||||
'should make an rpc call for console log in %s mode',
|
'should make an rpc call for console log in %s mode',
|
||||||
|
@ -52,22 +83,178 @@ describe('JsTaskRunner', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('runOnceForAllItems', () => {
|
describe('built-in methods and variables available in the context', () => {
|
||||||
const executeForAllItems = async ({
|
const inputItems = [{ a: 1 }];
|
||||||
code,
|
|
||||||
|
const testExpressionForAllItems = async (
|
||||||
|
expression: string,
|
||||||
|
expected: IDataObject | string | number | boolean,
|
||||||
|
) => {
|
||||||
|
const needsWrapping = typeof expected !== 'object';
|
||||||
|
const outcome = await executeForAllItems({
|
||||||
|
code: needsWrapping ? `return { val: ${expression} }` : `return ${expression}`,
|
||||||
inputItems,
|
inputItems,
|
||||||
settings,
|
|
||||||
}: { code: string; inputItems: IDataObject[]; settings?: Partial<JSExecSettings> }) => {
|
|
||||||
return await execTaskWithParams({
|
|
||||||
task: newTaskWithSettings({
|
|
||||||
code,
|
|
||||||
nodeMode: 'runOnceForAllItems',
|
|
||||||
...settings,
|
|
||||||
}),
|
|
||||||
taskData: newAllCodeTaskData(inputItems.map(wrapIntoJson)),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
expect(outcome.result).toEqual([wrapIntoJson(needsWrapping ? { val: expected } : expected)]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const testExpressionForEachItem = async (
|
||||||
|
expression: string,
|
||||||
|
expected: IDataObject | string | number | boolean,
|
||||||
|
) => {
|
||||||
|
const needsWrapping = typeof expected !== 'object';
|
||||||
|
const outcome = await executeForEachItem({
|
||||||
|
code: needsWrapping ? `return { val: ${expression} }` : `return ${expression}`,
|
||||||
|
inputItems,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(outcome.result).toEqual([
|
||||||
|
withPairedItem(0, wrapIntoJson(needsWrapping ? { val: expected } : expected)),
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const testGroups = {
|
||||||
|
// https://docs.n8n.io/code/builtin/current-node-input/
|
||||||
|
'current node input': [
|
||||||
|
['$input.first()', inputItems[0]],
|
||||||
|
['$input.last()', inputItems[inputItems.length - 1]],
|
||||||
|
['$input.params', { manualTriggerParam: 'empty' }],
|
||||||
|
],
|
||||||
|
// https://docs.n8n.io/code/builtin/output-other-nodes/
|
||||||
|
'output of other nodes': [
|
||||||
|
['$("Trigger").first()', inputItems[0]],
|
||||||
|
['$("Trigger").last()', inputItems[inputItems.length - 1]],
|
||||||
|
['$("Trigger").params', { manualTriggerParam: 'empty' }],
|
||||||
|
],
|
||||||
|
// https://docs.n8n.io/code/builtin/date-time/
|
||||||
|
'date and time': [
|
||||||
|
['$now', expect.any(DateTime)],
|
||||||
|
['$today', expect.any(DateTime)],
|
||||||
|
['{dt: DateTime}', { dt: expect.any(Function) }],
|
||||||
|
],
|
||||||
|
// https://docs.n8n.io/code/builtin/jmespath/
|
||||||
|
JMESPath: [['{ val: $jmespath([{ f: 1 },{ f: 2 }], "[*].f") }', { val: [1, 2] }]],
|
||||||
|
// https://docs.n8n.io/code/builtin/n8n-metadata/
|
||||||
|
'n8n metadata': [
|
||||||
|
[
|
||||||
|
'$execution',
|
||||||
|
{
|
||||||
|
id: 'exec-id',
|
||||||
|
mode: 'test',
|
||||||
|
resumeFormUrl: 'http://formWaitingBaseUrl/exec-id',
|
||||||
|
resumeUrl: 'http://webhookWaitingBaseUrl/exec-id',
|
||||||
|
customData: {
|
||||||
|
get: expect.any(Function),
|
||||||
|
getAll: expect.any(Function),
|
||||||
|
set: expect.any(Function),
|
||||||
|
setAll: expect.any(Function),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
['$("Trigger").isExecuted', true],
|
||||||
|
['$nodeVersion', 2],
|
||||||
|
['$prevNode.name', 'Trigger'],
|
||||||
|
['$prevNode.outputIndex', 0],
|
||||||
|
['$runIndex', 0],
|
||||||
|
['{ wf: $workflow }', { wf: { active: true, id: '1', name: 'Test Workflow' } }],
|
||||||
|
['$vars', { var: 'value' }],
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [groupName, tests] of Object.entries(testGroups)) {
|
||||||
|
describe(`${groupName} runOnceForAllItems`, () => {
|
||||||
|
test.each(tests)(
|
||||||
|
'should have the %s available in the context',
|
||||||
|
async (expression, expected) => {
|
||||||
|
await testExpressionForAllItems(expression, expected);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(`${groupName} runOnceForEachItem`, () => {
|
||||||
|
test.each(tests)(
|
||||||
|
'should have the %s available in the context',
|
||||||
|
async (expression, expected) => {
|
||||||
|
await testExpressionForEachItem(expression, expected);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('$env', () => {
|
||||||
|
it('should have the env available in context when access has not been blocked', async () => {
|
||||||
|
const outcome = await execTaskWithParams({
|
||||||
|
task: newTaskWithSettings({
|
||||||
|
code: 'return { val: $env.VAR1 }',
|
||||||
|
nodeMode: 'runOnceForAllItems',
|
||||||
|
}),
|
||||||
|
taskData: newAllCodeTaskData(inputItems.map(wrapIntoJson), {
|
||||||
|
envProviderState: {
|
||||||
|
isEnvAccessBlocked: false,
|
||||||
|
isProcessAvailable: true,
|
||||||
|
env: { VAR1: 'value' },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(outcome.result).toEqual([wrapIntoJson({ val: 'value' })]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be possible to access env if it has been blocked', async () => {
|
||||||
|
await expect(
|
||||||
|
execTaskWithParams({
|
||||||
|
task: newTaskWithSettings({
|
||||||
|
code: 'return { val: $env.VAR1 }',
|
||||||
|
nodeMode: 'runOnceForAllItems',
|
||||||
|
}),
|
||||||
|
taskData: newAllCodeTaskData(inputItems.map(wrapIntoJson), {
|
||||||
|
envProviderState: {
|
||||||
|
isEnvAccessBlocked: true,
|
||||||
|
isProcessAvailable: true,
|
||||||
|
env: { VAR1: 'value' },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
).rejects.toThrow('access to env vars denied');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not be possible to iterate $env', async () => {
|
||||||
|
const outcome = await execTaskWithParams({
|
||||||
|
task: newTaskWithSettings({
|
||||||
|
code: 'return Object.values($env).concat(Object.keys($env))',
|
||||||
|
nodeMode: 'runOnceForAllItems',
|
||||||
|
}),
|
||||||
|
taskData: newAllCodeTaskData(inputItems.map(wrapIntoJson), {
|
||||||
|
envProviderState: {
|
||||||
|
isEnvAccessBlocked: false,
|
||||||
|
isProcessAvailable: true,
|
||||||
|
env: { VAR1: '1', VAR2: '2', VAR3: '3' },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(outcome.result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not expose task runner's env variables even if no env state is received", async () => {
|
||||||
|
process.env.N8N_RUNNERS_N8N_URI = 'http://127.0.0.1:5679';
|
||||||
|
const outcome = await execTaskWithParams({
|
||||||
|
task: newTaskWithSettings({
|
||||||
|
code: 'return { val: $env.N8N_RUNNERS_N8N_URI }',
|
||||||
|
nodeMode: 'runOnceForAllItems',
|
||||||
|
}),
|
||||||
|
taskData: newAllCodeTaskData(inputItems.map(wrapIntoJson), {
|
||||||
|
envProviderState: undefined,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(outcome.result).toEqual([wrapIntoJson({ val: undefined })]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('runOnceForAllItems', () => {
|
||||||
describe('continue on fail', () => {
|
describe('continue on fail', () => {
|
||||||
it('should return an item with the error if continueOnFail is true', async () => {
|
it('should return an item with the error if continueOnFail is true', async () => {
|
||||||
const outcome = await executeForAllItems({
|
const outcome = await executeForAllItems({
|
||||||
|
@ -181,21 +368,6 @@ describe('JsTaskRunner', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('runForEachItem', () => {
|
describe('runForEachItem', () => {
|
||||||
const executeForEachItem = async ({
|
|
||||||
code,
|
|
||||||
inputItems,
|
|
||||||
settings,
|
|
||||||
}: { code: string; inputItems: IDataObject[]; settings?: Partial<JSExecSettings> }) => {
|
|
||||||
return await execTaskWithParams({
|
|
||||||
task: newTaskWithSettings({
|
|
||||||
code,
|
|
||||||
nodeMode: 'runOnceForEachItem',
|
|
||||||
...settings,
|
|
||||||
}),
|
|
||||||
taskData: newAllCodeTaskData(inputItems.map(wrapIntoJson)),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('continue on fail', () => {
|
describe('continue on fail', () => {
|
||||||
it('should return an item with the error if continueOnFail is true', async () => {
|
it('should return an item with the error if continueOnFail is true', async () => {
|
||||||
const outcome = await executeForEachItem({
|
const outcome = await executeForEachItem({
|
||||||
|
|
|
@ -65,6 +65,9 @@ export const newAllCodeTaskData = (
|
||||||
const manualTriggerNode = newNode({
|
const manualTriggerNode = newNode({
|
||||||
name: 'Trigger',
|
name: 'Trigger',
|
||||||
type: 'n8n-nodes-base.manualTrigger',
|
type: 'n8n-nodes-base.manualTrigger',
|
||||||
|
parameters: {
|
||||||
|
manualTriggerParam: 'empty',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -116,15 +119,32 @@ export const newAllCodeTaskData = (
|
||||||
siblingParameters: {},
|
siblingParameters: {},
|
||||||
mode: 'manual',
|
mode: 'manual',
|
||||||
selfData: {},
|
selfData: {},
|
||||||
|
envProviderState: {
|
||||||
|
env: {},
|
||||||
|
isEnvAccessBlocked: true,
|
||||||
|
isProcessAvailable: true,
|
||||||
|
},
|
||||||
additionalData: {
|
additionalData: {
|
||||||
formWaitingBaseUrl: '',
|
executionId: 'exec-id',
|
||||||
instanceBaseUrl: '',
|
instanceBaseUrl: '',
|
||||||
restartExecutionId: '',
|
restartExecutionId: '',
|
||||||
restApiUrl: '',
|
restApiUrl: '',
|
||||||
webhookBaseUrl: '',
|
formWaitingBaseUrl: 'http://formWaitingBaseUrl',
|
||||||
webhookTestBaseUrl: '',
|
webhookBaseUrl: 'http://webhookBaseUrl',
|
||||||
webhookWaitingBaseUrl: '',
|
webhookTestBaseUrl: 'http://webhookTestBaseUrl',
|
||||||
variables: {},
|
webhookWaitingBaseUrl: 'http://webhookWaitingBaseUrl',
|
||||||
|
variables: {
|
||||||
|
var: 'value',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
executeData: {
|
||||||
|
node: codeNode,
|
||||||
|
data: {
|
||||||
|
main: [codeNodeInputData],
|
||||||
|
},
|
||||||
|
source: {
|
||||||
|
main: [{ previousNode: manualTriggerNode.name }],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
...opts,
|
...opts,
|
||||||
};
|
};
|
||||||
|
|
|
@ -17,6 +17,7 @@ import type {
|
||||||
INodeParameters,
|
INodeParameters,
|
||||||
IRunExecutionData,
|
IRunExecutionData,
|
||||||
WorkflowExecuteMode,
|
WorkflowExecuteMode,
|
||||||
|
EnvProviderState,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import * as a from 'node:assert';
|
import * as a from 'node:assert';
|
||||||
import { runInNewContext, type Context } from 'node:vm';
|
import { runInNewContext, type Context } from 'node:vm';
|
||||||
|
@ -63,6 +64,7 @@ export interface AllCodeTaskData {
|
||||||
connectionInputData: INodeExecutionData[];
|
connectionInputData: INodeExecutionData[];
|
||||||
siblingParameters: INodeParameters;
|
siblingParameters: INodeParameters;
|
||||||
mode: WorkflowExecuteMode;
|
mode: WorkflowExecuteMode;
|
||||||
|
envProviderState?: EnvProviderState;
|
||||||
executeData?: IExecuteData;
|
executeData?: IExecuteData;
|
||||||
defaultReturnRunIndex: number;
|
defaultReturnRunIndex: number;
|
||||||
selfData: IDataObject;
|
selfData: IDataObject;
|
||||||
|
@ -262,6 +264,13 @@ export class JsTaskRunner extends TaskRunner {
|
||||||
allData.defaultReturnRunIndex,
|
allData.defaultReturnRunIndex,
|
||||||
allData.selfData,
|
allData.selfData,
|
||||||
allData.contextNodeName,
|
allData.contextNodeName,
|
||||||
|
// Make sure that even if we don't receive the envProviderState for
|
||||||
|
// whatever reason, we don't expose the task runner's env to the code
|
||||||
|
allData.envProviderState ?? {
|
||||||
|
env: {},
|
||||||
|
isEnvAccessBlocked: false,
|
||||||
|
isProcessAvailable: true,
|
||||||
|
},
|
||||||
).getDataProxy();
|
).getDataProxy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,17 @@
|
||||||
import {
|
import type {
|
||||||
type IExecuteFunctions,
|
EnvProviderState,
|
||||||
type Workflow,
|
IExecuteFunctions,
|
||||||
type IRunExecutionData,
|
Workflow,
|
||||||
type INodeExecutionData,
|
IRunExecutionData,
|
||||||
type ITaskDataConnections,
|
INodeExecutionData,
|
||||||
type INode,
|
ITaskDataConnections,
|
||||||
type WorkflowParameters,
|
INode,
|
||||||
type INodeParameters,
|
WorkflowParameters,
|
||||||
type WorkflowExecuteMode,
|
INodeParameters,
|
||||||
type IExecuteData,
|
WorkflowExecuteMode,
|
||||||
type IDataObject,
|
IExecuteData,
|
||||||
type IWorkflowExecuteAdditionalData,
|
IDataObject,
|
||||||
|
IWorkflowExecuteAdditionalData,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
|
|
||||||
|
@ -42,6 +43,7 @@ export interface TaskData {
|
||||||
connectionInputData: INodeExecutionData[];
|
connectionInputData: INodeExecutionData[];
|
||||||
siblingParameters: INodeParameters;
|
siblingParameters: INodeParameters;
|
||||||
mode: WorkflowExecuteMode;
|
mode: WorkflowExecuteMode;
|
||||||
|
envProviderState: EnvProviderState;
|
||||||
executeData?: IExecuteData;
|
executeData?: IExecuteData;
|
||||||
defaultReturnRunIndex: number;
|
defaultReturnRunIndex: number;
|
||||||
selfData: IDataObject;
|
selfData: IDataObject;
|
||||||
|
@ -76,6 +78,7 @@ export interface AllCodeTaskData {
|
||||||
connectionInputData: INodeExecutionData[];
|
connectionInputData: INodeExecutionData[];
|
||||||
siblingParameters: INodeParameters;
|
siblingParameters: INodeParameters;
|
||||||
mode: WorkflowExecuteMode;
|
mode: WorkflowExecuteMode;
|
||||||
|
envProviderState: EnvProviderState;
|
||||||
executeData?: IExecuteData;
|
executeData?: IExecuteData;
|
||||||
defaultReturnRunIndex: number;
|
defaultReturnRunIndex: number;
|
||||||
selfData: IDataObject;
|
selfData: IDataObject;
|
||||||
|
@ -137,6 +140,7 @@ export class TaskManager {
|
||||||
connectionInputData: INodeExecutionData[],
|
connectionInputData: INodeExecutionData[],
|
||||||
siblingParameters: INodeParameters,
|
siblingParameters: INodeParameters,
|
||||||
mode: WorkflowExecuteMode,
|
mode: WorkflowExecuteMode,
|
||||||
|
envProviderState: EnvProviderState,
|
||||||
executeData?: IExecuteData,
|
executeData?: IExecuteData,
|
||||||
defaultReturnRunIndex = -1,
|
defaultReturnRunIndex = -1,
|
||||||
selfData: IDataObject = {},
|
selfData: IDataObject = {},
|
||||||
|
@ -153,6 +157,7 @@ export class TaskManager {
|
||||||
itemIndex,
|
itemIndex,
|
||||||
siblingParameters,
|
siblingParameters,
|
||||||
mode,
|
mode,
|
||||||
|
envProviderState,
|
||||||
executeData,
|
executeData,
|
||||||
defaultReturnRunIndex,
|
defaultReturnRunIndex,
|
||||||
selfData,
|
selfData,
|
||||||
|
@ -311,6 +316,7 @@ export class TaskManager {
|
||||||
contextNodeName: jd.contextNodeName,
|
contextNodeName: jd.contextNodeName,
|
||||||
defaultReturnRunIndex: jd.defaultReturnRunIndex,
|
defaultReturnRunIndex: jd.defaultReturnRunIndex,
|
||||||
mode: jd.mode,
|
mode: jd.mode,
|
||||||
|
envProviderState: jd.envProviderState,
|
||||||
node: jd.node,
|
node: jd.node,
|
||||||
runExecutionData: jd.runExecutionData,
|
runExecutionData: jd.runExecutionData,
|
||||||
runIndex: jd.runIndex,
|
runIndex: jd.runIndex,
|
||||||
|
|
|
@ -6,6 +6,13 @@
|
||||||
import type { PushType } from '@n8n/api-types';
|
import type { PushType } from '@n8n/api-types';
|
||||||
import { GlobalConfig } from '@n8n/config';
|
import { GlobalConfig } from '@n8n/config';
|
||||||
import { WorkflowExecute } from 'n8n-core';
|
import { WorkflowExecute } from 'n8n-core';
|
||||||
|
import {
|
||||||
|
ApplicationError,
|
||||||
|
ErrorReporterProxy as ErrorReporter,
|
||||||
|
NodeOperationError,
|
||||||
|
Workflow,
|
||||||
|
WorkflowHooks,
|
||||||
|
} from 'n8n-workflow';
|
||||||
import type {
|
import type {
|
||||||
IDataObject,
|
IDataObject,
|
||||||
IExecuteData,
|
IExecuteData,
|
||||||
|
@ -28,13 +35,7 @@ import type {
|
||||||
ITaskDataConnections,
|
ITaskDataConnections,
|
||||||
ExecuteWorkflowOptions,
|
ExecuteWorkflowOptions,
|
||||||
IWorkflowExecutionDataProcess,
|
IWorkflowExecutionDataProcess,
|
||||||
} from 'n8n-workflow';
|
EnvProviderState,
|
||||||
import {
|
|
||||||
ApplicationError,
|
|
||||||
ErrorReporterProxy as ErrorReporter,
|
|
||||||
NodeOperationError,
|
|
||||||
Workflow,
|
|
||||||
WorkflowHooks,
|
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
|
|
||||||
|
@ -1008,6 +1009,7 @@ export async function getBase(
|
||||||
connectionInputData: INodeExecutionData[],
|
connectionInputData: INodeExecutionData[],
|
||||||
siblingParameters: INodeParameters,
|
siblingParameters: INodeParameters,
|
||||||
mode: WorkflowExecuteMode,
|
mode: WorkflowExecuteMode,
|
||||||
|
envProviderState: EnvProviderState,
|
||||||
executeData?: IExecuteData,
|
executeData?: IExecuteData,
|
||||||
defaultReturnRunIndex?: number,
|
defaultReturnRunIndex?: number,
|
||||||
selfData?: IDataObject,
|
selfData?: IDataObject,
|
||||||
|
@ -1028,6 +1030,7 @@ export async function getBase(
|
||||||
connectionInputData,
|
connectionInputData,
|
||||||
siblingParameters,
|
siblingParameters,
|
||||||
mode,
|
mode,
|
||||||
|
envProviderState,
|
||||||
executeData,
|
executeData,
|
||||||
defaultReturnRunIndex,
|
defaultReturnRunIndex,
|
||||||
selfData,
|
selfData,
|
||||||
|
|
|
@ -11,6 +11,7 @@ import type {
|
||||||
IExecuteData,
|
IExecuteData,
|
||||||
IDataObject,
|
IDataObject,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
import { createEnvProviderState } from 'n8n-workflow';
|
||||||
|
|
||||||
export const createAgentStartJob = (
|
export const createAgentStartJob = (
|
||||||
additionalData: IWorkflowExecuteAdditionalData,
|
additionalData: IWorkflowExecuteAdditionalData,
|
||||||
|
@ -49,6 +50,7 @@ export const createAgentStartJob = (
|
||||||
connectionInputData,
|
connectionInputData,
|
||||||
siblingParameters,
|
siblingParameters,
|
||||||
mode,
|
mode,
|
||||||
|
createEnvProviderState(),
|
||||||
executeData,
|
executeData,
|
||||||
defaultReturnRunIndex,
|
defaultReturnRunIndex,
|
||||||
selfData,
|
selfData,
|
||||||
|
|
|
@ -22,6 +22,7 @@ import type { WorkflowActivationError } from './errors/workflow-activation.error
|
||||||
import type { WorkflowOperationError } from './errors/workflow-operation.error';
|
import type { WorkflowOperationError } from './errors/workflow-operation.error';
|
||||||
import type { ExecutionStatus } from './ExecutionStatus';
|
import type { ExecutionStatus } from './ExecutionStatus';
|
||||||
import type { Workflow } from './Workflow';
|
import type { Workflow } from './Workflow';
|
||||||
|
import type { EnvProviderState } from './WorkflowDataProxyEnvProvider';
|
||||||
import type { WorkflowHooks } from './WorkflowHooks';
|
import type { WorkflowHooks } from './WorkflowHooks';
|
||||||
|
|
||||||
export interface IAdditionalCredentialOptions {
|
export interface IAdditionalCredentialOptions {
|
||||||
|
@ -2256,6 +2257,7 @@ export interface IWorkflowExecuteAdditionalData {
|
||||||
connectionInputData: INodeExecutionData[],
|
connectionInputData: INodeExecutionData[],
|
||||||
siblingParameters: INodeParameters,
|
siblingParameters: INodeParameters,
|
||||||
mode: WorkflowExecuteMode,
|
mode: WorkflowExecuteMode,
|
||||||
|
envProviderState: EnvProviderState,
|
||||||
executeData?: IExecuteData,
|
executeData?: IExecuteData,
|
||||||
defaultReturnRunIndex?: number,
|
defaultReturnRunIndex?: number,
|
||||||
selfData?: IDataObject,
|
selfData?: IDataObject,
|
||||||
|
|
|
@ -30,6 +30,8 @@ import {
|
||||||
import * as NodeHelpers from './NodeHelpers';
|
import * as NodeHelpers from './NodeHelpers';
|
||||||
import { deepCopy } from './utils';
|
import { deepCopy } from './utils';
|
||||||
import type { Workflow } from './Workflow';
|
import type { Workflow } from './Workflow';
|
||||||
|
import type { EnvProviderState } from './WorkflowDataProxyEnvProvider';
|
||||||
|
import { createEnvProvider, createEnvProviderState } from './WorkflowDataProxyEnvProvider';
|
||||||
import { getPinDataIfManualExecution } from './WorkflowDataProxyHelpers';
|
import { getPinDataIfManualExecution } from './WorkflowDataProxyHelpers';
|
||||||
|
|
||||||
export function isResourceLocatorValue(value: unknown): value is INodeParameterResourceLocator {
|
export function isResourceLocatorValue(value: unknown): value is INodeParameterResourceLocator {
|
||||||
|
@ -66,6 +68,7 @@ export class WorkflowDataProxy {
|
||||||
private defaultReturnRunIndex = -1,
|
private defaultReturnRunIndex = -1,
|
||||||
private selfData: IDataObject = {},
|
private selfData: IDataObject = {},
|
||||||
private contextNodeName: string = activeNodeName,
|
private contextNodeName: string = activeNodeName,
|
||||||
|
private envProviderState?: EnvProviderState,
|
||||||
) {
|
) {
|
||||||
this.runExecutionData = isScriptingNode(this.contextNodeName, workflow)
|
this.runExecutionData = isScriptingNode(this.contextNodeName, workflow)
|
||||||
? runExecutionData !== null
|
? runExecutionData !== null
|
||||||
|
@ -487,40 +490,6 @@ export class WorkflowDataProxy {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a proxy to query data from the environment
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private envGetter() {
|
|
||||||
const that = this;
|
|
||||||
return new Proxy(
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
has: () => true,
|
|
||||||
get(_, name) {
|
|
||||||
if (name === 'isProxy') return true;
|
|
||||||
|
|
||||||
if (typeof process === 'undefined') {
|
|
||||||
throw new ExpressionError('not accessible via UI, please run node', {
|
|
||||||
runIndex: that.runIndex,
|
|
||||||
itemIndex: that.itemIndex,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (process.env.N8N_BLOCK_ENV_ACCESS_IN_NODE === 'true') {
|
|
||||||
throw new ExpressionError('access to env vars denied', {
|
|
||||||
causeDetailed:
|
|
||||||
'If you need access please contact the administrator to remove the environment variable ‘N8N_BLOCK_ENV_ACCESS_IN_NODE‘',
|
|
||||||
runIndex: that.runIndex,
|
|
||||||
itemIndex: that.itemIndex,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return process.env[name.toString()];
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private prevNodeGetter() {
|
private prevNodeGetter() {
|
||||||
const allowedValues = ['name', 'outputIndex', 'runIndex'];
|
const allowedValues = ['name', 'outputIndex', 'runIndex'];
|
||||||
const that = this;
|
const that = this;
|
||||||
|
@ -1303,7 +1272,11 @@ export class WorkflowDataProxy {
|
||||||
|
|
||||||
$binary: {}, // Placeholder
|
$binary: {}, // Placeholder
|
||||||
$data: {}, // Placeholder
|
$data: {}, // Placeholder
|
||||||
$env: this.envGetter(),
|
$env: createEnvProvider(
|
||||||
|
that.runIndex,
|
||||||
|
that.itemIndex,
|
||||||
|
that.envProviderState ?? createEnvProviderState(),
|
||||||
|
),
|
||||||
$evaluateExpression: (expression: string, itemIndex?: number) => {
|
$evaluateExpression: (expression: string, itemIndex?: number) => {
|
||||||
itemIndex = itemIndex || that.itemIndex;
|
itemIndex = itemIndex || that.itemIndex;
|
||||||
return that.workflow.expression.getParameterValue(
|
return that.workflow.expression.getParameterValue(
|
||||||
|
|
75
packages/workflow/src/WorkflowDataProxyEnvProvider.ts
Normal file
75
packages/workflow/src/WorkflowDataProxyEnvProvider.ts
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
import { ExpressionError } from './errors/expression.error';
|
||||||
|
|
||||||
|
export type EnvProviderState = {
|
||||||
|
isProcessAvailable: boolean;
|
||||||
|
isEnvAccessBlocked: boolean;
|
||||||
|
env: Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Captures a snapshot of the environment variables and configuration
|
||||||
|
* that can be used to initialize an environment provider.
|
||||||
|
*/
|
||||||
|
export function createEnvProviderState(): EnvProviderState {
|
||||||
|
const isProcessAvailable = typeof process !== 'undefined';
|
||||||
|
const isEnvAccessBlocked = isProcessAvailable
|
||||||
|
? process.env.N8N_BLOCK_ENV_ACCESS_IN_NODE === 'true'
|
||||||
|
: false;
|
||||||
|
const env: Record<string, string> =
|
||||||
|
!isProcessAvailable || isEnvAccessBlocked ? {} : (process.env as Record<string, string>);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isProcessAvailable,
|
||||||
|
isEnvAccessBlocked,
|
||||||
|
env,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a proxy that provides access to the environment variables
|
||||||
|
* in the `WorkflowDataProxy`. Use the `createEnvProviderState` to
|
||||||
|
* create the default state object that is needed for the proxy,
|
||||||
|
* unless you need something specific.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* createEnvProvider(
|
||||||
|
* runIndex,
|
||||||
|
* itemIndex,
|
||||||
|
* createEnvProviderState(),
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
export function createEnvProvider(
|
||||||
|
runIndex: number,
|
||||||
|
itemIndex: number,
|
||||||
|
providerState: EnvProviderState,
|
||||||
|
): Record<string, string> {
|
||||||
|
return new Proxy(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
has() {
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
get(_, name) {
|
||||||
|
if (name === 'isProxy') return true;
|
||||||
|
|
||||||
|
if (!providerState.isProcessAvailable) {
|
||||||
|
throw new ExpressionError('not accessible via UI, please run node', {
|
||||||
|
runIndex,
|
||||||
|
itemIndex,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (providerState.isEnvAccessBlocked) {
|
||||||
|
throw new ExpressionError('access to env vars denied', {
|
||||||
|
causeDetailed:
|
||||||
|
'If you need access please contact the administrator to remove the environment variable ‘N8N_BLOCK_ENV_ACCESS_IN_NODE‘',
|
||||||
|
runIndex,
|
||||||
|
itemIndex,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return providerState.env[name.toString()];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
|
@ -18,6 +18,7 @@ export * from './NodeHelpers';
|
||||||
export * from './RoutingNode';
|
export * from './RoutingNode';
|
||||||
export * from './Workflow';
|
export * from './Workflow';
|
||||||
export * from './WorkflowDataProxy';
|
export * from './WorkflowDataProxy';
|
||||||
|
export * from './WorkflowDataProxyEnvProvider';
|
||||||
export * from './WorkflowHooks';
|
export * from './WorkflowHooks';
|
||||||
export * from './VersionedNodeType';
|
export * from './VersionedNodeType';
|
||||||
export * from './TypeValidation';
|
export * from './TypeValidation';
|
||||||
|
|
87
packages/workflow/test/WorkflowDataProxyEnvProvider.test.ts
Normal file
87
packages/workflow/test/WorkflowDataProxyEnvProvider.test.ts
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
import { ExpressionError } from '../src/errors/expression.error';
|
||||||
|
import { createEnvProvider, createEnvProviderState } from '../src/WorkflowDataProxyEnvProvider';
|
||||||
|
|
||||||
|
describe('createEnvProviderState', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
delete process.env.N8N_BLOCK_ENV_ACCESS_IN_NODE;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the state with process available and env access allowed', () => {
|
||||||
|
expect(createEnvProviderState()).toEqual({
|
||||||
|
isProcessAvailable: true,
|
||||||
|
isEnvAccessBlocked: false,
|
||||||
|
env: process.env,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should block env access when N8N_BLOCK_ENV_ACCESS_IN_NODE is set to "true"', () => {
|
||||||
|
process.env.N8N_BLOCK_ENV_ACCESS_IN_NODE = 'true';
|
||||||
|
|
||||||
|
expect(createEnvProviderState()).toEqual({
|
||||||
|
isProcessAvailable: true,
|
||||||
|
isEnvAccessBlocked: true,
|
||||||
|
env: {},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle process not being available', () => {
|
||||||
|
const originalProcess = global.process;
|
||||||
|
try {
|
||||||
|
// @ts-expect-error process is read-only
|
||||||
|
global.process = undefined;
|
||||||
|
|
||||||
|
expect(createEnvProviderState()).toEqual({
|
||||||
|
isProcessAvailable: false,
|
||||||
|
isEnvAccessBlocked: false,
|
||||||
|
env: {},
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
global.process = originalProcess;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createEnvProvider', () => {
|
||||||
|
it('should return true when checking for a property using "has"', () => {
|
||||||
|
const proxy = createEnvProvider(0, 0, createEnvProviderState());
|
||||||
|
expect('someProperty' in proxy).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the value from process.env if access is allowed', () => {
|
||||||
|
process.env.TEST_ENV_VAR = 'test_value';
|
||||||
|
const proxy = createEnvProvider(0, 0, createEnvProviderState());
|
||||||
|
expect(proxy.TEST_ENV_VAR).toBe('test_value');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw ExpressionError when process is unavailable', () => {
|
||||||
|
const originalProcess = global.process;
|
||||||
|
// @ts-expect-error process is read-only
|
||||||
|
global.process = undefined;
|
||||||
|
try {
|
||||||
|
const proxy = createEnvProvider(1, 1, createEnvProviderState());
|
||||||
|
|
||||||
|
expect(() => proxy.someEnvVar).toThrowError(
|
||||||
|
new ExpressionError('not accessible via UI, please run node', {
|
||||||
|
runIndex: 1,
|
||||||
|
itemIndex: 1,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
global.process = originalProcess;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw ExpressionError when env access is blocked', () => {
|
||||||
|
process.env.N8N_BLOCK_ENV_ACCESS_IN_NODE = 'true';
|
||||||
|
const proxy = createEnvProvider(1, 1, createEnvProviderState());
|
||||||
|
|
||||||
|
expect(() => proxy.someEnvVar).toThrowError(
|
||||||
|
new ExpressionError('access to env vars denied', {
|
||||||
|
causeDetailed:
|
||||||
|
'If you need access please contact the administrator to remove the environment variable ‘N8N_BLOCK_ENV_ACCESS_IN_NODE‘',
|
||||||
|
runIndex: 1,
|
||||||
|
itemIndex: 1,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
|
@ -641,6 +641,10 @@ importers:
|
||||||
ws:
|
ws:
|
||||||
specifier: '>=8.17.1'
|
specifier: '>=8.17.1'
|
||||||
version: 8.17.1
|
version: 8.17.1
|
||||||
|
devDependencies:
|
||||||
|
luxon:
|
||||||
|
specifier: 'catalog:'
|
||||||
|
version: 3.4.4
|
||||||
|
|
||||||
packages/@n8n_io/eslint-config:
|
packages/@n8n_io/eslint-config:
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
|
Loading…
Reference in a new issue