Add service accounts and operator mode
Service accounts (SERVICE-ACCOUNTS.md) are a scoped, non-human credential: not a users row, so they never touch OIDC sync, login or the is_admin flag. Instance scope can create a team and mint a team-scoped account for it; team scope is owner-equivalent for that one team and nothing else. This is what unblocks terdut-operator's DESIGN.md §6 — no more impersonating a human admin, and a real rotation story instead of the unworkable delete-and-re-bootstrap /api/bootstrap can't actually do. - migration 014: service_accounts + service_account_keys - POST /api/service-accounts, POST/DELETE .../keys, GET ?name= self-lookup - AuthMiddleware resolves a tdsa_-prefixed key to a distinct principal; a team-scoped account gets a synthetic single membership so requireTeamMember/requireTeamOwner work on it unmodified - handleCreateTeam accepts an instance-scoped caller; the team it creates has no human owner, which is the expected shape for one an operator is about to hand a team-scoped credential to Operator mode (TERDUT_OPERATOR_MODE / values.operatorMode) declares an install gitops-managed: session and user-API-key writes to teams, escalation policies, dead man's switches and integrations get 403 reason=operator_managed, while a service account's writes still go through. Team membership/invites and the schedule are deliberately left out — never gitops-managed by design, and still human day-to-day work. /api/auth/config reports operator_mode so the web UI can grey these sections out from the start rather than only after a write fails. Also: GET /api/version (both terdut-tui and terdut-operator currently detect server capability by route-probing; this gives them a real answer), and a PUT for dead man's switches so a reconciler can update one in place instead of deleting and recreating it.
This commit is contained in:
+23
-14
@@ -126,12 +126,17 @@ hashes to a `service_account_keys.key_hash` resolves to a distinct principal
|
||||
type, not a synthesized `models.User`. `requireTeamMember`/`requireTeamOwner`
|
||||
treat a matching team-scoped service account as owner-equivalent for that one
|
||||
team (satisfies the same checks a real team owner would), and an instance-scoped
|
||||
one as satisfying `AdminOnly` for team-creation/listing purposes only — never
|
||||
for user-management endpoints (`POST /api/users`, `PUT /api/users/{id}/admin`,
|
||||
etc.), which stay human-admin-only. Anywhere identity is recorded for a human
|
||||
(incident timeline `acknowledged_by`/`assigned_to`, audit-relevant fields), a
|
||||
service-account principal is stored and displayed distinctly, e.g.
|
||||
`service-account:terdut-operator`, never coerced into a `user_id` FK.
|
||||
one as satisfying `AdminOnly` for team-creation/listing purposes **and** for
|
||||
minting a `team`-scoped service account against any team (`POST
|
||||
/api/service-accounts {"scope":"team","teamID":...}`) — this second permission
|
||||
is what lets an operator-style caller create a team, then immediately mint that
|
||||
team its own narrower credential, without a human in the loop for every team.
|
||||
Neither permission extends to user-management endpoints (`POST /api/users`,
|
||||
`PUT /api/users/{id}/admin`, etc.), which stay human-admin-only. Anywhere
|
||||
identity is recorded for a human (incident timeline
|
||||
`acknowledged_by`/`assigned_to`, audit-relevant fields), a service-account
|
||||
principal is stored and displayed distinctly, e.g. `service-account:terdut-operator`,
|
||||
never coerced into a `user_id` FK.
|
||||
|
||||
## What this unblocks
|
||||
|
||||
@@ -146,14 +151,18 @@ Directly resolves `terdut-operator` DESIGN.md §6's two broken assumptions:
|
||||
2. **Rotation becomes real.** `POST /api/service-accounts/{id}/keys` + revoke the
|
||||
old one — no destructive DB-level workaround, no re-triggering a single-shot
|
||||
endpoint that can't fire twice.
|
||||
3. **Cross-namespace credential mirroring becomes unnecessary.** Once team-scoped
|
||||
accounts exist, `terdut-operator`'s `TerdutServer` controller can mint one
|
||||
key per `TerdutTeam` directly into that `TerdutTeam`'s own namespace
|
||||
(owner-referenced to the CR) instead of mirroring one shared,
|
||||
server-admin-equivalent credential into every consenting namespace. This
|
||||
also closes the blast-radius gap that mirroring left open: a leaked Secret
|
||||
today would expose every team on the server; a leaked team-scoped key
|
||||
exposes exactly one team.
|
||||
3. **Cross-namespace credential mirroring is no longer needed at all.**
|
||||
`terdut-operator`'s current design holds every credential — instance- and
|
||||
team-scoped alike — privately in the operator's own namespace, never in
|
||||
the namespace of the CR each one authenticates for; reconciliation happens
|
||||
entirely inside the operator's controller loop, so no CR owner ever needs
|
||||
read access to a terdut-server credential regardless of same- or
|
||||
cross-namespace `serverRef`. Team scoping is still what bounds the blast
|
||||
radius of any individual credential: a leaked team-scoped key exposes
|
||||
exactly one team's resources, never the whole server, which is what makes
|
||||
holding many credentials in one place (the operator's namespace) an
|
||||
acceptable trade rather than reintroducing the mirrored design's
|
||||
server-admin-equivalent-everywhere problem.
|
||||
|
||||
## Suggested sequencing
|
||||
|
||||
|
||||
Reference in New Issue
Block a user