node-vault-client 2.0.0 → 2.1.0
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/README.md +293 -5
- package/package.json +17 -12
- package/src/Lease.js +16 -5
- package/src/MountResolver.js +220 -0
- package/src/VaultApiClient.js +52 -14
- package/src/VaultClient.js +334 -29
- package/src/VaultNodeConfig.js +79 -30
- package/src/auth/VaultAppRoleAuth.js +10 -10
- package/src/auth/VaultBaseAuth.js +24 -4
- package/src/auth/VaultIAMAuth.js +12 -17
- package/src/auth/VaultKubernetesAuth.js +10 -6
- package/src/auth/VaultTokenAuth.js +1 -1
- package/src/errors.js +32 -2
- package/src/kvTransform.js +90 -0
|
@@ -1,10 +1,30 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
// `long-timeout` is unmaintained (last release 2016) but tiny (~50 LOC), dependency-free and
|
|
4
|
+
// stable: it only works around setTimeout's 32-bit signed-int limit (~24.8 days), which real
|
|
5
|
+
// Vault token TTLs can exceed. Audited and deliberately kept as-is (see issue #111); revisit
|
|
6
|
+
// only if a maintained alternative or a Node.js core fix appears.
|
|
3
7
|
const lt = require('long-timeout');
|
|
4
|
-
const prettyMs = require('pretty-ms');
|
|
5
8
|
const AuthToken = require('./AuthToken');
|
|
6
9
|
const errors = require('../errors');
|
|
7
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Formats a millisecond duration as a short human-readable string
|
|
13
|
+
* (e.g. "1s", "2m 5s", "1h 3m"). Replaces the former `pretty-ms` dependency.
|
|
14
|
+
*/
|
|
15
|
+
function humanizeMs(ms) {
|
|
16
|
+
const totalSeconds = Math.round(ms / 1000);
|
|
17
|
+
if (totalSeconds < 60) {
|
|
18
|
+
return `${totalSeconds}s`;
|
|
19
|
+
}
|
|
20
|
+
const totalMinutes = Math.floor(totalSeconds / 60);
|
|
21
|
+
if (totalMinutes < 60) {
|
|
22
|
+
return `${totalMinutes}m ${totalSeconds % 60}s`;
|
|
23
|
+
}
|
|
24
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
25
|
+
return `${hours}h ${totalMinutes % 60}m`;
|
|
26
|
+
}
|
|
27
|
+
|
|
8
28
|
class VaultBaseAuth {
|
|
9
29
|
|
|
10
30
|
/**
|
|
@@ -48,8 +68,8 @@ class VaultBaseAuth {
|
|
|
48
68
|
|
|
49
69
|
if (this.__authToken.isRenewable()) {
|
|
50
70
|
this._log.debug(
|
|
51
|
-
'setting refresh timer for token
|
|
52
|
-
authToken.
|
|
71
|
+
'setting refresh timer for token (accessor=%s)',
|
|
72
|
+
authToken.getAccessor()
|
|
53
73
|
);
|
|
54
74
|
this.__setupTokenRefreshTimer(this.__authToken);
|
|
55
75
|
}
|
|
@@ -135,7 +155,7 @@ class VaultBaseAuth {
|
|
|
135
155
|
|
|
136
156
|
this._log.debug(
|
|
137
157
|
'sleeping for %s',
|
|
138
|
-
|
|
158
|
+
humanizeMs(timer)
|
|
139
159
|
);
|
|
140
160
|
}
|
|
141
161
|
|
package/src/auth/VaultIAMAuth.js
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
const VaultBaseAuth = require('./VaultBaseAuth');
|
|
4
4
|
const aws4 = require('aws4');
|
|
5
5
|
const { fromNodeProviderChain } = require('@aws-sdk/credential-providers');
|
|
6
|
-
const _ = require('lodash');
|
|
7
6
|
const errors = require('../errors');
|
|
8
7
|
|
|
9
8
|
/**
|
|
@@ -49,7 +48,8 @@ class VaultIAMAuth extends VaultBaseAuth {
|
|
|
49
48
|
* @typedef {Object} VaultIAMAuthConfig
|
|
50
49
|
* @property {String} role - Role name of the auth/{mount}/role/{name} backend.
|
|
51
50
|
* @property {AWSCredentials} [credentials] - Optional. AWS Credentials
|
|
52
|
-
* @property {String} [namespace] - Optional. Vault namespace
|
|
51
|
+
* @property {String} [namespace] - Optional. Vault namespace. Applied as the X-Vault-Namespace
|
|
52
|
+
* header to every request by {@link VaultApiClient}; see {@link VaultClient#constructor}.
|
|
53
53
|
* @property {String} [iam_server_id_header_value] - Optional. Header's value X-Vault-AWS-IAM-Server-ID.
|
|
54
54
|
* @property {String} [region] - Optional. AWS region used to sign the STS GetCallerIdentity
|
|
55
55
|
* request. When set, the request is signed against the regional STS endpoint
|
|
@@ -65,9 +65,12 @@ class VaultIAMAuth extends VaultBaseAuth {
|
|
|
65
65
|
constructor(api, logger, config, mount) {
|
|
66
66
|
super(api, logger, mount || 'aws');
|
|
67
67
|
|
|
68
|
+
if (!config || !config.role) {
|
|
69
|
+
throw new errors.InvalidArgumentsError('"role" should be provided for VaultIAMAuth');
|
|
70
|
+
}
|
|
71
|
+
|
|
68
72
|
this.__role = config.role;
|
|
69
73
|
this.__iam_server_id_header_value = config.iam_server_id_header_value;
|
|
70
|
-
this.__namespace = config.namespace;
|
|
71
74
|
this.__region = config.region;
|
|
72
75
|
|
|
73
76
|
|
|
@@ -86,29 +89,19 @@ class VaultIAMAuth extends VaultBaseAuth {
|
|
|
86
89
|
this.__role
|
|
87
90
|
);
|
|
88
91
|
|
|
89
|
-
var headers = {};
|
|
90
|
-
|
|
91
|
-
if (this.__namespace) {
|
|
92
|
-
headers = {
|
|
93
|
-
'X-Vault-Namespace': this.__namespace,
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
|
|
98
92
|
return Promise.resolve()
|
|
99
93
|
.then(() => this.__getCredentials())
|
|
100
94
|
.then((credentials) => {
|
|
101
95
|
return this.__apiClient.makeRequest(
|
|
102
96
|
'POST',
|
|
103
97
|
`/auth/${this._mount}/login`,
|
|
104
|
-
this.__getVaultAuthRequestBody(this.__getStsRequest(credentials))
|
|
105
|
-
headers
|
|
98
|
+
this.__getVaultAuthRequestBody(this.__getStsRequest(credentials))
|
|
106
99
|
);
|
|
107
100
|
})
|
|
108
101
|
.then((response) => {
|
|
109
102
|
this._log.debug(
|
|
110
|
-
'
|
|
111
|
-
response.auth.
|
|
103
|
+
'received token (accessor=%s)',
|
|
104
|
+
response.auth.accessor
|
|
112
105
|
);
|
|
113
106
|
return this._getTokenEntity(response.auth.client_token)
|
|
114
107
|
})
|
|
@@ -194,7 +187,9 @@ class VaultIAMAuth extends VaultBaseAuth {
|
|
|
194
187
|
* @private
|
|
195
188
|
*/
|
|
196
189
|
__headersLikeGolangStyle(headers) {
|
|
197
|
-
return
|
|
190
|
+
return Object.fromEntries(
|
|
191
|
+
Object.entries(headers).map(([key, value]) => [key, [`${value}`]])
|
|
192
|
+
);
|
|
198
193
|
}
|
|
199
194
|
|
|
200
195
|
_validateCredentialsConfig(credentials) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const VaultBaseAuth = require('./VaultBaseAuth');
|
|
3
|
+
const errors = require('../errors');
|
|
3
4
|
|
|
4
5
|
class VaultKubernetesAuth extends VaultBaseAuth {
|
|
5
6
|
/**
|
|
@@ -8,11 +9,17 @@ class VaultKubernetesAuth extends VaultBaseAuth {
|
|
|
8
9
|
* @param {Object} config
|
|
9
10
|
* @param {String} config.role - Role configured in Vault Kubernetes Auth backend under which we want to issue Vault token.
|
|
10
11
|
* @param {String} [config.tokenPath] - Path to the Kube JWT token. If omitted - default will be used.
|
|
12
|
+
* @param {String} [config.namespace] - Optional. Vault namespace. Applied as the X-Vault-Namespace
|
|
13
|
+
* header to every request by {@link VaultApiClient}; see {@link VaultClient#constructor}.
|
|
11
14
|
* @param {String} mount - Vault's mount point ("kubernetes" by default)
|
|
12
15
|
*/
|
|
13
16
|
constructor(apiClient, logger, config, mount) {
|
|
14
17
|
super(apiClient, logger, mount || 'kubernetes');
|
|
15
18
|
|
|
19
|
+
if (!config || !config.role) {
|
|
20
|
+
throw new errors.InvalidArgumentsError('"role" should be provided for VaultKubernetesAuth');
|
|
21
|
+
}
|
|
22
|
+
|
|
16
23
|
this.__role = config.role;
|
|
17
24
|
this.__tokenPath = '/var/run/secrets/kubernetes.io/serviceaccount/token';
|
|
18
25
|
if (config.tokenPath !== undefined) {
|
|
@@ -28,18 +35,15 @@ class VaultKubernetesAuth extends VaultBaseAuth {
|
|
|
28
35
|
|
|
29
36
|
const k8sJwtToken = fs.readFileSync(this.__tokenPath).toString();
|
|
30
37
|
this._log.debug(
|
|
31
|
-
'
|
|
32
|
-
k8sJwtToken
|
|
38
|
+
'read K8s service-account JWT from "%s" (%d bytes)',
|
|
39
|
+
this.__tokenPath, k8sJwtToken.length
|
|
33
40
|
);
|
|
34
41
|
|
|
35
42
|
return this.__apiClient.makeRequest('POST', `/auth/${this._mount}/login`, {
|
|
36
43
|
role: this.__role,
|
|
37
44
|
jwt: k8sJwtToken,
|
|
38
45
|
}).then((res) => {
|
|
39
|
-
this._log.debug(
|
|
40
|
-
'receive token: %s',
|
|
41
|
-
res.auth.client_token
|
|
42
|
-
);
|
|
46
|
+
this._log.debug('received Vault client token from Kubernetes login');
|
|
43
47
|
|
|
44
48
|
return this._getTokenEntity(res.auth.client_token);
|
|
45
49
|
});
|
|
@@ -15,7 +15,7 @@ class VaultTokenAuth extends VaultBaseAuth {
|
|
|
15
15
|
constructor(apiClient, logger, config, mount) {
|
|
16
16
|
super(apiClient, logger, mount || 'token');
|
|
17
17
|
|
|
18
|
-
if (!config.token) {
|
|
18
|
+
if (!config || !config.token) {
|
|
19
19
|
throw new errors.InvalidArgumentsError('Auth token should be provided for VaultTokenAuth');
|
|
20
20
|
}
|
|
21
21
|
|
package/src/errors.js
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
class VaultError extends Error {
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
/**
|
|
5
|
+
* @param {string} message
|
|
6
|
+
* @param {{cause?: *}} [options] - standard `Error` options. Pass `{ cause }` to chain the
|
|
7
|
+
* underlying error (e.g. a wrapped HTTP/transport failure); it is exposed as `error.cause`.
|
|
8
|
+
*/
|
|
9
|
+
constructor(message, options) {
|
|
10
|
+
super(message, options);
|
|
6
11
|
this.name = this.constructor.name;
|
|
7
12
|
this.message = message;
|
|
8
13
|
|
|
@@ -12,10 +17,35 @@ class VaultError extends Error {
|
|
|
12
17
|
class InvalidArgumentsError extends VaultError {}
|
|
13
18
|
class InvalidAWSCredentialsError extends InvalidArgumentsError {}
|
|
14
19
|
class AuthTokenExpiredError extends VaultError {}
|
|
20
|
+
class UnsupportedOperationError extends VaultError {}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A non-2xx HTTP response from the Vault server.
|
|
24
|
+
*
|
|
25
|
+
* Preserves the legacy plain-`Error` shape previously thrown by
|
|
26
|
+
* {@link VaultApiClient#makeRequest} — the message stays `"<status> - <raw body text>"` and the
|
|
27
|
+
* `statusCode`/`error` properties keep their meaning — while letting callers
|
|
28
|
+
* `instanceof`-check HTTP failures against the `VaultError` hierarchy.
|
|
29
|
+
*/
|
|
30
|
+
class VaultHttpError extends VaultError {
|
|
31
|
+
/**
|
|
32
|
+
* @param {number} statusCode - HTTP status code of the response.
|
|
33
|
+
* @param {string} text - raw response body text, used in the message.
|
|
34
|
+
* @param {*} [body] - parsed JSON body when the response was valid JSON, the raw text
|
|
35
|
+
* otherwise, or `undefined` for an empty body. Exposed as `error.error`.
|
|
36
|
+
*/
|
|
37
|
+
constructor(statusCode, text, body) {
|
|
38
|
+
super(`${statusCode} - ${text}`);
|
|
39
|
+
this.statusCode = statusCode;
|
|
40
|
+
this.error = body;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
15
43
|
|
|
16
44
|
module.exports = {
|
|
17
45
|
VaultError,
|
|
18
46
|
InvalidArgumentsError,
|
|
19
47
|
InvalidAWSCredentialsError,
|
|
20
48
|
AuthTokenExpiredError,
|
|
49
|
+
UnsupportedOperationError,
|
|
50
|
+
VaultHttpError,
|
|
21
51
|
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pure KV path-rewriting and response-normalizing helpers.
|
|
5
|
+
* No I/O; fully testable in isolation.
|
|
6
|
+
*
|
|
7
|
+
* rewritePath(version, op, mount, logicalPath) -> apiPath
|
|
8
|
+
* normalizeResponse(version, op, body) -> normalized body
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// Map of op -> v2 path segment inserted between mount and logical path.
|
|
12
|
+
const V2_SEGMENT = {
|
|
13
|
+
read: 'data',
|
|
14
|
+
write: 'data',
|
|
15
|
+
delete: 'data',
|
|
16
|
+
update: 'data',
|
|
17
|
+
list: 'metadata',
|
|
18
|
+
readMetadata: 'metadata',
|
|
19
|
+
deleteMetadata: 'metadata',
|
|
20
|
+
deleteVersions: 'delete',
|
|
21
|
+
undeleteVersions:'undelete',
|
|
22
|
+
destroyVersions: 'destroy',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Rewrite a logical path to the correct API path for the given engine version.
|
|
27
|
+
*
|
|
28
|
+
* @param {number} version - Engine version (2 = KV v2; anything else = passthrough)
|
|
29
|
+
* @param {string} op - Operation name (read, write, list, delete, update, …)
|
|
30
|
+
* @param {string} mount - Mount point (e.g. "secret" or "secret/")
|
|
31
|
+
* @param {string} logicalPath - Path relative to the mount (e.g. "team/svc")
|
|
32
|
+
* @returns {string}
|
|
33
|
+
*/
|
|
34
|
+
function rewritePath(version, op, mount, logicalPath) {
|
|
35
|
+
// Normalise trailing slash on mount
|
|
36
|
+
const m = mount.replace(/\/+$/, '');
|
|
37
|
+
|
|
38
|
+
if (version !== 2) {
|
|
39
|
+
// v1 / non-kv: simple concatenation
|
|
40
|
+
return logicalPath ? `${m}/${logicalPath}` : m;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const segment = V2_SEGMENT[op] || 'data';
|
|
44
|
+
return `${m}/${segment}/${logicalPath}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Normalise a Vault API response body for the given engine version and operation.
|
|
49
|
+
*
|
|
50
|
+
* For v1 / non-kv the body is always returned as-is.
|
|
51
|
+
* For v2:
|
|
52
|
+
* - read: body.data is replaced with body.data.data; body.metadata is set to body.data.metadata
|
|
53
|
+
* - readMetadata: body.data is replaced with the original body.data (unwrapped one level)
|
|
54
|
+
* - write/delete/update/…: body returned as-is
|
|
55
|
+
*
|
|
56
|
+
* @param {number} version
|
|
57
|
+
* @param {string} op
|
|
58
|
+
* @param {*} body
|
|
59
|
+
* @returns {*}
|
|
60
|
+
*/
|
|
61
|
+
function normalizeResponse(version, op, body) {
|
|
62
|
+
if (version !== 2) {
|
|
63
|
+
return body;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (op === 'read') {
|
|
67
|
+
if (!body || !body.data) {
|
|
68
|
+
return body;
|
|
69
|
+
}
|
|
70
|
+
// Promote inner data/metadata to top-level response fields
|
|
71
|
+
const result = Object.assign({}, body);
|
|
72
|
+
result.metadata = body.data.metadata;
|
|
73
|
+
result.data = body.data.data;
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (op === 'readMetadata') {
|
|
78
|
+
if (!body || !body.data) {
|
|
79
|
+
return body;
|
|
80
|
+
}
|
|
81
|
+
const result = Object.assign({}, body);
|
|
82
|
+
result.data = body.data;
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// All other v2 ops (write, list, delete, update, deleteVersions, …): passthrough
|
|
87
|
+
return body;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = { rewritePath, normalizeResponse };
|