node-vault-client 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,263 +1,366 @@
1
- 'use strict';
2
-
3
- const _ = require('lodash');
4
- const Lease = require('./Lease');
5
- const errors = require('./errors');
6
- const VaultApiClient = require('./VaultApiClient');
7
- const VaultAppRoleAuth = require('./auth/VaultAppRoleAuth');
8
- const VaultTokenAuth = require('./auth/VaultTokenAuth');
9
- const VaultIAMAuth = require('./auth/VaultIAMAuth');
10
- const VaultNodeConfig = require('./VaultNodeConfig');
11
- const VaultKubernetesAuth = require('./auth/VaultKubernetesAuth');
12
- const vaultInstances = {};
13
-
14
- class VaultClient {
15
-
16
- /**
17
- * Client constructor function.
18
- *
19
- * @param {Object} options
20
- * @param {Object} options.api
21
- * @param {String} options.api.url - the url of the vault server
22
- * @param {String} [options.api.apiVersion='v1']
23
- * @param {Object} options.auth
24
- * @param {String} options.auth.type
25
- * @param {Object} options.auth.config - auth configuration variables
26
- * @param {Object|false} options.logger - Logger that supports "error", "info", "warn", "trace", "debug" methods. Uses `console` by default. Pass `false` to disable logging.
27
- */
28
- constructor(options) {
29
- this.__log = this.__setupLogger(options.logger);
30
-
31
- this.__api = new VaultApiClient(
32
- options.api,
33
- this.__log
34
- );
35
-
36
- /**
37
- * @type {VaultBaseAuth}
38
- * @private
39
- */
40
- this.__auth = this.__getAuthProvider(
41
- options.auth,
42
- this.__api
43
- );
44
-
45
- this.__namespace = options.auth.config.namespace;
46
- }
47
-
48
- /**
49
- * Boot an instance of Vault
50
- *
51
- * The instance will be stored in a local hash. Calling Vault.boot multiple
52
- * times with the same name will return the same instance.
53
- *
54
- * @param {String} name - Vault instance name
55
- * @param {Object} [options] - options for {@link Vault#constructor}.
56
- * @return Vault
57
- */
58
- static boot(name, options) {
59
- if (options === undefined) {
60
- throw new errors.InvalidArgumentsError('Options should be provided');
61
- }
62
-
63
- let instance = vaultInstances[name];
64
- if (instance === undefined) {
65
- vaultInstances[name] = instance = new VaultClient(options);
66
- }
67
- return instance;
68
- }
69
-
70
- /**
71
- * Get an instance of Vault
72
- *
73
- * The instance will be stored in a local hash. Calling Vault.pop multiple
74
- * times with the same name will return the same instance.
75
- *
76
- * @param {String} name - Vault instance name
77
- * @return Vault
78
- */
79
- static get(name) {
80
- let instance = vaultInstances[name];
81
-
82
- if (instance === undefined) {
83
- throw new errors.InvalidArgumentsError('Invalid instance name');
84
- }
85
-
86
- return instance;
87
- }
88
-
89
- /**
90
- * Clear named Vault instance
91
- *
92
- * If no name passed all named instances will be cleared.
93
- *
94
- * @param {String} [name] - Vault instance name, all instances will be cleared if no name were passed
95
- */
96
- static clear(name) {
97
- if (typeof name === 'string') {
98
- delete vaultInstances[name];
99
- } else {
100
- for (let k in vaultInstances) {
101
- if (vaultInstances.hasOwnProperty(k)) {
102
- delete vaultInstances[k];
103
- }
104
- }
105
- }
106
- }
107
-
108
- /**
109
- * @protected
110
- *
111
- * @param {Object} authConfig
112
- * @param {string} authConfig.type
113
- * @param {string} authConfig.mount
114
- * @param {Object} authConfig.config
115
- * @param {VaultApiClient} api
116
- * @return {VaultBaseAuth}
117
- * @private
118
- */
119
- __getAuthProvider(authConfig, api) {
120
- this.__log.debug('creating vault auth method: "%s"', authConfig.type);
121
-
122
- switch (authConfig.type) {
123
- case 'iam':
124
- return new VaultIAMAuth(
125
- api,
126
- this.__log,
127
- authConfig.config,
128
- authConfig.mount
129
- );
130
- case 'appRole':
131
- return new VaultAppRoleAuth(
132
- api,
133
- this.__log,
134
- authConfig.config,
135
- authConfig.mount
136
- );
137
- case 'token':
138
- return new VaultTokenAuth(
139
- api,
140
- this.__log,
141
- authConfig.config,
142
- authConfig.mount
143
- );
144
- case 'kubernetes':
145
- return new VaultKubernetesAuth(
146
- api,
147
- this.__log,
148
- authConfig.config,
149
- authConfig.mount
150
- );
151
- }
152
-
153
- throw new errors.InvalidArgumentsError('Unsupported auth method')
154
- }
155
-
156
- /**
157
- * Populates Vault's values to NPM "config" module
158
- */
159
- fillNodeConfig() {
160
- const vaultConf = new VaultNodeConfig(this);
161
-
162
- return vaultConf.populate();
163
- }
164
-
165
- getHeaders(token) {
166
- if (this.__namespace) {
167
- return {
168
- 'X-Vault-Token': token.getId(),
169
- 'X-Vault-Namespace': this.__namespace
170
- }
171
- }
172
-
173
- return {'X-Vault-Token': token.getId()}
174
- }
175
-
176
- /**
177
- * Read secret from Vault
178
- * @param {string} path - path to the secret
179
- * @returns {Promise<Lease>}
180
- */
181
- read(path) {
182
- this.__log.debug('read secret %s', path);
183
- return this.__auth.getAuthToken()
184
- .then(token => this.__api.makeRequest('GET', path, null, this.getHeaders(token)))
185
- .then(res => {
186
- this.__log.debug('receive secret %s', path);
187
- return Lease.fromResponse(res);
188
- })
189
- .catch((reason) => {
190
- this.__log.error('read secret failed: %s', reason.message);
191
- throw reason;
192
- });
193
- }
194
-
195
- /**
196
- * Retrieves secrets list
197
- *
198
- * @param {string} path - path to the secret
199
- * @returns {Promise<Lease>}
200
- */
201
- list(path) {
202
- this.__log.debug('list secrets %s', path);
203
- return this.__auth.getAuthToken()
204
- .then(token => this.__api.makeRequest('LIST', path, null, this.getHeaders(token)))
205
- .then(res => {
206
- this.__log.debug('got secrets list %s', path);
207
- return Lease.fromResponse(res);
208
- })
209
- .catch((reason) => {
210
- this.__log.error('list secrets failed: %s', reason.message);
211
- throw reason;
212
- });
213
- }
214
-
215
- /**
216
- * Writes data to Vault
217
- *
218
- * @param path - path used to write data
219
- * @param {object} data - data to write
220
- * @returns {Promise<T | never>}
221
- */
222
- write(path, data) {
223
- this.__log.debug('write secret %s', path);
224
- return this.__auth.getAuthToken()
225
- .then((token) => this.__api.makeRequest('POST', path, data, this.getHeaders(token)))
226
- .then((response) => {
227
- this.__log.debug('secret %s was written', path);
228
- return response;
229
- })
230
- .catch((reason) => {
231
- this.__log.error('write secret failed: %s', reason.message);
232
- throw reason;
233
- });
234
- }
235
-
236
- /**
237
- * @private
238
- */
239
- __setupLogger(logger) {
240
- if (logger === false) {
241
- return {
242
- error: _.noop,
243
- warn: _.noop,
244
- info: _.noop,
245
- debug: _.noop,
246
- trace: _.noop,
247
- }
248
- } else if (_.intersection(_.functionsIn(logger), ['error', 'warn', 'info', 'debug', 'trace']).length >= 5) {
249
- return logger
250
- } else {
251
- return {
252
- error: console.error,
253
- warn: console.warn,
254
- info: console.info,
255
- trace: console.trace,
256
- // avoid output sensitive information
257
- debug: _.noop
258
- };
259
- }
260
- }
261
- }
262
-
263
- module.exports = VaultClient;
1
+ 'use strict';
2
+
3
+ const _ = require('lodash');
4
+ const Lease = require('./Lease');
5
+ const errors = require('./errors');
6
+ const VaultApiClient = require('./VaultApiClient');
7
+ const VaultAppRoleAuth = require('./auth/VaultAppRoleAuth');
8
+ const VaultTokenAuth = require('./auth/VaultTokenAuth');
9
+ const VaultIAMAuth = require('./auth/VaultIAMAuth');
10
+ const VaultNodeConfig = require('./VaultNodeConfig');
11
+ const VaultKubernetesAuth = require('./auth/VaultKubernetesAuth');
12
+ const MountResolver = require('./MountResolver');
13
+ const { rewritePath, normalizeResponse } = require('./kvTransform');
14
+ const vaultInstances = {};
15
+
16
+ class VaultClient {
17
+
18
+ /**
19
+ * Client constructor function.
20
+ *
21
+ * @param {Object} options
22
+ * @param {Object} options.api
23
+ * @param {String} options.api.url - the url of the vault server
24
+ * @param {String} [options.api.apiVersion='v1']
25
+ * @param {Object} [options.api.kv] - KV engine options
26
+ * @param {boolean} [options.api.kv.autoDetect=false] - When true, auto-detect the KV version per mount
27
+ * via a `sys/internal/ui/mounts/<path>` round-trip. When false (default) no detection call is
28
+ * ever made and read/list/write behave byte-for-byte as before unless an engine is
29
+ * listed in `options.api.engines`.
30
+ * @param {Object} [options.api.engines] - Static mount-to-version overrides, e.g. `{ secret: 2, legacy: 1 }`.
31
+ * Listed mounts resolve without any detection round-trip (works even with autoDetect off), making
32
+ * this a reliable fallback for Vaults that deny the detection endpoint.
33
+ * @param {Object} options.auth
34
+ * @param {String} options.auth.type
35
+ * @param {Object} options.auth.config - auth configuration variables
36
+ * @param {Object|false} options.logger - Logger that supports "error", "info", "warn", "trace", "debug" methods. Uses `console` by default. Pass `false` to disable logging.
37
+ */
38
+ constructor(options) {
39
+ this.__log = this.__setupLogger(options.logger);
40
+
41
+ this.__api = new VaultApiClient(
42
+ options.api,
43
+ this.__log
44
+ );
45
+
46
+ /**
47
+ * @type {VaultBaseAuth}
48
+ * @private
49
+ */
50
+ this.__auth = this.__getAuthProvider(
51
+ options.auth,
52
+ this.__api
53
+ );
54
+
55
+ this.__namespace = options.auth.config.namespace;
56
+
57
+ // KV v2 support (opt-in, non-breaking).
58
+ const kvOpts = (options.api && options.api.kv) || {};
59
+ const autoDetect = kvOpts.autoDetect === true;
60
+ const engines = (options.api && options.api.engines) || {};
61
+
62
+ // Detection (the sys/internal/ui/mounts round-trip) only happens when
63
+ // autoDetect is on. With engines set but autoDetect off, listed mounts use
64
+ // the override and any other mount is passthrough (v1) — no detection call
65
+ // is ever made. This keeps the default byte-for-byte and makes engines a
66
+ // working fallback for Vaults that deny the detection endpoint.
67
+ const resolverDisabled = !autoDetect;
68
+
69
+ // Build detectFn lazily — it uses __auth and __api which are already set above.
70
+ const detectFn = (path) => this.__detectMount(path);
71
+
72
+ this.__resolver = new MountResolver(detectFn, engines, this.__log, {
73
+ disabled: resolverDisabled,
74
+ });
75
+ }
76
+
77
+ /**
78
+ * Boot an instance of Vault
79
+ *
80
+ * The instance will be stored in a local hash. Calling Vault.boot multiple
81
+ * times with the same name will return the same instance.
82
+ *
83
+ * @param {String} name - Vault instance name
84
+ * @param {Object} [options] - options for {@link Vault#constructor}.
85
+ * @return Vault
86
+ */
87
+ static boot(name, options) {
88
+ if (options === undefined) {
89
+ throw new errors.InvalidArgumentsError('Options should be provided');
90
+ }
91
+
92
+ let instance = vaultInstances[name];
93
+ if (instance === undefined) {
94
+ vaultInstances[name] = instance = new VaultClient(options);
95
+ }
96
+ return instance;
97
+ }
98
+
99
+ /**
100
+ * Get an instance of Vault
101
+ *
102
+ * The instance will be stored in a local hash. Calling Vault.pop multiple
103
+ * times with the same name will return the same instance.
104
+ *
105
+ * @param {String} name - Vault instance name
106
+ * @return Vault
107
+ */
108
+ static get(name) {
109
+ let instance = vaultInstances[name];
110
+
111
+ if (instance === undefined) {
112
+ throw new errors.InvalidArgumentsError('Invalid instance name');
113
+ }
114
+
115
+ return instance;
116
+ }
117
+
118
+ /**
119
+ * Clear named Vault instance
120
+ *
121
+ * If no name passed all named instances will be cleared.
122
+ *
123
+ * @param {String} [name] - Vault instance name, all instances will be cleared if no name were passed
124
+ */
125
+ static clear(name) {
126
+ if (typeof name === 'string') {
127
+ delete vaultInstances[name];
128
+ } else {
129
+ for (let k in vaultInstances) {
130
+ if (vaultInstances.hasOwnProperty(k)) {
131
+ delete vaultInstances[k];
132
+ }
133
+ }
134
+ }
135
+ }
136
+
137
+ /**
138
+ * @protected
139
+ *
140
+ * @param {Object} authConfig
141
+ * @param {string} authConfig.type
142
+ * @param {string} authConfig.mount
143
+ * @param {Object} authConfig.config
144
+ * @param {VaultApiClient} api
145
+ * @return {VaultBaseAuth}
146
+ * @private
147
+ */
148
+ __getAuthProvider(authConfig, api) {
149
+ this.__log.debug('creating vault auth method: "%s"', authConfig.type);
150
+
151
+ switch (authConfig.type) {
152
+ case 'iam':
153
+ return new VaultIAMAuth(
154
+ api,
155
+ this.__log,
156
+ authConfig.config,
157
+ authConfig.mount
158
+ );
159
+ case 'appRole':
160
+ return new VaultAppRoleAuth(
161
+ api,
162
+ this.__log,
163
+ authConfig.config,
164
+ authConfig.mount
165
+ );
166
+ case 'token':
167
+ return new VaultTokenAuth(
168
+ api,
169
+ this.__log,
170
+ authConfig.config,
171
+ authConfig.mount
172
+ );
173
+ case 'kubernetes':
174
+ return new VaultKubernetesAuth(
175
+ api,
176
+ this.__log,
177
+ authConfig.config,
178
+ authConfig.mount
179
+ );
180
+ }
181
+
182
+ throw new errors.InvalidArgumentsError('Unsupported auth method')
183
+ }
184
+
185
+ /**
186
+ * Populates Vault's values to NPM "config" module
187
+ */
188
+ fillNodeConfig() {
189
+ const vaultConf = new VaultNodeConfig(this);
190
+
191
+ return vaultConf.populate();
192
+ }
193
+
194
+ getHeaders(token) {
195
+ if (this.__namespace) {
196
+ return {
197
+ 'X-Vault-Token': token.getId(),
198
+ 'X-Vault-Namespace': this.__namespace
199
+ }
200
+ }
201
+
202
+ return {'X-Vault-Token': token.getId()}
203
+ }
204
+
205
+ // -------------------------------------------------------------------------
206
+ // Detection helper (used by MountResolver's detectFn)
207
+ // -------------------------------------------------------------------------
208
+
209
+ /**
210
+ * Calls GET sys/internal/ui/mounts/<path> with the current auth token.
211
+ * Returns the parsed Vault response body.
212
+ * @private
213
+ */
214
+ __detectMount(path) {
215
+ return this.__auth.getAuthToken()
216
+ .then((token) => {
217
+ const detectPath = `sys/internal/ui/mounts/${path}`;
218
+ return this.__api.makeRequest('GET', detectPath, null, this.getHeaders(token));
219
+ });
220
+ }
221
+
222
+ // -------------------------------------------------------------------------
223
+ // Internal resolve + request helper
224
+ // -------------------------------------------------------------------------
225
+
226
+ /**
227
+ * Resolve the mount version for a path, rewrite the path, make the request,
228
+ * and return a { apiPath, body, version, mount } object.
229
+ *
230
+ * When the resolver is disabled (autoDetect:false, no matching engine), it
231
+ * preserves the caller's literal path byte-for-byte, keeping behaviour
232
+ * identical to the pre-KV-v2 client.
233
+ *
234
+ * @private
235
+ */
236
+ __resolveAndRequest(op, method, path, data) {
237
+ return this.__auth.getAuthToken()
238
+ .then((token) => {
239
+ const headers = this.getHeaders(token);
240
+
241
+ return this.__resolver.resolve(path)
242
+ .then((resolved) => {
243
+ const mount = resolved.mount;
244
+ const version = resolved.version;
245
+ let apiPath;
246
+ let requestData = data;
247
+
248
+ if (version === 2) {
249
+ // Split the path into mount + logical sub-path and rewrite for KV v2
250
+ const logicalPath = this.__logicalPath(path, mount);
251
+ apiPath = rewritePath(version, op, mount, logicalPath);
252
+ // Wrap data in { data: ... } on v2 writes
253
+ if (op === 'write' && data !== null && data !== undefined) {
254
+ requestData = { data };
255
+ }
256
+ } else {
257
+ // v1 / disabled: preserve the caller's literal path byte-for-byte,
258
+ // including any trailing slash, to maintain the old wire behaviour.
259
+ apiPath = path;
260
+ }
261
+
262
+ return this.__api.makeRequest(method, apiPath, requestData, headers)
263
+ .then((body) => ({ body, version, mount, apiPath }));
264
+ });
265
+ });
266
+ }
267
+
268
+ /**
269
+ * Extract the logical path after the mount prefix.
270
+ * e.g. path='secret/foo/bar', mount='secret' => 'foo/bar'
271
+ * @private
272
+ */
273
+ __logicalPath(path, mount) {
274
+ const prefix = mount.replace(/\/+$/, '');
275
+ if (path === prefix) return '';
276
+ if (path.startsWith(prefix + '/')) return path.slice(prefix.length + 1);
277
+ return path;
278
+ }
279
+
280
+ /**
281
+ * Read secret from Vault
282
+ * @param {string} path - path to the secret
283
+ * @returns {Promise<Lease>}
284
+ */
285
+ read(path) {
286
+ this.__log.debug('read secret %s', path);
287
+ return this.__resolveAndRequest('read', 'GET', path, null)
288
+ .then(({ body, version }) => {
289
+ this.__log.debug('receive secret %s', path);
290
+ const normalised = normalizeResponse(version, 'read', body);
291
+ return Lease.fromResponse(normalised);
292
+ })
293
+ .catch((reason) => {
294
+ this.__log.error('read secret failed: %s', reason.message);
295
+ throw reason;
296
+ });
297
+ }
298
+
299
+ /**
300
+ * Retrieves secrets list
301
+ *
302
+ * @param {string} path - path to the secret
303
+ * @returns {Promise<Lease>}
304
+ */
305
+ list(path) {
306
+ this.__log.debug('list secrets %s', path);
307
+ return this.__resolveAndRequest('list', 'LIST', path, null)
308
+ .then(({ body, version }) => {
309
+ this.__log.debug('got secrets list %s', path);
310
+ const normalised = normalizeResponse(version, 'list', body);
311
+ return Lease.fromResponse(normalised);
312
+ })
313
+ .catch((reason) => {
314
+ this.__log.error('list secrets failed: %s', reason.message);
315
+ throw reason;
316
+ });
317
+ }
318
+
319
+ /**
320
+ * Writes data to Vault
321
+ *
322
+ * @param path - path used to write data
323
+ * @param {object} data - data to write
324
+ * @returns {Promise<T | never>}
325
+ */
326
+ write(path, data) {
327
+ this.__log.debug('write secret %s', path);
328
+ return this.__resolveAndRequest('write', 'POST', path, data)
329
+ .then(({ body }) => {
330
+ this.__log.debug('secret %s was written', path);
331
+ return body;
332
+ })
333
+ .catch((reason) => {
334
+ this.__log.error('write secret failed: %s', reason.message);
335
+ throw reason;
336
+ });
337
+ }
338
+
339
+ /**
340
+ * @private
341
+ */
342
+ __setupLogger(logger) {
343
+ if (logger === false) {
344
+ return {
345
+ error: _.noop,
346
+ warn: _.noop,
347
+ info: _.noop,
348
+ debug: _.noop,
349
+ trace: _.noop,
350
+ }
351
+ } else if (_.intersection(_.functionsIn(logger), ['error', 'warn', 'info', 'debug', 'trace']).length >= 5) {
352
+ return logger
353
+ } else {
354
+ return {
355
+ error: console.error,
356
+ warn: console.warn,
357
+ info: console.info,
358
+ trace: console.trace,
359
+ // avoid output sensitive information
360
+ debug: _.noop
361
+ };
362
+ }
363
+ }
364
+ }
365
+
366
+ module.exports = VaultClient;