mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-20 11:22:15 +00:00
feat(core): Add support for signed URLs for binary data (#14492)
Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import { sign, JsonWebTokenError, TokenExpiredError } from 'jsonwebtoken';
|
||||
import type { IBinaryData } from 'n8n-workflow';
|
||||
|
||||
import { type InstanceSettings } from '@/instance-settings';
|
||||
|
||||
import type { BinaryDataConfig } from '../binary-data.config';
|
||||
import { BinaryDataService } from '../binary-data.service';
|
||||
|
||||
const now = new Date('2025-01-01T01:23:45.678Z');
|
||||
jest.useFakeTimers({ now });
|
||||
|
||||
describe('BinaryDataService', () => {
|
||||
const signingSecret = 'test-signing-secret';
|
||||
const config = mock<BinaryDataConfig>({ signingSecret });
|
||||
const instanceSettings = mock<InstanceSettings>({ encryptionKey: 'test-encryption-key' });
|
||||
const binaryData = mock<IBinaryData>({ id: 'filesystem:id_123' });
|
||||
const validToken = sign({ id: binaryData.id }, signingSecret, { expiresIn: '1 day' });
|
||||
|
||||
let service: BinaryDataService;
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
|
||||
config.signingSecret = signingSecret;
|
||||
service = new BinaryDataService(instanceSettings, config);
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should derive the signingSecret from the encryption-key, if not provided via BinaryDataConfig', () => {
|
||||
config.signingSecret = undefined;
|
||||
|
||||
const service = new BinaryDataService(instanceSettings, config);
|
||||
|
||||
expect(service.signingSecret).toBe(
|
||||
'f7a78761c5cc17a2753e7e9d85d90e12de87d8131aea4479a11d1c7bb9655b20',
|
||||
);
|
||||
});
|
||||
|
||||
it('should use signingSecret as provided in BinaryDataConfig', () => {
|
||||
expect(service.signingSecret).toBe(signingSecret);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createSignedToken', () => {
|
||||
it('should throw for binary-data without an id', () => {
|
||||
const binaryData = mock<IBinaryData>({ id: undefined });
|
||||
|
||||
expect(() => service.createSignedToken(binaryData)).toThrow();
|
||||
});
|
||||
|
||||
it('should create a signed token for valid binary-data', () => {
|
||||
const token = service.createSignedToken(binaryData);
|
||||
|
||||
expect(token).toBe(validToken);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateSignedToken', () => {
|
||||
const invalidToken = sign({ id: binaryData.id }, 'fake-secret');
|
||||
const expiredToken = sign({ id: binaryData.id }, signingSecret, { expiresIn: '-1 day' });
|
||||
|
||||
it('should throw on invalid tokens', () => {
|
||||
expect(() => service.validateSignedToken(invalidToken)).toThrow(JsonWebTokenError);
|
||||
});
|
||||
|
||||
it('should throw on expired tokens', () => {
|
||||
expect(() => service.validateSignedToken(expiredToken)).toThrow(TokenExpiredError);
|
||||
});
|
||||
|
||||
it('should return binary-data id on valid tokens', () => {
|
||||
const result = service.validateSignedToken(validToken);
|
||||
expect(result).toBe(binaryData.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
8
packages/core/src/binary-data/binary-data.config.ts
Normal file
8
packages/core/src/binary-data/binary-data.config.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Config, Env } from '@n8n/config';
|
||||
|
||||
@Config
|
||||
export class BinaryDataConfig {
|
||||
/** Secret for creating publicly-accesible signed URLs for binary data */
|
||||
@Env('N8N_BINARY_DATA_SIGNING_SECRET')
|
||||
signingSecret?: string = undefined;
|
||||
}
|
||||
@@ -1,10 +1,15 @@
|
||||
import { Container, Service } from '@n8n/di';
|
||||
import { BINARY_ENCODING } from 'n8n-workflow';
|
||||
import { createHash } from 'crypto';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { BINARY_ENCODING, UnexpectedError } from 'n8n-workflow';
|
||||
import type { INodeExecutionData, IBinaryData } from 'n8n-workflow';
|
||||
import { readFile, stat } from 'node:fs/promises';
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
import type { Readable } from 'stream';
|
||||
|
||||
import { InstanceSettings } from '@/instance-settings';
|
||||
|
||||
import { BinaryDataConfig } from './binary-data.config';
|
||||
import type { BinaryData } from './types';
|
||||
import { areConfigModes, binaryToBuffer } from './utils';
|
||||
import { InvalidManagerError } from '../errors/invalid-manager.error';
|
||||
@@ -16,6 +21,14 @@ export class BinaryDataService {
|
||||
|
||||
private managers: Record<string, BinaryData.Manager> = {};
|
||||
|
||||
readonly signingSecret: string;
|
||||
|
||||
constructor({ encryptionKey }: InstanceSettings, binaryDataConfig: BinaryDataConfig) {
|
||||
this.signingSecret =
|
||||
binaryDataConfig.signingSecret ??
|
||||
createHash('sha256').update(`url-signing:${encryptionKey}`).digest('hex');
|
||||
}
|
||||
|
||||
async init(config: BinaryData.Config) {
|
||||
if (!areConfigModes(config.availableModes)) throw new InvalidModeError();
|
||||
|
||||
@@ -40,6 +53,23 @@ export class BinaryDataService {
|
||||
}
|
||||
}
|
||||
|
||||
createSignedToken(binaryData: IBinaryData, expiresIn = '1 day') {
|
||||
if (!binaryData.id) {
|
||||
throw new UnexpectedError('URL signing is not available in memory mode');
|
||||
}
|
||||
|
||||
const signingPayload: BinaryData.SigningPayload = {
|
||||
id: binaryData.id,
|
||||
};
|
||||
|
||||
return jwt.sign(signingPayload, this.signingSecret, { expiresIn });
|
||||
}
|
||||
|
||||
validateSignedToken(token: string) {
|
||||
const signedPayload = jwt.verify(token, this.signingSecret) as BinaryData.SigningPayload;
|
||||
return signedPayload.id;
|
||||
}
|
||||
|
||||
async copyBinaryFile(
|
||||
workflowId: string,
|
||||
executionId: string,
|
||||
|
||||
@@ -70,4 +70,8 @@ export namespace BinaryData {
|
||||
|
||||
rename(oldFileId: string, newFileId: string): Promise<void>;
|
||||
}
|
||||
|
||||
export type SigningPayload = {
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user