/** * @file server-mode-user-admin.test.js * @description Managing accounts from the dashboard (Users page): only an * admin may create a user, change a role, disable/enable, reset a password or * give a workspace, every change needs the admin's OWN password again * (EREAUTH, throttled), an admin can never demote or disable themselves, no * answer carries a password hash, each change lands in the audit log, and in * local mode — no login at all — the routes refuse. * @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.mkdtempSync(path.join(os.tmpdir(), "server-mode-user-admin-")); process.env.DASHBOARD_DB_PATH = path.join(TMP, "dashboard.db"); process.env.DASHBOARD_LIVENESS_PROBE = "0"; const { createApp, startServer } = require("../index"); const { db } = require("../db"); const auth = require("../lib/auth"); let server; let BASE; const ids = {}; const BOSS_PASS = "admin-pass-1"; async function call(method, p, { cookie, body, origin } = {}) { const res = await fetch(BASE + p, { method, headers: { "Content-Type": "application/json", ...(cookie ? { cookie } : {}), ...(origin ? { origin } : {}), }, body: body ? JSON.stringify(body) : undefined, }); return { status: res.status, body: await res.json().catch(() => null) }; } async function login(username, password) { const res = await fetch(BASE + "/api/auth/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username, password }), }); return res.status === 200 ? res.headers.get("set-cookie").split(";")[0] : null; } before(async () => { process.env.AGENTHUB_MODE = "server"; server = await startServer(createApp(), 0); BASE = `http://127.0.0.1:${server.address().port}`; ids.boss = (await auth.createUser("boss", BOSS_PASS, "admin")).id; ids.ana = (await auth.createUser("ana", "analyst-pass-1", "analyst")).id; ids.mia = (await auth.createUser("mia", "member-pass-1", "developer")).id; ids.lane = db .prepare("INSERT INTO lanes (cwd, tool) VALUES ('/w/ownerless', 'claude')") .run().lastInsertRowid; }); after(() => { delete process.env.AGENTHUB_MODE; server?.close(); fs.rmSync(TMP, { recursive: true, force: true }); }); describe("managing accounts from the dashboard", () => { it("lets an admin create a user, with their own password again", async () => { const boss = await login("boss", BOSS_PASS); const r = await call("POST", "/api/users", { cookie: boss, body: { username: "neo", password: "neo-pass-123", role: "developer", currentPassword: BOSS_PASS, }, }); assert.equal(r.status, 201); assert.deepEqual( { username: r.body.user.username, role: r.body.user.role }, { username: "neo", role: "developer" } ); assert.ok(!JSON.stringify(r.body).includes("scrypt"), "no password hash"); assert.ok(await login("neo", "neo-pass-123"), "the new user can log in"); const dup = await call("POST", "/api/users", { cookie: boss, body: { username: "neo", password: "neo-pass-123", currentPassword: BOSS_PASS }, }); assert.equal(dup.status, 409); assert.equal(dup.body.error.code, "EEXISTS"); // No minimum length — only an empty password is refused. const empty = await call("POST", "/api/users", { cookie: boss, body: { username: "zed", password: "", currentPassword: BOSS_PASS }, }); assert.equal(empty.status, 400); assert.equal(empty.body.error.code, "EBADPASSWORD"); const short = await call("POST", "/api/users", { cookie: boss, body: { username: "zed", password: "1", currentPassword: BOSS_PASS }, }); assert.equal(short.status, 201); }); it("refuses without the admin's password, or with a wrong one", async () => { const boss = await login("boss", BOSS_PASS); for (const currentPassword of [undefined, "not-the-password"]) { const r = await call("POST", "/api/users", { cookie: boss, body: { username: "eve", password: "eve-pass-123", currentPassword }, }); assert.equal(r.status, 403); assert.equal(r.body.error.code, "EREAUTH"); } assert.equal(auth.findUser("eve"), undefined); }); it("is an admin's only: an analyst is read-only, a member never reaches it", async () => { const body = { username: "eve", password: "eve-pass-123", currentPassword: "analyst-pass-1" }; const ana = await call("POST", "/api/users", { cookie: await login("ana", "analyst-pass-1"), body, }); assert.equal(ana.status, 403); const mia = await call("POST", "/api/users", { cookie: await login("mia", "member-pass-1"), body: { ...body, currentPassword: "member-pass-1" }, }); assert.equal(mia.status, 403); assert.equal(auth.findUser("eve"), undefined); }); it("refuses a write from another origin", async () => { const r = await call("POST", "/api/users", { cookie: await login("boss", BOSS_PASS), origin: "http://evil.example", body: { username: "eve", password: "eve-pass-123", currentPassword: BOSS_PASS }, }); assert.equal(r.status, 403); assert.equal(auth.findUser("eve"), undefined); }); it("changes a role, disables and enables, and resets a password", async () => { const boss = await login("boss", BOSS_PASS); const neo = auth.findUser("neo"); const role = await call("PATCH", `/api/users/${neo.id}`, { cookie: boss, body: { role: "analyst", currentPassword: BOSS_PASS }, }); assert.equal(role.status, 200); assert.equal(auth.findUser("neo").role, "analyst"); const neoLogin = await login("neo", "neo-pass-123"); const off = await call("PATCH", `/api/users/${neo.id}`, { cookie: boss, body: { disabled: true, currentPassword: BOSS_PASS }, }); assert.equal(off.status, 200); assert.equal(off.body.user.disabled, true); assert.equal((await call("GET", "/api/sessions", { cookie: neoLogin })).status, 401); assert.equal(await login("neo", "neo-pass-123"), null, "a disabled user cannot log in"); await call("PATCH", `/api/users/${neo.id}`, { cookie: boss, body: { disabled: false, currentPassword: BOSS_PASS }, }); const pw = await call("POST", `/api/users/${neo.id}/password`, { cookie: boss, body: { password: "neo-new-pass-9", currentPassword: BOSS_PASS }, }); assert.equal(pw.status, 200); assert.equal(await login("neo", "neo-pass-123"), null); assert.ok(await login("neo", "neo-new-pass-9")); const bad = await call("PATCH", `/api/users/${neo.id}`, { cookie: boss, body: { role: "root", currentPassword: BOSS_PASS }, }); assert.equal(bad.status, 400); const none = await call("PATCH", "/api/users/9999", { cookie: boss, body: { role: "developer", currentPassword: BOSS_PASS }, }); assert.equal(none.status, 404); }); it("never lets an admin demote or disable themselves", async () => { const boss = await login("boss", BOSS_PASS); for (const body of [{ role: "developer" }, { disabled: true }]) { const r = await call("PATCH", `/api/users/${ids.boss}`, { cookie: boss, body: { ...body, currentPassword: BOSS_PASS }, }); assert.equal(r.status, 409); assert.equal(r.body.error.code, "ESELF"); } assert.equal(auth.findUser("boss").role, "admin"); }); it("gives a workspace to a user", async () => { const boss = await login("boss", BOSS_PASS); const r = await call("POST", `/api/users/${ids.mia}/workspaces`, { cookie: boss, body: { laneId: ids.lane, currentPassword: BOSS_PASS }, }); assert.equal(r.status, 200); assert.equal( db.prepare("SELECT owner_id FROM lanes WHERE id = ?").get(ids.lane).owner_id, ids.mia ); const none = await call("POST", `/api/users/${ids.mia}/workspaces`, { cookie: boss, body: { laneId: 9999, currentPassword: BOSS_PASS }, }); assert.equal(none.status, 404); }); it("writes every change to the audit log", async () => { const boss = await login("boss", BOSS_PASS); const entries = (await call("GET", "/api/users/audit", { cookie: boss })).body.entries; const mine = entries.filter((e) => e.reader === "boss" && e.method !== "GET"); const paths = mine.map((e) => e.path).join("\n"); assert.match(paths, /create neo/); assert.match(paths, /role analyst/); assert.match(paths, /disable/); assert.match(paths, /password reset/); assert.match(paths, new RegExp(`workspace ${ids.lane}`)); assert.ok(!paths.includes("neo-new-pass-9"), "never the password itself"); }); it("throttles wrong confirmation passwords", async () => { const boss = await login("boss", BOSS_PASS); let last; for (let i = 0; i < 12; i++) { last = await call("PATCH", `/api/users/${ids.mia}`, { cookie: boss, body: { role: "analyst", currentPassword: "guess-" + i }, }); } assert.equal(last.status, 429); assert.equal(last.body.error.code, "ETOOMANY"); assert.equal(auth.findUser("mia").role, "developer"); }); }); describe("in local mode", () => { it("refuses every account change — there is no login to check", async () => { delete process.env.AGENTHUB_MODE; try { const r = await call("POST", "/api/users", { body: { username: "eve", password: "eve-pass-123", currentPassword: "x" }, }); assert.equal(r.status, 403); assert.equal(r.body.error.code, "ELOCALMODE"); assert.equal(auth.findUser("eve"), undefined); } finally { process.env.AGENTHUB_MODE = "server"; } }); });