AdminOnly/requireSelfOrAdmin reject a service-account caller, so an operator-managed server has no API path to create its first human login #23
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
AdminOnly(guardsPOST /api/users,GET/PUT /api/admin/settings) andrequireSelfOrAdmin(guardsPUT /api/users/{id}/password, among others)both gate on
userFromContext(ctx):userFromContextis only ever populated byserveAs(a human sessioncookie or a per-user API key). A service-account caller goes through
serveAsServiceAccountinstead, which stores its principal under adifferent context key (
ctxServiceAccount) thatuserFromContextneverreads. So any service-account Bearer token gets a flat 403 on these
routes, regardless of scope — there's no "instance-scoped accounts count
as admin" branch the way
isInstanceServiceAccount(ctx)already exists forservice_accounts.go/teams.go.Why this is more than a theoretical gap
terdut-operator's bootstrap flow (DESIGN.md §6) is:POST /api/bootstraponce, to get the first human admin's raw API key.account exists (
terdut-operator/internal/controller/terdutserver_bootstrap.go,reconcileBootstrap'sr.Delete(ctx, checkpoint)).After step 3, the only credential that exists anywhere for a server the
operator created is that instance-scoped service-account token. Because of
the bug above, that token can never call
/api/admin/settingsto flipsignup_mode, norPOST /api/users/PUT /api/users/{id}/passwordtocreate or provision a human login directly. There is no recovery path short
of writing to the
settings/userstables by hand — on anoperator-managed install, nobody can ever sign in through the normal web UI
unless they do that.
This isn't hypothetical:
terdut-operator/examples/demo/README.md'sdocumented "first login" step tells you to do exactly the broken thing
(
curl -H "Authorization: Bearer $operatorToken" -X PUT .../api/admin/settings -d '{"signup_mode":"open"}') and it 403s as written.A new
terdut-operator/examples/demo/run-demo.shautomation script worksaround it by
kubectl exec-ingpsqldirectly into the demo's ownPostgres to flip
signup_mode, which is fine for a throwaway kind demo butisn't something a real operator-managed install can reasonably do.
Suggested fix (not prescriptive — flagging the decision, not making it)
One of:
AdminOnly/requireSelfOrAdminthe same wayisInstanceServiceAccount(ctx)alreadylets one satisfy the team-shaped routes — this is probably the smallest
change and matches the trust model DESIGN.md §6 already describes
("blast radius: a team-scoped key can only touch its own team_id...";
the instance-scoped key is already meant to be able to act broadly).
human login" instead of reusing the general admin routes, if widening
AdminOnlyitself is considered too broad a privilege for a serviceaccount to hold.
Either way,
terdut-operator/examples/demo/README.md's first-login sectionshould be corrected once there's a real fix, instead of recommending a call
that 403s.
Repro
Against any server an operator created (i.e. one that has an instance-scoped
service account and no surviving human admin credential):
References
terdut-server/internal/api/middleware.go—AdminOnly,requireSelfOrAdmin,userFromContext,serveAsServiceAccount,isInstanceServiceAccount.terdut-server/internal/api/router.go— theAdminOnly-wrapped group(
POST /api/users,GET/PUT /api/admin/settings, etc.).terdut-operator/DESIGN.md§6 — bootstrap/credential lifecycle.terdut-operator/internal/controller/terdutserver_bootstrap.go—reconcileBootstrapdeleting the checkpointed admin key.terdut-operator/examples/demo/README.md— the first-login section thatcurrently documents the broken call.