Raised by terdut-operator's TerdutTeam controller (ROADMAP.md Stage 2): an instance-scoped service account has no way to recover a team's id after a 409 on POST /api/teams, unlike the equivalent, already-solved case for service accounts themselves (GET /api/service-accounts?name=). Also corrects a claim in SERVICE-ACCOUNTS.md's own text that doesn't match AdminOnly's actual code -- confirmed against source, not assumed.
4.3 KiB
Team lookup for service accounts: closing terdut-operator's create-path crash window
This is a design note for a feature, not an implementation plan — same posture as
SERVICE-ACCOUNTS.md, and raised for the same reason: terdut-operator's TerdutTeam
controller (ROADMAP.md Stage 2, a separate repo, no shared code) hit a gap this server has
no answer for yet.
The problem
POST /api/teams (handleCreateTeam, confirmed against internal/api/teams.go) lets an
instance-scoped service account create a team — it has its own explicit
isInstanceServiceAccount(...) branch alongside the human-user path, not gated by
AdminOnly. If that call succeeds server-side but the caller (TerdutTeam's controller)
crashes before persisting the resulting team ID locally, a retry's POST 409s on the name's
unique constraint (confirmed: the isUniqueViolation branch in the same handler).
Recovering from that 409 means looking the team up by name, and nothing today permits that for a service account:
GET /api/teams(handleListTeams) answers "what teams does the caller belong to", via ateam_membersjoin keyed onuserFromContext'scaller.ID— confirmed against source. A service account is never a member of anything, so this always returns empty for one, regardless of what exists.GET /api/admin/teams(handleAdminListTeams) is gated byAdminOnly, andAdminOnly's actual code (internal/api/middleware.go) checks onlyuserFromContext(...).IsAdmin— no branch for a service account at all, confirmed against source. This contradictsSERVICE-ACCOUNTS.md's own text, which claims "an instance-scoped [service account satisfies]AdminOnlyfor team-creation/listing purposes" — that claim doesn't match this endpoint's actual, shipped code. (Team creation is fine:handleCreateTeamisn't behindAdminOnlyat all, it has its own check. Only the listing half of that sentence is wrong.)
This is exactly the shape of gap SERVICE-ACCOUNTS.md's own GET /api/service-accounts?name=
closed for service accounts themselves (confirmed: that endpoint's own comment —
"the name lookup is open to any authenticated caller... what lets a service account find its
own account on the 403 that follows a second POST"). Teams never got the equivalent, because
nothing needed it until an operator started creating them unattended.
Goals
- A service-account-accessible way to look up one team by exact name, mirroring
GET /api/service-accounts?name=as closely as possible — same shape, same reasoning, same low sensitivity of what it discloses. - No change to today's behavior for an empty/no-name request.
Proposed shape
Extend GET /api/teams itself, the same way handleListServiceAccounts already branches on
a ?name= query param, rather than adding a new route:
nameunset (today's behavior, unchanged): the caller's own teams, viateam_members.name=<value>set: look up that one team by exact name — a one-or-zero-length array, not an error on no match, mirroringGET /api/service-accounts?name='s own response shape and status codes exactly. Deliberately not gated byisInstanceServiceAccountorAdminOnly: a human caller who's already a member sees this same information in their own team list regardless, and a non-member learning only that a name is taken — not who's in the team, not any of its data — is the same low-sensitivity disclosureGET /api/service-accounts?name=already accepts for service-account names.
What this unblocks
Directly resolves the crash-window gap in terdut-operator's TerdutTeam controller: on a
409 from POST /api/teams, GET /api/teams?name=<the same name> — authenticated with the
same instance-scoped credential that just got the 409 — finds the id, and the controller
proceeds as if its own create had returned it directly. The same adopt-on-409 pattern already
proven for service accounts (that repo's DESIGN.md §6 point 1, §5's general rule), not a
new one.
Suggested sequencing
Land this before TerdutTeam's create path is implemented — the same reasoning
SERVICE-ACCOUNTS.md gave for its own sequencing: writing that code against today's gap as a
"known-temporary workaround" is wasted effort when the fix is this small and this
well-precedented.