node-vault-client 2.0.0 → 2.0.1
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.
- package/package.json +6 -9
- package/src/VaultApiClient.js +25 -4
- package/src/VaultNodeConfig.js +30 -27
- package/src/auth/VaultBaseAuth.js +18 -2
- package/src/auth/VaultKubernetesAuth.js +3 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-vault-client",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "A Vault Client implemented in pure javascript for HashiCorp Vault. It supports variety of Auth Backends and performs lease renewal for issued auth token.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"url": "git+https://github.com/namecheap/node-vault-client.git"
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"scripts": {
|
|
16
16
|
"test": "mocha --exit \"test/**/*.test.mjs\"",
|
|
17
17
|
"test:unit": "mocha \"test/*.test.mjs\"",
|
|
18
|
+
"test:e2e": "mocha --exit \"test/e2e/**/*.test.mjs\"",
|
|
18
19
|
"coverage": "c8 npm run test:unit",
|
|
19
20
|
"lint": "eslint src/",
|
|
20
21
|
"version": "git add -A ."
|
|
@@ -34,13 +35,9 @@
|
|
|
34
35
|
"license": "Apache-2.0",
|
|
35
36
|
"dependencies": {
|
|
36
37
|
"@aws-sdk/credential-providers": "^3.382.0",
|
|
37
|
-
"assign-deep": "^1.0.1",
|
|
38
38
|
"aws4": "^1.8.0",
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"long-timeout": "^0.1.1",
|
|
42
|
-
"pretty-ms": "^2.1.0",
|
|
43
|
-
"url-join": "^2.0.5"
|
|
39
|
+
"lodash": "^4.17.21",
|
|
40
|
+
"long-timeout": "^0.1.1"
|
|
44
41
|
},
|
|
45
42
|
"peerDependencies": {
|
|
46
43
|
"config": ">=1 <4"
|
|
@@ -55,8 +52,8 @@
|
|
|
55
52
|
"c8": "^11.0.0",
|
|
56
53
|
"chai": "^6.0.0",
|
|
57
54
|
"deep-freeze": "^0.0.1",
|
|
58
|
-
"eslint": "^9.
|
|
59
|
-
"globals": "^
|
|
55
|
+
"eslint": "^9.39.4",
|
|
56
|
+
"globals": "^17.6.0",
|
|
60
57
|
"mocha": "^11.7.6",
|
|
61
58
|
"sinon": "^22.0.0",
|
|
62
59
|
"sinon-chai": "^4.0.0"
|
package/src/VaultApiClient.js
CHANGED
|
@@ -1,8 +1,27 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const urljoin = require('url-join');
|
|
4
3
|
const _ = require('lodash');
|
|
5
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Joins URL segments with single slashes while preserving the leading
|
|
7
|
+
* protocol (e.g. "https://"). Replaces the former `url-join` dependency.
|
|
8
|
+
*/
|
|
9
|
+
function joinUrl(...parts) {
|
|
10
|
+
return parts
|
|
11
|
+
.map((part, i) => {
|
|
12
|
+
let segment = String(part);
|
|
13
|
+
if (i > 0) {
|
|
14
|
+
segment = segment.replace(/^\/+/, '');
|
|
15
|
+
}
|
|
16
|
+
if (i < parts.length - 1) {
|
|
17
|
+
segment = segment.replace(/\/+$/, '');
|
|
18
|
+
}
|
|
19
|
+
return segment;
|
|
20
|
+
})
|
|
21
|
+
.filter((segment) => segment.length > 0)
|
|
22
|
+
.join('/');
|
|
23
|
+
}
|
|
24
|
+
|
|
6
25
|
class VaultApiClient {
|
|
7
26
|
|
|
8
27
|
/**
|
|
@@ -35,7 +54,7 @@ class VaultApiClient {
|
|
|
35
54
|
data = data === undefined ? null : data;
|
|
36
55
|
headers = headers === undefined ? {} : headers;
|
|
37
56
|
|
|
38
|
-
const uri =
|
|
57
|
+
const uri = joinUrl(this.__config.url, this.__config.apiVersion, path);
|
|
39
58
|
|
|
40
59
|
const requestOptions = this.__config.requestOptions || {};
|
|
41
60
|
|
|
@@ -80,10 +99,12 @@ class VaultApiClient {
|
|
|
80
99
|
throw error;
|
|
81
100
|
}
|
|
82
101
|
|
|
83
|
-
|
|
102
|
+
// Do not log the response body: Vault responses carry secret
|
|
103
|
+
// material (auth tokens, secret reads). Log status only.
|
|
104
|
+
this._logger.debug('%s %s -> %d',
|
|
84
105
|
method,
|
|
85
106
|
uri,
|
|
86
|
-
|
|
107
|
+
response.status
|
|
87
108
|
);
|
|
88
109
|
return body;
|
|
89
110
|
});
|
package/src/VaultNodeConfig.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const path = require('path');
|
|
4
|
-
const Bluebird = require('bluebird');
|
|
5
4
|
const _ = require('lodash');
|
|
6
|
-
const assignDeep = require('assign-deep');
|
|
7
5
|
|
|
8
6
|
const errors = require('./errors');
|
|
9
7
|
|
|
@@ -29,45 +27,54 @@ class VaultNodeConfig {
|
|
|
29
27
|
let requiredData = {};
|
|
30
28
|
|
|
31
29
|
this.__traverse(substitutionMap, (key, val) => {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
if (path === undefined || value === undefined) {
|
|
35
|
-
throw new errors.InvalidArgumentsError('Invalid format of substitution value');
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
requiredData[path] = null;
|
|
30
|
+
const { vaultPath } = this.__parseSubstitutionValue(val);
|
|
31
|
+
requiredData[vaultPath] = null;
|
|
39
32
|
});
|
|
40
33
|
|
|
41
|
-
|
|
34
|
+
const vaultPaths = Object.keys(requiredData);
|
|
42
35
|
|
|
43
|
-
return
|
|
44
|
-
|
|
36
|
+
return Promise.all(vaultPaths.map((vaultPath) => this.__vault.read(vaultPath))).then((leases) => {
|
|
37
|
+
const results = _.zipObject(vaultPaths, leases);
|
|
38
|
+
requiredData = _.mapValues(requiredData, (value, vaultPath) => results[vaultPath].getData());
|
|
45
39
|
|
|
46
40
|
this.__traverse(substitutionMap, (key, val, obj) => {
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
const res = requiredData[path][value];
|
|
41
|
+
const { vaultPath, value } = this.__parseSubstitutionValue(val);
|
|
42
|
+
const res = requiredData[vaultPath][value];
|
|
50
43
|
if (res === undefined) {
|
|
51
44
|
throw new errors.VaultError(`Can't find substitution for "${val}"`);
|
|
52
45
|
}
|
|
53
46
|
|
|
54
|
-
obj[key] = requiredData[
|
|
47
|
+
obj[key] = requiredData[vaultPath][value];
|
|
55
48
|
});
|
|
56
49
|
|
|
57
|
-
return
|
|
50
|
+
return _.merge(this.__nodeConfig, substitutionMap);
|
|
58
51
|
});
|
|
59
52
|
}
|
|
60
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Splits a substitution value of the form "<vaultPath>#<value>".
|
|
56
|
+
*
|
|
57
|
+
* @private
|
|
58
|
+
*/
|
|
59
|
+
__parseSubstitutionValue(val) {
|
|
60
|
+
const [vaultPath, value] = val.split('#');
|
|
61
|
+
if (!vaultPath || !value) {
|
|
62
|
+
throw new errors.InvalidArgumentsError('Invalid format of substitution value');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return { vaultPath, value };
|
|
66
|
+
}
|
|
67
|
+
|
|
61
68
|
/**
|
|
62
69
|
* @private
|
|
63
70
|
*/
|
|
64
71
|
__getSubstitutionMap() {
|
|
65
|
-
let
|
|
66
|
-
if (
|
|
67
|
-
|
|
72
|
+
let configDir = this.__nodeConfig.util.initParam('NODE_CONFIG_DIR', path.join(process.cwd(), 'config'));
|
|
73
|
+
if (configDir.indexOf('.') === 0) {
|
|
74
|
+
configDir = path.join(process.cwd(), configDir);
|
|
68
75
|
}
|
|
69
76
|
|
|
70
|
-
const fullFilename = path.join(
|
|
77
|
+
const fullFilename = path.join(configDir, 'custom-vault-variables.js');
|
|
71
78
|
|
|
72
79
|
let fileContent;
|
|
73
80
|
try {
|
|
@@ -84,12 +91,8 @@ class VaultNodeConfig {
|
|
|
84
91
|
}
|
|
85
92
|
|
|
86
93
|
__traverse(o, func) {
|
|
87
|
-
for (
|
|
88
|
-
if (
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (o[i] !== null && typeof(o[i]) === "object") {
|
|
94
|
+
for (const i of Object.keys(o)) {
|
|
95
|
+
if (o[i] !== null && typeof o[i] === 'object') {
|
|
93
96
|
//going one step down in the object tree!!
|
|
94
97
|
this.__traverse(o[i], func);
|
|
95
98
|
} else if (typeof o[i] === 'string') {
|
|
@@ -1,10 +1,26 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const lt = require('long-timeout');
|
|
4
|
-
const prettyMs = require('pretty-ms');
|
|
5
4
|
const AuthToken = require('./AuthToken');
|
|
6
5
|
const errors = require('../errors');
|
|
7
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Formats a millisecond duration as a short human-readable string
|
|
9
|
+
* (e.g. "1s", "2m 5s", "1h 3m"). Replaces the former `pretty-ms` dependency.
|
|
10
|
+
*/
|
|
11
|
+
function humanizeMs(ms) {
|
|
12
|
+
const totalSeconds = Math.round(ms / 1000);
|
|
13
|
+
if (totalSeconds < 60) {
|
|
14
|
+
return `${totalSeconds}s`;
|
|
15
|
+
}
|
|
16
|
+
const totalMinutes = Math.floor(totalSeconds / 60);
|
|
17
|
+
if (totalMinutes < 60) {
|
|
18
|
+
return `${totalMinutes}m ${totalSeconds % 60}s`;
|
|
19
|
+
}
|
|
20
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
21
|
+
return `${hours}h ${totalMinutes % 60}m`;
|
|
22
|
+
}
|
|
23
|
+
|
|
8
24
|
class VaultBaseAuth {
|
|
9
25
|
|
|
10
26
|
/**
|
|
@@ -135,7 +151,7 @@ class VaultBaseAuth {
|
|
|
135
151
|
|
|
136
152
|
this._log.debug(
|
|
137
153
|
'sleeping for %s',
|
|
138
|
-
|
|
154
|
+
humanizeMs(timer)
|
|
139
155
|
);
|
|
140
156
|
}
|
|
141
157
|
|
|
@@ -28,18 +28,15 @@ class VaultKubernetesAuth extends VaultBaseAuth {
|
|
|
28
28
|
|
|
29
29
|
const k8sJwtToken = fs.readFileSync(this.__tokenPath).toString();
|
|
30
30
|
this._log.debug(
|
|
31
|
-
'
|
|
32
|
-
k8sJwtToken
|
|
31
|
+
'read K8s service-account JWT from "%s" (%d bytes)',
|
|
32
|
+
this.__tokenPath, k8sJwtToken.length
|
|
33
33
|
);
|
|
34
34
|
|
|
35
35
|
return this.__apiClient.makeRequest('POST', `/auth/${this._mount}/login`, {
|
|
36
36
|
role: this.__role,
|
|
37
37
|
jwt: k8sJwtToken,
|
|
38
38
|
}).then((res) => {
|
|
39
|
-
this._log.debug(
|
|
40
|
-
'receive token: %s',
|
|
41
|
-
res.auth.client_token
|
|
42
|
-
);
|
|
39
|
+
this._log.debug('received Vault client token from Kubernetes login');
|
|
43
40
|
|
|
44
41
|
return this._getTokenEntity(res.auth.client_token);
|
|
45
42
|
});
|