n8n
Version:
n8n Workflow Automation Tool
551 lines • 22.9 kB
JavaScript
"use strict";
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
if (kind === "m") throw new TypeError("Private method is not writable");
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var _VaultProvider_currentToken, _VaultProvider_tokenInfo, _VaultProvider_http;
Object.defineProperty(exports, "__esModule", { value: true });
exports.VaultProvider = void 0;
const backend_common_1 = require("@n8n/backend-common");
const backend_network_1 = require("@n8n/backend-network");
const di_1 = require("@n8n/di");
const constants_1 = require("../constants");
const secrets_provider_errors_1 = require("../errors/secrets-provider-errors");
const external_secrets_config_1 = require("../external-secrets.config");
const types_1 = require("../types");
class VaultProvider extends types_1.SecretsProvider {
constructor(logger = di_1.Container.get(backend_common_1.Logger), outboundHttp = di_1.Container.get(backend_network_1.OutboundHttp)) {
super();
this.logger = logger;
this.outboundHttp = outboundHttp;
this.properties = [
constants_1.DOCS_HELP_NOTICE,
{
displayName: 'Vault URL',
name: 'url',
type: 'string',
required: true,
noDataExpression: true,
placeholder: 'e.g. https://example.com/v1/',
default: '',
},
{
displayName: 'Vault Namespace (optional)',
name: 'namespace',
type: 'string',
hint: 'Leave blank if not using namespaces',
required: false,
noDataExpression: true,
placeholder: 'e.g. admin',
default: '',
},
{
displayName: 'Authentication Method',
name: 'authMethod',
type: 'options',
required: true,
noDataExpression: true,
options: [
{ name: 'Token', value: 'token' },
{ name: 'Username and Password', value: 'usernameAndPassword' },
{ name: 'AppRole', value: 'appRole' },
],
default: 'token',
},
{
displayName: 'Token',
name: 'token',
type: 'string',
default: '',
required: true,
noDataExpression: true,
placeholder: 'e.g. hvs.2OCsZxZA6Z9lChbt0janOOZI',
typeOptions: { password: true },
displayOptions: {
show: {
authMethod: ['token'],
},
},
},
{
displayName: 'Username',
name: 'username',
type: 'string',
default: '',
required: true,
noDataExpression: true,
placeholder: 'Username',
displayOptions: {
show: {
authMethod: ['usernameAndPassword'],
},
},
},
{
displayName: 'Password',
name: 'password',
type: 'string',
default: '',
required: true,
noDataExpression: true,
placeholder: '***************',
typeOptions: { password: true },
displayOptions: {
show: {
authMethod: ['usernameAndPassword'],
},
},
},
{
displayName: 'Role ID',
name: 'roleId',
type: 'string',
default: '',
required: true,
noDataExpression: true,
placeholder: '59d6d1ca-47bb-4e7e-a40b-8be3bc5a0ba8',
displayOptions: {
show: {
authMethod: ['appRole'],
},
},
},
{
displayName: 'Secret ID',
name: 'secretId',
type: 'string',
default: '',
required: true,
noDataExpression: true,
placeholder: '84896a0c-1347-aa90-a4f6-aca8b7558780',
typeOptions: { password: true },
displayOptions: {
show: {
authMethod: ['appRole'],
},
},
},
{
displayName: 'KV Mount Path (optional)',
name: 'kvMountPath',
type: 'string',
default: '',
required: false,
noDataExpression: true,
placeholder: 'e.g. secret/',
hint: 'Specify the KV engine mount path to skip sys/mounts auto-discovery. Leave blank to auto-detect.',
},
{
displayName: 'KV Version',
name: 'kvVersion',
type: 'options',
default: '2',
required: false,
noDataExpression: true,
options: [
{ name: 'v1', value: '1' },
{ name: 'v2', value: '2' },
],
hint: 'Only used when KV Mount Path is specified.',
},
];
this.displayName = 'HashiCorp Vault';
this.name = 'vault';
this.cachedSecrets = {};
_VaultProvider_currentToken.set(this, null);
_VaultProvider_tokenInfo.set(this, null);
_VaultProvider_http.set(this, void 0);
this.refreshAbort = new AbortController();
this.tokenRefresh = async () => {
var _a;
if (this.refreshAbort.signal.aborted) {
return;
}
try {
await __classPrivateFieldGet(this, _VaultProvider_http, "f").request({ url: 'auth/token/renew-self', method: 'POST' });
_a = this, [({ set value(_b) { __classPrivateFieldSet(_a, _VaultProvider_tokenInfo, _b, "f"); } }).value] = await this.getTokenInfo();
if (!__classPrivateFieldGet(this, _VaultProvider_tokenInfo, "f")) {
this.logger.error('Failed to fetch token info during renewal. Cancelling all future renewals.');
return;
}
if (this.refreshAbort.signal.aborted) {
return;
}
this.setupTokenRefresh();
}
catch (error) {
this.logOperationFailure('Failed to renew Vault token. Attempting to reconnect.', {
operation: 'tokenRefresh',
error,
context: {
...(0, secrets_provider_errors_1.buildHttpProviderErrorContext)(error),
authMethod: this.settings.authMethod,
},
});
void this.connect();
}
};
this.logger = this.logger.scoped('external-secrets');
}
async init(settings) {
this.settings = settings.settings;
__classPrivateFieldSet(this, _VaultProvider_http, this.outboundHttp.requests({
baseURL: new URL(this.settings.url).toString(),
headers: () => this.buildAuthHeaders(),
ssrf: 'disabled',
}), "f");
this.logger.debug('Vault provider initialized');
}
async doConnect() {
var _a;
try {
if (this.settings.authMethod === 'token') {
__classPrivateFieldSet(this, _VaultProvider_currentToken, this.settings.token, "f");
}
else if (this.settings.authMethod === 'usernameAndPassword') {
__classPrivateFieldSet(this, _VaultProvider_currentToken, await this.authUsernameAndPassword(this.settings.username, this.settings.password), "f");
if (!__classPrivateFieldGet(this, _VaultProvider_currentToken, "f")) {
throw new Error('Failed to authenticate with Username and Password');
}
}
else if (this.settings.authMethod === 'appRole') {
__classPrivateFieldSet(this, _VaultProvider_currentToken, await this.authAppRole(this.settings.roleId, this.settings.secretId), "f");
if (!__classPrivateFieldGet(this, _VaultProvider_currentToken, "f")) {
throw new Error('Failed to authenticate with AppRole');
}
}
const [testSuccess, failureMessage] = await this.test();
if (!testSuccess) {
throw new Error(failureMessage ?? 'Connection test failed');
}
_a = this, [({ set value(_b) { __classPrivateFieldSet(_a, _VaultProvider_tokenInfo, _b, "f"); } }).value] = await this.getTokenInfo();
this.setupTokenRefresh();
}
catch (error) {
if (!this.isVaultAuthFailure(error)) {
this.logOperationFailure('Failed to connect Vault provider', {
operation: 'connect',
error,
context: {
...(0, secrets_provider_errors_1.buildHttpProviderErrorContext)(error),
authMethod: this.settings.authMethod,
},
});
}
throw error;
}
}
async disconnect() {
if (this.refreshTimeout !== null) {
clearTimeout(this.refreshTimeout);
}
this.refreshAbort.abort();
}
setupTokenRefresh() {
if (!__classPrivateFieldGet(this, _VaultProvider_tokenInfo, "f")) {
return;
}
if (__classPrivateFieldGet(this, _VaultProvider_tokenInfo, "f").expire_time === null) {
return;
}
if (!__classPrivateFieldGet(this, _VaultProvider_tokenInfo, "f").renewable) {
return;
}
const expireDate = new Date(__classPrivateFieldGet(this, _VaultProvider_tokenInfo, "f").expire_time);
setTimeout(this.tokenRefresh, (expireDate.valueOf() - Date.now()) / 2);
}
async authUsernameAndPassword(username, password) {
try {
const body = await __classPrivateFieldGet(this, _VaultProvider_http, "f").request({
method: 'POST',
url: `auth/userpass/login/${username}`,
json: true,
body: { password },
});
return body.auth.client_token;
}
catch (error) {
this.logOperationFailure('Vault provider username/password authentication failed', {
operation: 'connect',
error,
context: {
...(0, secrets_provider_errors_1.buildHttpProviderErrorContext)(error),
authMethod: 'usernameAndPassword',
},
});
return null;
}
}
async authAppRole(roleId, secretId) {
try {
const body = await __classPrivateFieldGet(this, _VaultProvider_http, "f").request({
method: 'POST',
url: 'auth/approle/login',
json: true,
body: { role_id: roleId, secret_id: secretId },
});
return body.auth.client_token;
}
catch (error) {
this.logOperationFailure('Vault provider AppRole authentication failed', {
operation: 'connect',
error,
context: {
...(0, secrets_provider_errors_1.buildHttpProviderErrorContext)(error),
authMethod: 'appRole',
},
});
return null;
}
}
async getTokenInfo() {
const resp = await this.requestFull({
method: 'GET',
url: 'auth/token/lookup-self',
json: true,
});
const body = resp.body;
if (resp.statusCode !== 200 || !body?.data) {
return [null, resp];
}
return [body.data, resp];
}
async getKVSecrets(mountPath, kvVersion, path) {
this.logger.debug(`Getting kv secrets from ${mountPath}${path} (version ${kvVersion})`);
let listPath = mountPath;
if (kvVersion === '2') {
listPath += 'metadata/';
}
listPath += path;
let listBody;
try {
const shouldPreferGet = di_1.Container.get(external_secrets_config_1.ExternalSecretsConfig).preferGet;
const url = `${listPath}${shouldPreferGet ? '?list=true' : ''}`;
const method = (shouldPreferGet ? 'GET' : 'LIST');
listBody = await __classPrivateFieldGet(this, _VaultProvider_http, "f").request({ url, method });
}
catch (error) {
const shouldPreferGet = di_1.Container.get(external_secrets_config_1.ExternalSecretsConfig).preferGet;
const vaultApiPath = `${listPath}${shouldPreferGet ? '?list=true' : ''}`;
const errorContext = (0, secrets_provider_errors_1.buildHttpProviderErrorContext)(error);
this.logger.debug('Vault provider failed to list KV secrets', {
providerName: this.name,
operation: 'update',
mountPath,
kvVersion,
vaultApiPath,
...errorContext,
});
return null;
}
const data = Object.fromEntries((await Promise.allSettled(listBody.data.keys.map(async (key) => {
if (key.endsWith('/')) {
return await this.getKVSecrets(mountPath, kvVersion, path + key);
}
let secretPath = mountPath;
if (kvVersion === '2') {
secretPath += 'data/';
}
secretPath += path + key;
try {
const secretBody = await __classPrivateFieldGet(this, _VaultProvider_http, "f").request({
url: secretPath,
method: 'GET',
});
this.logger.debug(`Vault provider retrieved secrets from ${secretPath}`);
return [
key,
kvVersion === '2' ? secretBody.data.data : secretBody.data,
];
}
catch (error) {
const errorContext = (0, secrets_provider_errors_1.buildHttpProviderErrorContext)(error);
this.logger.debug('Vault provider failed to read KV secret', {
providerName: this.name,
operation: 'update',
mountPath,
kvVersion,
secretPath,
...errorContext,
});
return null;
}
})))
.map((i) => (i.status === 'rejected' ? null : i.value))
.filter((v) => v !== null));
const name = path.substring(0, path.length - 1);
this.logger.debug(`Vault provider retrieved kv secrets from ${name}`);
return [name, data];
}
normalizeKvPath(mountPath) {
return mountPath.endsWith('/') ? mountPath : `${mountPath}/`;
}
async discoverKvMounts() {
const { kvMountPath, kvVersion } = this.settings;
if (kvMountPath) {
return [{ path: this.normalizeKvPath(kvMountPath), version: kvVersion ?? '2' }];
}
const mounts = await __classPrivateFieldGet(this, _VaultProvider_http, "f").request({
url: 'sys/mounts',
method: 'GET',
});
const kvMounts = Object.entries(mounts.data).filter(([, mount]) => mount.type === 'kv');
return kvMounts
.map(([basePath, mount]) => {
const version = mount.options?.version;
if (typeof version !== 'string') {
this.logger.debug(`Skipping KV mount "${basePath}" — no version in mount options`);
return null;
}
return { path: basePath, version };
})
.filter((entry) => entry !== null);
}
async testSecretAccess() {
const { kvMountPath, kvVersion } = this.settings;
let listUrl;
let forbiddenMessage;
let failureMessage;
if (kvMountPath) {
const normalizedPath = this.normalizeKvPath(kvMountPath);
const version = kvVersion ?? '2';
listUrl =
version === '2' ? `${normalizedPath}metadata/?list=true` : `${normalizedPath}?list=true`;
forbiddenMessage = `Permission denied accessing ${kvMountPath}. Check your token policies.`;
failureMessage = (status) => `Could not access KV mount at ${kvMountPath} (status ${status}).`;
}
else {
listUrl = 'sys/mounts';
forbiddenMessage =
"Couldn't list mounts. Please give these credentials 'read' access to sys/mounts.";
failureMessage = () => "Couldn't list mounts but it wasn't a permissions issue. Please consult your Vault admin.";
}
const resp = await this.requestFull({ url: listUrl, method: 'GET' });
if (resp.statusCode === 403) {
return [false, forbiddenMessage];
}
if (resp.statusCode === 200 || (kvMountPath && resp.statusCode === 404)) {
return [true];
}
return [false, failureMessage(resp.statusCode)];
}
async update() {
try {
const kvMounts = await this.discoverKvMounts();
const secrets = Object.fromEntries((await Promise.all(kvMounts.map(async ({ path, version }) => {
const value = await this.getKVSecrets(path, version, '');
if (value === null) {
return null;
}
return [path.substring(0, path.length - 1), value[1]];
}))).filter((entry) => entry !== null));
this.cachedSecrets = secrets;
this.logger.debug('Vault provider secrets updated');
}
catch (error) {
this.logOperationFailure('Failed to update Vault provider secrets', {
operation: 'update',
error,
context: (0, secrets_provider_errors_1.buildHttpProviderErrorContext)(error),
});
throw error;
}
}
async test() {
try {
const [token, tokenResp] = await this.getTokenInfo();
if (token === null) {
if (tokenResp.statusCode === 404) {
return [false, 'Could not find auth path. Try adding /v1/ to the end of your base URL.'];
}
return [false, 'Invalid credentials'];
}
return await this.testSecretAccess();
}
catch (error) {
this.logOperationFailure('Vault provider test failed', {
operation: 'test',
error,
context: {
...(0, secrets_provider_errors_1.buildHttpProviderErrorContext)(error),
vaultApiPath: 'auth/token/lookup-self',
},
});
if ((0, backend_network_1.isConnectionRefusedError)(error)) {
return [
false,
'Connection refused. Please check the host and port of the server are correct.',
];
}
return [false];
}
}
getSecret(name) {
return this.cachedSecrets[name];
}
hasSecret(name) {
return name in this.cachedSecrets;
}
getSecretNames() {
const getKeys = ([k, v]) => {
if (typeof v === 'object') {
const keys = [];
for (const key of Object.keys(v)) {
const value = v[key];
if (typeof value === 'object' && value !== null) {
keys.push(...getKeys([key, value]).map((ok) => `${k}.${ok}`));
}
else {
keys.push(`${k}.${key}`);
}
}
return keys;
}
return [k];
};
return Object.entries(this.cachedSecrets).flatMap(getKeys);
}
buildAuthHeaders() {
const headers = {};
if (this.settings.namespace) {
headers['X-Vault-Namespace'] = this.settings.namespace;
}
if (__classPrivateFieldGet(this, _VaultProvider_currentToken, "f")) {
headers['X-Vault-Token'] = __classPrivateFieldGet(this, _VaultProvider_currentToken, "f");
}
return headers;
}
async requestFull(options) {
return await __classPrivateFieldGet(this, _VaultProvider_http, "f").request({
...options,
returnFullResponse: true,
ignoreHttpStatusErrors: true,
});
}
isVaultAuthFailure(error) {
return (error instanceof Error &&
(error.message === 'Failed to authenticate with Username and Password' ||
error.message === 'Failed to authenticate with AppRole'));
}
logOperationFailure(message, params) {
(0, secrets_provider_errors_1.logSecretsProviderOperationFailure)({
logger: this.logger,
message,
providerName: this.name,
providerDisplayName: this.displayName,
operation: params.operation,
error: params.error,
context: params.context ?? {},
});
}
}
exports.VaultProvider = VaultProvider;
_VaultProvider_currentToken = new WeakMap(), _VaultProvider_tokenInfo = new WeakMap(), _VaultProvider_http = new WeakMap();
//# sourceMappingURL=vault.js.map