Compare commits

...

2 Commits

Author SHA1 Message Date
df20b8f18d wave-10: close out claim mapping and redirect URI, live in production
Confirmed by an actual live Okta sign-in after main (cc64c88) deployed: preferred_username is the right identity claim, and the redirect URI works. Matt matched his existing pre-Okta admin account rather than getting JIT-provisioned as a duplicate. Two deploy-time snags recorded, both Case B (config, not data): OKTA_CLIENT_ID/SECRET/ISSUER left empty in Portainer at first (caught cleanly by is_configured()), then OKTA_ISSUER missing its https:// scheme (surfaced as httpx.UnsupportedProtocol, not a deliberate app error - BL-028 still stands). Neither needed the backup. BTG pilot group now includes Cody and Cameron, awaiting Adrian.
2026-09-09 13:34:32 -07:00
cc64c88c3e T10.10: audit manage_users.py promote
cmd_promote() changed a user's role with no audit trail, unlike the
identical change from the web Admin Console (set_user_role() ->
log_event(), action "role_changed"). Not a new privilege - anyone
with container-exec access already has DB access directly, D16's own
trust-tier reasoning - but there was no record of who ran it or what
changed.

Now writes an AuditLog row matching set_user_role()'s shape, tagged
via:cli (mirrors JIT provisioning's via:okta_jit) since a container
shell exec carries no signed-in identity to attribute the change to.

Verified against a scratch SQLite db: audit row lands correctly, role
change persists, the no-such-user refusal still exits 1 clean.
2026-09-03 16:27:13 -07:00
2 changed files with 63 additions and 4 deletions

View File

@@ -156,6 +156,24 @@ Depends only on `main` as it stands after `D15`. Not sequenced behind any other
Wave 10 is complete. The three items in "Still open" below are external Wave 10 is complete. The three items in "Still open" below are external
(security team / Okta admin), not blocked on any task in this wave. (security team / Okta admin), not blocked on any task in this wave.
- **T10.10 — Audit `manage_users.py promote`.** Raised in review after T10.8:
`cmd_promote()` changed a user's role with no audit trail at all, unlike the
identical role change from the web Admin Console (`app.py`'s
`set_user_role()``log_event()`, action `"role_changed"`). Not a new
privilege — anyone with Portainer/container-exec access to `wp_api` already
has shell access to the database directly, same trust tier D16 already named
for this command — but there was no record of who ran it or what changed.
Built: `cmd_promote()` now writes an `AuditLog` row with the same
`action`/`detail` shape `set_user_role()` uses (`{"from": old_role, "to":
role}`), tagged `"via": "cli"` (mirrors JIT provisioning's own `"via":
"okta_jit"` tag) and `actor="cli:manage_users"` — a container shell exec
carries no signed-in identity to attribute the change to a real person, so
it names the tool rather than guessing one. Verified end to end against a
scratch SQLite database: the audit row lands with the exact expected shape,
the role change persists, and the existing "no such user" refusal still
exits 1 with no partial write.
- **T10.9 — Rollback-aware deploy runbook.** Raised after hazard review found - **T10.9 — Rollback-aware deploy runbook.** Raised after hazard review found
`DEPLOY-runbook-2026-08-04.md`'s Rollback section has no case for a migration whose `DEPLOY-runbook-2026-08-04.md`'s Rollback section has no case for a migration whose
`downgrade()` cannot restore the data it drops — see `D17`. `T10.4`'s `downgrade()` cannot restore the data it drops — see `D17`. `T10.4`'s
@@ -183,10 +201,33 @@ Depends only on `main` as it stands after `D15`. Not sequenced behind any other
## Still open ## Still open
- The OIDC claim mapping (`T10.3`). - The `Business Technology Group` pilot assignment in Okta. Originally six names
- Final confirmation of the redirect/callback URI (`https://wp.controls.dev/api/auth/okta/callback` (Carlee Swihart, Drew Hilliard, Matt Mabrey, Nick Siegfried, Rachel Schreiber, Terry
proposed, pending security). Sajan); Cody and Cameron added 2026-09-09. Adrian added only Matt at first,
- The `Business Technology Group` pilot assignment in Okta. deliberately, pending the live sign-in confirmation below — awaiting his response to
add the rest of the group now that it has.
Closed since first written: admin bootstrap and break-glass posture, previously open 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. questions, decided in `D16` (2026-09-03) and folded into `T10.4` above.
**Closed 2026-09-09, live in production:** the redirect/callback URI
(`https://wp.controls.dev/api/auth/okta/callback`) is confirmed working, and so is the
OIDC claim mapping (`T10.3`) — `preferred_username` (the code's documented default,
never actually confirmed by name in Request 50649's thread) is correct, no
`OKTA_IDENTITY_CLAIM` override needed. Both settled by an actual live sign-in against
the real Okta tenant after `main` was merged (`cc64c88`) and deployed: Matt signed in
as himself, matched his existing pre-Okta admin account by `find_user()` rather than
JIT-provisioning a duplicate (the account already existed — this app has ~40 real
users, not the seeded test fixture), landed on `index.html` signed in, admin role and
project access untouched. One real deploy-time snag on the way, worth recording since
it's exactly the Case B scenario `DEPLOY-runbook-2026-09-03.md` anticipated: the first
redeploy left `OKTA_CLIENT_ID`/`OKTA_CLIENT_SECRET`/`OKTA_ISSUER` as empty rows in
Portainer (env var names added, values never filled in) — caught via
`is_configured()`'s all-four-required check failing closed (the 503 "Sign-in is
temporarily unavailable"), not silently. A second snag after filling those in:
`OKTA_ISSUER` was pasted without its `https://` scheme, which surfaced as
`httpx.UnsupportedProtocol` from Authlib's OIDC discovery fetch rather than anything
the app's own code raises deliberately — the exact case the runbook's Notes flagged as
having no startup-time confirmation (`okta_auth.describe()` still has no caller,
`BL-028`). Both fixed by correcting the env var values in Portainer and redeploying;
neither needed the backup or the database.

View File

@@ -24,6 +24,7 @@ and .env resolve the same way the API does:
""" """
import argparse import argparse
import sys import sys
import uuid
from .db import SessionLocal, Base, engine from .db import SessionLocal, Base, engine
from . import models, auth from . import models, auth
@@ -44,7 +45,24 @@ def cmd_promote(args) -> None:
f"No user named '{args.username}'. This promotes an existing account, it " f"No user named '{args.username}'. This promotes an existing account, it "
f"doesn't create one — they need to sign in through Okta at least once first." f"doesn't create one — they need to sign in through Okta at least once first."
) )
old_role = u.role
u.role = role u.role = role
# Audited the same way a role change from the web Admin Console already is
# (server/app.py's set_user_role() -> log_event(), action "role_changed") —
# this command changes the same field and previously left no record of who
# ran it or what it changed (T10.10). "actor" can't name a real person here:
# a container shell exec carries no signed-in identity to attribute it to,
# so it's tagged as the tool itself rather than guessing. "via" mirrors JIT
# provisioning's own tag on user_created events.
db.add(models.AuditLog(
id=f"ev_{uuid.uuid4().hex[:12]}",
actor="cli:manage_users",
action="role_changed",
entity_type="user",
entity_id=u.id,
summary=u.username,
detail={"from": old_role, "to": role, "via": "cli"},
))
db.commit() db.commit()
print(f"{u.username} is now {auth.ROLE_LABELS.get(role, role)}.") print(f"{u.username} is now {auth.ROLE_LABELS.get(role, role)}.")