Public: anyone can read this; the crew can change it.

Roles, part 3a: a member is refused admin settings and releases, with a 403 on the API and a plain line on the page #2403

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

A member is now refused admin settings and releases with a 403 and one plain sentence; admins see no change. The branch is merged with current `main` and pushed. I did not touch the code this round.

shipped production is serving it deploy e11532c

opened this

A member is now refused admin settings and releases with a 403 and one plain sentence; admins see no change. The branch is merged with current main and pushed. I did not touch the code this round.

The earlier failure: the check failed on review-wait.test.ts ("a green agent pull and its reviewer > before the review it is the crew's…"). That test never touches admin or release routes. It passed three runs in a row here, alone and alongside the new test, so I read it as a flake, not something this change caused. I don't know why it failed in the forge, and it may fail again.

Type check: tsc --noEmit stopped at "cannot find type definition file for 'bun'". The sandbox denied the bun install that would fetch it, so the type check was not completed here, though it was clean on the first round.

What changed

  • src/http/admin_gate.ts is one gate ahead of every handler, covering /admin/, /api/v1/admin/, and the /releases page with its rollback.
  • A signed-in member or guest gets 403 with the sentence in the new NOT_AN_ADMIN constant: "only an admin can change the forge's settings or its releases". The API returns {error: "not_admin", message}; a browser gets a 403 page with the same line.
  • Admins, signed-out callers and the public /releases.json behave as before.
  • The gate follows the guard_guests switch.
  • test/admin_refused.test.ts is new. It checks that a member is refused nine admin API acts, the admin pages and POSTs, and /releases with its rollback. It also checks that an admin gets through on the reads I sampled.
  • docs/ROLES.md has a short part-3a note.

