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
(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
`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
@@ -183,10 +201,33 @@ Depends only on `main` as it stands after `D15`. Not sequenced behind any other
## Still open
- The OIDC claim mapping (`T10.3`).
- Final confirmation of the redirect/callback URI (`https://wp.controls.dev/api/auth/okta/callback`
proposed, pending security).
- The `Business Technology Group` pilot assignment in Okta.
- The `Business Technology Group` pilot assignment in Okta. Originally six names
(Carlee Swihart, Drew Hilliard, Matt Mabrey, Nick Siegfried, Rachel Schreiber, Terry
Sajan); Cody and Cameron added 2026-09-09. Adrian added only Matt at first,
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
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 sys
import uuid
from .db import SessionLocal, Base, engine
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"doesn't create one — they need to sign in through Okta at least once first."
)
old_role = u.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()
print(f"{u.username} is now {auth.ROLE_LABELS.get(role, role)}.")