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.
This commit is contained in:
2026-09-03 16:27:13 -07:00
parent dc13f9b0e3
commit cc64c88c3e
2 changed files with 36 additions and 0 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