mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-14 00:24:07 -08:00
cd08c8e4c6
Based on #7065 | Story: https://linear.app/n8n/issue/PAY-771 n8n on filesystem mode marks binary data to delete on manual execution deletion, on unsaved execution completion, and on every execution pruning cycle. We later prune binary data in a separate cycle via these marker files, based on the configured TTL. In the context of introducing an S3 client to manage binary data, the filesystem mode's mark-and-prune setup is too tightly coupled to the general binary data management client interface. This PR... - Ensures the deletion of an execution causes the deletion of any binary data associated to it. This does away with the need for binary data TTL and simplifies the filesystem mode's mark-and-prune setup. - Refactors all execution deletions (including pruning) to cause soft deletions, hard-deletes soft-deleted executions based on the existing pruning config, and adjusts execution endpoints to filter out soft-deleted executions. This reduces DB load, and keeps binary data around long enough for users to access it when building workflows with unsaved executions. - Moves all execution pruning work from an execution lifecycle hook to `execution.repository.ts`. This keeps related logic in a single place. - Removes all marking logic from the binary data manager. This simplifies the interface that the S3 client will meet. - Adds basic sanity-check tests to pruning logic and execution deletion. Out of scope: - Improving existing pruning logic. - Improving existing execution repository logic. - Adjusting dir structure for filesystem mode. --------- Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
260 lines
5.9 KiB
TypeScript
260 lines
5.9 KiB
TypeScript
import { v4 as uuid } from 'uuid';
|
|
import * as Db from '@/Db';
|
|
import { audit } from '@/audit';
|
|
import { INSTANCE_REPORT, WEBHOOK_VALIDATOR_NODE_TYPES } from '@/audit/constants';
|
|
import {
|
|
getRiskSection,
|
|
saveManualTriggerWorkflow,
|
|
MOCK_09990_N8N_VERSION,
|
|
simulateOutdatedInstanceOnce,
|
|
simulateUpToDateInstance,
|
|
} from './utils';
|
|
import * as testDb from '../shared/testDb';
|
|
import { toReportTitle } from '@/audit/utils';
|
|
import config from '@/config';
|
|
import { generateNanoId } from '@db/utils/generators';
|
|
|
|
import { LoggerProxy } from 'n8n-workflow';
|
|
import { getLogger } from '@/Logger';
|
|
|
|
LoggerProxy.init(getLogger());
|
|
|
|
beforeAll(async () => {
|
|
await testDb.init();
|
|
|
|
simulateUpToDateInstance();
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
await testDb.truncate(['Workflow']);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await testDb.terminate();
|
|
});
|
|
|
|
test('should report webhook lacking authentication', async () => {
|
|
const targetNodeId = uuid();
|
|
|
|
const details = {
|
|
id: generateNanoId(),
|
|
name: 'My Test Workflow',
|
|
active: true,
|
|
nodeTypes: {},
|
|
connections: {},
|
|
nodes: [
|
|
{
|
|
parameters: {
|
|
path: uuid(),
|
|
options: {},
|
|
},
|
|
id: targetNodeId,
|
|
name: 'Webhook',
|
|
type: 'n8n-nodes-base.webhook',
|
|
typeVersion: 1,
|
|
position: [0, 0] as [number, number],
|
|
webhookId: uuid(),
|
|
},
|
|
],
|
|
};
|
|
|
|
await Db.collections.Workflow.save(details);
|
|
|
|
const testAudit = await audit(['instance']);
|
|
|
|
const section = getRiskSection(
|
|
testAudit,
|
|
INSTANCE_REPORT.RISK,
|
|
INSTANCE_REPORT.SECTIONS.UNPROTECTED_WEBHOOKS,
|
|
);
|
|
|
|
if (!section.location) {
|
|
fail('Expected section to have locations');
|
|
}
|
|
|
|
expect(section.location).toHaveLength(1);
|
|
|
|
expect(section.location[0].nodeId).toBe(targetNodeId);
|
|
});
|
|
|
|
test('should not report webhooks having basic or header auth', async () => {
|
|
const promises = ['basicAuth', 'headerAuth'].map(async (authType) => {
|
|
const details = {
|
|
id: generateNanoId(),
|
|
name: 'My Test Workflow',
|
|
active: true,
|
|
nodeTypes: {},
|
|
connections: {},
|
|
nodes: [
|
|
{
|
|
parameters: {
|
|
path: uuid(),
|
|
authentication: authType,
|
|
options: {},
|
|
},
|
|
id: uuid(),
|
|
name: 'Webhook',
|
|
type: 'n8n-nodes-base.webhook',
|
|
typeVersion: 1,
|
|
position: [0, 0] as [number, number],
|
|
webhookId: uuid(),
|
|
},
|
|
],
|
|
};
|
|
|
|
return Db.collections.Workflow.save(details);
|
|
});
|
|
|
|
await Promise.all(promises);
|
|
|
|
const testAudit = await audit(['instance']);
|
|
if (Array.isArray(testAudit)) fail('audit is empty');
|
|
|
|
const report = testAudit[toReportTitle('instance')];
|
|
if (!report) {
|
|
fail('Expected test audit to have instance risk report');
|
|
}
|
|
|
|
for (const section of report.sections) {
|
|
expect(section.title).not.toBe(INSTANCE_REPORT.SECTIONS.UNPROTECTED_WEBHOOKS);
|
|
}
|
|
});
|
|
|
|
test('should not report webhooks validated by direct children', async () => {
|
|
const promises = [...WEBHOOK_VALIDATOR_NODE_TYPES].map(async (nodeType) => {
|
|
const details = {
|
|
id: generateNanoId(),
|
|
name: 'My Test Workflow',
|
|
active: true,
|
|
nodeTypes: {},
|
|
nodes: [
|
|
{
|
|
parameters: {
|
|
path: uuid(),
|
|
options: {},
|
|
},
|
|
id: uuid(),
|
|
name: 'Webhook',
|
|
type: 'n8n-nodes-base.webhook',
|
|
typeVersion: 1,
|
|
position: [0, 0] as [number, number],
|
|
webhookId: uuid(),
|
|
},
|
|
{
|
|
id: uuid(),
|
|
name: 'My Node',
|
|
type: nodeType,
|
|
typeVersion: 1,
|
|
position: [0, 0] as [number, number],
|
|
},
|
|
],
|
|
connections: {
|
|
Webhook: {
|
|
main: [
|
|
[
|
|
{
|
|
node: 'My Node',
|
|
type: 'main',
|
|
index: 0,
|
|
},
|
|
],
|
|
],
|
|
},
|
|
},
|
|
};
|
|
|
|
return Db.collections.Workflow.save(details);
|
|
});
|
|
|
|
await Promise.all(promises);
|
|
|
|
const testAudit = await audit(['instance']);
|
|
if (Array.isArray(testAudit)) fail('audit is empty');
|
|
|
|
const report = testAudit[toReportTitle('instance')];
|
|
if (!report) {
|
|
fail('Expected test audit to have instance risk report');
|
|
}
|
|
|
|
for (const section of report.sections) {
|
|
expect(section.title).not.toBe(INSTANCE_REPORT.SECTIONS.UNPROTECTED_WEBHOOKS);
|
|
}
|
|
});
|
|
|
|
test('should not report non-webhook node', async () => {
|
|
await saveManualTriggerWorkflow();
|
|
|
|
const testAudit = await audit(['instance']);
|
|
if (Array.isArray(testAudit)) fail('audit is empty');
|
|
|
|
const report = testAudit[toReportTitle('instance')];
|
|
|
|
if (!report) {
|
|
fail('Expected test audit to have instance risk report');
|
|
}
|
|
|
|
for (const section of report.sections) {
|
|
expect(section.title).not.toBe(INSTANCE_REPORT.SECTIONS.UNPROTECTED_WEBHOOKS);
|
|
}
|
|
});
|
|
|
|
test('should report outdated instance when outdated', async () => {
|
|
simulateOutdatedInstanceOnce();
|
|
|
|
const testAudit = await audit(['instance']);
|
|
|
|
const section = getRiskSection(
|
|
testAudit,
|
|
INSTANCE_REPORT.RISK,
|
|
INSTANCE_REPORT.SECTIONS.OUTDATED_INSTANCE,
|
|
);
|
|
|
|
if (!section.nextVersions) {
|
|
fail('Expected section to have next versions');
|
|
}
|
|
|
|
expect(section.nextVersions).toHaveLength(1);
|
|
|
|
expect(section.nextVersions[0].name).toBe(MOCK_09990_N8N_VERSION.name);
|
|
});
|
|
|
|
test('should not report outdated instance when up to date', async () => {
|
|
const testAudit = await audit(['instance']);
|
|
if (Array.isArray(testAudit)) fail('audit is empty');
|
|
|
|
const report = testAudit[toReportTitle('instance')];
|
|
if (!report) {
|
|
fail('Expected test audit to have instance risk report');
|
|
}
|
|
|
|
for (const section of report.sections) {
|
|
expect(section.title).not.toBe(INSTANCE_REPORT.SECTIONS.OUTDATED_INSTANCE);
|
|
}
|
|
});
|
|
|
|
test('should report security settings', async () => {
|
|
config.set('diagnostics.enabled', true);
|
|
|
|
const testAudit = await audit(['instance']);
|
|
|
|
const section = getRiskSection(
|
|
testAudit,
|
|
INSTANCE_REPORT.RISK,
|
|
INSTANCE_REPORT.SECTIONS.SECURITY_SETTINGS,
|
|
);
|
|
|
|
expect(section.settings).toMatchObject({
|
|
features: {
|
|
communityPackagesEnabled: true,
|
|
versionNotificationsEnabled: true,
|
|
templatesEnabled: true,
|
|
publicApiEnabled: false,
|
|
},
|
|
auth: {
|
|
authExcludeEndpoints: 'none',
|
|
},
|
|
nodes: { nodesExclude: 'none', nodesInclude: 'none' },
|
|
telemetry: { diagnosticsEnabled: true },
|
|
});
|
|
});
|