871274a3a0
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.
74 lines
4.3 KiB
Markdown
74 lines
4.3 KiB
Markdown
# 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=<value>` 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=<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.
|