Files
jason.woltje a8d9eecf65 organization: move packages/business to packages/organization (rename r2 stage B1)
git mv packages/business -> packages/organization (@mosaic/organization). The
old package stays as one-line re-export shims of each moved module, with
src/business.mjs re-exporting src/organization-config.mjs and src/cli.mjs
forwarding to the moved executable. BusinessError is OrganizationConfigError
(one constructor, name kept BusinessError); OrganizationError stays distinct.
The bus adapter gains organization-context.mjs (busOrganization), with
business.mjs (busBusiness) as the same function.

Import consumers, manifests, the lockfile, scripts/mosaic, stack-copy.sh
COPY_PATHS (both roots), seat sandbox RUNNER_FILES and the copy closure
walker (organization-imports.mjs, both roots required) move to the new path.
v1 serialization, v1 writers and the stage A fixtures are unchanged; the stage
A contract tests stay where they were and pass as before.
2026-11-09 11:47:48 -06:00

84 lines
5.1 KiB
JavaScript

// Row 210 (#1707, design D3 and D6): the address checks and the argv allowlist. Row 234 (#1740, r6 S1)
// moved the registry and the grants to bus store records, so the business file refuses them.
import { test } from "node:test";
import assert from "node:assert/strict";
import { readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { BusinessError, validateBusinessDocument } from "../src/index.mjs";
import { HOST_KEY as HOST_KEY_PATTERN, REFUSED_COMMANDS, USER, checkAddress, checkCommands, commandAllowed, ownMachine, validArgv } from "../src/hosts.mjs";
import { businessDoc, rolesCopy, tmp, tokenFile, FAR_DATE } from "./helpers.mjs";
const HOST_KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOv6Rq3Mvo9w2j3eYvYbqX0J1pJ0x9m8Zr0w1kJ8p0Qn";
function refuses(fn, pattern) {
assert.throws(fn, (error) => {
assert.ok(error instanceof BusinessError, `expected BusinessError, got ${error}`);
assert.match(error.message, pattern);
return true;
});
}
// The fixture with coder's definition declaring the host credential and no hosts.
function check(mutate, { declare = true } = {}) {
const root = tmp();
const rolesDir = rolesCopy(root);
if (declare) {
const file = join(rolesDir, "coder.json");
const coder = JSON.parse(readFileSync(file, "utf8"));
coder.credentials.push({ service: "host" });
writeFileSync(file, JSON.stringify(coder));
}
const doc = businessDoc(root);
mutate?.(doc, root);
return validateBusinessDocument(doc, join(root, "businesses", "acme.json"), { rolesDir });
}
// Row 234 (#1740, r6 S1): hosts and grants are bus store records, so the business file refuses both keys
// and any host.<id> key reference. The control loads.
test("S1: the business file refuses hosts at the top level and in a role, and host.<id> references", () => {
assert.equal("hosts" in check(), false);
assert.equal("hosts" in check().roles.coder, false);
const entry = { address: "10.1.1.53", port: 22, user: "localadmin", hostKeys: [HOST_KEY] };
refuses(() => check((d) => { d.hosts = { web1: entry }; }), /acme\.json hosts lives in the bus store/);
refuses(() => check((d) => { d.hosts = {}; }), /hosts lives in the bus store/);
refuses(() => check((d) => { d.roles.coder.hosts = { web1: { commands: [["systemctl", "status"]] } }; }), /roles\.coder\.hosts lives in the bus store/);
refuses(() => check((d) => { d.roles.coder.hosts = {}; }, { declare: false }), /roles\.coder\.hosts lives in the bus store/);
const key = (d, root) => { d.roles.coder.credentials["host.web1"] = { file: tokenFile(join(root, "secrets"), "coder-host.key"), rotateBy: FAR_DATE }; };
refuses(() => check(key), /host\.web1/);
});
test("T1: address, user and host key checks, which the enrollment raise uses", () => {
const own = ["127.0.0.2", "0.0.0.0", "::1", "::", "0:0:0:0:0:0:0:1", "::ffff:127.0.0.1", "::ffff:0.0.0.0", "localhost", "a.localhost",
"127.1", "2130706433", "0177.0.0.1", "0x7f.1", "bad_name", "a..b", "-a.b", "fe80::1%eth0", 7];
for (const address of own) refuses(() => checkAddress(address, "grant.address"), /grant\.address/);
for (const address of ["10.1.1.53", "host.example", "2001:db8::1"]) assert.equal(checkAddress(address, "grant.address"), address);
assert.equal(ownMachine("::ffff:7f00:1"), true);
assert.equal(ownMachine("::ffff:10.0.0.1"), false);
for (const user of ["Root", "a b", ""]) assert.equal(USER.test(user), false);
assert.equal(USER.test("localadmin"), true);
for (const key of [`${HOST_KEY} root@web1`, "ssh-dss AAAA"]) assert.equal(HOST_KEY_PATTERN.test(key), false);
assert.equal(HOST_KEY_PATTERN.test(HOST_KEY), true);
});
test("T26: no wildcard and no shell, runner or interpreter as a prefix", () => {
const at = /grant\.commands/;
for (const commands of ["any", "*", {}, [], [[]], [[7]], [["ls", ""]], [["ls"], ["ls"]], [["ls", "*"]], [["ls", "a?"]], [["ls", "[a]"]], [["bin/ls"]], [["ls\0"]]])
refuses(() => checkCommands(commands, "grant.commands"), at);
const names = [...REFUSED_COMMANDS, "python", "python3", "python3.12", "awk", "gawk", "mawk", "nawk", "php", "php8.2", "/bin/bash", "/usr/bin/env"];
for (const name of names) refuses(() => checkCommands([[name, "x"]], "grant.commands"), at);
for (const name of ["systemctl", "/usr/bin/journalctl", "tee"]) assert.deepEqual(checkCommands([[name]], "grant.commands"), [[name]]);
});
test("D6: argv shape and the literal prefix match", () => {
assert.equal(validArgv(["systemctl", "status", "nginx"]), true);
for (const argv of [[], "ls", [""], ["a\0"], [7], ["bin/ls"], Array(257).fill("a"), ["a".repeat(4097)], Array(9).fill("a".repeat(4000))])
assert.equal(validArgv(argv), false, JSON.stringify(argv).slice(0, 40));
assert.equal(validArgv(["/usr/bin/ls"]), true);
const commands = [["systemctl", "status"]];
assert.equal(commandAllowed(["systemctl", "status"], commands), true);
assert.equal(commandAllowed(["systemctl", "status", "nginx"], commands), true);
assert.equal(commandAllowed(["systemctl"], commands), false);
assert.equal(commandAllowed(["systemctl", "restart", "nginx"], commands), false);
assert.equal(commandAllowed(["systemctl", "status*"], commands), false);
});