// Row 199 (#1697, design D1, D3a, D4, section 6, T1 and T5): the per-agent credential store under // /secrets/agents///. Fake tokens in a scratch root only. import test from 'node:test'; import assert from 'node:assert/strict'; import { chmodSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, symlinkSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { Credentials } from '../src/credentials.mjs'; const TOKEN = 'fake_store_token_0123456789'; const ROTATED = 'fake_rotated_token_9876543210'; const AGENT = { business: 'acme', role: 'coder' }; function setup(t) { const root = mkdtempSync(join(tmpdir(), 'bus-store-')); t.after(() => rmSync(root, { recursive: true, force: true })); const storeRoot = join(root, 'secrets', 'agents'); mkdirSync(storeRoot, { recursive: true, mode: 0o700 }); chmodSync(join(root, 'secrets'), 0o700); const file = join(storeRoot, 'acme', 'coder', 'gitea'); const references = { 'acme/coder': { gitea: { service: 'gitea', store: true, file, rotateBy: '2099-01-01' } } }; const open = (extra = {}) => new Credentials({ references, storeRoot, repoRoots: [], dataRoot: '/nonexistent-data', ...extra }); const state = (c) => c.status().find((e) => e.instance === 'acme/coder' && e.service === 'gitea').state; const read = (c, session = AGENT) => c.useFor(session, 'gitea', async (token) => ({ same: token === TOKEN, rotated: token === ROTATED })); return { root, storeRoot, file, references, open, state, read }; } test('T1: a store reference with no file yet starts, and the agent is not-issued until issue', async (t) => { const s = setup(t); const c = s.open(); assert.equal(s.state(c), 'not-issued'); await assert.rejects(s.read(c), /credential-unavailable/); const status = c.issue('acme/coder', 'gitea', TOKEN, '2099-01-01'); assert.equal(status.state, 'valid'); assert.deepEqual(await s.read(c), { same: true, rotated: false }); assert.equal(statSync(s.file).mode & 0o777, 0o600); for (const dir of [join(s.storeRoot, 'acme'), join(s.storeRoot, 'acme', 'coder')]) assert.equal(statSync(dir).mode & 0o777, 0o700); assert.deepEqual(readdirSync(join(s.storeRoot, 'acme', 'coder')), ['gitea']); // A restart reads the issued file. assert.deepEqual(await s.read(s.open()), { same: true, rotated: false }); }); test('T1: a store path the reference does not own, or no store root, refuses the reference', (t) => { const s = setup(t); const other = join(s.storeRoot, 'acme', 'pm', 'gitea'); assert.throws(() => s.open({ references: { 'acme/coder': { gitea: { store: true, file: other, rotateBy: '2099-01-01' } } } }), /credential-reference/); assert.throws(() => new Credentials({ references: s.references, repoRoots: [], dataRoot: '/nonexistent-data' }), /credential-reference/); assert.throws(() => s.open({ references: { 'acme/@tester': { gitea: { store: true, file: join(s.storeRoot, 'acme', '@tester', 'gitea'), rotateBy: '2099-01-01' } } } }), /credential-reference/); }); test('T1: a wrong mode on the store root, a business dir, an agent dir or the file refuses start', (t) => { const s = setup(t); s.open().issue('acme/coder', 'gitea', TOKEN, '2099-01-01'); for (const [path, bad, good] of [ [s.storeRoot, 0o750, 0o700], [join(s.storeRoot, 'acme'), 0o705, 0o700], [join(s.storeRoot, 'acme', 'coder'), 0o770, 0o700], [s.file, 0o640, 0o600], ]) { chmodSync(path, bad); assert.throws(() => s.open(), /credential-file/, path); chmodSync(path, good); } s.open(); }); test('T1: a missing store root refuses start; a missing {file} reference still refuses start', (t) => { const s = setup(t); rmSync(s.storeRoot, { recursive: true }); assert.throws(() => s.open(), /credential-file/); assert.throws(() => s.open({ references: { coder: { gitea: { file: join(s.root, 'absent'), rotateBy: '2099-01-01' } } } }), /credential-file/); }); test('T1: a store file that is a directory or a symlink refuses start', (t) => { const s = setup(t); mkdirSync(join(s.storeRoot, 'acme', 'coder'), { recursive: true, mode: 0o700 }); chmodSync(join(s.storeRoot, 'acme'), 0o700); mkdirSync(s.file, { mode: 0o700 }); assert.throws(() => s.open(), /credential-file/); rmSync(s.file, { recursive: true }); const target = join(s.root, 'target'); writeFileSync(target, TOKEN + '\n', { mode: 0o600 }); symlinkSync(target, s.file); assert.throws(() => s.open(), /credential-file/); }); test('T2: useFor keys from the session only', async (t) => { const s = setup(t); const c = s.open(); c.issue('acme/coder', 'gitea', TOKEN, '2099-01-01'); await assert.rejects(s.read(c, { business: 'acme', role: 'pm' }), /credential-unavailable/); await assert.rejects(s.read(c, { business: 'acme', role: '@tester' }), /credential-not-yours/); await assert.rejects(s.read(c, null), /credential-not-yours/); await assert.rejects(s.read(c, { business: 'acme' }), /credential-not-yours/); }); test('T5: issue, rotate, revoke and delete are live with no restart, and revoke holds across one', async (t) => { const s = setup(t); const c = s.open(); let committed = 0; c.issue('acme/coder', 'gitea', TOKEN, '2099-01-01', () => committed++); assert.equal(committed, 1); // Rotate is a second issue: the new token is used at once, with no tmp file left. c.issue('acme/coder', 'gitea', ROTATED + '\n', '2099-06-01'); assert.deepEqual(await s.read(c), { same: false, rotated: true }); assert.equal(c.status()[0].date, '2099-06-01'); assert.deepEqual(readdirSync(join(s.storeRoot, 'acme', 'coder')), ['gitea']); // Revoke: memory first, then the file. assert.deepEqual(c.revoke('acme/coder', 'gitea'), { file: true }); assert.equal(s.state(c), 'revoked'); assert.equal(existsSync(s.file), false); await assert.rejects(s.read(c), /credential-unavailable/); const restarted = s.open(); await assert.rejects(s.read(restarted), /credential-unavailable/); assert.equal(s.state(restarted), 'not-issued'); // Delete removes the agent's dir. c.issue('acme/coder', 'gitea', TOKEN, '2099-01-01'); assert.deepEqual(c.remove('acme/coder'), { services: ['gitea'] }); assert.equal(existsSync(join(s.storeRoot, 'acme', 'coder')), false); await assert.rejects(s.read(c), /credential-unavailable/); }); test('T5: a failed commit leaves the token on disk but not in memory, and a bad token or date writes nothing', async (t) => { const s = setup(t); const c = s.open(); assert.throws(() => c.issue('acme/coder', 'gitea', 'short', '2099-01-01'), /credential-format/); assert.throws(() => c.issue('acme/coder', 'gitea', TOKEN, '2099-13-01'), /credential-date/); assert.throws(() => c.issue('acme/pm', 'gitea', TOKEN, '2099-01-01'), /credential-unknown/); assert.equal(existsSync(s.file), false); assert.throws(() => c.issue('acme/coder', 'gitea', TOKEN, '2099-01-01', () => { throw new Error('commit failed'); }), /commit failed/); assert.equal(existsSync(s.file), true); assert.equal(s.state(c), 'not-issued'); await assert.rejects(s.read(c), /credential-unavailable/); }); test('T5: revoke drops the token from memory before the unlink, so a failed unlink still leaves it revoked', async (t) => { const s = setup(t); const c = s.open(); c.issue('acme/coder', 'gitea', TOKEN, '2099-01-01'); assert.deepEqual(await s.read(c), { same: true, rotated: false }); // An agent dir the bus can't write: the unlink fails with EACCES. const agentDir = join(s.storeRoot, 'acme', 'coder'); chmodSync(agentDir, 0o500); try { assert.throws(() => c.revoke('acme/coder', 'gitea'), /credential-file/); } finally { chmodSync(agentDir, 0o700); } assert.equal(existsSync(s.file), true); assert.equal(s.state(c), 'revoked'); await assert.rejects(s.read(c), /credential-unavailable/); }); test('T5: revoke of a {file} reference forgets the token and never unlinks the file', async (t) => { const s = setup(t); const path = join(s.root, 'outside-token'); writeFileSync(path, TOKEN + '\n', { mode: 0o600 }); const c = s.open({ references: { 'acme/coder': { gitea: { file: path, rotateBy: '2099-01-01' } } } }); assert.deepEqual(await s.read(c), { same: true, rotated: false }); assert.deepEqual(c.revoke('acme/coder', 'gitea'), { file: false }); assert.equal(existsSync(path), true); await assert.rejects(s.read(c), /credential-unavailable/); }); test('T5: delete refuses an agent dir holding anything but tokens', (t) => { const s = setup(t); const c = s.open(); c.issue('acme/coder', 'gitea', TOKEN, '2099-01-01'); writeFileSync(join(s.storeRoot, 'acme', 'coder', 'notes'), 'x', { mode: 0o600 }); assert.throws(() => c.remove('acme/coder'), /credential-file/); assert.equal(existsSync(join(s.storeRoot, 'acme', 'coder', 'notes')), true); }); // Design D5: the name-keyed use() is the host's own, for @sync and the tracker's start check of each // role's token. Every other credential use goes through useFor, keyed from the broker's session. A // new use( call site outside this list fails here. test('D5: every credentials use( call site is on the allowlist', () => { const packages = new URL('../../', import.meta.url); const allowed = new Map([ ['bus/src/credentials.mjs', 1], // useFor's own call ['tasks/src/adapter.mjs', 1], // the host's use(instance) for @sync and probe ]); const found = new Map(); const walk = (dir, rel) => { for (const name of readdirSync(dir)) { if (name === 'node_modules' || name === 'tests') continue; const path = join(dir, name); if (statSync(path).isDirectory()) walk(path, `${rel}${name}/`); else if (name.endsWith('.mjs') || name.endsWith('.js')) { const code = readFileSync(path, 'utf8').split('\n').filter((line) => !/^\s*\/\//.test(line)).join('\n'); const n = (code.match(/(?:credentials|this)\.use\(/g) ?? []).length; if (n) found.set(`${rel}${name}`, n); } } }; for (const pkg of readdirSync(packages)) if (statSync(new URL(`${pkg}/src`, packages), { throwIfNoEntry: false })?.isDirectory()) walk(new URL(`${pkg}/src`, packages).pathname, `${pkg}/src/`); assert.deepEqual(Object.fromEntries([...found].sort()), Object.fromEntries([...allowed].sort())); });