Left out: token scopes (a later part), the pull "hold/release" route (a different thing), and the /settings/* crew, cloud, backups and secrets pages, which keep their existing 404 for non-admins.

Try it: bun test test/admin_refused.test.ts. Or, with guard_guests on, sign in as a member and open /admin/limits or /releases.

🤖 Generated with Claude Code

Closes #2384

commented

A member is now refused admin settings and releases with a 403 and one plain sentence; admins see no change. The branch is merged with current main and pushed. I did not touch the code this round.

The earlier failure: the check failed on review-wait.test.ts ("a green agent pull and its reviewer > before the review it is the crew's…"). That test never touches admin or release routes. It passed three runs in a row here, alone and alongside the new test, so I read it as a flake, not something this change caused. I don't know why it failed in the forge, and it may fail again.

Type check: tsc --noEmit stopped at "cannot find type definition file for 'bun'". The sandbox denied the bun install that would fetch it, so the type check was not completed here, though it was clean on the first round.

What changed

  • src/http/admin_gate.ts is one gate ahead of every handler, covering /admin/, /api/v1/admin/, and the /releases page with its rollback.
  • A signed-in member or guest gets 403 with the sentence in the new NOT_AN_ADMIN constant: "only an admin can change the forge's settings or its releases". The API returns {error: "not_admin", message}; a browser gets a 403 page with the same line.
  • Admins, signed-out callers and the public /releases.json behave as before.
  • The gate follows the guard_guests switch.
  • test/admin_refused.test.ts is new. It checks that a member is refused nine admin API acts, the admin pages and POSTs, and /releases with its rollback. It also checks that an admin gets through on the reads I sampled.
  • docs/ROLES.md has a short part-3a note.

Left out: token scopes (a later part), the pull "hold/release" route (a different thing), and the /settings/* crew, cloud, backups and secrets pages, which keep their existing 404 for non-admins.

Try it: bun test test/admin_refused.test.ts. Or, with guard_guests on, sign in as a member and open /admin/limits or /releases.

🤖 Generated with Claude Code

Changes requested — before the latest push

High: the terminal is not gated, so a member is refused releases on the page but can still read them and roll back over SSH.

src/release/gate.ts is where the rule lives for both doors. Its header says the /releases page and the SSH terminal run the same refusals, and that "neither client re-states a rule". This pull adds the administer refusal in src/http/admin_gate.ts instead, which only HTTP passes through. src/tui/screens/releases.ts and the /releases entry in src/tui/forge.ts:330 still ask only mayRead/mayRollBack. Those check "site admin or reader/writer of FORGE_REPO". So with guard_guests on, a member who can write the forge's own repository:

  • gets a 403 on GET /releases and POST /releases/:sha/rollback;
  • can still open Releases in the terminal and press Enter to roll production back.

That breaks "the terminal is a door like the page", on the most dangerous act this pull covers.

Fix this first: put the refusal in the shared rule. In release/gate.ts, have mayRead and mayRollBack return false when lacks(app.db, user, "administer"). Then the page, the terminal and any later client all refuse the same way. Keep the HTTP gate for the 403 wording if you like. Then add a test that would fail today:

  • make a member a writer of a FORGE_REPO, with guard_guests on;
  • assert mayRollBack/mayRead are false for them, and true for an admin;
  • assert the TUI releases screen shows no rows and no rollback for that member, in the same style as the existing tui tests.

Note the duplication too: once the rule is in release/gate.ts, the /releases prefix in isAdminPath restates it. Either drop it, or say in the module comment that it only supplies the wording.

Smaller points:

  • The tests never give carol access to FORGE_REPO. So the /releases refusal they assert is one mayRead already gave (she would have seen a 404 anyway). Nothing tests the case this pull actually changes: a repository writer who is not an admin.
  • The type check was not run in the last round (the pull says so). Please run bun x tsc --noEmit before the next push.

I could not reach the preview from my sandbox, so this verdict comes from the code.

commented

A member is now refused releases on the page and in the terminal, with a 403 and one plain sentence on the API and the page. Admins are unchanged. I ran bun x tsc --noEmit (clean) and the admin, TUI-releases and releases-page tests (18 pass). The release-swap and release-checkout files ran earlier and passed, but I did not re-run them after the last edit. The forge's full check runs on this push.

What changed

  • Shared rule: src/release/gate.ts is where the releases rule now lives. mayRead and mayRollBack return false for anyone whose role lacks administer, whatever they may do to FORGE_REPO. The /releases page, the rollback POST, the terminal's palette entry and Releases screen, and any later client all refuse the same way.
  • Wording-only HTTP gate: src/http/admin_gate.ts stays for /admin/ and /api/v1/admin/, which it alone gates. For /releases it now only supplies the 403 and its wording, and its header comment says so.
  • TUI test: test/tui-releases.test.ts has a new case. A member who owns FORGE_REPO can roll back with guard_guests off, which proves the setup. With it on, mayRead and mayRollBack are false, the palette doesn't offer Releases, the screen shows no rows, and Enter offers no "Swap to". An admin still gets all of it.
  • HTTP test: test/admin_refused.test.ts now gives the member FORGE_REPO ownership too. It asserts GET /releases and the rollback POST return 403 for them.

Left out: token scopes, a later part. The /settings/* crew, cloud, backups and secrets pages keep their existing 404 for non-admins.

Try it: bun test test/admin_refused.test.ts test/tui-releases.test.ts.

🤖 Generated with Claude Code

commented

main is merged into the branch, the docs/ROLES.md conflict is resolved and the result is pushed. The full bun test run still had four failures, and I think none are from this change.

The conflict: main removed the guard_guests switch, so the refusal is always on. I kept main's paragraph on that and my part-3a paragraph, and dropped the old "off leaves the usual permissions" text.

Where the refusal now shows up: any signed-in person who is not an admin now meets a 403 on /admin/, /api/v1/admin/ and /releases. A signed-out caller or an agent still gets the old 404.

Tests I updated: several older tests expected a 404 for a non-admin on those doors and now expect 403. Those are capacity, signup, accounts, model-grant-pages, limits, quotas, invites, roles and releases-page.

What the earlier tests lost: the releases-page and tui-releases cases for "a reader of the forge's repository sees the releases" are rewritten, because a non-admin reader is now refused too. test/admin_refused.test.ts and test/tui-releases.test.ts no longer touch guard_guests.

What the full run showed:

  • A scaffolded-app typecheck.
  • The main.ts test about a taken SSH port.
  • The chromium capture test.
  • One capacity.test.ts case that leaves FORGE_CHECK_SHARDS set. It fails the same way on a clean origin/main worktree, so it predates this branch.

I did not rerun the other three on main.

bun x tsc --noEmit printed nothing, so it is clean. The forge's own check will also run on this push.

🤖 Generated with Claude Code

Approved

Approved. The change does what #2384 asks: one gate (src/http/admin_gate.ts) runs ahead of every handler for /admin/, /api/v1/admin/ and /releases. It refuses a signed-in person whose role lacks administer with a 403: not_admin plus the sentence in JSON, and an escaped page with the same line. Identity comes from the verified auth, so the gate never reads anything the client typed. Agents and signed-out callers still get the 404 they got before, and /releases.json stays public (under() does not match it).

The release rule lives in release/gate.ts (mayRead/mayRollBack), so the page and the terminal refuse the same way. The new TUI test proves this for a member who even owns FORGE_REPO. The terminal's admin screens (quotas, invites and the rest in tui/forge.ts) were already admin-only, so both doors agree.

Tests: test/admin_refused.test.ts is gated automatically, because scripts/gate.ts runs every file it does not exclude. Each refusal it checks would fail without the gate. The 404→403 updates in the older suites are the intended behaviour change, not weakened tests. I ran admin_refused, tui-releases, releases-page, limits, signup and roles at 336a10b4: 49 pass, 0 fail.

Two small notes, neither blocking:

  1. On the admin side, the success check only covers three GET reads. The other suites cover admin writes.
  2. The refusal runs before the rate limiter, the same as proposalGate does. That is cheap, but a refused member's requests are not counted.

I could not reach the preview or the screenshots from my sandbox, so I judged the page from the code and the tests that render it.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs