node-vault
Version:
Javascript client for HashiCorp's Vault
249 lines (219 loc) • 8.77 kB
JavaScript
const originalDebug = require('debug')('node-vault');
const originalTv4 = require('tv4');
const originalCommands = require('./commands.js');
const originalMustache = require('mustache');
const util = require('util');
const request = require('postman-request');
class VaultError extends Error {}
class ApiResponseError extends VaultError {
constructor(message, response) {
super(message);
this.response = {
statusCode: response.statusCode,
body: response.body,
};
}
}
module.exports = (config = {}) => {
// load conditional dependencies
const debug = config.debug || originalDebug;
const tv4 = config.tv4 || originalTv4;
const commands = config.commands || originalCommands;
const mustache = config.mustache || originalMustache;
const rpDefaults = {
json: true,
resolveWithFullResponse: true,
simple: false,
strictSSL: !process.env.VAULT_SKIP_VERIFY,
};
if (config.rpDefaults) {
Object.keys(config.rpDefaults).forEach((key) => {
rpDefaults[key] = config.rpDefaults[key];
});
}
const rp = (() => {
if (config['request-promise'])
return config['request-promise'].defaults(rpDefaults);
return util.promisify(request.defaults(rpDefaults));
})();
const client = {};
function handleVaultResponse(response) {
if (!response) return Promise.reject(new VaultError('No response passed'));
debug(response.statusCode);
if (response.statusCode !== 200 && response.statusCode !== 204) {
// handle health response not as error
if (response.request.path.match(/sys\/health/) !== null) {
return Promise.resolve(response.body);
}
let message;
if (response.body && response.body.errors && response.body.errors.length > 0) {
message = response.body.errors[0];
} else {
message = `Status ${response.statusCode}`;
}
const error = new ApiResponseError(message, response);
return Promise.reject(error);
}
return Promise.resolve(response.body);
}
client.handleVaultResponse = handleVaultResponse;
// defaults
client.apiVersion = config.apiVersion || 'v1';
client.endpoint = config.endpoint || process.env.VAULT_ADDR || 'http://127.0.0.1:8200';
client.pathPrefix = config.pathPrefix || process.env.VAULT_PREFIX || '';
client.token = config.token || process.env.VAULT_TOKEN;
client.noCustomHTTPVerbs = config.noCustomHTTPVerbs || false;
client.namespace = config.namespace || process.env.VAULT_NAMESPACE;
client.kubernetesPath = config.kubernetesPath || 'kubernetes';
const requestSchema = {
type: 'object',
properties: {
path: {
type: 'string',
},
method: {
type: 'string',
},
},
required: ['path', 'method'],
};
// Handle any HTTP requests
client.request = (options = {}) => {
const valid = tv4.validate(options, requestSchema);
if (!valid) return Promise.reject(tv4.error);
let uri = `${client.endpoint}/${client.apiVersion}${client.pathPrefix}${options.path}`;
// Replace unicode encodings.
uri = uri.replace(///g, '/');
options.headers = options.headers || {};
if (typeof client.token === 'string' && client.token.length) {
options.headers['X-Vault-Token'] = options.headers['X-Vault-Token'] || client.token;
}
if (typeof client.namespace === 'string' && client.namespace.length) {
options.headers['X-Vault-Namespace'] = client.namespace;
}
options.uri = uri;
debug(options.method, uri);
if (options.json) debug(options.json);
return rp(options).then(client.handleVaultResponse);
};
client.help = (path, requestOptions) => {
debug(`help for ${path}`);
const options = { ...config.requestOptions, ...requestOptions };
options.path = `/${path}?help=1`;
options.method = 'GET';
return client.request(options);
};
client.write = (path, data, requestOptions) => {
debug('write %o to %s', data, path);
const options = { ...config.requestOptions, ...requestOptions };
options.path = `/${path}`;
options.json = data;
options.method = 'POST';
return client.request(options);
};
client.read = (path, requestOptions) => {
debug(`read ${path}`);
const options = { ...config.requestOptions, ...requestOptions };
options.path = `/${path}`;
options.method = 'GET';
return client.request(options);
};
client.list = (path, requestOptions) => {
debug(`list ${path}`);
const options = { ...config.requestOptions, ...requestOptions };
options.path = `/${path}`;
if (client.noCustomHTTPVerbs) {
options.path = `/${path}?list=1`;
options.method = 'GET';
} else {
options.path = `/${path}`;
options.method = 'LIST';
}
return client.request(options);
};
client.delete = (path, requestOptions) => {
debug(`delete ${path}`);
const options = { ...config.requestOptions, ...requestOptions };
options.path = `/${path}`;
options.method = 'DELETE';
return client.request(options);
};
function validate(json, schema) {
// ignore validation if no schema
if (schema === undefined) return Promise.resolve();
const valid = tv4.validate(json, schema);
if (!valid) {
debug(tv4.error.dataPath);
debug(tv4.error.message);
return Promise.reject(tv4.error);
}
return Promise.resolve();
}
function extendOptions(conf, options, args = {}) {
const hasArgs = Object.keys(args).length > 0;
if (!hasArgs) return Promise.resolve(options);
const querySchema = conf.schema.query;
if (querySchema) {
const params = [];
for (const key of Object.keys(querySchema.properties)) {
if (key in args) {
params.push(`${key}=${encodeURIComponent(args[key])}`);
}
}
if (params.length > 0) {
options.path += `?${params.join('&')}`;
}
}
const reqSchema = conf.schema.req;
if (reqSchema) {
const json = {};
for (const key of Object.keys(reqSchema.properties)) {
if (key in args) {
json[key] = args[key];
}
}
if (Object.keys(json).length > 0) {
options.json = json;
}
}
return Promise.resolve(options);
}
function generateFunction(name, conf) {
client[name] = (args = {}) => {
const options = { ...config.requestOptions, ...args.requestOptions };
options.method = conf.method;
// replace args in path.
options.path = mustache.render(conf.path, args);
// no schema object -> no validation
if (!conf.schema) {
if (options.method === 'POST' || options.method === 'PUT') {
options.json = args;
}
return client.request(options);
}
// else do validation of request URL and body
let promise = validate(args, conf.schema.req)
.then(() => validate(args, conf.schema.query))
.then(() => extendOptions(conf, options, args))
.then((extendedOptions) => client.request(extendedOptions));
if (conf.tokenSource) {
promise = promise.then((response) => {
const candidateToken = response.auth && response.auth.client_token;
if (candidateToken) {
client.token = candidateToken;
}
return response;
});
}
return promise;
};
}
client.generateFunction = generateFunction;
// protecting global object properties from being added
// enforcing the immutable rule: https://github.com/airbnb/javascript#iterators-and-generators
// going the functional way first defining a wrapper function
const assignFunctions = (commandName) => generateFunction(commandName, commands[commandName]);
Object.keys(commands).forEach(assignFunctions);
return client;
};
;