UI: password change enforcement + migration recovery

- Web UI admin password reset now enforces admin role
  server-side (was cookie-auth + CSRF only; any logged-in
  user could previously reset any account's password)
- Added self-service password change UI at GET/PUT /profile:
  current_password + new_password + confirm_password;
  server-side equality check; lockout + Argon2id verification;
  revokes all other sessions on success
- password_change_form.html fragment and profile.html page
- Nav bar actor name now links to /profile
- policy: ActionChangePassword + default rule -7 allowing
  human accounts to change their own password
- openapi.yaml: built-in rules count updated to -7

Migration recovery:
- mciasdb schema force --version N: new subcommand to clear
  dirty migration state without running SQL (break-glass)
- schema subcommands bypass auto-migration on open so the
  tool stays usable when the database is dirty
- Migrate(): shim no longer overrides schema_migrations
  when it already has an entry; duplicate-column error on
  the latest migration is force-cleaned and treated as
  success (handles columns added outside the runner)

Security:
- Admin role is now validated in handleAdminResetPassword
  before any DB access; non-admin receives 403
- handleSelfChangePassword follows identical lockout +
  constant-time Argon2id path as the REST self-service
  handler; current password required to prevent
  token-theft account takeover

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 15:33:19 -07:00
parent a9ebeb2ba1
commit f262ca7b4e
13 changed files with 412 additions and 24 deletions

View File

@@ -4,6 +4,59 @@ Source of truth for current development state.
---
All phases complete. **v1.0.0 tagged.** All packages pass `go test ./...`; `golangci-lint run ./...` clean.
### 2026-03-12 — Checkpoint: password change UI enforcement + migration recovery
**internal/ui/handlers_accounts.go**
- `handleAdminResetPassword`: added server-side admin role check at the top of
the handler; any authenticated non-admin calling this route now receives 403.
Previously only cookie validity + CSRF were checked.
**internal/ui/handlers_auth.go**
- Added `handleProfilePage`: renders the new `/profile` page for any
authenticated user.
- Added `handleSelfChangePassword`: self-service password change for non-admin
users; validates current password (Argon2id, lockout-checked), enforces
server-side confirmation equality check, hashes new password, revokes all
other sessions, audits as `{"via":"ui_self_service"}`.
**internal/ui/ui.go**
- Added `ProfileData` view model.
- Registered `GET /profile` and `PUT /profile/password` routes (cookie auth +
CSRF; no admin role required).
- Added `password_change_form.html` to shared template list; added `profile`
page template.
- Nav bar actor-name span changed to a link pointing to `/profile`.
**web/templates/fragments/password_change_form.html** (new)
- HTMX form with `current_password`, `new_password`, `confirm_password` fields.
- Client-side JS confirmation guard; server-side equality check in handler.
**web/templates/profile.html** (new)
- Profile page hosting the self-service password change form.
**internal/db/migrate.go**
- Compatibility shim now only calls `m.Force(legacyVersion)` when
`schema_migrations` is completely empty (`ErrNilVersion`); leaves existing
version entries (including dirty ones) alone to prevent re-running already-
attempted migrations.
- Added duplicate-column-name recovery: when `m.Up()` fails with "duplicate
column name" and the dirty version equals `LatestSchemaVersion`, the migrator
is force-cleaned and returns nil (handles databases where columns were added
outside the runner before migration 006 existed).
- Added `ForceSchemaVersion(database *DB, version int) error`: break-glass
exported function; forces golang-migrate version without running SQL.
**cmd/mciasdb/schema.go**
- Added `schema force --version N` subcommand backed by `db.ForceSchemaVersion`.
**cmd/mciasdb/main.go**
- `schema` commands now open the database via `openDBRaw` (no auto-migration)
so the tool stays usable when the database is in a dirty migration state.
- `openDB` refactored to call `openDBRaw` then `db.Migrate`.
- Updated usage text.
All tests pass; `golangci-lint run ./...` clean.
### 2026-03-12 — Password change: self-service and admin reset
Added the ability for users to change their own password and for admins to
@@ -394,9 +447,10 @@ All tests pass (`go test ./...`); `golangci-lint run ./...` reports 0 issues.
- `engine.go``Evaluate(input, operatorRules) (Effect, *Rule)`: pure function;
merges operator rules with default rules, sorts by priority, deny-wins,
then first allow, then default-deny
- `defaults.go`6 compiled-in rules (IDs -1 to -6, Priority 0): admin
wildcard, self-service logout/renew, self-service TOTP, system account own
pgcreds, system account own service token, public login/validate endpoints
- `defaults.go`7 compiled-in rules (IDs -1 to -7, Priority 0): admin
wildcard, self-service logout/renew, self-service TOTP, self-service password
change (human only), system account own pgcreds, system account own service
token, public login/validate endpoints
- `engine_wrapper.go``Engine` struct with `sync.RWMutex`; `SetRules()`
decodes DB records; `PolicyRecord` type avoids import cycle
- `engine_test.go` — 11 tests: DefaultDeny, AdminWildcard, SelfService*,