vvauth 2.2.0 → 2.4.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 +4 -1
- package/index.js +24 -28
- package/package.json +2 -2
- package/test/token-cache.js +289 -75
package/README.md
CHANGED
|
@@ -63,10 +63,13 @@ vauth env --ir://json
|
|
|
63
63
|
|
|
64
64
|
# Token cache
|
|
65
65
|
|
|
66
|
-
Tokens are cached in `~/.vauth/
|
|
66
|
+
Tokens are cached in `~/.vauth/tokens/<session>.creds`, isolated by Vault and
|
|
67
67
|
authentication. Tokens are reused, renewed within their lease’s final third, or
|
|
68
68
|
replaced when expired. Existing `VAULT_TOKEN` takes precedence over cached tokens.
|
|
69
69
|
|
|
70
|
+
Each session file is armored using `ssh-agent-crypt` and your SSH agent’s first
|
|
71
|
+
key. Without an SSH agent, disk caching is disabled.
|
|
72
|
+
|
|
70
73
|
Set `token_cache: false` in `.vauthrc` to disable disk cache reads and writes.
|
|
71
74
|
|
|
72
75
|
## Force reauthentication
|
package/index.js
CHANGED
|
@@ -6,9 +6,9 @@ const fs = require('fs');
|
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const url = require('url');
|
|
8
8
|
const {createHash} = require('crypto');
|
|
9
|
+
const {encrypt, decrypt} = require('ssh-agent-crypt');
|
|
9
10
|
const {spawn, execFileSync} = require('child_process');
|
|
10
11
|
const passthru = require('nyks/child_process/passthru');
|
|
11
|
-
const wait = require('nyks/child_process/wait');
|
|
12
12
|
const boolPrompt = require('cnyks/prompt/bool');
|
|
13
13
|
|
|
14
14
|
const {parse} = require('yaml');
|
|
@@ -89,6 +89,9 @@ class vvauth {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
|
|
92
|
+
if(!process.env.SSH_AUTH_SOCK)
|
|
93
|
+
this.rc.token_cache = false;
|
|
94
|
+
|
|
92
95
|
this.VAULT_ADDR = this.rc.vault_addr || process.env.VAULT_ADDR; //might be null
|
|
93
96
|
this.VAULT_TOKEN = process.env.VAULT_TOKEN;
|
|
94
97
|
|
|
@@ -112,40 +115,38 @@ class vvauth {
|
|
|
112
115
|
}
|
|
113
116
|
|
|
114
117
|
_token_cache_file() {
|
|
115
|
-
return path.join(os.homedir(), '.vauth', '
|
|
118
|
+
return path.join(os.homedir(), '.vauth', 'tokens', this._token_cache_key() + '.creds');
|
|
116
119
|
}
|
|
117
120
|
|
|
118
|
-
_read_token_cache() {
|
|
121
|
+
async _read_token_cache() {
|
|
119
122
|
if(this.rc.token_cache === false)
|
|
120
|
-
return
|
|
123
|
+
return;
|
|
121
124
|
try {
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
return cache;
|
|
125
|
+
const armor = fs.readFileSync(this._token_cache_file(), 'utf8');
|
|
126
|
+
return JSON.parse(await decrypt(armor));
|
|
125
127
|
} catch(err) {}
|
|
126
|
-
return {version : 1, tokens : {}};
|
|
127
128
|
}
|
|
128
129
|
|
|
129
|
-
_save_cached_token(token, metadata) {
|
|
130
|
+
async _save_cached_token(token, metadata) {
|
|
130
131
|
if(this.rc.token_cache === false)
|
|
131
132
|
return;
|
|
132
133
|
const ttl = metadata.ttl === undefined ? metadata.lease_duration : metadata.ttl;
|
|
133
134
|
const file = this._token_cache_file();
|
|
134
135
|
const temporary = file + '.next';
|
|
135
136
|
try {
|
|
136
|
-
const cache = this._read_token_cache();
|
|
137
|
-
const key = this._token_cache_key();
|
|
138
137
|
const {lease_duration} = metadata;
|
|
139
|
-
cache
|
|
138
|
+
const cache = {
|
|
140
139
|
token, lease_duration, renewable : metadata.renewable === true,
|
|
141
140
|
expires_at : ttl === 0 ? null : Date.now() + ttl * 1000,
|
|
142
141
|
};
|
|
143
142
|
fs.mkdirSync(path.dirname(file), {recursive : true, mode : 0o700});
|
|
144
|
-
|
|
143
|
+
const identity = process.env.VAUTH_USER_IDENTITY || process.env.VAUTH_USER_MAIL;
|
|
144
|
+
const armor = await encrypt(JSON.stringify(cache), identity);
|
|
145
|
+
fs.writeFileSync(temporary, armor, {mode : 0o600});
|
|
145
146
|
fs.renameSync(temporary, file);
|
|
146
147
|
} catch(err) {
|
|
147
148
|
// Cache failures must not prevent authentication or expose credentials.
|
|
148
|
-
|
|
149
|
+
console.error('Could not encrypt/write token cache; token not persisted');
|
|
149
150
|
} finally {
|
|
150
151
|
try { fs.unlinkSync(temporary); } catch(err) {}
|
|
151
152
|
}
|
|
@@ -158,11 +159,16 @@ class vvauth {
|
|
|
158
159
|
}
|
|
159
160
|
|
|
160
161
|
async connect(renew = false) {
|
|
162
|
+
const {ssh_auth, jwt_auth} = this.rc;
|
|
163
|
+
if(!ssh_auth && !jwt_auth) {
|
|
164
|
+
this.VAULT_TOKEN = undefined;
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
161
167
|
if(this._connected && !renew)
|
|
162
168
|
return this.VAULT_TOKEN;
|
|
163
169
|
this._connected = false;
|
|
164
170
|
|
|
165
|
-
const cached = this._read_token_cache()
|
|
171
|
+
const cached = await this._read_token_cache();
|
|
166
172
|
let token = this.VAULT_TOKEN || (cached && cached.token);
|
|
167
173
|
const had_token = !!token;
|
|
168
174
|
let metadata, operation = 'reused';
|
|
@@ -197,7 +203,6 @@ class vvauth {
|
|
|
197
203
|
}
|
|
198
204
|
|
|
199
205
|
if(!token) {
|
|
200
|
-
const {ssh_auth, jwt_auth} = this.rc;
|
|
201
206
|
const agent_path = process.env.SSH_AUTH_SOCK || (process.platform === 'win32' ? 'pageant' : null);
|
|
202
207
|
if(ssh_auth && agent_path) {
|
|
203
208
|
metadata = await this._login_vault_ssh({...ssh_auth}, agent_path);
|
|
@@ -213,7 +218,7 @@ class vvauth {
|
|
|
213
218
|
|
|
214
219
|
if(token) {
|
|
215
220
|
if(operation !== 'reused' || !cached || cached.token !== token)
|
|
216
|
-
this._save_cached_token(token, metadata);
|
|
221
|
+
await this._save_cached_token(token, metadata);
|
|
217
222
|
this._token_status(operation, metadata);
|
|
218
223
|
}
|
|
219
224
|
this.VAULT_TOKEN = token;
|
|
@@ -301,16 +306,7 @@ class vvauth {
|
|
|
301
306
|
let {'ssh-agent-crypt' : agent } = this.rc;
|
|
302
307
|
if(agent) {
|
|
303
308
|
const {path, identity} = agent;
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
child.stdin.end(fs.readFileSync(path));
|
|
307
|
-
child.stderr.pipe(process.stderr);
|
|
308
|
-
|
|
309
|
-
const [exit, body] = await Promise.all([wait(child, false), drain(child.stdout)]);
|
|
310
|
-
if(exit !== 0) {
|
|
311
|
-
console.error("Could not expand armored %s using %s", path, identity);
|
|
312
|
-
process.exit();
|
|
313
|
-
}
|
|
309
|
+
const body = await decrypt(fs.readFileSync(path), identity);
|
|
314
310
|
const result = JSON.parse(body);
|
|
315
311
|
secrets = {...secrets, ...result};
|
|
316
312
|
}
|
|
@@ -445,7 +441,7 @@ class vvauth {
|
|
|
445
441
|
let response = JSON.parse(await drain(res)).data;
|
|
446
442
|
if(!Number.isFinite(response.ttl) || response.ttl < 0)
|
|
447
443
|
throw new Error('Invalid Vault token TTL');
|
|
448
|
-
const cached = this._read_token_cache()
|
|
444
|
+
const cached = await this._read_token_cache();
|
|
449
445
|
if(cached && cached.token === token)
|
|
450
446
|
response.lease_duration = cached.lease_duration;
|
|
451
447
|
else
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vvauth",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "Vault Auth helper",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"mout": "^1.2.4",
|
|
19
19
|
"nyks": "^6.15.0",
|
|
20
20
|
"semver": "^7.5.4",
|
|
21
|
-
"ssh-agent-crypt": "^1.0
|
|
21
|
+
"ssh-agent-crypt": "^1.2.0",
|
|
22
22
|
"ssh2": "^1.16.0",
|
|
23
23
|
"yaml": "^2.6.1"
|
|
24
24
|
},
|
package/test/token-cache.js
CHANGED
|
@@ -1,21 +1,58 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const assert = require('node:assert/strict');
|
|
4
|
-
const {test} = require('node:test');
|
|
4
|
+
const {test, before, after} = require('node:test');
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const os = require('os');
|
|
7
7
|
const path = require('path');
|
|
8
8
|
const http = require('http');
|
|
9
|
-
const {execFile, spawn} = require('child_process');
|
|
9
|
+
const {execFile, spawn, spawnSync} = require('child_process');
|
|
10
10
|
const {promisify, format} = require('util');
|
|
11
11
|
const exec = promisify(execFile);
|
|
12
12
|
const Vauth = require('../index');
|
|
13
13
|
|
|
14
|
+
const {encrypt, decrypt} = require('ssh-agent-crypt');
|
|
15
|
+
const readCache = async file => JSON.parse(await decrypt(fs.readFileSync(file, 'utf8')));
|
|
16
|
+
const writeCache = async (file, cache) => fs.writeFileSync(file, await encrypt(JSON.stringify(cache)));
|
|
17
|
+
let agentDirectory, agentPid;
|
|
18
|
+
const previousSocket = process.env.SSH_AUTH_SOCK;
|
|
19
|
+
const previousIdentity = process.env.VAUTH_USER_IDENTITY;
|
|
20
|
+
const previousMail = process.env.VAUTH_USER_MAIL;
|
|
21
|
+
|
|
22
|
+
before(() => {
|
|
23
|
+
delete process.env.VAUTH_USER_IDENTITY;
|
|
24
|
+
delete process.env.VAUTH_USER_MAIL;
|
|
25
|
+
agentDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'vvauth-agent-'));
|
|
26
|
+
const key = path.join(agentDirectory, 'key');
|
|
27
|
+
assert.equal(spawnSync('ssh-keygen', ['-q', '-t', 'ed25519', '-N', '', '-f', key]).status, 0);
|
|
28
|
+
const agent = spawnSync('ssh-agent', ['-s'], {encoding : 'utf8'});
|
|
29
|
+
assert.equal(agent.status, 0);
|
|
30
|
+
process.env.SSH_AUTH_SOCK = agent.stdout.match(/SSH_AUTH_SOCK=([^;]+)/)[1];
|
|
31
|
+
agentPid = agent.stdout.match(/SSH_AGENT_PID=(\d+)/)[1];
|
|
32
|
+
assert.equal(spawnSync('ssh-add', [key]).status, 0);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
after(() => {
|
|
36
|
+
for(const [name, value] of [['VAUTH_USER_IDENTITY', previousIdentity], ['VAUTH_USER_MAIL', previousMail]]) {
|
|
37
|
+
if(value === undefined)
|
|
38
|
+
delete process.env[name];
|
|
39
|
+
else
|
|
40
|
+
process.env[name] = value;
|
|
41
|
+
}
|
|
42
|
+
if(agentPid)
|
|
43
|
+
process.kill(Number(agentPid));
|
|
44
|
+
if(previousSocket === undefined)
|
|
45
|
+
delete process.env.SSH_AUTH_SOCK;
|
|
46
|
+
else
|
|
47
|
+
process.env.SSH_AUTH_SOCK = previousSocket;
|
|
48
|
+
fs.rmSync(agentDirectory, {recursive : true, force : true});
|
|
49
|
+
});
|
|
50
|
+
|
|
14
51
|
const HOUR = 60 * 60;
|
|
15
52
|
|
|
16
53
|
async function fixture(t, options = {}) {
|
|
17
54
|
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'vvauth-test-'));
|
|
18
|
-
|
|
55
|
+
let file;
|
|
19
56
|
const requests = [];
|
|
20
57
|
const server = http.createServer(async (req, res) => {
|
|
21
58
|
let body = '';
|
|
@@ -48,20 +85,22 @@ async function fixture(t, options = {}) {
|
|
|
48
85
|
const instance = Object.create(Vauth.prototype);
|
|
49
86
|
instance.rc = {jwt_auth : {path : 'jwt', jwt : 'test-jwt', role : 'test-role'}};
|
|
50
87
|
instance.VAULT_ADDR = `http://127.0.0.1:${server.address().port}`;
|
|
51
|
-
instance._token_cache_file = () =>
|
|
88
|
+
instance._token_cache_file = () => path.join(directory, '.vauth', 'tokens', instance._token_cache_key() + '.creds');
|
|
52
89
|
return instance;
|
|
53
90
|
};
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
91
|
+
file = client()._token_cache_file();
|
|
92
|
+
const remaining = async (instance, ttl) => {
|
|
93
|
+
const file = instance._token_cache_file();
|
|
94
|
+
const cache = (await readCache(file));
|
|
95
|
+
cache.expires_at = ttl === 0 ? null : Date.now() + ttl * 1000;
|
|
96
|
+
await writeCache(file, cache);
|
|
58
97
|
};
|
|
59
|
-
const seed = (instance, lease = 12 * HOUR) => {
|
|
60
|
-
instance._save_cached_token('cached-token', {lease_duration : lease, renewable : options.renewable !== false});
|
|
98
|
+
const seed = async (instance, lease = 12 * HOUR) => {
|
|
99
|
+
await instance._save_cached_token('cached-token', {lease_duration : lease, renewable : options.renewable !== false});
|
|
61
100
|
if(options.ttl !== undefined)
|
|
62
|
-
remaining(instance, options.ttl);
|
|
101
|
+
await remaining(instance, options.ttl);
|
|
63
102
|
};
|
|
64
|
-
const read = instance =>
|
|
103
|
+
const read = async instance => readCache(instance._token_cache_file());
|
|
65
104
|
return {client, seed, read, requests, file, remaining};
|
|
66
105
|
}
|
|
67
106
|
|
|
@@ -69,31 +108,31 @@ test('login persists a token; a new instance reuses it without logging in', asyn
|
|
|
69
108
|
const {client, read, requests, file} = await fixture(t);
|
|
70
109
|
const first = client();
|
|
71
110
|
assert.equal(await first.connect(), 'fresh-token');
|
|
72
|
-
assert.equal(read(first).token, 'fresh-token');
|
|
73
|
-
assert.ok(read(first).expires_at > Date.now() + 23 * HOUR * 1000);
|
|
111
|
+
assert.equal((await read(first)).token, 'fresh-token');
|
|
112
|
+
assert.ok((await read(first)).expires_at > Date.now() + 23 * HOUR * 1000);
|
|
74
113
|
assert.equal(fs.statSync(file).mode & 0o777, 0o600);
|
|
75
114
|
assert.equal(fs.statSync(path.dirname(file)).mode & 0o777, 0o700);
|
|
76
115
|
const second = client();
|
|
77
116
|
assert.equal(await second.connect(), 'fresh-token');
|
|
78
117
|
assert.deepEqual(requests.map(req => req.path), ['/v1/auth/jwt/login']);
|
|
79
|
-
assert.deepEqual(fs.readdirSync(path.dirname(file)), [
|
|
118
|
+
assert.deepEqual(fs.readdirSync(path.dirname(file)), [path.basename(file)]);
|
|
80
119
|
});
|
|
81
120
|
|
|
82
121
|
test('a token in the last third of its lease is renewed and saved', async t => {
|
|
83
122
|
const {client, seed, read, requests} = await fixture(t, {ttl : HOUR});
|
|
84
123
|
const instance = client();
|
|
85
|
-
seed(instance, 12 * HOUR);
|
|
124
|
+
await seed(instance, 12 * HOUR);
|
|
86
125
|
assert.equal(await instance.connect(), 'cached-token');
|
|
87
126
|
assert.deepEqual(requests.map(req => req.path), ['/v1/auth/token/renew-self']);
|
|
88
127
|
assert.equal(requests[0].method, 'POST');
|
|
89
128
|
assert.equal(requests[0].token, 'cached-token');
|
|
90
|
-
assert.ok(read(instance).expires_at > Date.now() + 23 * HOUR * 1000);
|
|
129
|
+
assert.ok((await read(instance)).expires_at > Date.now() + 23 * HOUR * 1000);
|
|
91
130
|
});
|
|
92
131
|
|
|
93
132
|
test('more than one third remaining does not trigger renewal', async t => {
|
|
94
133
|
const {client, seed, requests} = await fixture(t, {ttl : 4 * HOUR + 1});
|
|
95
134
|
const instance = client();
|
|
96
|
-
seed(instance);
|
|
135
|
+
await seed(instance);
|
|
97
136
|
assert.equal(await instance.connect(), 'cached-token');
|
|
98
137
|
assert.equal(requests.length, 0);
|
|
99
138
|
});
|
|
@@ -101,19 +140,19 @@ test('more than one third remaining does not trigger renewal', async t => {
|
|
|
101
140
|
test('expired cached tokens are replaced with a new persisted login', async t => {
|
|
102
141
|
const {client, seed, read, requests, file} = await fixture(t);
|
|
103
142
|
const instance = client();
|
|
104
|
-
seed(instance);
|
|
105
|
-
const cache =
|
|
106
|
-
cache.
|
|
107
|
-
|
|
143
|
+
await seed(instance);
|
|
144
|
+
const cache = (await readCache(file));
|
|
145
|
+
cache.expires_at = Date.now() - 1000;
|
|
146
|
+
await writeCache(file, cache);
|
|
108
147
|
assert.equal(await instance.connect(), 'fresh-token');
|
|
109
|
-
assert.equal(read(instance).token, 'fresh-token');
|
|
148
|
+
assert.equal((await read(instance)).token, 'fresh-token');
|
|
110
149
|
assert.deepEqual(requests.map(req => req.path), ['/v1/auth/jwt/login']);
|
|
111
150
|
});
|
|
112
151
|
|
|
113
152
|
test('cached tokens are reused without checking revocation', async t => {
|
|
114
153
|
const {client, seed, requests} = await fixture(t, {lookup_status : 403});
|
|
115
154
|
const instance = client();
|
|
116
|
-
seed(instance);
|
|
155
|
+
await seed(instance);
|
|
117
156
|
assert.equal(await instance.connect(), 'cached-token');
|
|
118
157
|
assert.deepEqual(requests, []);
|
|
119
158
|
await assert.rejects(instance._lookup_token(instance.VAULT_TOKEN));
|
|
@@ -122,7 +161,7 @@ test('cached tokens are reused without checking revocation', async t => {
|
|
|
122
161
|
test('nonrenewable tokens nearing expiry trigger reauthentication', async t => {
|
|
123
162
|
const {client, seed, requests} = await fixture(t, {ttl : HOUR, renewable : false});
|
|
124
163
|
const instance = client();
|
|
125
|
-
seed(instance);
|
|
164
|
+
await seed(instance);
|
|
126
165
|
assert.equal(await instance.connect(), 'fresh-token');
|
|
127
166
|
assert.deepEqual(requests.map(req => req.path), ['/v1/auth/jwt/login']);
|
|
128
167
|
});
|
|
@@ -131,9 +170,9 @@ for(const status of [400, 403]) {
|
|
|
131
170
|
test(`rejected renewal (${status}) triggers a new persisted login`, async t => {
|
|
132
171
|
const {client, seed, read, requests} = await fixture(t, {ttl : HOUR, renew_status : status});
|
|
133
172
|
const instance = client();
|
|
134
|
-
seed(instance);
|
|
173
|
+
await seed(instance);
|
|
135
174
|
assert.equal(await instance.connect(), 'fresh-token');
|
|
136
|
-
assert.equal(read(instance).token, 'fresh-token');
|
|
175
|
+
assert.equal((await read(instance)).token, 'fresh-token');
|
|
137
176
|
assert.deepEqual(requests.map(req => req.path), ['/v1/auth/token/renew-self', '/v1/auth/jwt/login']);
|
|
138
177
|
});
|
|
139
178
|
}
|
|
@@ -141,7 +180,7 @@ for(const status of [400, 403]) {
|
|
|
141
180
|
test('server failures do not generate extra tokens, and connect can be retried', async t => {
|
|
142
181
|
const {client, seed, requests} = await fixture(t, {lookup_status : 500});
|
|
143
182
|
const instance = client();
|
|
144
|
-
seed(instance);
|
|
183
|
+
await seed(instance);
|
|
145
184
|
instance.VAULT_TOKEN = 'uncached-token';
|
|
146
185
|
await assert.rejects(instance.connect());
|
|
147
186
|
await assert.rejects(instance.connect());
|
|
@@ -151,7 +190,7 @@ test('server failures do not generate extra tokens, and connect can be retried',
|
|
|
151
190
|
test('renewal server failures do not generate extra tokens', async t => {
|
|
152
191
|
const {client, seed, requests} = await fixture(t, {ttl : HOUR, renew_status : 500});
|
|
153
192
|
const instance = client();
|
|
154
|
-
seed(instance);
|
|
193
|
+
await seed(instance);
|
|
155
194
|
await assert.rejects(instance.connect());
|
|
156
195
|
assert.deepEqual(requests.map(req => req.path), ['/v1/auth/token/renew-self']);
|
|
157
196
|
});
|
|
@@ -159,10 +198,10 @@ test('renewal server failures do not generate extra tokens', async t => {
|
|
|
159
198
|
test('an existing environment token is respected and cached', async t => {
|
|
160
199
|
const {client, seed, read, requests} = await fixture(t);
|
|
161
200
|
const instance = client();
|
|
162
|
-
seed(instance);
|
|
201
|
+
await seed(instance);
|
|
163
202
|
instance.VAULT_TOKEN = 'environment-token';
|
|
164
203
|
assert.equal(await instance.connect(), 'environment-token');
|
|
165
|
-
assert.equal(read(instance).token, 'environment-token');
|
|
204
|
+
assert.equal((await read(instance)).token, 'environment-token');
|
|
166
205
|
assert.equal(requests[0].token, 'environment-token');
|
|
167
206
|
assert.equal(requests.length, 1);
|
|
168
207
|
});
|
|
@@ -170,34 +209,35 @@ test('an existing environment token is respected and cached', async t => {
|
|
|
170
209
|
test('nonexpiring tokens are reused without renewal', async t => {
|
|
171
210
|
const {client, seed, read, requests} = await fixture(t, {ttl : 0});
|
|
172
211
|
const instance = client();
|
|
173
|
-
seed(instance, 0);
|
|
212
|
+
await seed(instance, 0);
|
|
174
213
|
assert.equal(await instance.connect(), 'cached-token');
|
|
175
|
-
assert.equal(read(instance).expires_at, null);
|
|
214
|
+
assert.equal((await read(instance)).expires_at, null);
|
|
176
215
|
assert.equal(requests.length, 0);
|
|
177
216
|
});
|
|
178
217
|
|
|
179
218
|
test('corrupt caches are replaced with a valid cache', async t => {
|
|
180
219
|
const {client, read, file} = await fixture(t);
|
|
181
|
-
fs.mkdirSync(path.dirname(file));
|
|
220
|
+
fs.mkdirSync(path.dirname(file), {recursive : true});
|
|
182
221
|
fs.writeFileSync(file, '{broken');
|
|
183
222
|
const instance = client();
|
|
184
223
|
assert.equal(await instance.connect(), 'fresh-token');
|
|
185
|
-
assert.equal(read(instance).token, 'fresh-token');
|
|
224
|
+
assert.equal((await read(instance)).token, 'fresh-token');
|
|
186
225
|
});
|
|
187
226
|
|
|
188
227
|
test('cache entries are isolated by Vault address and authentication configuration', async t => {
|
|
189
228
|
const {client, seed, read, file} = await fixture(t);
|
|
190
229
|
const first = client();
|
|
191
|
-
seed(first);
|
|
230
|
+
await seed(first);
|
|
192
231
|
const second = client();
|
|
193
232
|
second.rc.jwt_auth.role = 'other-role';
|
|
194
233
|
assert.equal(await second.connect(), 'fresh-token');
|
|
195
|
-
assert.
|
|
196
|
-
assert.equal(read(
|
|
234
|
+
assert.notEqual(first._token_cache_file(), second._token_cache_file());
|
|
235
|
+
assert.equal((await read(first)).token, 'cached-token');
|
|
236
|
+
assert.equal((await read(second)).token, 'fresh-token');
|
|
197
237
|
const third = client();
|
|
198
238
|
third.VAULT_ADDR = 'http://other-vault.test';
|
|
199
239
|
assert.notEqual(third._token_cache_key(), first._token_cache_key());
|
|
200
|
-
assert.equal(
|
|
240
|
+
assert.equal(fs.readdirSync(path.dirname(file)).length, 2);
|
|
201
241
|
assert.ok(!fs.readFileSync(file, 'utf8').includes('test-jwt'));
|
|
202
242
|
});
|
|
203
243
|
|
|
@@ -211,38 +251,41 @@ test('repeated connect calls reuse the established connection', async t => {
|
|
|
211
251
|
|
|
212
252
|
test('an unwritable cache does not prevent login', async t => {
|
|
213
253
|
const {client, file} = await fixture(t);
|
|
254
|
+
fs.mkdirSync(path.dirname(path.dirname(file)), {recursive : true});
|
|
214
255
|
fs.writeFileSync(path.dirname(file), 'not a directory');
|
|
215
256
|
assert.equal(await client().connect(), 'fresh-token');
|
|
216
257
|
});
|
|
217
258
|
|
|
218
|
-
test('
|
|
259
|
+
test('without an auth method inherited tokens and caches are ignored', async t => {
|
|
219
260
|
const {client, seed} = await fixture(t, {lookup_status : 403});
|
|
220
261
|
const instance = client();
|
|
221
262
|
instance.rc = {};
|
|
222
|
-
seed(instance);
|
|
263
|
+
await seed(instance);
|
|
223
264
|
instance.VAULT_TOKEN = 'uncached-token';
|
|
224
|
-
|
|
265
|
+
instance._read_token_cache = async () => { throw new Error('No auth: no cache lookup'); };
|
|
266
|
+
assert.equal(await instance.connect(), undefined);
|
|
267
|
+
assert.equal(instance.VAULT_TOKEN, undefined);
|
|
225
268
|
});
|
|
226
269
|
|
|
227
270
|
test('--renew ignores both environment and cached tokens and saves a new token', async t => {
|
|
228
271
|
const {client, seed, read, requests} = await fixture(t);
|
|
229
272
|
const instance = client();
|
|
230
|
-
seed(instance);
|
|
273
|
+
await seed(instance);
|
|
231
274
|
instance.VAULT_TOKEN = 'environment-token';
|
|
232
275
|
assert.equal(await instance.connect(true), 'fresh-token');
|
|
233
276
|
assert.deepEqual(requests.map(req => req.path), ['/v1/auth/jwt/login']);
|
|
234
|
-
assert.equal(read(instance).token, 'fresh-token');
|
|
235
|
-
assert.ok(read(instance).expires_at > Date.now() + 23 * HOUR * 1000);
|
|
277
|
+
assert.equal((await read(instance)).token, 'fresh-token');
|
|
278
|
+
assert.ok((await read(instance)).expires_at > Date.now() + 23 * HOUR * 1000);
|
|
236
279
|
});
|
|
237
280
|
|
|
238
281
|
test('--renew replaces the token of a previously connected instance', async t => {
|
|
239
282
|
const {client, seed, read, requests} = await fixture(t);
|
|
240
283
|
const instance = client();
|
|
241
|
-
seed(instance);
|
|
284
|
+
await seed(instance);
|
|
242
285
|
await instance.connect();
|
|
243
286
|
assert.equal(await instance.connect(true), 'fresh-token');
|
|
244
287
|
assert.deepEqual(requests.map(req => req.path), ['/v1/auth/jwt/login']);
|
|
245
|
-
assert.equal(read(instance).token, 'fresh-token');
|
|
288
|
+
assert.equal((await read(instance)).token, 'fresh-token');
|
|
246
289
|
});
|
|
247
290
|
|
|
248
291
|
test('each successive --renew call forces a new login', async t => {
|
|
@@ -253,11 +296,11 @@ test('each successive --renew call forces a new login', async t => {
|
|
|
253
296
|
assert.equal(requests.length, 2);
|
|
254
297
|
});
|
|
255
298
|
|
|
256
|
-
test('--renew without an authentication method
|
|
299
|
+
test('--renew without an authentication method remains a no-op', async t => {
|
|
257
300
|
const {client} = await fixture(t);
|
|
258
301
|
const instance = client();
|
|
259
302
|
instance.rc = {};
|
|
260
|
-
|
|
303
|
+
assert.equal(await instance.connect(true), undefined);
|
|
261
304
|
});
|
|
262
305
|
|
|
263
306
|
for(const flag of ['renew']) {
|
|
@@ -274,13 +317,13 @@ for(const flag of ['renew']) {
|
|
|
274
317
|
test(`real CLI ${command} --${flag} generates and caches a new token`, async t => {
|
|
275
318
|
const {client, seed, read, requests, file} = await fixture(t);
|
|
276
319
|
const instance = client();
|
|
277
|
-
seed(instance);
|
|
278
|
-
const home = path.dirname(path.dirname(file));
|
|
320
|
+
await seed(instance);
|
|
321
|
+
const home = path.dirname(path.dirname(path.dirname(file)));
|
|
279
322
|
const rc = path.join(home, '.vauthrc');
|
|
280
323
|
fs.writeFileSync(rc, JSON.stringify({vault_addr : instance.VAULT_ADDR, ...instance.rc}));
|
|
281
324
|
const options = {
|
|
282
325
|
cwd : home,
|
|
283
|
-
env : {...process.env, HOME : home, VAUTHRC : rc, VAULT_TOKEN : 'environment-token', SSH_AUTH_SOCK :
|
|
326
|
+
env : {...process.env, HOME : home, VAUTHRC : rc, VAULT_TOKEN : 'environment-token', SSH_AUTH_SOCK : process.env.SSH_AUTH_SOCK},
|
|
284
327
|
};
|
|
285
328
|
const cli = path.resolve(__dirname, '..', 'index.js');
|
|
286
329
|
const output = await exec(process.execPath, [cli, command, '--source', `--${flag}`], options);
|
|
@@ -292,7 +335,7 @@ for(const flag of ['renew']) {
|
|
|
292
335
|
assert.ok(!output.stderr.includes('environment-token'));
|
|
293
336
|
assert.deepEqual(requests.filter(req => !req.path.startsWith('/v1/identity/')).map(req => req.path), command === 'env'
|
|
294
337
|
? ['/v1/auth/jwt/login', '/v1/auth/token/lookup-self'] : ['/v1/auth/jwt/login']);
|
|
295
|
-
assert.equal(read(instance).token, 'fresh-token');
|
|
338
|
+
assert.equal((await read(instance)).token, 'fresh-token');
|
|
296
339
|
assert.equal(fs.statSync(file).mode & 0o777, 0o600);
|
|
297
340
|
requests.length = 0;
|
|
298
341
|
const reused = await exec(process.execPath, [cli, command, '--source'], {
|
|
@@ -309,8 +352,8 @@ for(const flag of ['renew']) {
|
|
|
309
352
|
test('installed venv forwards --renew through the real CLI', async t => {
|
|
310
353
|
const {client, seed, read, requests, file} = await fixture(t);
|
|
311
354
|
const instance = client();
|
|
312
|
-
seed(instance);
|
|
313
|
-
const home = path.dirname(path.dirname(file));
|
|
355
|
+
await seed(instance);
|
|
356
|
+
const home = path.dirname(path.dirname(path.dirname(file)));
|
|
314
357
|
const original_home = os.homedir;
|
|
315
358
|
instance._function_exists = async () => 1;
|
|
316
359
|
try {
|
|
@@ -328,7 +371,7 @@ test('installed venv forwards --renew through the real CLI', async t => {
|
|
|
328
371
|
fs.writeFileSync(rc, JSON.stringify({vault_addr : instance.VAULT_ADDR, ...instance.rc}));
|
|
329
372
|
const options = {
|
|
330
373
|
cwd : home,
|
|
331
|
-
env : {...process.env, HOME : home, VAUTHRC : rc, VAULT_TOKEN : '', SSH_AUTH_SOCK :
|
|
374
|
+
env : {...process.env, HOME : home, VAUTHRC : rc, VAULT_TOKEN : '', SSH_AUTH_SOCK : process.env.SSH_AUTH_SOCK, PATH : `${bin}:${process.env.PATH}`},
|
|
332
375
|
};
|
|
333
376
|
const initial = await exec('bash', ['-c', `${declaration}\nvenv; printf '%s' "$VAULT_TOKEN"`], options);
|
|
334
377
|
assert.equal(initial.stdout, 'cached-token');
|
|
@@ -341,7 +384,7 @@ test('installed venv forwards --renew through the real CLI', async t => {
|
|
|
341
384
|
assert.equal(output.stderr.split('\n').filter(line => line === `Vauth token: generated · ${instance.VAULT_ADDR} · 1d`).length, 1);
|
|
342
385
|
assert.ok(!output.stderr.includes('Vauth token: renewed'));
|
|
343
386
|
assert.ok(!output.stderr.includes('fresh-token'));
|
|
344
|
-
assert.equal(read(instance).token, 'fresh-token');
|
|
387
|
+
assert.equal((await read(instance)).token, 'fresh-token');
|
|
345
388
|
}
|
|
346
389
|
assert.equal(requests.filter(req => req.path.endsWith('/login')).length, 1);
|
|
347
390
|
assert.ok(!requests.some(req => req.path.endsWith('/renew-self')));
|
|
@@ -350,7 +393,7 @@ test('installed venv forwards --renew through the real CLI', async t => {
|
|
|
350
393
|
for(const answer of ['yes', 'no', '']) {
|
|
351
394
|
test(`install asks before overriding an existing function: ${JSON.stringify(answer)}`, async t => {
|
|
352
395
|
const {file} = await fixture(t);
|
|
353
|
-
const home = path.dirname(path.dirname(file));
|
|
396
|
+
const home = path.dirname(path.dirname(path.dirname(file)));
|
|
354
397
|
const bashrc = path.join(home, '.bashrc');
|
|
355
398
|
const original = '# before\nfunction venv() { source <(/usr/bin/env vauth env --source); }\n# after\n';
|
|
356
399
|
fs.writeFileSync(bashrc, original);
|
|
@@ -430,7 +473,7 @@ for(const options of [
|
|
|
430
473
|
test(`one final status per connection: ${JSON.stringify(options)}`, async t => {
|
|
431
474
|
const {client, seed} = await fixture(t, options);
|
|
432
475
|
const instance = client();
|
|
433
|
-
seed(instance);
|
|
476
|
+
await seed(instance);
|
|
434
477
|
const calls = [];
|
|
435
478
|
instance._token_status = (operation, metadata) => {
|
|
436
479
|
calls.push(...captureStatus(() => Vauth.prototype._token_status.call(instance, operation, metadata)));
|
|
@@ -445,7 +488,7 @@ for(const options of [
|
|
|
445
488
|
test('failed connections never announce successful token status', async t => {
|
|
446
489
|
const {client, seed} = await fixture(t, {lookup_status : 500});
|
|
447
490
|
const instance = client();
|
|
448
|
-
seed(instance);
|
|
491
|
+
await seed(instance);
|
|
449
492
|
instance.VAULT_TOKEN = 'uncached-token';
|
|
450
493
|
const calls = [];
|
|
451
494
|
instance._token_status = operation => calls.push(operation);
|
|
@@ -464,10 +507,10 @@ for(const [lease, remaining, renew] of [
|
|
|
464
507
|
test(`proportional threshold: lease=${lease}, remaining=${remaining}`, async t => {
|
|
465
508
|
const {client, seed, read, requests} = await fixture(t, {ttl : remaining});
|
|
466
509
|
const instance = client();
|
|
467
|
-
seed(instance, lease);
|
|
510
|
+
await seed(instance, lease);
|
|
468
511
|
await instance.connect();
|
|
469
512
|
assert.equal(requests.some(req => req.path.endsWith('/renew-self')), renew);
|
|
470
|
-
assert.equal(read(instance).lease_duration, renew ? 24 * HOUR : lease);
|
|
513
|
+
assert.equal((await read(instance)).lease_duration, renew ? 24 * HOUR : lease);
|
|
471
514
|
});
|
|
472
515
|
}
|
|
473
516
|
|
|
@@ -475,13 +518,13 @@ test('lookups never reset the original lease duration', async t => {
|
|
|
475
518
|
const options = {ttl : 20 * HOUR};
|
|
476
519
|
const {client, seed, read, requests, remaining} = await fixture(t, options);
|
|
477
520
|
const instance = client();
|
|
478
|
-
seed(instance, 24 * HOUR);
|
|
521
|
+
await seed(instance, 24 * HOUR);
|
|
479
522
|
await instance.connect();
|
|
480
|
-
assert.equal(read(instance).lease_duration, 24 * HOUR);
|
|
481
|
-
remaining(instance, 12 * HOUR);
|
|
523
|
+
assert.equal((await read(instance)).lease_duration, 24 * HOUR);
|
|
524
|
+
await remaining(instance, 12 * HOUR);
|
|
482
525
|
await client().connect();
|
|
483
|
-
assert.equal(read(instance).lease_duration, 24 * HOUR);
|
|
484
|
-
remaining(instance, 8 * HOUR);
|
|
526
|
+
assert.equal((await read(instance)).lease_duration, 24 * HOUR);
|
|
527
|
+
await remaining(instance, 8 * HOUR);
|
|
485
528
|
await client().connect();
|
|
486
529
|
assert.equal(requests.filter(req => req.path.endsWith('/renew-self')).length, 1);
|
|
487
530
|
});
|
|
@@ -490,10 +533,10 @@ test('renewal updates the lease baseline to the newly granted duration', async t
|
|
|
490
533
|
const options = {ttl : 20 * 60};
|
|
491
534
|
const {client, seed, read, requests, remaining} = await fixture(t, options);
|
|
492
535
|
const instance = client();
|
|
493
|
-
seed(instance, HOUR);
|
|
536
|
+
await seed(instance, HOUR);
|
|
494
537
|
await instance.connect();
|
|
495
|
-
assert.equal(read(instance).lease_duration, 24 * HOUR);
|
|
496
|
-
remaining(instance, 8 * HOUR);
|
|
538
|
+
assert.equal((await read(instance)).lease_duration, 24 * HOUR);
|
|
539
|
+
await remaining(instance, 8 * HOUR);
|
|
497
540
|
await client().connect();
|
|
498
541
|
assert.equal(requests.filter(req => req.path.endsWith('/renew-self')).length, 2);
|
|
499
542
|
});
|
|
@@ -501,7 +544,7 @@ test('renewal updates the lease baseline to the newly granted duration', async t
|
|
|
501
544
|
test('normal reuse never writes the token cache', async t => {
|
|
502
545
|
const {client, seed, file, requests} = await fixture(t);
|
|
503
546
|
const instance = client();
|
|
504
|
-
seed(instance);
|
|
547
|
+
await seed(instance);
|
|
505
548
|
instance.VAULT_TOKEN = 'cached-token';
|
|
506
549
|
const before = fs.readFileSync(file, 'utf8');
|
|
507
550
|
const stat = fs.statSync(file);
|
|
@@ -520,7 +563,7 @@ test('lookup returns the lease duration and preserves a cached renewal baseline'
|
|
|
520
563
|
const uncached = await instance._lookup_token('environment-token');
|
|
521
564
|
assert.equal(uncached.lease_duration, 24 * HOUR);
|
|
522
565
|
assert.equal(uncached.ttl, HOUR);
|
|
523
|
-
seed(instance, 12 * HOUR);
|
|
566
|
+
await seed(instance, 12 * HOUR);
|
|
524
567
|
const cached = await instance._lookup_token('cached-token');
|
|
525
568
|
assert.equal(cached.lease_duration, 12 * HOUR);
|
|
526
569
|
assert.equal(requests.length, 2);
|
|
@@ -539,7 +582,7 @@ test('token_cache false skips all cache filesystem access', async t => {
|
|
|
539
582
|
test('token_cache false ignores existing tokens on disk without deleting them', async t => {
|
|
540
583
|
const {client, seed, requests, file} = await fixture(t);
|
|
541
584
|
const instance = client();
|
|
542
|
-
seed(instance);
|
|
585
|
+
await seed(instance);
|
|
543
586
|
const before = fs.readFileSync(file, 'utf8');
|
|
544
587
|
instance.rc.token_cache = false;
|
|
545
588
|
assert.equal(await instance.connect(), 'fresh-token');
|
|
@@ -563,14 +606,14 @@ test('token_cache false still respects an environment token without persisting i
|
|
|
563
606
|
test('real CLI reads token_cache false from .vauthrc', async t => {
|
|
564
607
|
const {client, file, requests} = await fixture(t);
|
|
565
608
|
const instance = client();
|
|
566
|
-
const home = path.dirname(path.dirname(file));
|
|
609
|
+
const home = path.dirname(path.dirname(path.dirname(file)));
|
|
567
610
|
const rc = path.join(home, '.vauthrc');
|
|
568
611
|
fs.writeFileSync(rc, JSON.stringify({vault_addr : instance.VAULT_ADDR, ...instance.rc, token_cache : false}));
|
|
569
612
|
const cli = path.resolve(__dirname, '..', 'index.js');
|
|
570
613
|
for(let i = 0; i < 2; i++) {
|
|
571
614
|
const output = await exec(process.execPath, [cli, 'login', '--source'], {
|
|
572
615
|
cwd : home,
|
|
573
|
-
env : {...process.env, HOME : home, VAUTHRC : rc, VAULT_TOKEN : '', SSH_AUTH_SOCK :
|
|
616
|
+
env : {...process.env, HOME : home, VAUTHRC : rc, VAULT_TOKEN : '', SSH_AUTH_SOCK : process.env.SSH_AUTH_SOCK},
|
|
574
617
|
});
|
|
575
618
|
assert.ok(output.stdout.includes("export VAULT_TOKEN='fresh-token'"));
|
|
576
619
|
assert.ok(output.stderr.includes('Vauth token: generated'));
|
|
@@ -578,3 +621,174 @@ test('real CLI reads token_cache false from .vauthrc', async t => {
|
|
|
578
621
|
assert.equal(fs.existsSync(file), false);
|
|
579
622
|
assert.equal(requests.filter(req => req.path.endsWith('/login')).length, 2);
|
|
580
623
|
});
|
|
624
|
+
|
|
625
|
+
test('disk cache contains only armor, interoperable with ssh-agent-crypt CLI', async t => {
|
|
626
|
+
const {client, file} = await fixture(t);
|
|
627
|
+
const instance = client();
|
|
628
|
+
await instance.connect();
|
|
629
|
+
const armor = fs.readFileSync(file, 'utf8');
|
|
630
|
+
assert.match(armor, /^ssh-agent-crypt:v2:/);
|
|
631
|
+
assert.ok(!armor.includes('fresh-token'));
|
|
632
|
+
assert.ok(!armor.includes('lease_duration'));
|
|
633
|
+
assert.ok(!armor.includes('test-jwt'));
|
|
634
|
+
const cli = require.resolve('ssh-agent-crypt/bin/ssh-agent-crypt');
|
|
635
|
+
const decoded = spawnSync('bash', [cli, '-decrypt'], {input : armor, encoding : 'utf8'});
|
|
636
|
+
assert.equal(decoded.status, 0);
|
|
637
|
+
assert.equal(JSON.parse(decoded.stdout).token, 'fresh-token');
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
test('a plaintext cache is not accepted as a token cache', async t => {
|
|
641
|
+
const {client, file, requests} = await fixture(t);
|
|
642
|
+
const instance = client();
|
|
643
|
+
fs.mkdirSync(path.dirname(file), {recursive : true});
|
|
644
|
+
fs.writeFileSync(file, JSON.stringify({token : 'plaintext-token', lease_duration : 86400, expires_at : Date.now() + 86400000}));
|
|
645
|
+
assert.equal(await instance.connect(), 'fresh-token');
|
|
646
|
+
assert.deepEqual(requests.map(req => req.path), ['/v1/auth/jwt/login']);
|
|
647
|
+
assert.match(fs.readFileSync(file, 'utf8'), /^ssh-agent-crypt:v2:/);
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
test('without an SSH agent cache is skipped silently', async t => {
|
|
651
|
+
const {client, file} = await fixture(t);
|
|
652
|
+
const instance = client();
|
|
653
|
+
instance._token_cache_file = () => { throw new Error('No agent: no disk access'); };
|
|
654
|
+
const socket = process.env.SSH_AUTH_SOCK;
|
|
655
|
+
const original = console.error;
|
|
656
|
+
const messages = [];
|
|
657
|
+
try {
|
|
658
|
+
delete process.env.SSH_AUTH_SOCK;
|
|
659
|
+
instance.rc.token_cache = false;
|
|
660
|
+
console.error = (...args) => messages.push(format(...args));
|
|
661
|
+
assert.equal(await instance.connect(), 'fresh-token');
|
|
662
|
+
} finally {
|
|
663
|
+
process.env.SSH_AUTH_SOCK = socket;
|
|
664
|
+
console.error = original;
|
|
665
|
+
}
|
|
666
|
+
assert.equal(fs.existsSync(file), false);
|
|
667
|
+
assert.equal(fs.existsSync(file + '.next'), false);
|
|
668
|
+
assert.ok(!messages.some(message => message.includes('token not persisted')));
|
|
669
|
+
assert.ok(!messages.join('\n').includes('fresh-token'));
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
test('without an SSH agent existing armor is left untouched', async t => {
|
|
673
|
+
const {client, seed, file} = await fixture(t);
|
|
674
|
+
const instance = client();
|
|
675
|
+
await seed(instance);
|
|
676
|
+
const before = fs.readFileSync(file, 'utf8');
|
|
677
|
+
const socket = process.env.SSH_AUTH_SOCK;
|
|
678
|
+
try {
|
|
679
|
+
delete process.env.SSH_AUTH_SOCK;
|
|
680
|
+
instance.rc.token_cache = false;
|
|
681
|
+
await instance._save_cached_token('replacement-token', {lease_duration : 86400, renewable : true});
|
|
682
|
+
} finally {
|
|
683
|
+
process.env.SSH_AUTH_SOCK = socket;
|
|
684
|
+
}
|
|
685
|
+
assert.equal(fs.readFileSync(file, 'utf8'), before);
|
|
686
|
+
assert.equal(fs.existsSync(file + '.next'), false);
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
test('constructor disables token caching when no SSH agent is configured', async t => {
|
|
691
|
+
const {client, file} = await fixture(t);
|
|
692
|
+
const instance = client();
|
|
693
|
+
const home = path.dirname(path.dirname(path.dirname(file)));
|
|
694
|
+
const rc = path.join(home, '.vauthrc');
|
|
695
|
+
fs.writeFileSync(rc, JSON.stringify({vault_addr : instance.VAULT_ADDR, ...instance.rc, token_cache : true}));
|
|
696
|
+
const module_path = path.resolve(__dirname, '..', 'index.js');
|
|
697
|
+
const script = `const Vauth = require(${JSON.stringify(module_path)});
|
|
698
|
+
const instance = new Vauth();
|
|
699
|
+
process.stdout.write(String(instance.rc.token_cache));
|
|
700
|
+
instance.login().catch(() => process.exit(1));`;
|
|
701
|
+
const output = await exec(process.execPath, ['-e', script], {
|
|
702
|
+
cwd : home,
|
|
703
|
+
env : {...process.env, HOME : home, VAUTHRC : rc, VAULT_TOKEN : '', SSH_AUTH_SOCK : ''},
|
|
704
|
+
});
|
|
705
|
+
assert.equal(output.stdout.trim(), 'false');
|
|
706
|
+
assert.ok(!output.stderr.includes('token not persisted'));
|
|
707
|
+
assert.equal(fs.existsSync(file), false);
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
for(const selected of ['identity', 'mail', 'default']) {
|
|
712
|
+
test(`cache encryption selects ${selected}; decryption uses embedded fingerprint`, async t => {
|
|
713
|
+
const {client, file} = await fixture(t);
|
|
714
|
+
const instance = client();
|
|
715
|
+
const directory = path.dirname(path.dirname(path.dirname(file)));
|
|
716
|
+
const key = path.join(directory, 'selected-key');
|
|
717
|
+
const comment = 'selected-cache@test';
|
|
718
|
+
assert.equal(spawnSync('ssh-keygen', ['-q', '-t', 'ed25519', '-N', '', '-C', comment, '-f', key]).status, 0);
|
|
719
|
+
assert.equal(spawnSync('ssh-add', [key]).status, 0);
|
|
720
|
+
try {
|
|
721
|
+
if(selected === 'identity') {
|
|
722
|
+
process.env.VAUTH_USER_IDENTITY = comment;
|
|
723
|
+
process.env.VAUTH_USER_MAIL = 'missing-key@test';
|
|
724
|
+
} else if(selected === 'mail') {
|
|
725
|
+
process.env.VAUTH_USER_MAIL = comment;
|
|
726
|
+
}
|
|
727
|
+
assert.equal(await instance.connect(), 'fresh-token');
|
|
728
|
+
const fingerprint = spawnSync('ssh-keygen', ['-lf', key + '.pub'], {encoding : 'utf8'}).stdout.split(/\s+/)[1];
|
|
729
|
+
const armor = fs.readFileSync(file, 'utf8');
|
|
730
|
+
assert.equal(armor.startsWith(`ssh-agent-crypt:v2:${fingerprint}.`), selected !== 'default');
|
|
731
|
+
delete process.env.VAUTH_USER_IDENTITY;
|
|
732
|
+
delete process.env.VAUTH_USER_MAIL;
|
|
733
|
+
assert.equal(await client().connect(), 'fresh-token');
|
|
734
|
+
} finally {
|
|
735
|
+
spawnSync('ssh-add', ['-d', key]);
|
|
736
|
+
delete process.env.VAUTH_USER_IDENTITY;
|
|
737
|
+
delete process.env.VAUTH_USER_MAIL;
|
|
738
|
+
}
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
for(const explicit of [false, true]) {
|
|
743
|
+
test(`.creds uses the JS module without a CLI (explicit identity=${explicit})`, async t => {
|
|
744
|
+
const {client, file} = await fixture(t);
|
|
745
|
+
const instance = client();
|
|
746
|
+
const credentials = path.join(path.dirname(path.dirname(path.dirname(file))), '.creds');
|
|
747
|
+
const identity = spawnSync('ssh-add', ['-l'], {encoding : 'utf8'}).stdout.split(/\s+/)[1];
|
|
748
|
+
fs.writeFileSync(credentials, await encrypt(JSON.stringify({TEST_SECRET : 'module-secret'})));
|
|
749
|
+
instance.rc['ssh-agent-crypt'] = {path : credentials};
|
|
750
|
+
if(explicit)
|
|
751
|
+
instance.rc['ssh-agent-crypt'].identity = identity;
|
|
752
|
+
instance.rc.env = {map : {EXPORTED_SECRET : '$${secrets.TEST_SECRET}'}};
|
|
753
|
+
instance._vault_get_profile = async () => ({profile : {}, database : {}});
|
|
754
|
+
const previousPath = process.env.PATH;
|
|
755
|
+
try {
|
|
756
|
+
process.env.PATH = path.dirname(credentials);
|
|
757
|
+
const env = await instance.env();
|
|
758
|
+
assert.equal(env.EXPORTED_SECRET, 'module-secret');
|
|
759
|
+
} finally {
|
|
760
|
+
process.env.PATH = previousPath;
|
|
761
|
+
}
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
test('.creds decryption failures reject instead of exiting successfully', async t => {
|
|
766
|
+
const {client, file} = await fixture(t);
|
|
767
|
+
const instance = client();
|
|
768
|
+
const credentials = path.join(path.dirname(path.dirname(path.dirname(file))), '.creds');
|
|
769
|
+
fs.writeFileSync(credentials, 'not an armored payload');
|
|
770
|
+
instance.rc['ssh-agent-crypt'] = {path : credentials};
|
|
771
|
+
instance._vault_get_profile = async () => ({profile : {}, database : {}});
|
|
772
|
+
await assert.rejects(instance.env(), /Unsupported format header/);
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
for(const inherited of ['', 'inherited-token']) {
|
|
776
|
+
test(`local .creds environment works offline with inherited token=${!!inherited}`, async t => {
|
|
777
|
+
const {client, file, requests} = await fixture(t, {lookup_status : 503});
|
|
778
|
+
const instance = client();
|
|
779
|
+
const credentials = path.join(path.dirname(path.dirname(path.dirname(file))), '.creds');
|
|
780
|
+
fs.writeFileSync(credentials, await encrypt(JSON.stringify({ARM_CLIENT_SECRET : 'local-secret', VAULT_TOKEN : 'local-token'})));
|
|
781
|
+
instance.rc = {
|
|
782
|
+
'ssh-agent-crypt' : {path : credentials},
|
|
783
|
+
env : {map : {ARM_CLIENT_SECRET : '$${secrets.ARM_CLIENT_SECRET}', VAULT_TOKEN : '$${secrets.VAULT_TOKEN}'}},
|
|
784
|
+
};
|
|
785
|
+
instance.VAULT_TOKEN = inherited;
|
|
786
|
+
instance._lookup_token = async () => { throw new Error('Local environment must not contact Vault'); };
|
|
787
|
+
instance._read_token_cache = async () => { throw new Error('Local environment must not read the token cache'); };
|
|
788
|
+
const env = await instance.env();
|
|
789
|
+
assert.equal(env.ARM_CLIENT_SECRET, 'local-secret');
|
|
790
|
+
assert.equal(env.VAULT_TOKEN, 'local-token');
|
|
791
|
+
assert.equal(env.VAULT_ADDR, instance.VAULT_ADDR);
|
|
792
|
+
assert.deepEqual(requests, []);
|
|
793
|
+
});
|
|
794
|
+
}
|