mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
refactor(core): Prevent a server from starting if it's configured to use S3, but the license does not allow it (#13532)
This commit is contained in:
parent
7fb88e623f
commit
223ec2d9c9
|
@ -148,7 +148,7 @@ export abstract class BaseCommand extends Command {
|
||||||
const isSelected = config.getEnv('binaryDataManager.mode') === 's3';
|
const isSelected = config.getEnv('binaryDataManager.mode') === 's3';
|
||||||
const isAvailable = config.getEnv('binaryDataManager.availableModes').includes('s3');
|
const isAvailable = config.getEnv('binaryDataManager.availableModes').includes('s3');
|
||||||
|
|
||||||
if (!isSelected && !isAvailable) return;
|
if (!isSelected) return;
|
||||||
|
|
||||||
if (isSelected && !isAvailable) {
|
if (isSelected && !isAvailable) {
|
||||||
throw new UserError(
|
throw new UserError(
|
||||||
|
@ -157,51 +157,19 @@ export abstract class BaseCommand extends Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
const isLicensed = Container.get(License).isFeatureEnabled(LICENSE_FEATURES.BINARY_DATA_S3);
|
const isLicensed = Container.get(License).isFeatureEnabled(LICENSE_FEATURES.BINARY_DATA_S3);
|
||||||
|
if (!isLicensed) {
|
||||||
if (isSelected && isAvailable && isLicensed) {
|
this.logger.error(
|
||||||
this.logger.debug(
|
'No license found for S3 storage. \n Either set `N8N_DEFAULT_BINARY_DATA_MODE` to something else, or upgrade to a license that supports this feature.',
|
||||||
'License found for external storage - object store to init in read-write mode',
|
|
||||||
);
|
);
|
||||||
|
return this.exit(1);
|
||||||
await this._initObjectStoreService();
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSelected && isAvailable && !isLicensed) {
|
this.logger.debug('License found for external storage - Initializing object store service');
|
||||||
this.logger.debug(
|
|
||||||
'No license found for external storage - object store to init with writes blocked. To enable writes, please upgrade to a license that supports this feature.',
|
|
||||||
);
|
|
||||||
|
|
||||||
await this._initObjectStoreService({ isReadOnly: true });
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isSelected && isAvailable) {
|
|
||||||
this.logger.debug(
|
|
||||||
'External storage unselected but available - object store to init with writes unused',
|
|
||||||
);
|
|
||||||
|
|
||||||
await this._initObjectStoreService();
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _initObjectStoreService(options = { isReadOnly: false }) {
|
|
||||||
const objectStoreService = Container.get(ObjectStoreService);
|
|
||||||
|
|
||||||
this.logger.debug('Initializing object store service');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await objectStoreService.init();
|
await Container.get(ObjectStoreService).init();
|
||||||
objectStoreService.setReadonly(options.isReadOnly);
|
|
||||||
|
|
||||||
this.logger.debug('Object store init completed');
|
this.logger.debug('Object store init completed');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const error = e instanceof Error ? e : new Error(`${e}`);
|
const error = e instanceof Error ? e : new Error(`${e}`);
|
||||||
|
|
||||||
this.logger.debug('Object store init failed', { error });
|
this.logger.debug('Object store init failed', { error });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { GlobalConfig } from '@n8n/config';
|
||||||
import { Container, Service } from '@n8n/di';
|
import { Container, Service } from '@n8n/di';
|
||||||
import type { TEntitlement, TFeatures, TLicenseBlock } from '@n8n_io/license-sdk';
|
import type { TEntitlement, TFeatures, TLicenseBlock } from '@n8n_io/license-sdk';
|
||||||
import { LicenseManager } from '@n8n_io/license-sdk';
|
import { LicenseManager } from '@n8n_io/license-sdk';
|
||||||
import { InstanceSettings, ObjectStoreService, Logger } from 'n8n-core';
|
import { InstanceSettings, Logger } from 'n8n-core';
|
||||||
|
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
import { SettingsRepository } from '@/databases/repositories/settings.repository';
|
import { SettingsRepository } from '@/databases/repositories/settings.repository';
|
||||||
|
@ -106,7 +106,6 @@ export class License {
|
||||||
|
|
||||||
const features = this.manager.getFeatures();
|
const features = this.manager.getFeatures();
|
||||||
this.checkIsLicensedForMultiMain(features);
|
this.checkIsLicensedForMultiMain(features);
|
||||||
this.checkIsLicensedForBinaryDataS3(features);
|
|
||||||
|
|
||||||
this.logger.debug('License initialized');
|
this.logger.debug('License initialized');
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
|
@ -135,7 +134,6 @@ export class License {
|
||||||
this.logger.debug('License feature change detected', _features);
|
this.logger.debug('License feature change detected', _features);
|
||||||
|
|
||||||
this.checkIsLicensedForMultiMain(_features);
|
this.checkIsLicensedForMultiMain(_features);
|
||||||
this.checkIsLicensedForBinaryDataS3(_features);
|
|
||||||
|
|
||||||
if (this.instanceSettings.isMultiMain && !this.instanceSettings.isLeader) {
|
if (this.instanceSettings.isMultiMain && !this.instanceSettings.isLeader) {
|
||||||
this.logger
|
this.logger
|
||||||
|
@ -405,23 +403,6 @@ export class License {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Ensures that the instance is licensed for binary data S3 if S3 is selected and available
|
|
||||||
*/
|
|
||||||
private checkIsLicensedForBinaryDataS3(features: TFeatures) {
|
|
||||||
const isS3Selected = config.getEnv('binaryDataManager.mode') === 's3';
|
|
||||||
const isS3Available = config.getEnv('binaryDataManager.availableModes').includes('s3');
|
|
||||||
const isS3Licensed = features['feat:binaryDataS3'];
|
|
||||||
|
|
||||||
if (isS3Selected && isS3Available && !isS3Licensed) {
|
|
||||||
this.logger.debug(
|
|
||||||
'License changed with no support for external storage - blocking writes on object store. To restore writes, please upgrade to a license that supports this feature.',
|
|
||||||
);
|
|
||||||
|
|
||||||
Container.get(ObjectStoreService).setReadonly(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enableAutoRenewals() {
|
enableAutoRenewals() {
|
||||||
this.manager?.enableAutoRenewals();
|
this.manager?.enableAutoRenewals();
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { mock } from 'jest-mock-extended';
|
||||||
import { Readable } from 'stream';
|
import { Readable } from 'stream';
|
||||||
|
|
||||||
import { ObjectStoreService } from '@/binary-data/object-store/object-store.service.ee';
|
import { ObjectStoreService } from '@/binary-data/object-store/object-store.service.ee';
|
||||||
import { writeBlockedMessage } from '@/binary-data/object-store/utils';
|
|
||||||
|
|
||||||
jest.mock('axios');
|
jest.mock('axios');
|
||||||
|
|
||||||
|
@ -128,23 +127,6 @@ describe('put()', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should block if read-only', async () => {
|
|
||||||
objectStoreService.setReadonly(true);
|
|
||||||
|
|
||||||
const metadata = { fileName: 'file.txt', mimeType: 'text/plain' };
|
|
||||||
|
|
||||||
const promise = objectStoreService.put(fileId, mockBuffer, metadata);
|
|
||||||
|
|
||||||
await expect(promise).resolves.not.toThrow();
|
|
||||||
|
|
||||||
const result = await promise;
|
|
||||||
|
|
||||||
expect(result.status).toBe(403);
|
|
||||||
expect(result.statusText).toBe('Forbidden');
|
|
||||||
|
|
||||||
expect(result.data).toBe(writeBlockedMessage(fileId));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw an error on request failure', async () => {
|
it('should throw an error on request failure', async () => {
|
||||||
const metadata = { fileName: 'file.txt', mimeType: 'text/plain' };
|
const metadata = { fileName: 'file.txt', mimeType: 'text/plain' };
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { Service } from '@n8n/di';
|
||||||
import { sign } from 'aws4';
|
import { sign } from 'aws4';
|
||||||
import type { Request as Aws4Options } from 'aws4';
|
import type { Request as Aws4Options } from 'aws4';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import type { AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig, Method } from 'axios';
|
import type { AxiosRequestConfig, Method } from 'axios';
|
||||||
import { ApplicationError } from 'n8n-workflow';
|
import { ApplicationError } from 'n8n-workflow';
|
||||||
import { createHash } from 'node:crypto';
|
import { createHash } from 'node:crypto';
|
||||||
import type { Readable } from 'stream';
|
import type { Readable } from 'stream';
|
||||||
|
@ -11,7 +11,7 @@ import type { Readable } from 'stream';
|
||||||
import { Logger } from '@/logging/logger';
|
import { Logger } from '@/logging/logger';
|
||||||
|
|
||||||
import type { ListPage, MetadataResponseHeaders, RawListPage, RequestOptions } from './types';
|
import type { ListPage, MetadataResponseHeaders, RawListPage, RequestOptions } from './types';
|
||||||
import { isStream, parseXml, writeBlockedMessage } from './utils';
|
import { isStream, parseXml } from './utils';
|
||||||
import type { BinaryData } from '../types';
|
import type { BinaryData } from '../types';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
|
@ -20,8 +20,6 @@ export class ObjectStoreService {
|
||||||
|
|
||||||
private isReady = false;
|
private isReady = false;
|
||||||
|
|
||||||
private isReadOnly = false;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly logger: Logger,
|
private readonly logger: Logger,
|
||||||
private readonly s3Config: S3Config,
|
private readonly s3Config: S3Config,
|
||||||
|
@ -48,10 +46,6 @@ export class ObjectStoreService {
|
||||||
this.setReady(true);
|
this.setReady(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
setReadonly(newState: boolean) {
|
|
||||||
this.isReadOnly = newState;
|
|
||||||
}
|
|
||||||
|
|
||||||
setReady(newState: boolean) {
|
setReady(newState: boolean) {
|
||||||
this.isReady = newState;
|
this.isReady = newState;
|
||||||
}
|
}
|
||||||
|
@ -73,8 +67,6 @@ export class ObjectStoreService {
|
||||||
* @doc https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObject.html
|
* @doc https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObject.html
|
||||||
*/
|
*/
|
||||||
async put(filename: string, buffer: Buffer, metadata: BinaryData.PreWriteMetadata = {}) {
|
async put(filename: string, buffer: Buffer, metadata: BinaryData.PreWriteMetadata = {}) {
|
||||||
if (this.isReadOnly) return await this.blockWrite(filename);
|
|
||||||
|
|
||||||
const headers: Record<string, string | number> = {
|
const headers: Record<string, string | number> = {
|
||||||
'Content-Length': buffer.length,
|
'Content-Length': buffer.length,
|
||||||
'Content-MD5': createHash('md5').update(buffer).digest('base64'),
|
'Content-MD5': createHash('md5').update(buffer).digest('base64'),
|
||||||
|
@ -204,20 +196,6 @@ export class ObjectStoreService {
|
||||||
return page as ListPage;
|
return page as ListPage;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async blockWrite(filename: string): Promise<AxiosResponse> {
|
|
||||||
const logMessage = writeBlockedMessage(filename);
|
|
||||||
|
|
||||||
this.logger.warn(logMessage);
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: 403,
|
|
||||||
statusText: 'Forbidden',
|
|
||||||
data: logMessage,
|
|
||||||
headers: {},
|
|
||||||
config: {} as InternalAxiosRequestConfig,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private async request<T>(
|
private async request<T>(
|
||||||
method: Method,
|
method: Method,
|
||||||
rawPath = '',
|
rawPath = '',
|
||||||
|
|
|
@ -14,7 +14,3 @@ export async function parseXml<T>(xml: string): Promise<T> {
|
||||||
valueProcessors: [parseNumbers, parseBooleans],
|
valueProcessors: [parseNumbers, parseBooleans],
|
||||||
}) as Promise<T>);
|
}) as Promise<T>);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function writeBlockedMessage(filename: string) {
|
|
||||||
return `Request to write file "${filename}" to object storage was blocked because S3 storage is not available with your current license. Please upgrade to a license that supports this feature, or set N8N_DEFAULT_BINARY_DATA_MODE to an option other than "s3".`;
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in a new issue