mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-21 20:20:03 +02:00
269 lines
8.5 KiB
TypeScript
269 lines
8.5 KiB
TypeScript
import { Logger } from '@n8n/backend-common';
|
|
import { GlobalConfig } from '@n8n/config';
|
|
import { SettingsRepository } from '@n8n/db';
|
|
import { OnPubSubEvent } from '@n8n/decorators';
|
|
import { Container, Service } from '@n8n/di';
|
|
import { Cipher } from 'n8n-core';
|
|
import type { ICredentialDataDecryptedObject } from 'n8n-workflow';
|
|
import { deepCopy, jsonParse } from 'n8n-workflow';
|
|
|
|
import { CredentialTypes } from '@/credential-types';
|
|
import type { ICredentialsOverwrite } from '@/interfaces';
|
|
|
|
import { StaticAuthService } from './services/static-auth-service';
|
|
|
|
const CREDENTIALS_OVERWRITE_KEY = 'credentialsOverwrite';
|
|
|
|
@Service()
|
|
export class CredentialsOverwrites {
|
|
private overwriteData: ICredentialsOverwrite = {};
|
|
|
|
private resolvedTypes: string[] = [];
|
|
|
|
constructor(
|
|
private readonly globalConfig: GlobalConfig,
|
|
private readonly credentialTypes: CredentialTypes,
|
|
private readonly logger: Logger,
|
|
private readonly settings: SettingsRepository,
|
|
private readonly cipher: Cipher,
|
|
) {}
|
|
|
|
/** Registered by FrontendService to regenerate credential types after overwrites change, without this module depending on it. */
|
|
private reloadHandler?: () => Promise<void>;
|
|
|
|
registerReloadHandler(handler: () => Promise<void>) {
|
|
this.reloadHandler = handler;
|
|
}
|
|
|
|
async init() {
|
|
const data = this.globalConfig.credentials.overwrite.data;
|
|
if (data) {
|
|
this.logger.debug('Loading overwrite credentials from static envvar');
|
|
const overwriteData = jsonParse<ICredentialsOverwrite>(data, {
|
|
errorMessage: 'The credentials-overwrite is not valid JSON.',
|
|
});
|
|
|
|
this.setPlainData(overwriteData);
|
|
}
|
|
|
|
const persistence = this.globalConfig.credentials.overwrite.persistence;
|
|
|
|
if (persistence) {
|
|
this.logger.debug('Loading overwrite credentials from database');
|
|
await this.loadOverwriteDataFromDB(false);
|
|
}
|
|
}
|
|
|
|
private reloading = false;
|
|
|
|
@OnPubSubEvent('reload-overwrite-credentials')
|
|
async reloadOverwriteCredentials() {
|
|
await this.loadOverwriteDataFromDB(true);
|
|
}
|
|
|
|
async loadOverwriteDataFromDB(reloadFrontend: boolean) {
|
|
if (this.reloading) return;
|
|
try {
|
|
this.reloading = true;
|
|
this.logger.debug('Loading overwrite credentials from DB');
|
|
const data = await this.settings.findByKey(CREDENTIALS_OVERWRITE_KEY);
|
|
|
|
if (data) {
|
|
const decryptedData = await this.cipher.decryptV2(data.value);
|
|
const overwriteData = jsonParse<ICredentialsOverwrite>(decryptedData, {
|
|
errorMessage: 'The credentials-overwrite is not valid JSON.',
|
|
});
|
|
|
|
await this.setData(overwriteData, false, reloadFrontend);
|
|
}
|
|
} catch (error) {
|
|
this.logger.error('Error loading overwrite credentials', { error });
|
|
} finally {
|
|
this.reloading = false;
|
|
}
|
|
}
|
|
|
|
private async broadcastReloadOverwriteCredentialsCommand(): Promise<void> {
|
|
const { Publisher } = await import('@/scaling/pubsub/publisher.service.js');
|
|
await Container.get(Publisher).publishCommand({ command: 'reload-overwrite-credentials' });
|
|
}
|
|
|
|
async saveOverwriteDataToDB(overwriteData: ICredentialsOverwrite, broadcast: boolean = true) {
|
|
const data = await this.cipher.encryptV2(JSON.stringify(overwriteData));
|
|
const setting = this.settings.create({
|
|
key: CREDENTIALS_OVERWRITE_KEY,
|
|
value: data,
|
|
loadOnStartup: false,
|
|
});
|
|
await this.settings.save(setting);
|
|
|
|
if (broadcast) {
|
|
await this.broadcastReloadOverwriteCredentialsCommand();
|
|
}
|
|
}
|
|
|
|
getOverwriteEndpointMiddleware() {
|
|
const { endpointAuthToken } = this.globalConfig.credentials.overwrite;
|
|
return StaticAuthService.getStaticAuthMiddleware(endpointAuthToken);
|
|
}
|
|
|
|
setPlainData(overwriteData: ICredentialsOverwrite) {
|
|
// If data gets reinitialized reset the resolved types cache
|
|
this.resolvedTypes.length = 0;
|
|
|
|
this.overwriteData = overwriteData;
|
|
|
|
for (const type in overwriteData) {
|
|
const overwrites = this.getOverwrites(type);
|
|
|
|
if (overwrites && Object.keys(overwrites).length) {
|
|
this.overwriteData[type] = overwrites;
|
|
}
|
|
}
|
|
}
|
|
|
|
async setData(
|
|
overwriteData: ICredentialsOverwrite,
|
|
storeInDb: boolean = true,
|
|
reloadFrontend: boolean = true,
|
|
) {
|
|
this.setPlainData(overwriteData);
|
|
if (storeInDb && this.globalConfig.credentials.overwrite.persistence) {
|
|
await this.saveOverwriteDataToDB(overwriteData, true);
|
|
}
|
|
|
|
if (reloadFrontend) {
|
|
await this.reloadHandler?.();
|
|
}
|
|
}
|
|
|
|
applyOverwrite(type: string, data: ICredentialDataDecryptedObject) {
|
|
const overwrites = this.get(type);
|
|
|
|
if (overwrites === undefined) {
|
|
return data;
|
|
}
|
|
|
|
// For skip-list types, don't apply overwrite if the credential has been
|
|
// customized (any overwrite field has a non-empty value that differs from
|
|
// the overwrite value). Since overwrites are never persisted to the DB,
|
|
// any non-empty stored value that differs from the overwrite is user-set.
|
|
if (this.globalConfig.credentials.overwrite?.skipTypes?.includes(type)) {
|
|
const isFieldCustomized = (key: string) => {
|
|
const storedValue = data[key];
|
|
return (
|
|
storedValue !== null &&
|
|
storedValue !== undefined &&
|
|
storedValue !== '' &&
|
|
storedValue !== overwrites[key]
|
|
);
|
|
};
|
|
if (Object.keys(overwrites).some(isFieldCustomized)) {
|
|
return data;
|
|
}
|
|
}
|
|
|
|
const returnData = deepCopy(data);
|
|
// Overwrite only if there is currently no data set
|
|
for (const key of Object.keys(overwrites)) {
|
|
const current = returnData[key];
|
|
if (current === null || current === undefined || current === '') {
|
|
returnData[key] = overwrites[key];
|
|
}
|
|
}
|
|
|
|
return returnData;
|
|
}
|
|
|
|
getOverwrites(type: string): ICredentialDataDecryptedObject | undefined {
|
|
if (this.resolvedTypes.includes(type)) {
|
|
// Type got already resolved and can so returned directly
|
|
return this.overwriteData[type];
|
|
}
|
|
|
|
if (!this.credentialTypes.recognizes(type)) {
|
|
this.logger.warn(`Unknown credential type ${type} in Credential overwrites`);
|
|
return;
|
|
}
|
|
|
|
const credentialTypeData = this.credentialTypes.getByName(type);
|
|
|
|
if (credentialTypeData.extends === undefined) {
|
|
this.resolvedTypes.push(type);
|
|
return this.overwriteData[type];
|
|
}
|
|
|
|
const overwrites: ICredentialDataDecryptedObject = {};
|
|
for (const credentialsTypeName of credentialTypeData.extends) {
|
|
Object.assign(overwrites, this.getOverwrites(credentialsTypeName));
|
|
}
|
|
|
|
if (this.overwriteData[type] !== undefined) {
|
|
Object.assign(overwrites, this.overwriteData[type]);
|
|
}
|
|
|
|
this.resolvedTypes.push(type);
|
|
|
|
return overwrites;
|
|
}
|
|
|
|
private get(name: string): ICredentialDataDecryptedObject | undefined {
|
|
const parentTypes = this.credentialTypes.getParentTypes(name);
|
|
const entries = [name, ...parentTypes]
|
|
.reverse()
|
|
.map((type) => this.overwriteData[type])
|
|
.filter((type): type is ICredentialDataDecryptedObject => !!type);
|
|
|
|
if (entries.length === 0) return undefined;
|
|
|
|
return entries.reduce(
|
|
(acc, current) => Object.assign(acc, current),
|
|
{} as ICredentialDataDecryptedObject,
|
|
);
|
|
}
|
|
|
|
getAll(): ICredentialsOverwrite {
|
|
return this.overwriteData;
|
|
}
|
|
|
|
supportsManagedAuth(type: string): boolean {
|
|
return this.get(type) !== undefined;
|
|
}
|
|
|
|
/**
|
|
* OAuth credential type whose client this instance provides via overwrites
|
|
* (clientId + clientSecret), excluding skip-list types where managed
|
|
* creation is disabled. These types support one-click connect in the editor.
|
|
*/
|
|
isManagedOAuthType(type: string): boolean {
|
|
if (!this.credentialTypes.recognizes(type)) return false;
|
|
|
|
// OAuth2 identifies its client with clientId/clientSecret, OAuth1 with
|
|
// consumerKey/consumerSecret. Detect the version so the right pair is checked
|
|
// (checking clientId/clientSecret for both would never match an OAuth1 client).
|
|
const parentTypes = this.credentialTypes.getParentTypes(type);
|
|
const extendsBase = (base: string) => type === base || parentTypes.includes(base);
|
|
const clientFields = extendsBase('oAuth2Api')
|
|
? (['clientId', 'clientSecret'] as const)
|
|
: extendsBase('oAuth1Api')
|
|
? (['consumerKey', 'consumerSecret'] as const)
|
|
: undefined;
|
|
if (!clientFields) return false;
|
|
|
|
if (this.globalConfig.credentials.overwrite?.skipTypes?.includes(type)) return false;
|
|
|
|
const overwrites = this.get(type);
|
|
return !!overwrites && clientFields.every((field) => field in overwrites);
|
|
}
|
|
|
|
usesManagedAuth(type: string, data: Record<string, unknown>): boolean {
|
|
const overwrites = this.get(type);
|
|
if (overwrites === undefined) return false;
|
|
|
|
// Managed iff applying the overwrite actually injects a value. Delegating to
|
|
// applyOverwrite keeps this in lockstep with the skip-list / customization rules.
|
|
const applied = this.applyOverwrite(type, data as ICredentialDataDecryptedObject);
|
|
return Object.keys(overwrites).some((key) => applied[key] !== data[key]);
|
|
}
|
|
}
|