# 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 a `team_members` join keyed on `userFromContext`'s `caller.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 by `AdminOnly`, and `AdminOnly`'s actual code (`internal/api/middleware.go`) checks only `userFromContext(...).IsAdmin` — no branch for a service account at all, confirmed against source. This contradicts `SERVICE-ACCOUNTS.md`'s own text, which claims "an instance-scoped [service account satisfies] `AdminOnly` for team-creation/listing purposes" — that claim doesn't match this endpoint's actual, shipped code. (Team *creation* is fine: `handleCreateTeam` isn't behind `AdminOnly` at 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: - `name` unset (today's behavior, unchanged): the caller's own teams, via `team_members`. - `name=` set: look up that one team by exact name — a one-or-zero-length array, not an error on no match, mirroring `GET /api/service-accounts?name=`'s own response shape and status codes exactly. Deliberately **not** gated by `isInstanceServiceAccount` or `AdminOnly`: 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 disclosure `GET /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=` — 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.