Skip to content

Commit 9edc271

Browse files
authored
feat: Initialise data redaction module, execution data redaction service (#25975)
1 parent 25c6d14 commit 9edc271

4 files changed

Lines changed: 326 additions & 0 deletions

File tree

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { Logger } from '@n8n/backend-common';
2+
import { mockInstance } from '@n8n/backend-test-utils';
3+
import { Container } from '@n8n/di';
4+
import { mock } from 'jest-mock-extended';
5+
6+
import { ExecutionRedactionService } from '../executions/execution-redaction.service';
7+
import { RedactionModule } from '../redaction.module';
8+
9+
describe('RedactionModule', () => {
10+
let module: RedactionModule;
11+
let executionRedactionService: jest.Mocked<ExecutionRedactionService>;
12+
const originalEnv = process.env.N8N_ENABLE_EXECUTION_REDACTION;
13+
14+
beforeEach(() => {
15+
jest.clearAllMocks();
16+
Container.reset();
17+
18+
const logger = mockInstance(Logger);
19+
executionRedactionService = mock<ExecutionRedactionService>();
20+
executionRedactionService.init.mockResolvedValue(undefined);
21+
Container.set(ExecutionRedactionService, executionRedactionService);
22+
Container.set(Logger, logger);
23+
24+
module = new RedactionModule();
25+
});
26+
27+
afterEach(() => {
28+
// Restore original environment variable
29+
if (originalEnv !== undefined) {
30+
process.env.N8N_ENABLE_EXECUTION_REDACTION = originalEnv;
31+
} else {
32+
delete process.env.N8N_ENABLE_EXECUTION_REDACTION;
33+
}
34+
});
35+
36+
describe('init', () => {
37+
it.each([
38+
['not set', undefined],
39+
['"false"', 'false'],
40+
['empty string', ''],
41+
['"1"', '1'],
42+
])('should not initialize when N8N_ENABLE_EXECUTION_REDACTION is %s', async (_, value) => {
43+
if (value === undefined) {
44+
delete process.env.N8N_ENABLE_EXECUTION_REDACTION;
45+
} else {
46+
process.env.N8N_ENABLE_EXECUTION_REDACTION = value;
47+
}
48+
49+
await module.init();
50+
51+
expect(executionRedactionService.init).not.toHaveBeenCalled();
52+
});
53+
54+
it('should initialize and call ExecutionRedactionService.init() when N8N_ENABLE_EXECUTION_REDACTION is "true"', async () => {
55+
process.env.N8N_ENABLE_EXECUTION_REDACTION = 'true';
56+
57+
await module.init();
58+
59+
expect(executionRedactionService.init).toHaveBeenCalledTimes(1);
60+
});
61+
});
62+
});
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import { Logger } from '@n8n/backend-common';
2+
import { mockInstance } from '@n8n/backend-test-utils';
3+
import type { IExecutionDb } from '@n8n/db';
4+
import type { ExecutionStatus, WorkflowExecuteMode } from 'n8n-workflow';
5+
6+
import {
7+
ExecutionRedactionService,
8+
type ExecutionRedactionOptions,
9+
} from '../execution-redaction.service';
10+
11+
describe('ExecutionRedactionService', () => {
12+
const logger = mockInstance(Logger);
13+
let service: ExecutionRedactionService;
14+
15+
beforeEach(() => {
16+
service = new ExecutionRedactionService(logger);
17+
});
18+
19+
describe('processExecution', () => {
20+
const createMockExecution = (): IExecutionDb => {
21+
// @ts-expect-error - Partial mock data for testing
22+
return {
23+
id: 'execution-123',
24+
mode: 'manual' as WorkflowExecuteMode,
25+
createdAt: new Date('2024-01-01'),
26+
startedAt: new Date('2024-01-01'),
27+
stoppedAt: new Date('2024-01-01'),
28+
workflowId: 'workflow-123',
29+
finished: true,
30+
retryOf: undefined,
31+
retrySuccessId: undefined,
32+
status: 'success' as ExecutionStatus,
33+
waitTill: null,
34+
storedAt: 'db',
35+
data: {
36+
version: 1,
37+
resultData: {
38+
runData: {},
39+
},
40+
executionData: {
41+
contextData: {},
42+
nodeExecutionStack: [],
43+
metadata: {},
44+
waitingExecution: {},
45+
waitingExecutionSource: null,
46+
},
47+
},
48+
workflowData: {
49+
id: 'workflow-123',
50+
name: 'Test Workflow',
51+
active: false,
52+
isArchived: false,
53+
createdAt: new Date('2024-01-01'),
54+
updatedAt: new Date('2024-01-01'),
55+
nodes: [],
56+
connections: {},
57+
settings: {},
58+
staticData: {},
59+
activeVersionId: null,
60+
},
61+
} as IExecutionDb;
62+
};
63+
64+
it('should return unmodified execution when no options provided', async () => {
65+
const execution = createMockExecution();
66+
67+
const result = await service.processExecution(execution);
68+
69+
expect(result).toBe(execution);
70+
expect(result).toEqual(execution);
71+
});
72+
73+
it('should return unmodified execution when applyRedaction is false', async () => {
74+
const execution = createMockExecution();
75+
const options: ExecutionRedactionOptions = {
76+
applyRedaction: false,
77+
};
78+
79+
const result = await service.processExecution(execution, options);
80+
81+
expect(result).toBe(execution);
82+
expect(result).toEqual(execution);
83+
});
84+
85+
it('should return unmodified execution when applyRedaction is true (stub behavior)', async () => {
86+
const execution = createMockExecution();
87+
const options: ExecutionRedactionOptions = {
88+
applyRedaction: true,
89+
};
90+
91+
const result = await service.processExecution(execution, options);
92+
93+
// Stub implementation should return unmodified execution
94+
expect(result).toBe(execution);
95+
expect(result).toEqual(execution);
96+
});
97+
98+
it('should return unmodified execution with context options', async () => {
99+
const execution = createMockExecution();
100+
const options: ExecutionRedactionOptions = {
101+
applyRedaction: true,
102+
context: {
103+
userId: 'user-123',
104+
projectId: 'project-123',
105+
},
106+
};
107+
108+
const result = await service.processExecution(execution, options);
109+
110+
expect(result).toBe(execution);
111+
expect(result).toEqual(execution);
112+
});
113+
114+
it('should log debug message when processing execution', async () => {
115+
const execution = createMockExecution();
116+
const options: ExecutionRedactionOptions = {
117+
applyRedaction: true,
118+
};
119+
120+
await service.processExecution(execution, options);
121+
122+
expect(logger.debug).toHaveBeenCalledWith('Processing execution for redaction', {
123+
executionId: execution.id,
124+
options,
125+
});
126+
});
127+
});
128+
129+
describe('canUserReveal', () => {
130+
it('should return false (stub behavior)', async () => {
131+
const userId = 'user-123';
132+
const executionId = 'execution-123';
133+
134+
const result = await service.canUserReveal(userId, executionId);
135+
136+
expect(result).toBe(false);
137+
});
138+
139+
it('should log debug message when checking reveal permissions', async () => {
140+
const userId = 'user-123';
141+
const executionId = 'execution-123';
142+
143+
await service.canUserReveal(userId, executionId);
144+
145+
expect(logger.debug).toHaveBeenCalledWith('Checking reveal permissions', {
146+
userId,
147+
executionId,
148+
});
149+
});
150+
151+
it('should return false for different user and execution combinations', async () => {
152+
const result1 = await service.canUserReveal('user-1', 'execution-1');
153+
const result2 = await service.canUserReveal('user-2', 'execution-2');
154+
155+
expect(result1).toBe(false);
156+
expect(result2).toBe(false);
157+
});
158+
});
159+
});
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import type { IExecutionDb } from '@n8n/db';
2+
import { Service } from '@n8n/di';
3+
import { Logger } from '@n8n/backend-common';
4+
5+
export interface ExecutionRedactionOptions {
6+
applyRedaction?: boolean;
7+
context?: Record<string, unknown>;
8+
}
9+
10+
/**
11+
* Service responsible for redacting sensitive data from executions.
12+
* This service acts as a facade and delegates to the redaction module.
13+
*/
14+
@Service()
15+
export class ExecutionRedactionService {
16+
constructor(private readonly logger: Logger) {}
17+
18+
/**
19+
* Initializes the execution redaction service.
20+
* This is a stub implementation that will be extended when the redaction module is fully implemented.
21+
*/
22+
async init(): Promise<void> {
23+
this.logger.debug('Initializing ExecutionRedactionService...');
24+
// Stub implementation: no initialization needed yet
25+
// TODO: Add actual initialization logic when redaction module is implemented, loading from env, etc
26+
}
27+
28+
/**
29+
* Main entry point for redaction logic.
30+
* Processes an execution and applies redaction based on the provided options.
31+
*
32+
* @param execution - The execution to process
33+
* @param options - Options for redaction processing
34+
* @returns The processed execution (currently returns unmodified execution as stub)
35+
*
36+
* @example
37+
* ```typescript
38+
* const redactedExecution = await executionRedactionService.processExecution(
39+
* execution,
40+
* { applyRedaction: true }
41+
* );
42+
* ```
43+
*/
44+
async processExecution(
45+
execution: IExecutionDb,
46+
options: ExecutionRedactionOptions = {},
47+
): Promise<IExecutionDb> {
48+
this.logger.debug('Processing execution for redaction', {
49+
executionId: execution.id,
50+
options,
51+
});
52+
53+
// Stub implementation: return unmodified execution
54+
// TODO: Delegate to redaction module when implemented
55+
return execution;
56+
}
57+
58+
/**
59+
* Checks whether a user has permission to reveal redacted data in an execution.
60+
*
61+
* @param userId - The ID of the user requesting access
62+
* @param executionId - The ID of the execution to check
63+
* @returns `true` if the user can reveal redacted data, `false` otherwise
64+
* (currently returns `false` as stub)
65+
*
66+
* @example
67+
* ```typescript
68+
* const canReveal = await executionRedactionService.canUserReveal(
69+
* userId,
70+
* executionId
71+
* );
72+
* if (canReveal) {
73+
* // Show full execution data
74+
* }
75+
* ```
76+
*/
77+
async canUserReveal(userId: string, executionId: string): Promise<boolean> {
78+
this.logger.debug('Checking reveal permissions', {
79+
userId,
80+
executionId,
81+
});
82+
83+
// Stub implementation: return false (no reveal permission)
84+
// TODO: Implement actual permission check when redaction module is available
85+
return false;
86+
}
87+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type { ModuleInterface } from '@n8n/decorators';
2+
import { BackendModule } from '@n8n/decorators';
3+
import { Container } from '@n8n/di';
4+
5+
function isExecutionRedactionEnabled(): boolean {
6+
return process.env.N8N_ENABLE_EXECUTION_REDACTION === 'true';
7+
}
8+
9+
@BackendModule({ name: 'redaction', instanceTypes: ['main'] })
10+
export class RedactionModule implements ModuleInterface {
11+
async init() {
12+
if (!isExecutionRedactionEnabled()) {
13+
return;
14+
}
15+
const { ExecutionRedactionService } = await import('./executions/execution-redaction.service');
16+
await Container.get(ExecutionRedactionService).init();
17+
}
18+
}

0 commit comments

Comments
 (0)