mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-24 20:24:05 -08:00
refactor(core): Add external secrets log scope (#11224)
This commit is contained in:
parent
475d72e0bc
commit
88c1c4ad7b
|
@ -4,6 +4,7 @@ import { StringArray } from '../utils';
|
||||||
/** Scopes (areas of functionality) to filter logs by. */
|
/** Scopes (areas of functionality) to filter logs by. */
|
||||||
export const LOG_SCOPES = [
|
export const LOG_SCOPES = [
|
||||||
'concurrency',
|
'concurrency',
|
||||||
|
'external-secrets',
|
||||||
'license',
|
'license',
|
||||||
'multi-main-setup',
|
'multi-main-setup',
|
||||||
'pubsub',
|
'pubsub',
|
||||||
|
@ -64,6 +65,7 @@ export class LoggingConfig {
|
||||||
* Supported log scopes:
|
* Supported log scopes:
|
||||||
*
|
*
|
||||||
* - `concurrency`
|
* - `concurrency`
|
||||||
|
* - `external-secrets`
|
||||||
* - `license`
|
* - `license`
|
||||||
* - `multi-main-setup`
|
* - `multi-main-setup`
|
||||||
* - `pubsub`
|
* - `pubsub`
|
||||||
|
|
|
@ -13,7 +13,7 @@ import {
|
||||||
FailedProvider,
|
FailedProvider,
|
||||||
MockProviders,
|
MockProviders,
|
||||||
} from '@test/external-secrets/utils';
|
} from '@test/external-secrets/utils';
|
||||||
import { mockInstance } from '@test/mocking';
|
import { mockInstance, mockLogger } from '@test/mocking';
|
||||||
|
|
||||||
describe('External Secrets Manager', () => {
|
describe('External Secrets Manager', () => {
|
||||||
const connectedDate = '2023-08-01T12:32:29.000Z';
|
const connectedDate = '2023-08-01T12:32:29.000Z';
|
||||||
|
@ -49,7 +49,7 @@ describe('External Secrets Manager', () => {
|
||||||
license.isExternalSecretsEnabled.mockReturnValue(true);
|
license.isExternalSecretsEnabled.mockReturnValue(true);
|
||||||
settingsRepo.getEncryptedSecretsProviderSettings.mockResolvedValue(settings);
|
settingsRepo.getEncryptedSecretsProviderSettings.mockResolvedValue(settings);
|
||||||
manager = new ExternalSecretsManager(
|
manager = new ExternalSecretsManager(
|
||||||
mock(),
|
mockLogger(),
|
||||||
settingsRepo,
|
settingsRepo,
|
||||||
license,
|
license,
|
||||||
providersMock,
|
providersMock,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Cipher } from 'n8n-core';
|
import { Cipher } from 'n8n-core';
|
||||||
import { jsonParse, type IDataObject, ApplicationError } from 'n8n-workflow';
|
import { jsonParse, type IDataObject, ApplicationError, ensureError } from 'n8n-workflow';
|
||||||
import { Service } from 'typedi';
|
import { Service } from 'typedi';
|
||||||
|
|
||||||
import { SettingsRepository } from '@/databases/repositories/settings.repository';
|
import { SettingsRepository } from '@/databases/repositories/settings.repository';
|
||||||
|
@ -39,7 +39,9 @@ export class ExternalSecretsManager {
|
||||||
private readonly cipher: Cipher,
|
private readonly cipher: Cipher,
|
||||||
private readonly eventService: EventService,
|
private readonly eventService: EventService,
|
||||||
private readonly publisher: Publisher,
|
private readonly publisher: Publisher,
|
||||||
) {}
|
) {
|
||||||
|
this.logger = this.logger.scoped('external-secrets');
|
||||||
|
}
|
||||||
|
|
||||||
async init(): Promise<void> {
|
async init(): Promise<void> {
|
||||||
if (!this.initialized) {
|
if (!this.initialized) {
|
||||||
|
@ -57,6 +59,8 @@ export class ExternalSecretsManager {
|
||||||
}
|
}
|
||||||
return await this.initializingPromise;
|
return await this.initializingPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.logger.debug('External secrets manager initialized');
|
||||||
}
|
}
|
||||||
|
|
||||||
shutdown() {
|
shutdown() {
|
||||||
|
@ -66,6 +70,8 @@ export class ExternalSecretsManager {
|
||||||
void p.disconnect().catch(() => {});
|
void p.disconnect().catch(() => {});
|
||||||
});
|
});
|
||||||
Object.values(this.initRetryTimeouts).forEach((v) => clearTimeout(v));
|
Object.values(this.initRetryTimeouts).forEach((v) => clearTimeout(v));
|
||||||
|
|
||||||
|
this.logger.debug('External secrets manager shut down');
|
||||||
}
|
}
|
||||||
|
|
||||||
async reloadAllProviders(backoff?: number) {
|
async reloadAllProviders(backoff?: number) {
|
||||||
|
@ -77,6 +83,8 @@ export class ExternalSecretsManager {
|
||||||
for (const provider of providers) {
|
for (const provider of providers) {
|
||||||
await this.reloadProvider(provider, backoff);
|
await this.reloadProvider(provider, backoff);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.logger.debug('External secrets managed reloaded all providers');
|
||||||
}
|
}
|
||||||
|
|
||||||
broadcastReloadExternalSecretsProviders() {
|
broadcastReloadExternalSecretsProviders() {
|
||||||
|
@ -191,6 +199,8 @@ export class ExternalSecretsManager {
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.logger.debug('External secrets manager updated secrets');
|
||||||
}
|
}
|
||||||
|
|
||||||
getProvider(provider: string): SecretsProvider | undefined {
|
getProvider(provider: string): SecretsProvider | undefined {
|
||||||
|
@ -261,6 +271,8 @@ export class ExternalSecretsManager {
|
||||||
if (newProvider) {
|
if (newProvider) {
|
||||||
this.providers[provider] = newProvider;
|
this.providers[provider] = newProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.logger.debug(`External secrets manager reloaded provider ${provider}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async setProviderSettings(provider: string, data: IDataObject, userId?: string) {
|
async setProviderSettings(provider: string, data: IDataObject, userId?: string) {
|
||||||
|
@ -382,8 +394,12 @@ export class ExternalSecretsManager {
|
||||||
try {
|
try {
|
||||||
await this.providers[provider].update();
|
await this.providers[provider].update();
|
||||||
this.broadcastReloadExternalSecretsProviders();
|
this.broadcastReloadExternalSecretsProviders();
|
||||||
|
this.logger.debug(`External secrets manager updated provider ${provider}`);
|
||||||
return true;
|
return true;
|
||||||
} catch {
|
} catch (error) {
|
||||||
|
this.logger.debug(`External secrets manager failed to update provider ${provider}`, {
|
||||||
|
error: ensureError(error),
|
||||||
|
});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import type { INodeProperties } from 'n8n-workflow';
|
import type { INodeProperties } from 'n8n-workflow';
|
||||||
|
import Container from 'typedi';
|
||||||
|
|
||||||
import { UnknownAuthTypeError } from '@/errors/unknown-auth-type.error';
|
import { UnknownAuthTypeError } from '@/errors/unknown-auth-type.error';
|
||||||
import { DOCS_HELP_NOTICE, EXTERNAL_SECRETS_NAME_REGEX } from '@/external-secrets/constants';
|
import { DOCS_HELP_NOTICE, EXTERNAL_SECRETS_NAME_REGEX } from '@/external-secrets/constants';
|
||||||
import type { SecretsProvider, SecretsProviderState } from '@/interfaces';
|
import type { SecretsProvider, SecretsProviderState } from '@/interfaces';
|
||||||
|
import { Logger } from '@/logging/logger.service';
|
||||||
|
|
||||||
import { AwsSecretsClient } from './aws-secrets-client';
|
import { AwsSecretsClient } from './aws-secrets-client';
|
||||||
import type { AwsSecretsManagerContext } from './types';
|
import type { AwsSecretsManagerContext } from './types';
|
||||||
|
@ -76,10 +78,16 @@ export class AwsSecretsManager implements SecretsProvider {
|
||||||
|
|
||||||
private client: AwsSecretsClient;
|
private client: AwsSecretsClient;
|
||||||
|
|
||||||
|
constructor(private readonly logger = Container.get(Logger)) {
|
||||||
|
this.logger = this.logger.scoped('external-secrets');
|
||||||
|
}
|
||||||
|
|
||||||
async init(context: AwsSecretsManagerContext) {
|
async init(context: AwsSecretsManagerContext) {
|
||||||
this.assertAuthType(context);
|
this.assertAuthType(context);
|
||||||
|
|
||||||
this.client = new AwsSecretsClient(context.settings);
|
this.client = new AwsSecretsClient(context.settings);
|
||||||
|
|
||||||
|
this.logger.debug('AWS Secrets Manager provider initialized');
|
||||||
}
|
}
|
||||||
|
|
||||||
async test() {
|
async test() {
|
||||||
|
@ -87,9 +95,15 @@ export class AwsSecretsManager implements SecretsProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
async connect() {
|
async connect() {
|
||||||
const [wasSuccessful] = await this.test();
|
const [wasSuccessful, errorMsg] = await this.test();
|
||||||
|
|
||||||
this.state = wasSuccessful ? 'connected' : 'error';
|
this.state = wasSuccessful ? 'connected' : 'error';
|
||||||
|
|
||||||
|
if (wasSuccessful) {
|
||||||
|
this.logger.debug('AWS Secrets Manager provider connected');
|
||||||
|
} else {
|
||||||
|
this.logger.error('AWS Secrets Manager provider failed to connect', { errorMsg });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async disconnect() {
|
async disconnect() {
|
||||||
|
@ -104,6 +118,8 @@ export class AwsSecretsManager implements SecretsProvider {
|
||||||
this.cachedSecrets = Object.fromEntries(
|
this.cachedSecrets = Object.fromEntries(
|
||||||
supportedSecrets.map((s) => [s.secretName, s.secretValue]),
|
supportedSecrets.map((s) => [s.secretName, s.secretValue]),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.logger.debug('AWS Secrets Manager provider secrets updated');
|
||||||
}
|
}
|
||||||
|
|
||||||
getSecret(name: string) {
|
getSecret(name: string) {
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
import type { SecretClient } from '@azure/keyvault-secrets';
|
import type { SecretClient } from '@azure/keyvault-secrets';
|
||||||
|
import { ensureError } from 'n8n-workflow';
|
||||||
import type { INodeProperties } from 'n8n-workflow';
|
import type { INodeProperties } from 'n8n-workflow';
|
||||||
|
import Container from 'typedi';
|
||||||
|
|
||||||
import { DOCS_HELP_NOTICE, EXTERNAL_SECRETS_NAME_REGEX } from '@/external-secrets/constants';
|
import { DOCS_HELP_NOTICE, EXTERNAL_SECRETS_NAME_REGEX } from '@/external-secrets/constants';
|
||||||
import type { SecretsProvider, SecretsProviderState } from '@/interfaces';
|
import type { SecretsProvider, SecretsProviderState } from '@/interfaces';
|
||||||
|
import { Logger } from '@/logging/logger.service';
|
||||||
|
|
||||||
import type { AzureKeyVaultContext } from './types';
|
import type { AzureKeyVaultContext } from './types';
|
||||||
|
|
||||||
|
@ -64,8 +67,14 @@ export class AzureKeyVault implements SecretsProvider {
|
||||||
|
|
||||||
private settings: AzureKeyVaultContext['settings'];
|
private settings: AzureKeyVaultContext['settings'];
|
||||||
|
|
||||||
|
constructor(private readonly logger = Container.get(Logger)) {
|
||||||
|
this.logger = this.logger.scoped('external-secrets');
|
||||||
|
}
|
||||||
|
|
||||||
async init(context: AzureKeyVaultContext) {
|
async init(context: AzureKeyVaultContext) {
|
||||||
this.settings = context.settings;
|
this.settings = context.settings;
|
||||||
|
|
||||||
|
this.logger.debug('Azure Key Vault provider initialized');
|
||||||
}
|
}
|
||||||
|
|
||||||
async connect() {
|
async connect() {
|
||||||
|
@ -78,8 +87,12 @@ export class AzureKeyVault implements SecretsProvider {
|
||||||
const credential = new ClientSecretCredential(tenantId, clientId, clientSecret);
|
const credential = new ClientSecretCredential(tenantId, clientId, clientSecret);
|
||||||
this.client = new SecretClient(`https://${vaultName}.vault.azure.net/`, credential);
|
this.client = new SecretClient(`https://${vaultName}.vault.azure.net/`, credential);
|
||||||
this.state = 'connected';
|
this.state = 'connected';
|
||||||
} catch {
|
this.logger.debug('Azure Key Vault provider connected');
|
||||||
|
} catch (error) {
|
||||||
this.state = 'error';
|
this.state = 'error';
|
||||||
|
this.logger.error('Azure Key Vault provider failed to connect', {
|
||||||
|
error: ensureError(error),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,6 +132,8 @@ export class AzureKeyVault implements SecretsProvider {
|
||||||
acc[cur.name] = cur.value;
|
acc[cur.name] = cur.value;
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
|
this.logger.debug('Azure Key Vault provider secrets updated');
|
||||||
}
|
}
|
||||||
|
|
||||||
getSecret(name: string) {
|
getSecret(name: string) {
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import type { SecretManagerServiceClient as GcpClient } from '@google-cloud/secret-manager';
|
import type { SecretManagerServiceClient as GcpClient } from '@google-cloud/secret-manager';
|
||||||
import { jsonParse, type INodeProperties } from 'n8n-workflow';
|
import { ensureError, jsonParse, type INodeProperties } from 'n8n-workflow';
|
||||||
|
import Container from 'typedi';
|
||||||
|
|
||||||
import { DOCS_HELP_NOTICE, EXTERNAL_SECRETS_NAME_REGEX } from '@/external-secrets/constants';
|
import { DOCS_HELP_NOTICE, EXTERNAL_SECRETS_NAME_REGEX } from '@/external-secrets/constants';
|
||||||
import type { SecretsProvider, SecretsProviderState } from '@/interfaces';
|
import type { SecretsProvider, SecretsProviderState } from '@/interfaces';
|
||||||
|
import { Logger } from '@/logging/logger.service';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
GcpSecretsManagerContext,
|
GcpSecretsManagerContext,
|
||||||
|
@ -38,6 +40,10 @@ export class GcpSecretsManager implements SecretsProvider {
|
||||||
|
|
||||||
private settings: GcpSecretAccountKey;
|
private settings: GcpSecretAccountKey;
|
||||||
|
|
||||||
|
constructor(private readonly logger = Container.get(Logger)) {
|
||||||
|
this.logger = this.logger.scoped('external-secrets');
|
||||||
|
}
|
||||||
|
|
||||||
async init(context: GcpSecretsManagerContext) {
|
async init(context: GcpSecretsManagerContext) {
|
||||||
this.settings = this.parseSecretAccountKey(context.settings.serviceAccountKey);
|
this.settings = this.parseSecretAccountKey(context.settings.serviceAccountKey);
|
||||||
}
|
}
|
||||||
|
@ -53,8 +59,12 @@ export class GcpSecretsManager implements SecretsProvider {
|
||||||
projectId,
|
projectId,
|
||||||
});
|
});
|
||||||
this.state = 'connected';
|
this.state = 'connected';
|
||||||
} catch {
|
this.logger.debug('GCP Secrets Manager provider connected');
|
||||||
|
} catch (error) {
|
||||||
this.state = 'error';
|
this.state = 'error';
|
||||||
|
this.logger.debug('GCP Secrets Manager provider failed to connect', {
|
||||||
|
error: ensureError(error),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,6 +124,8 @@ export class GcpSecretsManager implements SecretsProvider {
|
||||||
if (cur) acc[cur.name] = cur.value;
|
if (cur) acc[cur.name] = cur.value;
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
|
this.logger.debug('GCP Secrets Manager provider secrets updated');
|
||||||
}
|
}
|
||||||
|
|
||||||
getSecret(name: string) {
|
getSecret(name: string) {
|
||||||
|
|
|
@ -237,6 +237,7 @@ export class VaultProvider extends SecretsProvider {
|
||||||
|
|
||||||
constructor(readonly logger = Container.get(Logger)) {
|
constructor(readonly logger = Container.get(Logger)) {
|
||||||
super();
|
super();
|
||||||
|
this.logger = this.logger.scoped('external-secrets');
|
||||||
}
|
}
|
||||||
|
|
||||||
async init(settings: SecretsProviderSettings): Promise<void> {
|
async init(settings: SecretsProviderSettings): Promise<void> {
|
||||||
|
@ -257,6 +258,8 @@ export class VaultProvider extends SecretsProvider {
|
||||||
}
|
}
|
||||||
return config;
|
return config;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.logger.debug('Vault provider initialized');
|
||||||
}
|
}
|
||||||
|
|
||||||
async connect(): Promise<void> {
|
async connect(): Promise<void> {
|
||||||
|
@ -408,6 +411,7 @@ export class VaultProvider extends SecretsProvider {
|
||||||
kvVersion: string,
|
kvVersion: string,
|
||||||
path: string,
|
path: string,
|
||||||
): Promise<[string, IDataObject] | null> {
|
): Promise<[string, IDataObject] | null> {
|
||||||
|
this.logger.debug(`Getting kv secrets from ${mountPath}${path} (version ${kvVersion})`);
|
||||||
let listPath = mountPath;
|
let listPath = mountPath;
|
||||||
if (kvVersion === '2') {
|
if (kvVersion === '2') {
|
||||||
listPath += 'metadata/';
|
listPath += 'metadata/';
|
||||||
|
@ -441,6 +445,7 @@ export class VaultProvider extends SecretsProvider {
|
||||||
secretPath += path + key;
|
secretPath += path + key;
|
||||||
try {
|
try {
|
||||||
const secretResp = await this.#http.get<VaultResponse<IDataObject>>(secretPath);
|
const secretResp = await this.#http.get<VaultResponse<IDataObject>>(secretPath);
|
||||||
|
this.logger.debug(`Vault provider retrieved secrets from ${secretPath}`);
|
||||||
return [
|
return [
|
||||||
key,
|
key,
|
||||||
kvVersion === '2'
|
kvVersion === '2'
|
||||||
|
@ -457,6 +462,7 @@ export class VaultProvider extends SecretsProvider {
|
||||||
.filter((v): v is [string, IDataObject] => v !== null),
|
.filter((v): v is [string, IDataObject] => v !== null),
|
||||||
);
|
);
|
||||||
const name = path.substring(0, path.length - 1);
|
const name = path.substring(0, path.length - 1);
|
||||||
|
this.logger.debug(`Vault provider retrieved kv secrets from ${name}`);
|
||||||
return [name, data];
|
return [name, data];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -479,6 +485,7 @@ export class VaultProvider extends SecretsProvider {
|
||||||
).filter((v): v is [string, IDataObject] => v !== null),
|
).filter((v): v is [string, IDataObject] => v !== null),
|
||||||
);
|
);
|
||||||
this.cachedSecrets = secrets;
|
this.cachedSecrets = secrets;
|
||||||
|
this.logger.debug('Vault provider secrets updated');
|
||||||
}
|
}
|
||||||
|
|
||||||
async test(): Promise<[boolean] | [boolean, string]> {
|
async test(): Promise<[boolean] | [boolean, string]> {
|
||||||
|
|
|
@ -18,7 +18,7 @@ import {
|
||||||
MockProviders,
|
MockProviders,
|
||||||
TestFailProvider,
|
TestFailProvider,
|
||||||
} from '../../shared/external-secrets/utils';
|
} from '../../shared/external-secrets/utils';
|
||||||
import { mockInstance } from '../../shared/mocking';
|
import { mockInstance, mockLogger } from '../../shared/mocking';
|
||||||
import { createOwner, createUser } from '../shared/db/users';
|
import { createOwner, createUser } from '../shared/db/users';
|
||||||
import type { SuperAgentTest } from '../shared/types';
|
import type { SuperAgentTest } from '../shared/types';
|
||||||
import { setupTestServer } from '../shared/utils';
|
import { setupTestServer } from '../shared/utils';
|
||||||
|
@ -52,12 +52,14 @@ async function getExternalSecretsSettings(): Promise<ExternalSecretsSettings | n
|
||||||
|
|
||||||
const eventService = mock<EventService>();
|
const eventService = mock<EventService>();
|
||||||
|
|
||||||
|
const logger = mockLogger();
|
||||||
|
|
||||||
const resetManager = async () => {
|
const resetManager = async () => {
|
||||||
Container.get(ExternalSecretsManager).shutdown();
|
Container.get(ExternalSecretsManager).shutdown();
|
||||||
Container.set(
|
Container.set(
|
||||||
ExternalSecretsManager,
|
ExternalSecretsManager,
|
||||||
new ExternalSecretsManager(
|
new ExternalSecretsManager(
|
||||||
mock(),
|
logger,
|
||||||
Container.get(SettingsRepository),
|
Container.get(SettingsRepository),
|
||||||
Container.get(License),
|
Container.get(License),
|
||||||
mockProvidersInstance,
|
mockProvidersInstance,
|
||||||
|
@ -108,6 +110,18 @@ beforeAll(async () => {
|
||||||
const member = await createUser();
|
const member = await createUser();
|
||||||
authMemberAgent = testServer.authAgentFor(member);
|
authMemberAgent = testServer.authAgentFor(member);
|
||||||
config.set('userManagement.isInstanceOwnerSetUp', true);
|
config.set('userManagement.isInstanceOwnerSetUp', true);
|
||||||
|
Container.set(
|
||||||
|
ExternalSecretsManager,
|
||||||
|
new ExternalSecretsManager(
|
||||||
|
logger,
|
||||||
|
Container.get(SettingsRepository),
|
||||||
|
Container.get(License),
|
||||||
|
mockProvidersInstance,
|
||||||
|
Container.get(Cipher),
|
||||||
|
eventService,
|
||||||
|
mock(),
|
||||||
|
),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
|
Loading…
Reference in a new issue