Join: merge A3 (wip/registry-a3)
Conflicts, both sides kept: artifact-store-tools' invokes (the assigner's and indexer's read tools, followed through views, beside the registry seats' hold, release and remove), its main (the views reader and the seat asker), its manifest row; gitea's invokes (the indexer's read tool, the hand act and the builder's versions) and its consumes of the indexer's views. gitea's account test takes the packages account's names; repoCheckFacts, removed on main, is not imported. No catalogue module reads the indexer's state. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
@@ -23,7 +23,7 @@ test("importing the bundle resolves while the broker keeps refusing the binds",
|
||||
await import(${JSON.stringify(index)});
|
||||
console.log("imported");
|
||||
process.exit(0);`;
|
||||
const env = { ...process.env, MESH_GITEA_TOKEN: "x", MESH_GITEA_URL: "http://127.0.0.1:1" };
|
||||
const env = { ...process.env, GITEA_TOKEN: "x", MESH_GITEA_URL: "http://127.0.0.1:1" };
|
||||
for (const k of ["MESH_GITEA_STATE_DIR", "MESH_GITEA_ADMIN_USER", "MESH_GITEA_ADMIN_PASSWORD", "MESH_SERVED_MODULE"]) delete env[k];
|
||||
const child = spawn(process.execPath, ["--input-type=module", "-e", script], { cwd: join(here, ".."), env, stdio: ["ignore", "pipe", "pipe"] });
|
||||
let out = "";
|
||||
@@ -86,7 +86,7 @@ test("a bundle whose read of the checker's owed is granted late writes the lates
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
console.log(JSON.stringify({ tools, bound, watched, statuses }));
|
||||
process.exit(0);`;
|
||||
const env = { ...process.env, MESH_GITEA_TOKEN: "x" };
|
||||
const env = { ...process.env, GITEA_TOKEN: "x" };
|
||||
for (const k of ["MESH_GITEA_STATE_DIR", "MESH_GITEA_ADMIN_USER", "MESH_GITEA_ADMIN_PASSWORD", "MESH_SERVED_MODULE", "MESH_GITEA_URL"]) delete env[k];
|
||||
const child = spawn(process.execPath, ["--input-type=module", "-e", script], { cwd: join(here, ".."), env, stdio: ["ignore", "pipe", "pipe"] });
|
||||
let out = "";
|
||||
@@ -141,7 +141,7 @@ test("importing the bundle resolves, its tools registered, while the forge never
|
||||
const { collectTools } = await import("@novox/mesh-sdk/tools");
|
||||
console.log(JSON.stringify({ imported: true, tools: collectTools().flatMap((r) => r.tools.map((t) => t.name)) }));
|
||||
process.exit(0);`;
|
||||
const env = { ...process.env, MESH_GITEA_TOKEN: "x", MESH_GITEA_ADMIN_USER: "admin", MESH_GITEA_HOOK_URL: "http://127.0.0.1:1" };
|
||||
const env = { ...process.env, GITEA_TOKEN: "x", MESH_GITEA_ADMIN_USER: "admin", MESH_GITEA_HOOK_URL: "http://127.0.0.1:1" };
|
||||
for (const k of ["MESH_SERVED_MODULE", "MESH_GITEA_ADMIN_PASSWORD", "MESH_GITEA_CONFIG_FILE"]) delete env[k];
|
||||
const child = spawn(process.execPath, ["--input-type=module", "-e", script], { cwd: join(here, ".."), env, stdio: ["ignore", "pipe", "pipe"] });
|
||||
let out = "";
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { grant, holds, PACKAGE_TEAM, READ_TEAM, teamFor, type Forge } from "../provisioner/teams.ts";
|
||||
|
||||
// Defends novox/hq issue 624: the lab reads the forge with a credential of its own that can write no
|
||||
// package. A consumer that contributes {"access": "read"} joins a team with code and package read only.
|
||||
|
||||
function aForge(): { forge: Forge; did: string[] } {
|
||||
const did: string[] = [];
|
||||
const teams: Record<string, number> = {};
|
||||
const forge: Forge = {
|
||||
async ensureOrg(org) { did.push(`org ${org}`); },
|
||||
async ensureTeam(org, team, packageWrite) {
|
||||
did.push(`team ${org}/${team} packages:${packageWrite ? "write" : "read"}`);
|
||||
return (teams[team] ??= Object.keys(teams).length + 1);
|
||||
},
|
||||
async ensureUser(username) { did.push(`user ${username}`); },
|
||||
async addUserToTeam(id, username) { did.push(`add ${username} to ${id}`); },
|
||||
async removeUserFromTeam(id, username) { did.push(`remove ${username} from ${id}`); },
|
||||
async holdsTeamMember(org, team, username) { did.push(`holds ${username} in ${org}/${team}`); return true; },
|
||||
};
|
||||
return { forge, did };
|
||||
}
|
||||
|
||||
test("a consumer that asks nothing, or write, publishes: the package team", () => {
|
||||
assert.deepEqual(teamFor({}), { team: PACKAGE_TEAM, packageWrite: true });
|
||||
assert.deepEqual(teamFor({ access: "write" }), { team: PACKAGE_TEAM, packageWrite: true });
|
||||
});
|
||||
|
||||
test("a consumer that asks to read joins the readers, who write no package", () => {
|
||||
assert.deepEqual(teamFor({ access: "read" }), { team: READ_TEAM, packageWrite: false });
|
||||
assert.throws(() => teamFor({ access: "admin" }), /not "read" or "write"/);
|
||||
});
|
||||
|
||||
test("a reader is made, put on the readers team, and taken off the package team", async () => {
|
||||
const { forge, did } = aForge();
|
||||
await grant(forge, { as: "lab-shanks", password: "pw", values: { access: "read" } });
|
||||
assert.deepEqual(did, [
|
||||
"org novox",
|
||||
"team novox/readers packages:read",
|
||||
"user lab-shanks",
|
||||
"add lab-shanks to 1",
|
||||
"team novox/packages packages:write",
|
||||
"remove lab-shanks from 2",
|
||||
]);
|
||||
did.length = 0;
|
||||
await holds(forge, { as: "lab-shanks", password: "pw", values: { access: "read" } });
|
||||
assert.deepEqual(did, ["holds lab-shanks in novox/readers"]);
|
||||
});
|
||||
|
||||
test("a publisher is made and put on the package team, as before", async () => {
|
||||
const { forge, did } = aForge();
|
||||
await grant(forge, { as: "build-agent-ace", password: "pw", values: {} });
|
||||
assert.deepEqual(did, ["org novox", "team novox/packages packages:write", "user build-agent-ace", "add build-agent-ace to 1"]);
|
||||
});
|
||||
@@ -293,11 +293,11 @@ test("first start: mints with the admin account, keeps the token at 0600, asks f
|
||||
assert.equal(repos[0]?.full_name, "novox/hq");
|
||||
assert.equal(forge.mints, 1);
|
||||
// Never write:admin (novox/hq ADR 0266): the forge's administration is the operator's, not a tool's.
|
||||
assert.deepEqual(forge.lastScopes, ["write:repository", "write:issue", "read:user"]);
|
||||
assert.deepEqual(forge.lastScopes, ["write:repository", "write:issue", "read:user", "read:package"]);
|
||||
assert.deepEqual(forge.lastScopes, [...TOKEN_SCOPES]);
|
||||
const token = forge.tokens.get("mesh-tools")!;
|
||||
assert.equal(await readFile(file, "utf8"), token + "\n");
|
||||
assert.equal(await readFile(file + ".scopes", "utf8"), "read:user,write:issue,write:repository\n");
|
||||
assert.equal(await readFile(file + ".scopes", "utf8"), "read:package,read:user,write:issue,write:repository\n");
|
||||
assert.equal((await stat(file)).mode & 0o777, 0o600);
|
||||
// Said that it minted, and where it keeps it — never what it is.
|
||||
assert.ok(logs.some((l) => l.startsWith("minted a token")), logs.join("\n"));
|
||||
@@ -500,16 +500,18 @@ test("a configured token wins, and is reported rather than minted over when the
|
||||
const { env } = await delivered(forge);
|
||||
const before = forge.mints;
|
||||
|
||||
const client = GiteaClient.fromEnv({ ...env, MESH_GITEA_TOKEN: "one-somebody-pasted-in" });
|
||||
const client = GiteaClient.fromEnv({ ...env, GITEA_TOKEN: "one-somebody-pasted-in" });
|
||||
|
||||
await assert.rejects(client.listRepos(), /rejected the configured Gitea token \(401\)/);
|
||||
assert.equal(forge.mints, before);
|
||||
// MESH_GITEA_TOKEN is no word of the mesh's (nothing sets it, checks/environment): it is not read, the module mints.
|
||||
await assert.doesNotReject(GiteaClient.fromEnv({ ...env, MESH_GITEA_TOKEN: "one-somebody-pasted-in" }).listRepos());
|
||||
});
|
||||
|
||||
test("nothing to mint with and no token: the client says what is missing", async () => {
|
||||
assert.throws(
|
||||
() => GiteaClient.fromEnv({ MESH_GITEA_URL: forge.url, MESH_GITEA_ADMIN_USER: ADMIN }),
|
||||
/set MESH_GITEA_TOKEN, or MESH_GITEA_ADMIN_PASSWORD_FILE, MESH_GITEA_STATE_DIR/,
|
||||
/set GITEA_TOKEN, or MESH_GITEA_ADMIN_PASSWORD_FILE, MESH_GITEA_STATE_DIR/,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -14,9 +14,10 @@ import { mkdtemp, readFile, writeFile, mkdir } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { MintedToken } from "../dist/token.js";
|
||||
import { MintedToken, TOKEN_SCOPES } from "../dist/token.js";
|
||||
import { GiteaClient } from "../dist/client.js";
|
||||
import { TOOLS_ACCOUNT, TOOLS_TEAM_UNITS, BUILDER_ACCOUNT, AGENT_ACCOUNT, meshAccounts } from "../dist/tools-account.js";
|
||||
import { PACKAGES_ACCOUNT, PACKAGES_TEAM_UNITS, PACKAGES_TOKEN_REFUSED, keepPackagesToken, packagesToken, renewOnRefusal } from "../dist/packages-account.js";
|
||||
|
||||
const ADMIN = "mesh-admin";
|
||||
const PASSWORD = "the-vault-minted-this";
|
||||
@@ -116,6 +117,13 @@ function fakeForge() {
|
||||
}
|
||||
}
|
||||
if (req.method === "GET" && p === "/user") return json(res, 200, { login: me, is_admin: isAdmin(me) });
|
||||
// The organisation's packages: a token whose scope reaches packages, of a member of a team writing them.
|
||||
if (req.method === "GET" && p === "/packages/novox") {
|
||||
const scopes = tokens.find((t) => `token ${t.sha1}` === req.headers.authorization)?.scopes ?? [];
|
||||
if (!scopes.some((s) => /:package$/.test(s))) return json(res, 403, { message: "token does not have at least one of required scope(s): [read:package]" });
|
||||
if (!teams.some((t) => t.members.has(me) && t.units.units_map?.["repo.packages"] === "write")) return json(res, 403, { message: "no access to the packages" });
|
||||
return json(res, 200, []);
|
||||
}
|
||||
m = p.match(/^\/users\/([^/]+)$/);
|
||||
if (m && req.method === "GET") {
|
||||
const u = users.get(decodeURIComponent(m[1]));
|
||||
@@ -359,6 +367,44 @@ test("the manifest hands the tools their account, and the Go forge bundle holds
|
||||
const forgeBundle = manifest.build.artifacts.find((a: any) => a.name === "forge");
|
||||
assert.ok(forgeBundle.env.MESH_GITEA_TOKEN_FILE);
|
||||
assert.equal(forgeBundle.env.MESH_GITEA_ADMIN_PASSWORD_FILE, undefined);
|
||||
// The package registry's bundle reads with the tools' token and writes with the packages account's (design 51).
|
||||
const packages = manifest.build.artifacts.find((a: any) => a.name === "package-registry");
|
||||
assert.equal(packages.env.MESH_GITEA_TOKEN_FILE, forgeBundle.env.MESH_GITEA_TOKEN_FILE);
|
||||
assert.equal(packages.env.MESH_GITEA_PACKAGES_USER, PACKAGES_ACCOUNT);
|
||||
assert.match(packages.env.MESH_GITEA_PACKAGES_TOKEN_FILE, /^\$\{dir:runtime-state\}\/packages-token$/);
|
||||
assert.equal(packages.env.MESH_GITEA_ADMIN_PASSWORD_FILE, undefined);
|
||||
assert.equal(code.env.MESH_GITEA_STATE_DIR, "${dir:runtime-state}", "the watcher mints the packages token where the bundle reads it");
|
||||
});
|
||||
|
||||
test("the package seats write as an account of their own: plain, in a team writing only packages, its token write:package alone, renewed when the forge rejects it", async () => {
|
||||
const env = await environment();
|
||||
const source = packagesToken(forge.url, env, () => {});
|
||||
assert.equal(await keepPackagesToken(source, forge.url), "the package seats' write token holds");
|
||||
const account = forge.users.get(PACKAGES_ACCOUNT);
|
||||
assert.ok(account, "the module made the packages account");
|
||||
assert.equal(account!.admin, false, "the packages account is no administrator");
|
||||
const held = forge.tokens.filter((t) => t.name === "mesh-packages");
|
||||
assert.deepEqual(held.map((t) => [t.user, t.scopes]), [[PACKAGES_ACCOUNT, ["write:package"]]]);
|
||||
assert.equal(await readFile(join(env.MESH_GITEA_STATE_DIR, "packages-token"), "utf8"), held[0].sha1 + "\n");
|
||||
assert.equal(await readFile(join(env.MESH_GITEA_STATE_DIR, "packages-token.scopes"), "utf8"), "write:package for mesh-packages\n");
|
||||
const team = forge.teams.find((t) => t.name === "mesh-packages");
|
||||
assert.ok(team?.members.has(PACKAGES_ACCOUNT));
|
||||
assert.deepEqual(team!.units.units_map, PACKAGES_TEAM_UNITS.units_map, "the team writes packages and nothing else");
|
||||
// The forge drops the token behind the module's back: the Go bundle's refusal (the finding's put) mints it afresh and
|
||||
// clears the finding; no timer looks at it, and another key of the findings state is not its own.
|
||||
forge.tokens.splice(forge.tokens.indexOf(held[0]), 1);
|
||||
const cleared: string[] = [];
|
||||
const said: string[] = [];
|
||||
const renew = renewOnRefusal(source, forge.url, { delete: async (k) => { cleared.push(k); } }, (l) => said.push(l));
|
||||
await renew({ key: "seat.gitea.other", op: "put" });
|
||||
await renew({ key: PACKAGES_TOKEN_REFUSED, op: "delete" });
|
||||
assert.equal(forge.tokens.filter((t) => t.name === "mesh-packages").length, 0, "nothing renews but the refusal");
|
||||
await renew({ key: PACKAGES_TOKEN_REFUSED, op: "put" });
|
||||
assert.deepEqual(said, ["the forge rejected the package seats' write token; minted afresh"]);
|
||||
assert.deepEqual(cleared, [PACKAGES_TOKEN_REFUSED]);
|
||||
assert.equal(forge.tokens.filter((t) => t.name === "mesh-packages").length, 1);
|
||||
// The tools' token reads packages too: read:package (design 51's read-only tool token).
|
||||
assert.ok(TOKEN_SCOPES.includes("read:package"));
|
||||
});
|
||||
|
||||
// Review of issue 469: what reads the rules' list is moved to the branch, and nothing leaves the tools without a token.
|
||||
|
||||
Reference in New Issue
Block a user