99 lines
5.6 KiB
JavaScript
99 lines
5.6 KiB
JavaScript
// The five C-suite roles (REQ-ROLE-4, REQ-ROLE-1, vikunja:19/301; Mos Q123). Each role file is
|
|
// policy at least privilege: read-only tools, message.send and record.write within role, no
|
|
// credentials. Only the chief of staff launches them (REQ-COS-1, REQ-LAUNCH-1). A widened file
|
|
// fails here. Fixtures live in a temp directory and nothing reads the real config.
|
|
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { ACTIONS, BusinessError, classify, loadBusiness, loadRole, resolveInstance, validateRoleDocument } from "../src/index.mjs";
|
|
import { businessDoc, launchDoc, REPO_ROLES, systemFor, tmp, writeJson } from "./helpers.mjs";
|
|
|
|
const CSUITE = ["chief-executive-officer", "chief-financial-officer", "chief-information-officer", "chief-information-security-officer", "chief-marketing-officer"];
|
|
const APPROVED = Object.freeze({
|
|
tools: ["read", "grep", "find", "ls"],
|
|
network: "api-only",
|
|
authority: { withinRole: ["message.send", "record.write"], crossRole: [] },
|
|
credentials: [],
|
|
// Least privilege (vikunja:19/306): an advisory role talks to the user, the chief of staff, the pm
|
|
// and the cto, so it can't send work to the coder or reviewer around the pm.
|
|
messaging: { sendTo: ["human", "chief-of-staff", "pm", "cto"], receiveFrom: ["human", "chief-of-staff", "pm", "cto"] },
|
|
});
|
|
|
|
// Jason's business file has the same shape: the five instances by definition name, no credentials
|
|
// or tracker bot, listed in launch.instances beside the existing launchable instances.
|
|
function fixture(mutate = () => {}) {
|
|
const root = tmp();
|
|
const doc = businessDoc(root, "acme");
|
|
for (const name of CSUITE) doc.roles[name] = { definition: name };
|
|
doc.launch = launchDoc("chief", ["coder", "reviewer", ...CSUITE]);
|
|
mutate(doc);
|
|
writeJson(join(root, "businesses", "acme.json"), doc);
|
|
return { root, business: loadBusiness("acme", { dir: root, rolesDir: REPO_ROLES }) };
|
|
}
|
|
const resolved = ({ root, business }, instance) => resolveInstance({ system: systemFor(root), business, instance });
|
|
|
|
test("each definition loads, with a contract file, and is exactly the approved list", () => {
|
|
for (const name of CSUITE) {
|
|
const role = loadRole(REPO_ROLES, name);
|
|
assert.equal(role.roleVersion, 2);
|
|
assert.equal(role.name, name);
|
|
assert.equal(role.contract, `${name}.md`);
|
|
assert.ok(existsSync(join(REPO_ROLES, `${name}.md`)), name);
|
|
const { title, ...rest } = JSON.parse(readFileSync(join(REPO_ROLES, `${name}.json`), "utf8"));
|
|
assert.equal(typeof title, "string");
|
|
assert.deepEqual(rest, { roleVersion: 2, name, contract: `${name}.md`, ...APPROVED }, name);
|
|
const text = readFileSync(join(REPO_ROLES, `${name}.md`), "utf8");
|
|
assert.match(text, new RegExp(`^# Role contract: .* \\(${name}\\)\\n`), name);
|
|
assert.match(text, /launched by the chief of staff and by no other\s+role/, name);
|
|
}
|
|
});
|
|
|
|
test("an organization file that declares them passes, and each resolves to least privilege", () => {
|
|
const f = fixture();
|
|
for (const name of CSUITE) {
|
|
const record = resolved(f, name);
|
|
assert.equal(record.definition, name);
|
|
assert.deepEqual(record.limits, { tools: APPROVED.tools, network: "api-only", authority: APPROVED.authority });
|
|
assert.deepEqual(record.credentials, {});
|
|
assert.equal(record.launch, null);
|
|
for (const action of ACTIONS) assert.equal(classify(record, action), ["message.send", "record.write"].includes(action) ? "within" : "gated", `${name} ${action}`);
|
|
assert.ok(!record.limits.tools.includes("web") && !record.limits.tools.includes("bash") && !record.limits.tools.includes("write"), name);
|
|
}
|
|
});
|
|
|
|
test("the chief of staff launches each one; no other role does", () => {
|
|
const f = fixture();
|
|
const chief = resolved(f, "chief");
|
|
assert.deepEqual([...chief.launch.instances].sort(), ["coder", "reviewer", ...CSUITE].sort());
|
|
assert.equal(classify(chief, "role.launch"), "within");
|
|
for (const other of ["pm", "cto", "coder", "reviewer", ...CSUITE]) {
|
|
const record = resolved(f, other);
|
|
assert.equal(record.launch, null, other);
|
|
assert.equal(classify(record, "role.launch"), "gated", other);
|
|
}
|
|
});
|
|
|
|
test("a C-suite role can't be made the launcher: launch.by must hold role.launch", () => {
|
|
assert.throws(() => fixture((doc) => (doc.launch = launchDoc("chief-executive-officer", ["coder"]))),
|
|
(e) => e instanceof BusinessError && /doesn't hold role\.launch/.test(e.message));
|
|
});
|
|
|
|
test("limits can narrow a C-suite role but never widen it", () => {
|
|
const f = fixture((doc) => (doc.roles["chief-financial-officer"].vars = { "limits.authority": ["message.send", "spend", "task.create"], "limits.tools": ["read", "bash"] }));
|
|
const record = resolved(f, "chief-financial-officer");
|
|
assert.deepEqual(record.limits.authority, { withinRole: ["message.send"], crossRole: [] });
|
|
assert.deepEqual(record.limits.tools, ["read"]);
|
|
});
|
|
|
|
test("any unknown key, tool, action or version in a role file is refused", () => {
|
|
const base = JSON.parse(readFileSync(join(REPO_ROLES, "chief-executive-officer.json"), "utf8"));
|
|
const root = tmp();
|
|
writeFileSync(join(root, "chief-executive-officer.md"), "# contract\n\n## Authority\n");
|
|
const check = (extra) => validateRoleDocument({ ...base, ...extra }, join(root, "chief-executive-officer.json"));
|
|
assert.equal(check({}).name, "chief-executive-officer");
|
|
for (const extra of [{ arbiter: "technical" }, { launch: {} }, { tools: ["read", "browser"] }, { roleVersion: 3 }, { authority: { withinRole: ["message.send", "deploy.prod"], crossRole: [] } }]) {
|
|
assert.throws(() => check(extra), BusinessError, JSON.stringify(extra));
|
|
}
|
|
});
|