Skip to content

gateway: user revocation as the third kill switch (UserDetail + People) - #1694

Merged
Evanfeenstra merged 1 commit into
mainfrom
gateway-user-revoke-ui
Sep 15, 2026
Merged

Evanfeenstra merged 1 commit into
mainfrom
gateway-user-revoke-ui

Conversation

@Evanfeenstra

Copy link
Copy Markdown
Collaborator

What

Adds the user axis to the dashboard's kill switches, next to run and agent, on top of the existing phase-6 revoke_user_before:<user_id> cutoff.

Setting the cutoff makes the hot path reject every macaroon whose user authorization was issued before it: all of the user's in-flight runs, whatever agent carries them, and every new spawn under the current authorization, until Hive issues a fresh one. Per swarm, no TTL, DELETE clears.

Backend

  • PUT|GET|DELETE /_plugin/revoke/user/:id move from bearer-only to cookie-or-bearer (CSRF header on cookie mutations), the same posture as the kill routes. /_plugin/revoke/nonce/* stays bearer-only; server.go splits the prefix.
  • New GET /_plugin/revoke/users: every cutoff on the swarm, newest first, from a bifrost:revoke_users ZSET index maintained by SetUserRevokeCutoff / ClearUserRevokeCutoff. The per-user string key stays authoritative: the list re-reads it and prunes index members whose key is gone (direct Redis DELs self-heal). Hive's reconcile sweep can read this per swarm.
  • user_id is validated like the kill ids.

UI

  • UserDetail: header badge, an Authorization card (cutoff + effect), and a Revoke user / Clear revoke switch with typed confirmation. The modal states the blast radius ("N in-flight runs across M agents") from /users/:id/quota's in-flight list, fetched only while open. Copy says "authorizations issued before the cutoff are rejected" rather than "revoked", because after Hive re-issues the cutoff is still set and still true.
  • People: a Revoked column from one /revoke/users read.
  • KillConfirmModal grows a user target; apiFetch accepts PUT.

Docs

phase-6 schema + status note, phase-9 users views + endpoint table, ui/AGENTS.md.

Tests

  • adminapi: cookie+CSRF accepted on user routes, 403 without CSRF, nonce routes still 401 on a cookie, list ordering + method guard + self-heal, 401/503 tables extended.
  • auth: index maintained on set/clear, list newest-first + limit + prune, empty/unavailable, malformed id.
  • go test ./internal/... green; make tygo regenerated types.ts; npm run typecheck + npm run build clean.

Verified in the browser

Against a Node mock of the routes the two pages touch (same recipe as #1685): People renders the column, UserDetail opens the typed modal with the blast-radius line, revoke flips badge/card/button and the People badge, clear flips them back. Not yet run against a real gateway + Redis.

Caveats

  • Like every hook check, the cutoff only rejects once enforce_macaroons is on; shadow mode logs.
  • Whether this is "until Hive re-issues" or just "kill everything in flight" depends on Hive reusing the long-lived user authorization the spec describes vs minting one per spawn. Hive side is next.
  • Cutoffs written before this PR are not in the index until re-set (the per-user GET still works).

The dashboard already has per-run and per-agent kill switches; this
adds the user axis on top of the existing revoke_user_before cutoff.

Backend
- /_plugin/revoke/user/:id (PUT/GET/DELETE) moves from bearer-only to
  cookie-or-bearer with CSRF on cookie mutations, the same posture as
  the kill routes. /_plugin/revoke/nonce/* stays bearer-only; server.go
  splits the prefix.
- New GET /_plugin/revoke/users lists every cutoff on the swarm, newest
  first, from a revoke_users ZSET index that SetUserRevokeCutoff /
  ClearUserRevokeCutoff maintain. The per-user string key stays
  authoritative: the list re-reads it and prunes index members whose
  key is gone, so direct Redis DELs self-heal.
- user_id is validated like the kill ids (no whitespace or '/').

UI
- UserDetail: header badge, Authorization card (cutoff + effect), and
  a Revoke user / Clear revoke switch with typed confirmation. The
  modal spells out the blast radius from /users/:id/quota's in-flight
  list while open.
- People: a Revoked column from one /revoke/users read.
- KillConfirmModal grows a "user" target; apiFetch accepts PUT.

Tests: cookie+CSRF accepted on user routes, 403 without CSRF, nonce
routes still 401 on a cookie, list ordering, method guard, index
self-heal (adminapi + auth packages). Docs: phase-6 schema/status,
phase-9 users views + endpoint table, ui/AGENTS.md.
@Evanfeenstra
Evanfeenstra merged commit 45cb875 into main Sep 15, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant