/** * @file server-s3-git.test.js * @description S3 plan, P3b: in a member's folder the dashboard runs git, gh * and file writes AS the member (sudo), and only non-secret variables cross * sudo (it logs its command line). A fake `sudo` on PATH records the argv; a * fake passwd and server.conf make `mia` a provisioned member. Outside the * users root nothing changes. The credential store is never handed to a * member's git, and a typed password is refused. * @author Nguyễn Ngọc Trí Vĩ */ const { describe, it, before, after } = require("node:test"); const assert = require("node:assert/strict"); const path = require("node:path"); const fs = require("node:fs"); const os = require("node:os"); const TMP = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "server-s3-git-"))); const USERS = path.join(TMP, "users"); const HOME = path.join(USERS, "mia"); const HANDOFF = path.join(HOME, ".agenthub", "handoff"); const BIN = path.join(TMP, "bin"); const LOG = path.join(TMP, "sudo.log"); process.env.DASHBOARD_DB_PATH = path.join(TMP, "dashboard.db"); process.env.AGENTHUB_SERVER_CONF = path.join(TMP, "server.conf"); process.env.AGENTHUB_PASSWD = path.join(TMP, "passwd"); const auth = require("../lib/auth"); const linux = require("../lib/linux-accounts"); const runAs = require("../lib/run-as"); const { git } = require("../lib/worktree"); const { gitConfigFor } = require("../lib/git-credentials"); const sudoCalls = () => fs.existsSync(LOG) ? fs .readFileSync(LOG, "utf8") .trim() .split("\n") .map((l) => JSON.parse(l)) : []; before(async () => { fs.mkdirSync(HANDOFF, { recursive: true }); fs.mkdirSync(BIN, { recursive: true }); // A fake sudo: records its argv, then answers like the command would. fs.writeFileSync( path.join(BIN, "sudo"), `#!/usr/bin/env node\nrequire("fs").appendFileSync(${JSON.stringify(LOG)}, JSON.stringify(process.argv.slice(2)) + "\\n");\nprocess.stdout.write("fake-git-out\\n");\n`, { mode: 0o755 } ); process.env.PATH = `${BIN}:${process.env.PATH}`; fs.writeFileSync(process.env.AGENTHUB_PASSWD, "ah-mia:x:5001:5001::/:/bin/sh\n"); fs.writeFileSync( process.env.AGENTHUB_SERVER_CONF, `HOST_USER=h\nHOST_UID=1\nUSERS_ROOT=${USERS}\nPORT=4820\n` ); linux.__resetConfCache(); runAs.__resetCache(); linux.__setRunImpl(() => "ok"); process.env.AGENTHUB_MODE = "server"; await auth.createUser("mia", "member-pass-1", "developer"); }); after(() => { delete process.env.AGENTHUB_MODE; fs.rmSync(TMP, { recursive: true, force: true }); }); describe("git and files in a member's folder run as the member", () => { it("maps a path under the users root to its member", () => { assert.deepEqual(runAs.accountForPath(path.join(HOME, "proj")), { name: "mia", login: "ah-mia", home: HOME, uid: 5001, }); assert.equal(runAs.accountForPath(TMP), null); }); it("runs git through sudo -u with only plain variables", async () => { fs.rmSync(LOG, { force: true }); const out = await git(HOME, ["status", "--porcelain"], { literal: true, env: { GIT_INDEX_FILE: "/x/index" }, }); assert.equal(out.stdout, "fake-git-out\n"); assert.deepEqual(sudoCalls(), [ [ "-n", "-u", "ah-mia", "GIT_TERMINAL_PROMPT=0", `HOME=${HOME}`, "GIT_INDEX_FILE=/x/index", "GIT_LITERAL_PATHSPECS=1", "git", "status", "--porcelain", ], ]); }); it("leaves git outside the users root alone", async () => { fs.rmSync(LOG, { force: true }); await git(TMP, ["--version"]); assert.deepEqual(sudoCalls(), []); }); it("writes a member's file through write-as, with the content on stdin", () => { const seen = []; runAs.__setExecImpl((cmd, argv, opts) => seen.push({ cmd, argv, input: opts.input })); runAs.writeFileAs(path.join(HOME, "a.txt"), "hello"); runAs.writeFileAs(path.join(HOME, ".gitignore"), "/x\n", { append: true }); assert.deepEqual(seen, [ { cmd: "sudo", argv: ["-n", "-u", "ah-mia", "/opt/agenthub/libexec/write-as", path.join(HOME, "a.txt")], input: "hello", }, { cmd: "sudo", argv: [ "-n", "-u", "ah-mia", "/opt/agenthub/libexec/write-as", path.join(HOME, ".gitignore"), "append", ], input: "/x\n", }, ]); const mine = path.join(TMP, "host.txt"); runAs.writeFileAs(mine, "direct"); assert.equal(fs.readFileSync(mine, "utf8"), "direct"); assert.equal(seen.length, 2); }); it("names a scratch index the member's git creates, and removes it as the member", () => { const seen = []; runAs.__setExecImpl((cmd, argv) => seen.push([cmd, ...argv])); const s = runAs.scratchFile(path.join(HOME, "proj"), "agenthub-index-"); assert.equal(path.dirname(s.file), HANDOFF); assert.ok(!fs.existsSync(s.file), "the dashboard writes nothing in a member's home"); s.remove(); assert.deepEqual( seen.map((c) => c.slice(-2)), [ [s.file, "remove"], [`${s.file}.lock`, "remove"], ] ); assert.ok(seen.every((c) => c[0] === "sudo" && c.includes("ah-mia"))); const host = runAs.scratchFile(TMP, "agenthub-index-"); assert.equal(path.dirname(path.dirname(host.file)), os.tmpdir()); host.remove(); }); it("never hands a member's git the credential store, nor a typed password", async () => { assert.deepEqual(await gitConfigFor(HOME), { args: [], env: {} }); await assert.rejects( gitConfigFor(HOME, { protocol: "https", host: "github.com", username: "u", password: "p" }), { code: "EMEMBERCRED" } ); }); });