D16: Okta admin bootstrap and break-glass posture; correct T10.4 scope
Decision, raised during T10.4 hazard review: - Admin bootstrap: manage_users.py moves from creating an admin account to promoting an existing one, by username, on a row Okta's JIT provisioning (T10.3) already created. Rejected blind account creation — the exact OKTA_IDENTITY_CLAIM format is still unconfirmed by security, and a hand-typed username that doesn't match it produces an orphaned second account instead of promoting the real one. Ongoing (non-bootstrap) admin naming needs no new work: html/users.js's existing role dropdown already handles it. - Break glass: none, by design, matching the precedent already on record for the abandoned LDAPS design (D13/D14) rather than assumed to carry over untested. If Okta is unreachable, the app is unreachable for everyone until Okta is restored. Rejected a toggleable emergency local login — it would reintroduce the stored credential D15 exists to eliminate. Also corrects T10.4's scope in wave-10.md: hazard review found real call sites of hash_password/verify_password/password_problem the original bullet didn't name (create_user(), admin_reset_password(), users.js's admin forms, browser_check.py/launcher_check.py fixtures), plus a verification-gate ordering problem (smoketest.py and seed_demo.py authenticate via POST /api/auth/login, which T10.4 removes, and both are named explicitly in CLAUDE.md's verification section). Fixed by having T10.4 switch both scripts to mint a session with auth.create_token() directly, the same technique browser_check.py already uses, rather than waiting on T10.7. D16
This commit is contained in:
77
docs/waves/decisions-2026-09-03.md
Normal file
77
docs/waves/decisions-2026-09-03.md
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
# Decisions — 2026-09-03
|
||||||
|
|
||||||
|
## D16: Okta admin bootstrap, break-glass posture, and the real scope of T10.4
|
||||||
|
|
||||||
|
Raised during hazard review for T10.4 (remove the local password path). D15 settled
|
||||||
|
*that* local passwords go away and Okta OIDC is the sole replacement; it did not settle
|
||||||
|
how an admin account gets named once there is no password to set, or what happens if
|
||||||
|
Okta itself is unreachable. Both are decided here.
|
||||||
|
|
||||||
|
### Admin bootstrap
|
||||||
|
|
||||||
|
`server/manage_users.py` stays the bootstrap tool — its own docstring already says so
|
||||||
|
("the `/api/auth/users` endpoint needs an existing admin, so you have to bootstrap one
|
||||||
|
here") — but it changes from *creating* an account to *promoting* one:
|
||||||
|
|
||||||
|
- An operator with shell/DB access on the server runs it against an account that
|
||||||
|
already signed in through Okta once and was JIT-provisioned by T10.3 (landing at
|
||||||
|
`project_user`, per that task). The command sets `role = admin` on that existing row
|
||||||
|
by username.
|
||||||
|
- It does **not** create a `User` row from scratch and does not touch `password_hash`
|
||||||
|
(the column is gone after T10.4's migration).
|
||||||
|
|
||||||
|
Rejected: minting a brand-new admin row by hand-typed username. `OKTA_IDENTITY_CLAIM`'s
|
||||||
|
exact format is still unconfirmed by security (open item carried from D15/wave-10.md).
|
||||||
|
A hand-typed username that doesn't exactly match what Okta actually sends produces a
|
||||||
|
second, orphaned account instead of promoting the real one. Promoting an
|
||||||
|
already-JIT-provisioned row sidesteps that entirely — it never has to guess the future
|
||||||
|
claim value.
|
||||||
|
|
||||||
|
Ongoing (non-bootstrap) admin naming needs no new work: `html/users.js` already has a
|
||||||
|
live role dropdown (`roleSelect`, gated by server-supplied `grantable_roles`) that lets
|
||||||
|
an existing admin promote any other account, including one JIT-provisioned via Okta.
|
||||||
|
That path is unrelated to the password removal and keeps working unchanged.
|
||||||
|
|
||||||
|
### Break glass
|
||||||
|
|
||||||
|
No break-glass path, by design. If Okta is unreachable or misconfigured, the app is
|
||||||
|
unreachable for everyone, including admins, until Okta is restored.
|
||||||
|
|
||||||
|
This matches the precedent already on record for the abandoned LDAPS design (D13/D14):
|
||||||
|
"LDAPS is the *only* path, no local fallback, no break-glass." Carried forward
|
||||||
|
deliberately rather than assumed to still apply, given Okta's failure modes differ from
|
||||||
|
an internal LDAP bind — considered and confirmed, not defaulted into.
|
||||||
|
|
||||||
|
Rejected: a toggleable emergency local login gated behind an env flag. It would
|
||||||
|
reintroduce a stored local credential, exactly what D15 exists to eliminate, for a
|
||||||
|
scenario (Okta down) judged less likely and less costly than the standing risk of a
|
||||||
|
forgotten emergency backdoor.
|
||||||
|
|
||||||
|
The server-shell CLI (`manage_users.py`, promoting an existing row) is not a formal
|
||||||
|
break-glass mechanism — it cannot help if no account has ever signed in through Okta —
|
||||||
|
but it is the same trust tier as "someone with SSH/container access to prod could
|
||||||
|
already edit the database directly," and it costs no new engineering.
|
||||||
|
|
||||||
|
### T10.4 scope correction
|
||||||
|
|
||||||
|
Hazard review found real call sites of `hash_password` / `verify_password` /
|
||||||
|
`password_problem` that the original T10.4 bullet in `wave-10.md` didn't name and that
|
||||||
|
break the moment those functions are deleted:
|
||||||
|
|
||||||
|
- `create_user()` and `admin_reset_password()` in `server/app.py` (the admin console's
|
||||||
|
"add user" and "reset password" routes).
|
||||||
|
- `html/users.js`'s "add user" form (`nu-password` field) and "Reset password" button.
|
||||||
|
- `server/manage_users.py`'s `create`, `create-admin`, and `reset-password` subcommands
|
||||||
|
(see bootstrap section above for its replacement).
|
||||||
|
- `tests/browser_check.py` and `tests/launcher_check.py`, which call
|
||||||
|
`auth.hash_password()` to seed fixture rows.
|
||||||
|
|
||||||
|
Also found: `server/smoketest.py` and `server/seed_demo.py` authenticate via
|
||||||
|
`POST /api/auth/login`, which T10.4 removes, and CLAUDE.md's own verification section
|
||||||
|
names both scripts as required checks. Fix folded into T10.4 rather than deferred to
|
||||||
|
T10.7: both scripts switch to minting a session with `auth.create_token()` and setting
|
||||||
|
the cookie directly, the same technique `tests/browser_check.py` already uses instead
|
||||||
|
of scripting a login form. No live Okta tenant needed, and T10.4 no longer depends on
|
||||||
|
T10.7's timing.
|
||||||
|
|
||||||
|
`wave-10.md`'s T10.4 bullet is updated to reflect this full scope.
|
||||||
@@ -29,9 +29,29 @@ Depends only on `main` as it stands after `D15`. Not sequenced behind any other
|
|||||||
yet answered. Build with a configurable claim name and a documented default, not a
|
yet answered. Build with a configurable claim name and a documented default, not a
|
||||||
hardcoded one, so the answer can drop in without a code change.
|
hardcoded one, so the answer can drop in without a code change.
|
||||||
|
|
||||||
- **T10.4 — Remove the local password path entirely.** Drop `password_hash`, remove the
|
- **T10.4 — Remove the local password path entirely.** Drop `password_hash` (Alembic
|
||||||
bcrypt-based `login()`, remove the username/password form. Real deletion, matching
|
migration; plain `op.drop_column`, matching existing precedent for other NOT NULL
|
||||||
`D15`'s "full replacement," not a toggle or a fallback.
|
columns on `users` — no `batch_alter_table` needed), remove the bcrypt-based
|
||||||
|
`login()`, remove the username/password form. Real deletion, matching `D15`'s "full
|
||||||
|
replacement," not a toggle or a fallback. Scope corrected by `D16` after hazard
|
||||||
|
review turned up more call sites than the original bullet named:
|
||||||
|
- `create_user()` and `admin_reset_password()` in `server/app.py` (admin console's
|
||||||
|
"add user" and "reset password" routes) — rework to drop the password field
|
||||||
|
entirely rather than break.
|
||||||
|
- `html/users.js`'s "add user" form (`nu-password`) and "Reset password" button —
|
||||||
|
matching frontend change.
|
||||||
|
- `server/manage_users.py` — reworked per `D16` from account *creation* to
|
||||||
|
*promotion*: `create-admin`/`create`/`reset-password` (password-based) are
|
||||||
|
replaced by a promote-by-username command that operates on a row Okta's JIT
|
||||||
|
provisioning (T10.3) already created, never a hand-typed new one. This is now
|
||||||
|
the documented admin-bootstrap path — see `D16`.
|
||||||
|
- `tests/browser_check.py` and `tests/launcher_check.py` — stop calling
|
||||||
|
`auth.hash_password()` to seed fixture rows.
|
||||||
|
- `server/smoketest.py` and `server/seed_demo.py` — currently authenticate via
|
||||||
|
`POST /api/auth/login`. Switch to minting a session with `auth.create_token()`
|
||||||
|
directly, the same technique `browser_check.py` already uses, so both scripts
|
||||||
|
(named explicitly in `CLAUDE.md`'s verification section) keep working without
|
||||||
|
depending on `T10.7`'s timing.
|
||||||
|
|
||||||
- **T10.5 — Frontend: login becomes a redirect, not a form.** `login.html`/`login.js`
|
- **T10.5 — Frontend: login becomes a redirect, not a form.** `login.html`/`login.js`
|
||||||
change to a "Sign in with Okta" flow. Sign-out lands back on the app's own login page.
|
change to a "Sign in with Okta" flow. Sign-out lands back on the app's own login page.
|
||||||
@@ -53,3 +73,6 @@ Depends only on `main` as it stands after `D15`. Not sequenced behind any other
|
|||||||
- Final confirmation of the redirect/callback URI (`https://wp.controls.dev/api/auth/okta/callback`
|
- Final confirmation of the redirect/callback URI (`https://wp.controls.dev/api/auth/okta/callback`
|
||||||
proposed, pending security).
|
proposed, pending security).
|
||||||
- The `Business Technology Group` pilot assignment in Okta.
|
- The `Business Technology Group` pilot assignment in Okta.
|
||||||
|
|
||||||
|
Closed since first written: admin bootstrap and break-glass posture, previously open
|
||||||
|
questions, decided in `D16` (2026-09-03) and folded into `T10.4` above.
|
||||||
|
|||||||
Reference in New Issue
Block a user