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:
कारतोफ्फेलस्क्रिप्ट™ 2025-02-27 09:21:45 +01:00 committed by GitHub
parent 7fb88e623f
commit 223ec2d9c9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 10 additions and 105 deletions

View file

@ -148,7 +148,7 @@ export abstract class BaseCommand extends Command {
const isSelected = config.getEnv('binaryDataManager.mode') === 's3';
const isAvailable = config.getEnv('binaryDataManager.availableModes').includes('s3');
if (!isSelected && !isAvailable) return;
if (!isSelected) return;
if (isSelected && !isAvailable) {
throw new UserError(
@ -157,51 +157,19 @@ export abstract class BaseCommand extends Command {
}
const isLicensed = Container.get(License).isFeatureEnabled(LICENSE_FEATURES.BINARY_DATA_S3);
if (isSelected && isAvailable && isLicensed) {
this.logger.debug(
'License found for external storage - object store to init in read-write mode',
if (!isLicensed) {
this.logger.error(
'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.',
);
await this._initObjectStoreService();
return;
return this.exit(1);
}
if (isSelected && isAvailable && !isLicensed) {
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');
this.logger.debug('License found for external storage - Initializing object store service');
try {
await objectStoreService.init();
objectStoreService.setReadonly(options.isReadOnly);
await Container.get(ObjectStoreService).init();
this.logger.debug('Object store init completed');
} catch (e) {
const error = e instanceof Error ? e : new Error(`${e}`);
this.logger.debug('Object store init failed', { error });
}
}

View file

@ -2,7 +2,7 @@ import { GlobalConfig } from '@n8n/config';
import { Container, Service } from '@n8n/di';
import type { TEntitlement, TFeatures, TLicenseBlock } 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 { SettingsRepository } from '@/databases/repositories/settings.repository';
@ -106,7 +106,6 @@ export class License {
const features = this.manager.getFeatures();
this.checkIsLicensedForMultiMain(features);
this.checkIsLicensedForBinaryDataS3(features);
this.logger.debug('License initialized');
} catch (error: unknown) {
@ -135,7 +134,6 @@ export class License {
this.logger.debug('License feature change detected', _features);
this.checkIsLicensedForMultiMain(_features);
this.checkIsLicensedForBinaryDataS3(_features);
if (this.instanceSettings.isMultiMain && !this.instanceSettings.isLeader) {
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() {
this.manager?.enableAutoRenewals();
}

View file

@ -4,7 +4,6 @@ import { mock } from 'jest-mock-extended';
import { Readable } from 'stream';
import { ObjectStoreService } from '@/binary-data/object-store/object-store.service.ee';
import { writeBlockedMessage } from '@/binary-data/object-store/utils';
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 () => {
const metadata = { fileName: 'file.txt', mimeType: 'text/plain' };

View file

@ -3,7 +3,7 @@ import { Service } from '@n8n/di';
import { sign } from 'aws4';
import type { Request as Aws4Options } from 'aws4';
import axios from 'axios';
import type { AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig, Method } from 'axios';
import type { AxiosRequestConfig, Method } from 'axios';
import { ApplicationError } from 'n8n-workflow';
import { createHash } from 'node:crypto';
import type { Readable } from 'stream';
@ -11,7 +11,7 @@ import type { Readable } from 'stream';
import { Logger } from '@/logging/logger';
import type { ListPage, MetadataResponseHeaders, RawListPage, RequestOptions } from './types';
import { isStream, parseXml, writeBlockedMessage } from './utils';
import { isStream, parseXml } from './utils';
import type { BinaryData } from '../types';
@Service()
@ -20,8 +20,6 @@ export class ObjectStoreService {
private isReady = false;
private isReadOnly = false;
constructor(
private readonly logger: Logger,
private readonly s3Config: S3Config,
@ -48,10 +46,6 @@ export class ObjectStoreService {
this.setReady(true);
}
setReadonly(newState: boolean) {
this.isReadOnly = newState;
}
setReady(newState: boolean) {
this.isReady = newState;
}
@ -73,8 +67,6 @@ export class ObjectStoreService {
* @doc https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObject.html
*/
async put(filename: string, buffer: Buffer, metadata: BinaryData.PreWriteMetadata = {}) {
if (this.isReadOnly) return await this.blockWrite(filename);
const headers: Record<string, string | number> = {
'Content-Length': buffer.length,
'Content-MD5': createHash('md5').update(buffer).digest('base64'),
@ -204,20 +196,6 @@ export class ObjectStoreService {
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>(
method: Method,
rawPath = '',

View file

@ -14,7 +14,3 @@ export async function parseXml<T>(xml: string): Promise<T> {
valueProcessors: [parseNumbers, parseBooleans],
}) 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".`;
}