SCRUM-205 — admin-auth: bootstrap-root command¶
Plan ref: AA-6 (docs/11-admin-plane-plan.md). Stacked on SCRUM-204.
Behaviour¶
admin_auth bootstrap-root [-email root@otomo.internal] [-name Root] [-rotate]
reads the root password from ADMIN_AUTH_ROOT_PASSWORD_FILE (default
/run/secrets/admin_root_password; one trailing \n/\r\n trimmed; ≥16 chars;
warns if group/world-readable; the password is never printed). Needs only
ADMIN_AUTH_DATABASE_URL.
| State | Result |
|---|---|
| no root | creates it (is_root, roles {admin}, created_by NULL), audit root.bootstrap → root created: <email> |
| root exists | changes nothing → root already exists: <email> (+ a note if -email differs; the existing email is kept) |
root exists, -rotate |
new hash, failures/lock reset, all root sessions revoked, audit root.rotate → root password rotated; N sessions revoked |
no root, -rotate |
creates it (says nothing existed to rotate) |
| concurrent runs | the single-root unique index decides; the losers report "already exists" |
Root is the break-glass account (password-only, no TOTP); its job is creating personal admin accounts (SCRUM-206).
Test-infrastructure fix (from SCRUM-198): go test ./... runs packages in
parallel, and store.TestMigrateUpDownUp drops the staff tables while the server
and bootstrap packages use them — a latent flake. Every DB-backed test now holds a
shared Postgres advisory lock for its duration.
How to verify¶
cd services/admin_auth
export ADMIN_AUTH_TEST_DATABASE_URL='postgres://USER:PASS@127.0.0.1:5433/admin_auth_test?sslmode=disable'
go vet ./... && go test -race -count=2 ./...
go test -count=1 -v ./internal/bootstrap/
| Test | Proves |
|---|---|
TestRunCreatesRoot |
row shape; password verifies with the file minus the newline; one root.bootstrap row |
TestRunIsIdempotent |
second run: same hash, no new audit row |
TestRunRotatesRoot |
new hash, old password rejected, root's sessions revoked, failures/lock reset, one root.rotate |
TestRunRotateWithNoRootCreates |
as named |
TestReadPasswordFileRules |
missing / empty / 15 chars → error that does not contain the file contents |
TestRunConcurrentBootstrap |
4 at once → exactly one root, every call succeeds |
TestBootstrapRootCanLogIn |
the created root logs in through POST /admin-auth/login |
CLI check done during review:
short password -> "must be at least 16 characters", exit 1
0644 file -> warning, "root created: root@otomo.internal", exit 0
-email other@x -> "root already exists … keeping the existing root email", exit 0
-rotate (new file) -> "root password rotated; 0 sessions revoked", exit 0
audit -> root.bootstrap, root.rotate
Results at time of writing¶
go vet,go test -race -count=2(7 packages) against Postgres 16: pass; the tests leave no root row behind.
How it was built¶
DeepSeek run scoped (Landlock) to services/admin_auth (313 s, ~47k output
tokens). It also found and fixed the cross-package test race above. Claude review:
logic read, suite run twice, CLI exercised. No changes needed.