a4dd60f6b8
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.
44 lines
2.2 KiB
SQL
44 lines
2.2 KiB
SQL
-- Service accounts: a scoped, non-human credential for automation (e.g.
|
|
-- terdut-operator) that needs to manage teams, escalation policies, dead
|
|
-- man's switches, integrations and OIDC group bindings without impersonating
|
|
-- a human user. See SERVICE-ACCOUNTS.md for the design this implements.
|
|
--
|
|
-- Deliberately not a users row: no password_hash, no is_admin, no
|
|
-- user_identities linkage, so a service account can never be pulled into
|
|
-- OIDC group sync or password login, and is never mistaken for a human in an
|
|
-- audit trail.
|
|
--
|
|
-- scope is 'instance' (acts with the same reach system administration has
|
|
-- over teams: create one, list them, mint a 'team'-scoped account against
|
|
-- any of them) or 'team' (acts as that one team's owner, and nothing else).
|
|
-- The CHECK ties team_id's presence to scope directly, rather than leaving it
|
|
-- to application code to keep the two consistent.
|
|
CREATE TABLE service_accounts (
|
|
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
|
name TEXT NOT NULL UNIQUE,
|
|
scope TEXT NOT NULL CHECK (scope IN ('instance', 'team')),
|
|
team_id BIGINT REFERENCES teams(id) ON DELETE CASCADE,
|
|
created_by BIGINT REFERENCES users(id) ON DELETE SET NULL,
|
|
created_at BIGINT NOT NULL DEFAULT FLOOR(EXTRACT(EPOCH FROM now()))::bigint,
|
|
CONSTRAINT service_accounts_scope_team_id_chk CHECK (
|
|
(scope = 'team' AND team_id IS NOT NULL) OR
|
|
(scope = 'instance' AND team_id IS NULL)
|
|
)
|
|
);
|
|
|
|
CREATE INDEX service_accounts_team_id_idx ON service_accounts(team_id);
|
|
|
|
-- One account, many keys: rotation is minting a new one and revoking the
|
|
-- old, the same shape api_keys already has, so an account's identity and
|
|
-- audit history survive a rotation instead of being recreated by it.
|
|
CREATE TABLE service_account_keys (
|
|
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
|
service_account_id BIGINT NOT NULL REFERENCES service_accounts(id) ON DELETE CASCADE,
|
|
key_hash TEXT NOT NULL UNIQUE,
|
|
name TEXT NOT NULL,
|
|
created_at BIGINT NOT NULL DEFAULT FLOOR(EXTRACT(EPOCH FROM now()))::bigint,
|
|
last_used_at BIGINT
|
|
);
|
|
|
|
CREATE INDEX service_account_keys_service_account_id_idx ON service_account_keys(service_account_id);
|