feat: Add support for $env in the js task runner (no-changelog) (#11177)

This commit is contained in:
Tomi Turtiainen
2024-10-09 17:31:45 +03:00
committed by GitHub
parent 939c0dfc4c
commit e94cda3837
13 changed files with 445 additions and 88 deletions

View File

@@ -22,6 +22,7 @@ import type { WorkflowActivationError } from './errors/workflow-activation.error
import type { WorkflowOperationError } from './errors/workflow-operation.error';
import type { ExecutionStatus } from './ExecutionStatus';
import type { Workflow } from './Workflow';
import type { EnvProviderState } from './WorkflowDataProxyEnvProvider';
import type { WorkflowHooks } from './WorkflowHooks';
export interface IAdditionalCredentialOptions {
@@ -2256,6 +2257,7 @@ export interface IWorkflowExecuteAdditionalData {
connectionInputData: INodeExecutionData[],
siblingParameters: INodeParameters,
mode: WorkflowExecuteMode,
envProviderState: EnvProviderState,
executeData?: IExecuteData,
defaultReturnRunIndex?: number,
selfData?: IDataObject,

View File

@@ -30,6 +30,8 @@ import {
import * as NodeHelpers from './NodeHelpers';
import { deepCopy } from './utils';
import type { Workflow } from './Workflow';
import type { EnvProviderState } from './WorkflowDataProxyEnvProvider';
import { createEnvProvider, createEnvProviderState } from './WorkflowDataProxyEnvProvider';
import { getPinDataIfManualExecution } from './WorkflowDataProxyHelpers';
export function isResourceLocatorValue(value: unknown): value is INodeParameterResourceLocator {
@@ -66,6 +68,7 @@ export class WorkflowDataProxy {
private defaultReturnRunIndex = -1,
private selfData: IDataObject = {},
private contextNodeName: string = activeNodeName,
private envProviderState?: EnvProviderState,
) {
this.runExecutionData = isScriptingNode(this.contextNodeName, workflow)
? 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() {
const allowedValues = ['name', 'outputIndex', 'runIndex'];
const that = this;
@@ -1303,7 +1272,11 @@ export class WorkflowDataProxy {
$binary: {}, // Placeholder
$data: {}, // Placeholder
$env: this.envGetter(),
$env: createEnvProvider(
that.runIndex,
that.itemIndex,
that.envProviderState ?? createEnvProviderState(),
),
$evaluateExpression: (expression: string, itemIndex?: number) => {
itemIndex = itemIndex || that.itemIndex;
return that.workflow.expression.getParameterValue(

View 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()];
},
},
);
}

View File

@@ -18,6 +18,7 @@ export * from './NodeHelpers';
export * from './RoutingNode';
export * from './Workflow';
export * from './WorkflowDataProxy';
export * from './WorkflowDataProxyEnvProvider';
export * from './WorkflowHooks';
export * from './VersionedNodeType';
export * from './TypeValidation';

View 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,
}),
);
});
});