Blobs of Gizmoduck's r50485384e9on origin/gizmoduck/row210-r5 (basead95c888c), the 23 manifest paths, manifest 688d38b9. Duckworth approve round 2 (rev 2592) and round 3 (comment 29851, rev 2614). Round 3 is a rebase-only round over row 237 (d7d943199): broker.mjs is a clean 3-way merge (610af0c4), the 210 hunks sit away from 237's insert, and range-diff against r4c43008749is all equal. Mutants 52 of 53 killed on the merged tree. M22b is equivalent (rev 2584); its coverage moves to row 235. Preflight singles: seat sandbox 10/0, bus project-runtime 3/0. Gate onad95c888cplus these paths: 15 package suites 2796 pass 0 fail, 10 scripts/test-*.sh suites 429 pass 0 fail (plus 156 node tests in the queue suite). Refactor moved to9d165be8csince, two briefs touching no manifest path. No live change: the stack runs from copy4429a104funtil the 10-24 re-pin window.
143 lines
9.7 KiB
JavaScript
143 lines
9.7 KiB
JavaScript
// Row 199 (#1697, design section 6, T4, T5): `mosaic credentials`. issue and rotate read the token on
|
|
// stdin and pass it once to credentials.issue; nothing printed carries it. --tester takes status and
|
|
// revoke only. A fake transport records the calls; no bus runs here.
|
|
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { spawnSync } from "node:child_process";
|
|
import { join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { main } from "../src/cli.mjs";
|
|
import { io, tmp } from "./helpers.mjs";
|
|
|
|
const TOKEN = "fake_cli_token_0123456789";
|
|
const MOSAIC = fileURLToPath(new URL("../../../scripts/mosaic", import.meta.url));
|
|
const ROWS = [
|
|
{ service: "gitea", account: null, role: "coder", dateKind: "rotateBy", date: "2099-01-01", state: "valid" },
|
|
{ service: "vikunja", account: "bot", role: "coder", dateKind: "expires", date: "2099-01-01", state: "not-issued" },
|
|
{ service: "gitea", account: null, role: "reviewer", dateKind: "rotateBy", date: "2099-01-01", state: "revoked" },
|
|
];
|
|
|
|
function deps(calls, rows = ROWS) {
|
|
const answer = (who) => (business) => async (verb, args) => {
|
|
calls.push({ who, business, verb, args });
|
|
if (verb === "credentials") return rows;
|
|
if (verb === "credentials.issue") return { agent: args.agent, verb: "issue", service: args.service, date: args.date, state: "valid" };
|
|
return { agent: args.agent, verb: verb.slice(12), services: [] };
|
|
};
|
|
return { system: { dataRoot: "/nonexistent" }, transport: answer("human"), testerTransport: answer("tester") };
|
|
}
|
|
const withStdin = (x, text) => {
|
|
x.stdin.end(text);
|
|
return x;
|
|
};
|
|
|
|
test("row 199: issue and rotate send the stdin token once to credentials.issue and print no value", async () => {
|
|
const calls = [];
|
|
const d = deps(calls);
|
|
const x = withStdin(io(), TOKEN + "\n");
|
|
await main(["credentials", "issue", "coder", "gitea", "--rotate-by", "2099-01-01", "--business", "acme"], x, d);
|
|
assert.deepEqual(calls, [{ who: "human", business: "acme", verb: "credentials.issue", args: { agent: "coder", service: "gitea", token: TOKEN + "\n", date: "2099-01-01" } }]);
|
|
assert.equal(x.out.text.includes(TOKEN), false);
|
|
assert.equal(JSON.parse(x.out.text).state, "valid");
|
|
// rotate checks the entry is issued before it reads stdin.
|
|
calls.length = 0;
|
|
await main(["credentials", "rotate", "coder", "gitea", "--rotate-by", "2099-06-01", "--business", "acme"], withStdin(io(), TOKEN), d);
|
|
assert.deepEqual(calls.map((c) => c.verb), ["credentials", "credentials.issue"]);
|
|
calls.length = 0;
|
|
await assert.rejects(main(["credentials", "rotate", "coder", "vikunja", "--expires", "2099-06-01", "--business", "acme"], withStdin(io(), TOKEN), d), /no issued vikunja credential to rotate; use issue/);
|
|
assert.deepEqual(calls.map((c) => c.verb), ["credentials"]);
|
|
// No token on stdin, the date flag of the other kind, a token in argv: refused before any write.
|
|
calls.length = 0;
|
|
await assert.rejects(main(["credentials", "issue", "coder", "gitea", "--rotate-by", "2099-01-01", "--business", "acme"], withStdin(io(), "\n"), d), /no token on stdin/);
|
|
await assert.rejects(main(["credentials", "issue", "coder", "gitea", "--expires", "2099-01-01", "--business", "acme"], withStdin(io(), TOKEN), d), /mosaic credentials/);
|
|
await assert.rejects(main(["credentials", "issue", "coder", "gitea", TOKEN, "--rotate-by", "2099-01-01", "--business", "acme"], withStdin(io(), TOKEN), d), /mosaic credentials/);
|
|
await assert.rejects(main(["credentials", "issue", "coder", "gitea", "--rotate-by", "2099-01-01", "--business", "acme"], withStdin(io(), "x".repeat(17000)), d), /too long/);
|
|
assert.equal(calls.filter((c) => c.verb === "credentials.issue").length, 0);
|
|
// From inside an agent run, the human's verbs refuse before any call.
|
|
await assert.rejects(main(["credentials", "issue", "coder", "gitea", "--rotate-by", "2099-01-01", "--business", "acme"], withStdin(io({ CLAUDECODE: "1" }), TOKEN), d), /outside any agent run/);
|
|
});
|
|
|
|
// gizmoduck-0050: a gitea key past its rotate-by date is rotation-due, and rotate is how a human
|
|
// clears it. Every state the bus's status() gives a held token rotates; the other two refuse.
|
|
test("row 199: rotate goes ahead on each held state, rotation-due included, and refuses not-issued, revoked and an unknown entry", async () => {
|
|
const states = { valid: true, "rotation-due": true, expiring: true, expired: true, "not-issued": false, revoked: false };
|
|
for (const [state, held] of Object.entries(states)) {
|
|
const calls = [];
|
|
const d = deps(calls, [{ service: "gitea", account: null, role: "coder", dateKind: "rotateBy", date: "2020-01-01", state }]);
|
|
const run = main(["credentials", "rotate", "coder", "gitea", "--rotate-by", "2099-06-01", "--business", "acme"], withStdin(io(), TOKEN), d);
|
|
if (held) await run;
|
|
else await assert.rejects(run, /no issued gitea credential to rotate; use issue/);
|
|
assert.deepEqual(calls.map((c) => c.verb), held ? ["credentials", "credentials.issue"] : ["credentials"], state);
|
|
}
|
|
const calls = [];
|
|
await assert.rejects(main(["credentials", "rotate", "tester", "gitea", "--rotate-by", "2099-06-01", "--business", "acme"], withStdin(io(), TOKEN), deps(calls)), /no issued gitea credential/);
|
|
assert.deepEqual(calls.map((c) => c.verb), ["credentials"]);
|
|
});
|
|
|
|
test("row 199: status, revoke and delete; the tester runs status and revoke only", async () => {
|
|
const calls = [];
|
|
const d = deps(calls);
|
|
const x = io();
|
|
await main(["credentials", "status", "coder", "--business", "acme"], x, d);
|
|
assert.equal(x.out.text, "coder\tgitea\tvalid\trotateBy 2099-01-01\ncoder\tvikunja\tnot-issued\texpires 2099-01-01\n");
|
|
const j = io();
|
|
await main(["credentials", "--json", "--business", "acme", "--tester"], j, d);
|
|
assert.equal(JSON.parse(j.out.text).length, 3);
|
|
await main(["credentials", "revoke", "coder", "gitea", "--reason", "fired", "--business", "acme", "--tester"], io(), d);
|
|
await main(["credentials", "revoke", "coder", "--reason", "fired", "--business", "acme"], io(), d);
|
|
await main(["credentials", "delete", "coder", "--reason", "fired", "--business", "acme"], io(), d);
|
|
assert.deepEqual(calls.map((c) => [c.who, c.verb, c.args]), [
|
|
["human", "credentials", {}],
|
|
["tester", "credentials", {}],
|
|
["tester", "credentials.revoke", { agent: "coder", service: "gitea", reason: "fired" }],
|
|
["human", "credentials.revoke", { agent: "coder", reason: "fired" }],
|
|
["human", "credentials.delete", { agent: "coder", reason: "fired" }],
|
|
]);
|
|
calls.length = 0;
|
|
for (const args of [
|
|
["issue", "coder", "gitea", "--rotate-by", "2099-01-01", "--tester"],
|
|
["rotate", "coder", "gitea", "--rotate-by", "2099-01-01", "--tester"],
|
|
["delete", "coder", "--reason", "x", "--tester"],
|
|
]) await assert.rejects(main(["credentials", ...args, "--business", "acme"], withStdin(io(), TOKEN), d), /--tester takes only status and revoke/);
|
|
for (const args of [["revoke", "coder"], ["delete", "coder"], ["delete", "coder", "gitea", "--reason", "x"], ["revoke", "coder", "model", "--reason", "x"], ["purge", "coder"]])
|
|
await assert.rejects(main(["credentials", ...args, "--business", "acme"], io(), d), /mosaic credentials/, args.join(" "));
|
|
assert.equal(calls.length, 0);
|
|
});
|
|
|
|
// Row 210 (#1707, design T23, D9): a host key issue prints the public line and the authorized_keys
|
|
// suggestion for ops-01, and never the private key it read on stdin.
|
|
test("row 210: issue of a host.<id> kind prints public material only", async () => {
|
|
const body = "b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW";
|
|
const pem = `-----BEGIN OPENSSH PRIVATE KEY-----\n${body}\n-----END OPENSSH PRIVATE KEY-----\n`;
|
|
const publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOv6Rq3Mvo9w2j3eYvYbqX0J1pJ0x9m8Zr0w1kJ8p0Qn";
|
|
const fingerprint = "SHA256:abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG";
|
|
const calls = [];
|
|
const d = deps(calls);
|
|
const human = d.transport;
|
|
d.transport = (business) => async (verb, args) =>
|
|
verb === "credentials.issue" ? (calls.push({ verb, args }), { agent: args.agent, verb: "issue", service: args.service, date: args.date, state: "valid", publicKey, fingerprint }) : human(business)(verb, args);
|
|
const x = withStdin(io(), pem);
|
|
await main(["credentials", "issue", "coder", "host.web1", "--rotate-by", "2099-01-01", "--business", "acme"], x, d);
|
|
assert.deepEqual(calls, [{ verb: "credentials.issue", args: { agent: "coder", service: "host.web1", token: pem, date: "2099-01-01" } }]);
|
|
assert.ok(x.out.text.includes(`authorized_keys line for ops-01:\nfrom="<stack host address>",restrict ${publicKey} mosaic-acme-coder-web1\n`), x.out.text);
|
|
assert.ok(x.out.text.includes(fingerprint));
|
|
for (const text of [x.out.text, x.err.text]) assert.equal(text.includes(body) || text.includes("PRIVATE KEY"), false);
|
|
// --json prints the result alone, still public material only.
|
|
const j = withStdin(io(), pem);
|
|
await main(["credentials", "issue", "coder", "host.web1", "--rotate-by", "2099-01-01", "--business", "acme", "--json"], j, d);
|
|
assert.equal(JSON.parse(j.out.text).publicKey, publicKey);
|
|
assert.equal(j.out.text.includes(body), false);
|
|
// A host key takes --rotate-by, not --expires.
|
|
await assert.rejects(main(["credentials", "issue", "coder", "host.web1", "--expires", "2099-01-01", "--business", "acme"], withStdin(io(), pem), d), /mosaic credentials/);
|
|
assert.equal(calls.length, 2);
|
|
});
|
|
|
|
test("row 199: scripts/mosaic sends credentials to packages/cli, not the seat CLI", (t) => {
|
|
const root = tmp(t, "mosaic-cli-cred-route-");
|
|
const env = { PATH: process.env.PATH, HOME: join(root, "home"), MOSAIC_CONFIG: join(root, "none.json") };
|
|
const r = spawnSync(MOSAIC, ["credentials", "purge"], { env, encoding: "utf8" });
|
|
assert.equal(r.status, 4);
|
|
assert.match(r.stderr, /^mosaic: mosaic credentials \[status \[AGENT\]\]/);
|
|
assert.doesNotMatch(r.stderr, /mosaic launch </);
|
|
});
|