feat(pool): pin a user to one Claude/Codex pool account
An admin pins a server-mode user to one pool account per tool (account_pins; PUT /api/accounts/pins with the password step-up and audit, the Users page's Pin a user, agenthub accounts pin|unpin). The workspace owner's pin wins over the balancer whenever the pane may use the account; a pinned login still waits while another pool login holds the home.
This commit is contained in:
@@ -4,7 +4,8 @@
|
||||
* admin may add or remove an account, each change needs the admin's OWN
|
||||
* password again (EREAUTH), the token only goes in — no answer (the add, the
|
||||
* list) ever carries it — the audit log names the account, not its secret, and
|
||||
* in local mode the routes refuse.
|
||||
* in local mode the routes refuse. An admin pins a user to one account
|
||||
* (`PUT /api/accounts/pins`): that user's new panes skip the balancer.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
@@ -144,3 +145,75 @@ describe("the account pool from the Users page", () => {
|
||||
assert.ok(claudePool.pickAccount(), "team-1 is still there");
|
||||
});
|
||||
});
|
||||
|
||||
describe("pinning a user to one account", () => {
|
||||
const pins = require("../lib/account-pins");
|
||||
const pin = (cookie, body) =>
|
||||
call("PUT", "/api/accounts/pins", {
|
||||
cookie,
|
||||
body: { tool: "claude", currentPassword: BOSS_PASS, ...body },
|
||||
});
|
||||
|
||||
it("lets an admin pin and unpin, audited, and new panes follow the pin", async () => {
|
||||
const boss = await login("boss", BOSS_PASS);
|
||||
const miaId = db.prepare("SELECT id FROM users WHERE username = 'mia'").get().id;
|
||||
claudePool.addAccount("team-z", `sk-ant-oat01-${"z".repeat(40)}`);
|
||||
assert.equal(claudePool.pickAccount(null, miaId).name, "team-1", "the balancer's pick");
|
||||
const r = await pin(boss, { userId: miaId, account: "team-z" });
|
||||
assert.equal(r.status, 200);
|
||||
assert.deepEqual(r.body.pin, { userId: miaId, username: "mia", account: "team-z" });
|
||||
assert.equal(claudePool.pickAccount(null, miaId).name, "team-z");
|
||||
assert.equal(claudePool.pickAccount(null, null).name, "team-1", "nobody else moves");
|
||||
const audit = db.prepare("SELECT owner_id, path FROM audit_log ORDER BY id DESC LIMIT 1").get();
|
||||
assert.equal(audit.owner_id, miaId);
|
||||
assert.match(audit.path, /pin mia to claude account team-z/);
|
||||
const list = await call("GET", "/api/accounts", { cookie: boss });
|
||||
assert.deepEqual(
|
||||
list.body.pins.claude.map((p) => [p.username, p.account]),
|
||||
[["mia", "team-z"]]
|
||||
);
|
||||
assert.deepEqual(list.body.pins.codex, []);
|
||||
const off = await pin(boss, { userId: miaId, account: null });
|
||||
assert.equal(off.body.pin.account, null);
|
||||
assert.equal(claudePool.pickAccount(null, miaId).name, "team-1");
|
||||
claudePool.removeAccount("team-z");
|
||||
});
|
||||
|
||||
it("refuses an unknown account or user, a wrong password, and a non-admin", async () => {
|
||||
const boss = await login("boss", BOSS_PASS);
|
||||
const miaId = db.prepare("SELECT id FROM users WHERE username = 'mia'").get().id;
|
||||
assert.equal(
|
||||
(await pin(boss, { userId: miaId, account: "nope" })).body.error.code,
|
||||
"ENOACCOUNT"
|
||||
);
|
||||
assert.equal((await pin(boss, { userId: 9999, account: "team-1" })).body.error.code, "ENOUSER");
|
||||
assert.equal(
|
||||
(await pin(boss, { userId: miaId, account: "team-1", currentPassword: "x" })).body.error.code,
|
||||
"EREAUTH"
|
||||
);
|
||||
const ana = await login("ana", "analyst-pass-1");
|
||||
const w = await call("PUT", "/api/accounts/pins", {
|
||||
cookie: ana,
|
||||
body: { tool: "claude", userId: miaId, account: "team-1", currentPassword: "analyst-pass-1" },
|
||||
});
|
||||
assert.equal(w.status, 403);
|
||||
assert.equal(pins.pinFor(miaId, "claude"), null);
|
||||
});
|
||||
|
||||
it("is ignored in local mode and dropped with its user", async () => {
|
||||
const miaId = db.prepare("SELECT id FROM users WHERE username = 'mia'").get().id;
|
||||
pins.setPin(miaId, "claude", "team-1");
|
||||
delete process.env.AGENTHUB_MODE;
|
||||
try {
|
||||
assert.equal(pins.pinFor(miaId, "claude"), null);
|
||||
} finally {
|
||||
process.env.AGENTHUB_MODE = "server";
|
||||
}
|
||||
assert.equal(pins.pinFor(miaId, "claude"), "team-1");
|
||||
db.prepare("DELETE FROM users WHERE id = ?").run(miaId);
|
||||
assert.equal(
|
||||
db.prepare("SELECT COUNT(*) AS n FROM account_pins WHERE user_id = ?").get(miaId).n,
|
||||
0
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user