n8n/packages/cli/test/integration/execution-redaction.test.ts

921 lines
29 KiB
TypeScript

import {
createTeamProject,
linkUserToProject,
createWorkflow,
testDb,
mockInstance,
} from '@n8n/backend-test-utils';
import type { User } from '@n8n/db';
import { stringify } from 'flatted';
import type { INode, IRunExecutionData, IWorkflowBase, WorkflowExecuteMode } from 'n8n-workflow';
import { createRunExecutionData, NodeApiError, NodeOperationError } from 'n8n-workflow';
import { ConcurrencyControlService } from '@/concurrency/concurrency-control.service';
import { WaitTracker } from '@/wait-tracker';
import { createExecution } from './shared/db/executions';
import {
createMember,
createMemberWithApiKey,
createOwner,
createOwnerWithApiKey,
} from './shared/db/users';
import { setupTestServer } from './shared/utils';
mockInstance(WaitTracker);
mockInstance(ConcurrencyControlService, {
// @ts-expect-error Private property
isEnabled: false,
});
const testServer = setupTestServer({
endpointGroups: ['executions', 'workflows', 'publicApi'],
modules: ['redaction'],
});
let owner: User;
let member: User;
let publicApiOwner: User;
let publicApiMember: User;
beforeEach(async () => {
await testDb.truncate(['ExecutionEntity', 'WorkflowEntity', 'SharedWorkflow']);
testServer.license.reset();
testServer.license.enable('feat:dataRedaction');
owner = await createOwner();
member = await createMember();
publicApiOwner = await createOwnerWithApiKey();
publicApiMember = await createMemberWithApiKey();
});
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
const SENSITIVE_JSON = { secret: 'sensitive-value', apiKey: 'sk-1234' };
const BINARY_DATA = {
data: 'base64data',
mimeType: 'text/plain',
fileName: 'secret.txt',
};
function buildRunExecutionData(opts: {
policy?: 'none' | 'non-manual' | 'all';
mode?: WorkflowExecuteMode;
}): IRunExecutionData {
return createRunExecutionData({
resultData: {
runData: {
'Test Node': [
{
startTime: 0,
executionIndex: 0,
executionTime: 0,
executionStatus: 'success',
source: [],
data: {
main: [
[
{
json: { ...SENSITIVE_JSON },
binary: { file: { ...BINARY_DATA } },
},
],
],
},
},
],
},
},
executionData: opts.policy
? {
runtimeData: {
version: 1 as const,
establishedAt: Date.now(),
source: opts.mode ?? 'trigger',
redaction: { version: 1 as const, policy: opts.policy },
},
}
: undefined,
});
}
async function createExecutionWithRedaction(opts: {
workflow: IWorkflowBase;
mode?: WorkflowExecuteMode;
policy?: 'none' | 'non-manual' | 'all';
}) {
const runData = buildRunExecutionData({ policy: opts.policy, mode: opts.mode });
return await createExecution(
{ data: stringify(runData), mode: opts.mode ?? 'trigger' },
opts.workflow,
);
}
function parseResponseData(responseBody: { data: { data: string } }): IRunExecutionData {
// The response data field is flatted-stringified
const { parse } = require('flatted');
return parse(responseBody.data.data) as IRunExecutionData;
}
function assertRedacted(
data: IRunExecutionData,
expectedReason = 'workflow_redaction_policy',
expectedCanReveal = true,
) {
const items = data.resultData.runData['Test Node'][0].data!.main[0]!;
expect(items.length).toBeGreaterThan(0);
for (const item of items) {
expect(item.json).toEqual({});
expect(item.binary).toBeUndefined();
expect(item.redaction).toEqual({ redacted: true, reason: expectedReason });
}
expect(data.redactionInfo).toEqual({
isRedacted: true,
reason: expectedReason,
canReveal: expectedCanReveal,
});
}
function assertNotRedacted(data: IRunExecutionData) {
const items = data.resultData.runData['Test Node'][0].data!.main[0]!;
expect(items.length).toBeGreaterThan(0);
for (const item of items) {
expect(item.json).toEqual(expect.objectContaining({ secret: 'sensitive-value' }));
}
expect(data.redactionInfo).toBeUndefined();
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
describe('GET /executions/:id — Execution Redaction', () => {
describe('policy-based redaction (no query param)', () => {
test('policy "none" with trigger mode — returns unredacted', async () => {
const workflow = await createWorkflow({}, owner);
const execution = await createExecutionWithRedaction({
workflow,
mode: 'trigger',
policy: 'none',
});
const response = await testServer
.authAgentFor(owner)
.get(`/executions/${execution.id}`)
.expect(200);
assertNotRedacted(parseResponseData(response.body));
});
test('policy "none" with manual mode — returns unredacted', async () => {
const workflow = await createWorkflow({}, owner);
const execution = await createExecutionWithRedaction({
workflow,
mode: 'manual',
policy: 'none',
});
const response = await testServer
.authAgentFor(owner)
.get(`/executions/${execution.id}`)
.expect(200);
assertNotRedacted(parseResponseData(response.body));
});
test('policy "non-manual" with manual mode — returns unredacted', async () => {
const workflow = await createWorkflow({}, owner);
const execution = await createExecutionWithRedaction({
workflow,
mode: 'manual',
policy: 'non-manual',
});
const response = await testServer
.authAgentFor(owner)
.get(`/executions/${execution.id}`)
.expect(200);
assertNotRedacted(parseResponseData(response.body));
});
test('policy "non-manual" with trigger mode — returns redacted', async () => {
const workflow = await createWorkflow({}, owner);
const execution = await createExecutionWithRedaction({
workflow,
mode: 'trigger',
policy: 'non-manual',
});
const response = await testServer
.authAgentFor(owner)
.get(`/executions/${execution.id}`)
.expect(200);
assertRedacted(parseResponseData(response.body));
});
test('policy "non-manual" with webhook mode — returns redacted', async () => {
const workflow = await createWorkflow({}, owner);
const execution = await createExecutionWithRedaction({
workflow,
mode: 'webhook',
policy: 'non-manual',
});
const response = await testServer
.authAgentFor(owner)
.get(`/executions/${execution.id}`)
.expect(200);
assertRedacted(parseResponseData(response.body));
});
test('policy "all" with manual mode — returns redacted', async () => {
const workflow = await createWorkflow({}, owner);
const execution = await createExecutionWithRedaction({
workflow,
mode: 'manual',
policy: 'all',
});
const response = await testServer
.authAgentFor(owner)
.get(`/executions/${execution.id}`)
.expect(200);
assertRedacted(parseResponseData(response.body));
});
test('policy "all" with trigger mode — returns redacted', async () => {
const workflow = await createWorkflow({}, owner);
const execution = await createExecutionWithRedaction({
workflow,
mode: 'trigger',
policy: 'all',
});
const response = await testServer
.authAgentFor(owner)
.get(`/executions/${execution.id}`)
.expect(200);
assertRedacted(parseResponseData(response.body));
});
test('no runtimeData and no workflow settings — defaults to "none", returns unredacted', async () => {
const workflow = await createWorkflow({}, owner);
// No policy passed → no runtimeData.redaction
const execution = await createExecutionWithRedaction({
workflow,
mode: 'trigger',
});
const response = await testServer
.authAgentFor(owner)
.get(`/executions/${execution.id}`)
.expect(200);
assertNotRedacted(parseResponseData(response.body));
});
test('no runtimeData but workflow settings has policy "all" — falls back to settings, returns redacted', async () => {
const workflow = await createWorkflow({ settings: { redactionPolicy: 'all' } }, owner);
// No runtimeData.redaction → falls back to workflowData.settings.redactionPolicy
const execution = await createExecutionWithRedaction({
workflow,
mode: 'trigger',
});
const response = await testServer
.authAgentFor(owner)
.get(`/executions/${execution.id}`)
.expect(200);
assertRedacted(parseResponseData(response.body));
});
});
describe('explicit redactExecutionData=true query param', () => {
test('always redacts even when policy is "none"', async () => {
const workflow = await createWorkflow({}, owner);
const execution = await createExecutionWithRedaction({
workflow,
mode: 'trigger',
policy: 'none',
});
const response = await testServer
.authAgentFor(owner)
.get(`/executions/${execution.id}`)
.query({ redactExecutionData: 'true' })
.expect(200);
assertRedacted(parseResponseData(response.body), 'user_requested');
});
});
describe('explicit redactExecutionData=false query param (reveal)', () => {
test('owner can reveal unredacted data', async () => {
const workflow = await createWorkflow({}, owner);
const execution = await createExecutionWithRedaction({
workflow,
mode: 'trigger',
policy: 'all',
});
const response = await testServer
.authAgentFor(owner)
.get(`/executions/${execution.id}`)
.query({ redactExecutionData: 'false' })
.expect(200);
assertNotRedacted(parseResponseData(response.body));
});
test('project editor without execution:reveal scope gets 403 with structured error', async () => {
testServer.license.enable('feat:sharing');
const teamProject = await createTeamProject();
await linkUserToProject(member, teamProject, 'project:editor');
const workflow = await createWorkflow({}, teamProject);
const execution = await createExecutionWithRedaction({
workflow,
mode: 'trigger',
policy: 'all',
});
const response = await testServer
.authAgentFor(member)
.get(`/executions/${execution.id}`)
.query({ redactExecutionData: 'false' })
.expect(403);
expect(response.status).toBe(403);
expect(response.body.message).toContain('execution:reveal');
});
test('project editor without execution:reveal scope can still reveal when policy allows it (policy=none)', async () => {
testServer.license.enable('feat:sharing');
const teamProject = await createTeamProject();
await linkUserToProject(member, teamProject, 'project:editor');
const workflow = await createWorkflow({}, teamProject);
// policy='none' means policyAllowsReveal=true — no permission check needed
const execution = await createExecutionWithRedaction({
workflow,
mode: 'trigger',
policy: 'none',
});
const response = await testServer
.authAgentFor(member)
.get(`/executions/${execution.id}`)
.query({ redactExecutionData: 'false' })
.expect(200);
assertNotRedacted(parseResponseData(response.body));
});
});
describe('policy fallback precedence', () => {
test('runtimeData policy takes precedence over workflowData.settings', async () => {
// Workflow settings say "all" but runtimeData says "none"
const workflow = await createWorkflow({ settings: { redactionPolicy: 'all' } }, owner);
const execution = await createExecutionWithRedaction({
workflow,
mode: 'trigger',
policy: 'none', // runtimeData wins
});
const response = await testServer
.authAgentFor(owner)
.get(`/executions/${execution.id}`)
.expect(200);
assertNotRedacted(parseResponseData(response.body));
});
test('falls back to workflowData.settings when runtimeData is missing', async () => {
const workflow = await createWorkflow({ settings: { redactionPolicy: 'all' } }, owner);
// No policy → no runtimeData.redaction → falls back to settings
const execution = await createExecutionWithRedaction({
workflow,
mode: 'trigger',
});
const response = await testServer
.authAgentFor(owner)
.get(`/executions/${execution.id}`)
.expect(200);
assertRedacted(parseResponseData(response.body));
});
});
describe('error redaction through DB round-trip', () => {
const mockNode: INode = {
id: 'node-1',
name: 'Test Node',
type: 'n8n-nodes-base.httpRequest',
typeVersion: 1,
position: [0, 0],
parameters: {},
};
test('task-level NodeApiError is redacted after DB round-trip', async () => {
const workflow = await createWorkflow({}, owner);
const error = new NodeApiError(mockNode, { message: 'Bad Gateway' }, { httpCode: '502' });
const runData = buildRunExecutionData({ policy: 'all', mode: 'trigger' });
runData.resultData.runData['Test Node'][0].error = error;
const execution = await createExecution(
{ data: stringify(runData), mode: 'trigger' },
workflow,
);
const response = await testServer
.authAgentFor(owner)
.get(`/executions/${execution.id}`)
.expect(200);
const data = parseResponseData(response.body);
const taskData = data.resultData.runData['Test Node'][0];
expect(taskData.error).toBeUndefined();
expect(taskData.redactedError).toEqual({ type: 'NodeApiError', httpCode: null });
});
test('workflow-level NodeOperationError is redacted after DB round-trip', async () => {
const workflow = await createWorkflow({}, owner);
const error = new NodeOperationError(mockNode, 'Workflow operation failed');
const runData = buildRunExecutionData({ policy: 'all', mode: 'trigger' });
runData.resultData.error = error;
const execution = await createExecution(
{ data: stringify(runData), mode: 'trigger' },
workflow,
);
const response = await testServer
.authAgentFor(owner)
.get(`/executions/${execution.id}`)
.expect(200);
const data = parseResponseData(response.body);
expect(data.resultData.error).toBeUndefined();
expect(data.resultData.redactedError).toEqual({ type: 'NodeOperationError' });
expect(data.resultData.redactedError).not.toHaveProperty('httpCode');
});
});
});
// ---------------------------------------------------------------------------
// GET /workflows/:workflowId/executions/last-successful — Execution Redaction
// ---------------------------------------------------------------------------
describe('GET /workflows/:workflowId/executions/last-successful — Execution Redaction', () => {
describe('policy-based redaction (no query param)', () => {
test('policy "none" with trigger mode — returns unredacted', async () => {
const workflow = await createWorkflow({}, owner);
await createExecutionWithRedaction({ workflow, mode: 'trigger', policy: 'none' });
const response = await testServer
.authAgentFor(owner)
.get(`/workflows/${workflow.id}/executions/last-successful`)
.expect(200);
assertNotRedacted(response.body.data.data as IRunExecutionData);
});
test('policy "non-manual" with trigger mode — returns redacted', async () => {
const workflow = await createWorkflow({}, owner);
await createExecutionWithRedaction({ workflow, mode: 'trigger', policy: 'non-manual' });
const response = await testServer
.authAgentFor(owner)
.get(`/workflows/${workflow.id}/executions/last-successful`)
.expect(200);
assertRedacted(response.body.data.data as IRunExecutionData);
});
test('policy "non-manual" with manual mode — returns unredacted', async () => {
const workflow = await createWorkflow({}, owner);
await createExecutionWithRedaction({ workflow, mode: 'manual', policy: 'non-manual' });
const response = await testServer
.authAgentFor(owner)
.get(`/workflows/${workflow.id}/executions/last-successful`)
.expect(200);
assertNotRedacted(response.body.data.data as IRunExecutionData);
});
test('policy "all" with trigger mode — returns redacted', async () => {
const workflow = await createWorkflow({}, owner);
await createExecutionWithRedaction({ workflow, mode: 'trigger', policy: 'all' });
const response = await testServer
.authAgentFor(owner)
.get(`/workflows/${workflow.id}/executions/last-successful`)
.expect(200);
assertRedacted(response.body.data.data as IRunExecutionData);
});
});
describe('explicit redactExecutionData=true query param', () => {
test('always redacts even when policy is "none"', async () => {
const workflow = await createWorkflow({}, owner);
await createExecutionWithRedaction({ workflow, mode: 'trigger', policy: 'none' });
const response = await testServer
.authAgentFor(owner)
.get(`/workflows/${workflow.id}/executions/last-successful`)
.query({ redactExecutionData: 'true' })
.expect(200);
assertRedacted(response.body.data.data as IRunExecutionData, 'user_requested');
});
});
describe('explicit redactExecutionData=false query param (reveal)', () => {
test('owner can reveal unredacted data when policy is "all"', async () => {
const workflow = await createWorkflow({}, owner);
await createExecutionWithRedaction({ workflow, mode: 'trigger', policy: 'all' });
const response = await testServer
.authAgentFor(owner)
.get(`/workflows/${workflow.id}/executions/last-successful`)
.query({ redactExecutionData: 'false' })
.expect(200);
assertNotRedacted(response.body.data.data as IRunExecutionData);
});
});
test('returns null when no successful execution exists', async () => {
const workflow = await createWorkflow({}, owner);
const response = await testServer
.authAgentFor(owner)
.get(`/workflows/${workflow.id}/executions/last-successful`)
.expect(200);
expect(response.body.data).toBeNull();
});
});
// ---------------------------------------------------------------------------
// GET /api/v1/executions/:id — Execution Redaction
// ---------------------------------------------------------------------------
describe('GET /api/v1/executions/:id — Execution Redaction', () => {
describe('policy-based redaction (includeData=true)', () => {
test('policy "none" with trigger mode — returns unredacted', async () => {
const workflow = await createWorkflow({}, publicApiMember);
const execution = await createExecutionWithRedaction({
workflow,
mode: 'trigger',
policy: 'none',
});
const response = await testServer
.publicApiAgentFor(publicApiMember)
.get(`/executions/${execution.id}?includeData=true`)
.expect(200);
assertNotRedacted(response.body.data as IRunExecutionData);
});
test('policy "non-manual" with trigger mode — redacts for member (canReveal=false)', async () => {
const workflow = await createWorkflow({}, publicApiMember);
const execution = await createExecutionWithRedaction({
workflow,
mode: 'trigger',
policy: 'non-manual',
});
const response = await testServer
.publicApiAgentFor(publicApiMember)
.get(`/executions/${execution.id}?includeData=true`)
.expect(200);
assertRedacted(response.body.data as IRunExecutionData, 'workflow_redaction_policy', false);
});
test('policy "non-manual" with trigger mode — redacts for owner (canReveal=true)', async () => {
const workflow = await createWorkflow({}, publicApiOwner);
const execution = await createExecutionWithRedaction({
workflow,
mode: 'trigger',
policy: 'non-manual',
});
const response = await testServer
.publicApiAgentFor(publicApiOwner)
.get(`/executions/${execution.id}?includeData=true`)
.expect(200);
assertRedacted(response.body.data as IRunExecutionData, 'workflow_redaction_policy', true);
});
test('policy "non-manual" with manual mode — returns unredacted', async () => {
const workflow = await createWorkflow({}, publicApiMember);
const execution = await createExecutionWithRedaction({
workflow,
mode: 'manual',
policy: 'non-manual',
});
const response = await testServer
.publicApiAgentFor(publicApiMember)
.get(`/executions/${execution.id}?includeData=true`)
.expect(200);
assertNotRedacted(response.body.data as IRunExecutionData);
});
test('policy "all" with trigger mode — redacts for member (canReveal=false)', async () => {
const workflow = await createWorkflow({}, publicApiMember);
const execution = await createExecutionWithRedaction({
workflow,
mode: 'trigger',
policy: 'all',
});
const response = await testServer
.publicApiAgentFor(publicApiMember)
.get(`/executions/${execution.id}?includeData=true`)
.expect(200);
assertRedacted(response.body.data as IRunExecutionData, 'workflow_redaction_policy', false);
});
test('includeData=false — returns execution without data field', async () => {
const workflow = await createWorkflow({}, publicApiMember);
const execution = await createExecutionWithRedaction({
workflow,
mode: 'trigger',
policy: 'all',
});
const response = await testServer
.publicApiAgentFor(publicApiMember)
.get(`/executions/${execution.id}`)
.expect(200);
expect(response.body.data).toBeUndefined();
});
});
describe('explicit redactExecutionData=true query param', () => {
test('always redacts even when policy is "none"', async () => {
const workflow = await createWorkflow({}, publicApiOwner);
const execution = await createExecutionWithRedaction({
workflow,
mode: 'trigger',
policy: 'none',
});
const response = await testServer
.publicApiAgentFor(publicApiOwner)
.get(`/executions/${execution.id}?includeData=true&redactExecutionData=true`)
.expect(200);
assertRedacted(response.body.data as IRunExecutionData, 'user_requested');
});
});
describe('explicit redactExecutionData=false query param (reveal)', () => {
test('owner can reveal unredacted data', async () => {
const workflow = await createWorkflow({}, publicApiOwner);
const execution = await createExecutionWithRedaction({
workflow,
mode: 'trigger',
policy: 'all',
});
const response = await testServer
.publicApiAgentFor(publicApiOwner)
.get(`/executions/${execution.id}?includeData=true&redactExecutionData=false`)
.expect(200);
assertNotRedacted(response.body.data as IRunExecutionData);
});
test('member without execution:reveal scope gets 403', async () => {
testServer.license.enable('feat:sharing');
const teamProject = await createTeamProject();
await linkUserToProject(publicApiMember, teamProject, 'project:editor');
const workflow = await createWorkflow({}, teamProject);
const execution = await createExecutionWithRedaction({
workflow,
mode: 'trigger',
policy: 'all',
});
const response = await testServer
.publicApiAgentFor(publicApiMember)
.get(`/executions/${execution.id}?includeData=true&redactExecutionData=false`)
.expect(403);
expect(response.status).toBe(403);
expect(response.body.message).toContain('execution:reveal');
});
test('member without execution:reveal scope can still reveal when policy allows it (policy=none)', async () => {
testServer.license.enable('feat:sharing');
const teamProject = await createTeamProject();
await linkUserToProject(publicApiMember, teamProject, 'project:editor');
const workflow = await createWorkflow({}, teamProject);
const execution = await createExecutionWithRedaction({
workflow,
mode: 'trigger',
policy: 'none',
});
const response = await testServer
.publicApiAgentFor(publicApiMember)
.get(`/executions/${execution.id}?includeData=true&redactExecutionData=false`)
.expect(200);
assertNotRedacted(response.body.data as IRunExecutionData);
});
});
});
// ---------------------------------------------------------------------------
// GET /api/v1/executions — Execution Redaction
// ---------------------------------------------------------------------------
describe('GET /api/v1/executions — Execution Redaction', () => {
test('policy "non-manual" with trigger mode — redacts all executions in list', async () => {
const workflow = await createWorkflow({}, publicApiMember);
await createExecutionWithRedaction({
workflow,
mode: 'trigger',
policy: 'non-manual',
});
const response = await testServer
.publicApiAgentFor(publicApiMember)
.get('/executions?includeData=true')
.expect(200);
expect(response.body.data).toHaveLength(1);
assertRedacted(
response.body.data[0].data as IRunExecutionData,
'workflow_redaction_policy',
false,
);
});
test('policy "none" — list returns unredacted when includeData=true', async () => {
const workflow = await createWorkflow({}, publicApiMember);
await createExecutionWithRedaction({
workflow,
mode: 'trigger',
policy: 'none',
});
const response = await testServer
.publicApiAgentFor(publicApiMember)
.get('/executions?includeData=true')
.expect(200);
expect(response.body.data).toHaveLength(1);
assertNotRedacted(response.body.data[0].data as IRunExecutionData);
});
test('includeData=false — returns executions without data field', async () => {
const workflow = await createWorkflow({}, publicApiMember);
await createExecutionWithRedaction({
workflow,
mode: 'trigger',
policy: 'all',
});
const response = await testServer
.publicApiAgentFor(publicApiMember)
.get('/executions')
.expect(200);
expect(response.body.data).toHaveLength(1);
expect(response.body.data[0].data).toBeUndefined();
});
test('batch: multiple executions across multiple workflows — each redacted independently', async () => {
const workflow1 = await createWorkflow({}, publicApiMember);
const workflow2 = await createWorkflow({}, publicApiMember);
// workflow1: policy "non-manual", trigger → should be redacted
await createExecutionWithRedaction({
workflow: workflow1,
mode: 'trigger',
policy: 'non-manual',
});
// workflow1: policy "non-manual", manual → should NOT be redacted
await createExecutionWithRedaction({
workflow: workflow1,
mode: 'manual',
policy: 'non-manual',
});
// workflow2: policy "none", trigger → should NOT be redacted
await createExecutionWithRedaction({ workflow: workflow2, mode: 'trigger', policy: 'none' });
// workflow2: policy "all", webhook → should be redacted
await createExecutionWithRedaction({ workflow: workflow2, mode: 'webhook', policy: 'all' });
const response = await testServer
.publicApiAgentFor(publicApiMember)
.get('/executions?includeData=true')
.expect(200);
expect(response.body.data).toHaveLength(4);
const results = response.body.data as Array<{
workflowId: string;
mode: string;
data: IRunExecutionData;
}>;
const wf1Trigger = results.find((e) => e.workflowId === workflow1.id && e.mode === 'trigger');
const wf1Manual = results.find((e) => e.workflowId === workflow1.id && e.mode === 'manual');
const wf2Trigger = results.find((e) => e.workflowId === workflow2.id && e.mode === 'trigger');
const wf2Webhook = results.find((e) => e.workflowId === workflow2.id && e.mode === 'webhook');
assertRedacted(wf1Trigger!.data, 'workflow_redaction_policy', false);
assertNotRedacted(wf1Manual!.data);
assertNotRedacted(wf2Trigger!.data);
assertRedacted(wf2Webhook!.data, 'workflow_redaction_policy', false);
});
describe('explicit redactExecutionData=true query param', () => {
test('always redacts even when policy is "none"', async () => {
const workflow = await createWorkflow({}, publicApiOwner);
await createExecutionWithRedaction({
workflow,
mode: 'trigger',
policy: 'none',
});
const response = await testServer
.publicApiAgentFor(publicApiOwner)
.get('/executions?includeData=true&redactExecutionData=true')
.expect(200);
expect(response.body.data).toHaveLength(1);
assertRedacted(response.body.data[0].data as IRunExecutionData, 'user_requested');
});
});
describe('explicit redactExecutionData=false query param (reveal)', () => {
test('owner can reveal unredacted data in list', async () => {
const workflow = await createWorkflow({}, publicApiOwner);
await createExecutionWithRedaction({
workflow,
mode: 'trigger',
policy: 'all',
});
const response = await testServer
.publicApiAgentFor(publicApiOwner)
.get('/executions?includeData=true&redactExecutionData=false')
.expect(200);
expect(response.body.data).toHaveLength(1);
assertNotRedacted(response.body.data[0].data as IRunExecutionData);
});
test('member without execution:reveal scope gets 403 in list', async () => {
testServer.license.enable('feat:sharing');
const teamProject = await createTeamProject();
await linkUserToProject(publicApiMember, teamProject, 'project:editor');
const workflow = await createWorkflow({}, teamProject);
await createExecutionWithRedaction({
workflow,
mode: 'trigger',
policy: 'all',
});
const response = await testServer
.publicApiAgentFor(publicApiMember)
.get('/executions?includeData=true&redactExecutionData=false')
.expect(403);
expect(response.status).toBe(403);
expect(response.body.message).toContain('execution:reveal');
});
});
});