Checkpoint: password reset, rule expiry, migrations
- Self-service and admin password-change endpoints
(PUT /v1/auth/password, PUT /v1/accounts/{id}/password)
- Policy rule time-scoped expiry (not_before / expires_at)
with migration 000006 and engine filtering
- golang-migrate integration; embedded SQL migrations
- PolicyRecord fieldalignment lint fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
193
PROGRESS.md
193
PROGRESS.md
@@ -4,6 +4,156 @@ 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 — Password change: self-service and admin reset
|
||||
|
||||
Added the ability for users to change their own password and for admins to
|
||||
reset any human account's password.
|
||||
|
||||
**Two new REST endpoints:**
|
||||
|
||||
- `PUT /v1/auth/password` — self-service: authenticated user changes their own
|
||||
password; requires `current_password` for verification; revokes all tokens
|
||||
except the caller's current session on success.
|
||||
- `PUT /v1/accounts/{id}/password` — admin reset: no current password needed;
|
||||
revokes all active sessions for the target account.
|
||||
|
||||
**internal/model/model.go**
|
||||
- Added `EventPasswordChanged = "password_changed"` audit event constant.
|
||||
|
||||
**internal/db/accounts.go**
|
||||
- Added `RevokeAllUserTokensExcept(accountID, exceptJTI, reason)`: revokes all
|
||||
non-expired tokens for an account except one specific JTI (used by the
|
||||
self-service flow to preserve the caller's session).
|
||||
|
||||
**internal/server/server.go**
|
||||
- `handleAdminSetPassword`: admin password reset handler; validates new
|
||||
password, hashes with Argon2id, revokes all target tokens, writes audit event.
|
||||
- `handleChangePassword`: self-service handler; verifies current password with
|
||||
Argon2id (same lockout/timing path as login), hashes new password, revokes
|
||||
all other tokens, clears failure counter.
|
||||
- Both routes registered in `Handler()`.
|
||||
|
||||
**internal/ui/handlers_accounts.go**
|
||||
- `handleAdminResetPassword`: web UI counterpart to the admin REST handler;
|
||||
renders `password_reset_result` fragment on success.
|
||||
|
||||
**internal/ui/ui.go**
|
||||
- `PUT /accounts/{id}/password` route registered with admin+CSRF middleware.
|
||||
- `templates/fragments/password_reset_form.html` added to shared template list.
|
||||
|
||||
**web/templates/fragments/password_reset_form.html** (new)
|
||||
- HTMX form fragment for the admin password reset UI.
|
||||
- `password_reset_result` template shows a success flash message followed by
|
||||
the reset form.
|
||||
|
||||
**web/templates/account_detail.html**
|
||||
- Added "Reset Password" card (human accounts only) using the new fragment.
|
||||
|
||||
**cmd/mciasctl/main.go**
|
||||
- `auth change-password`: self-service password change; both passwords always
|
||||
prompted interactively (no flag form — prevents shell-history exposure).
|
||||
- `account set-password -id UUID`: admin reset; new password always prompted
|
||||
interactively (no flag form).
|
||||
- `auth login`: `-password` flag removed; password always prompted.
|
||||
- `account create`: `-password` flag removed; password always prompted for
|
||||
human accounts.
|
||||
- All passwords read via `term.ReadPassword` (terminal echo disabled); raw
|
||||
byte slices zeroed after use.
|
||||
|
||||
**openapi.yaml + web/static/openapi.yaml**
|
||||
- `PUT /v1/auth/password`: self-service endpoint documented (Auth tag).
|
||||
- `PUT /v1/accounts/{id}/password`: admin reset documented (Admin — Accounts
|
||||
tag).
|
||||
|
||||
**ARCHITECTURE.md**
|
||||
- API endpoint tables updated with both new endpoints.
|
||||
- New "Password Change Flows" section in §6 (Session Management) documents the
|
||||
self-service and admin flows, their security properties, and differences.
|
||||
|
||||
All tests pass; golangci-lint clean.
|
||||
|
||||
### 2026-03-12 — Checkpoint: fix fieldalignment lint warning
|
||||
|
||||
**internal/policy/engine_wrapper.go**
|
||||
- Reordered `PolicyRecord` fields: `*time.Time` pointer fields moved before
|
||||
string fields, shrinking the GC pointer-scan bitmap from 56 to 40 bytes
|
||||
(govet fieldalignment)
|
||||
|
||||
All tests pass; `golangci-lint run ./...` clean.
|
||||
|
||||
### 2026-03-12 — Add time-scoped policy rule expiry
|
||||
|
||||
Policy rules now support optional `not_before` and `expires_at` fields for
|
||||
time-limited validity windows. Rules outside their validity window are
|
||||
automatically excluded at cache-load time (`Engine.SetRules`).
|
||||
|
||||
**internal/db/migrations/000006_policy_rule_expiry.up.sql** (new)
|
||||
- `ALTER TABLE policy_rules ADD COLUMN not_before TEXT DEFAULT NULL`
|
||||
- `ALTER TABLE policy_rules ADD COLUMN expires_at TEXT DEFAULT NULL`
|
||||
|
||||
**internal/db/migrate.go**
|
||||
- `LatestSchemaVersion` bumped from 5 to 6
|
||||
|
||||
**internal/model/model.go**
|
||||
- Added `NotBefore *time.Time` and `ExpiresAt *time.Time` to `PolicyRuleRecord`
|
||||
|
||||
**internal/db/policy.go**
|
||||
- `policyRuleCols` updated with `not_before, expires_at`
|
||||
- `CreatePolicyRule`: new params `notBefore, expiresAt *time.Time`
|
||||
- `UpdatePolicyRule`: new params `notBefore, expiresAt **time.Time` (double-pointer
|
||||
for three-state semantics: nil=no change, non-nil→nil=clear, non-nil→value=set)
|
||||
- `finishPolicyRuleScan`: extended to populate `NotBefore`/`ExpiresAt` via
|
||||
`nullableTime()`
|
||||
- Added `formatNullableTime(*time.Time) *string` helper
|
||||
|
||||
**internal/policy/engine_wrapper.go**
|
||||
- Added `NotBefore *time.Time` and `ExpiresAt *time.Time` to `PolicyRecord`
|
||||
- `SetRules`: filters out rules where `not_before > now()` or `expires_at <= now()`
|
||||
after the existing `Enabled` check
|
||||
|
||||
**internal/server/handlers_policy.go**
|
||||
- `policyRuleResponse`: added `not_before` and `expires_at` (RFC3339, omitempty)
|
||||
- `createPolicyRuleRequest`: added `not_before` and `expires_at`
|
||||
- `updatePolicyRuleRequest`: added `not_before`, `expires_at`,
|
||||
`clear_not_before`, `clear_expires_at`
|
||||
- `handleCreatePolicyRule`: parses/validates RFC3339 times; rejects
|
||||
`expires_at <= not_before`
|
||||
- `handleUpdatePolicyRule`: parses times, handles clear booleans via
|
||||
double-pointer pattern
|
||||
|
||||
**internal/ui/**
|
||||
- `PolicyRuleView`: added `NotBefore`, `ExpiresAt`, `IsExpired`, `IsPending`
|
||||
- `policyRuleToView`: populates time fields and computes expired/pending status
|
||||
- `handleCreatePolicyRule`: parses `datetime-local` form inputs for time fields
|
||||
|
||||
**web/templates/fragments/**
|
||||
- `policy_form.html`: added `datetime-local` inputs for not_before and expires_at
|
||||
- `policy_row.html`: shows time info and expired/scheduled badges
|
||||
|
||||
**cmd/mciasctl/main.go**
|
||||
- `policyCreate`: added `-not-before` and `-expires-at` flags (RFC3339)
|
||||
- `policyUpdate`: added `-not-before`, `-expires-at`, `-clear-not-before`,
|
||||
`-clear-expires-at` flags
|
||||
|
||||
**openapi.yaml**
|
||||
- `PolicyRule` schema: added `not_before` and `expires_at` (nullable date-time)
|
||||
- Create request: added `not_before` and `expires_at`
|
||||
- Update request: added `not_before`, `expires_at`, `clear_not_before`,
|
||||
`clear_expires_at`
|
||||
|
||||
**Tests**
|
||||
- `internal/db/policy_test.go`: 5 new tests — `WithExpiresAt`, `WithNotBefore`,
|
||||
`WithBothTimes`, `SetExpiresAt`, `ClearExpiresAt`; all existing tests updated
|
||||
with new `CreatePolicyRule`/`UpdatePolicyRule` signatures
|
||||
- `internal/policy/engine_test.go`: 4 new tests — `SkipsExpiredRule`,
|
||||
`SkipsNotYetActiveRule`, `IncludesActiveWindowRule`, `NilTimesAlwaysActive`
|
||||
|
||||
**ARCHITECTURE.md**
|
||||
- Schema: added `not_before` and `expires_at` columns to `policy_rules` DDL
|
||||
- Added Scenario D (time-scoped access) to §20
|
||||
|
||||
All new and existing policy tests pass; no new lint warnings.
|
||||
|
||||
### 2026-03-12 — Integrate golang-migrate for database migrations
|
||||
|
||||
**internal/db/migrations/** (new directory — 5 embedded SQL files)
|
||||
@@ -232,7 +382,7 @@ All tests pass (`go test ./...`); `golangci-lint run ./...` reports 0 issues.
|
||||
- [x] Phase 6: mciasdb — direct SQLite maintenance tool
|
||||
- [x] Phase 7: gRPC interface (alternate transport; dual-stack with REST)
|
||||
- [x] Phase 8: Operational artifacts (Makefile, Dockerfile, systemd, man pages, install script)
|
||||
- [x] Phase 9: Client libraries (Go, Rust, Common Lisp, Python)
|
||||
- [ ] Phase 9: Client libraries (Go, Rust, Common Lisp, Python) — designed in ARCHITECTURE.md §19 but not yet implemented; `clients/` directory does not exist
|
||||
- [x] Phase 10: Policy engine — ABAC with machine/service gating
|
||||
---
|
||||
### 2026-03-11 — Phase 10: Policy engine (ABAC + machine/service gating)
|
||||
@@ -336,44 +486,15 @@ All tests pass; `go test ./...` clean; `golangci-lint run ./...` clean.
|
||||
|
||||
All 5 packages pass `go test ./...`; `golangci-lint run ./...` clean.
|
||||
|
||||
### 2026-03-11 — Phase 9: Client libraries
|
||||
### 2026-03-11 — Phase 9: Client libraries (DESIGNED, NOT IMPLEMENTED)
|
||||
|
||||
**clients/testdata/** — shared JSON fixtures
|
||||
- login_response.json, account_response.json, accounts_list_response.json
|
||||
- validate_token_response.json, public_key_response.json, pgcreds_response.json
|
||||
- error_response.json, roles_response.json
|
||||
|
||||
**clients/go/** — Go client library
|
||||
- Module: `git.wntrmute.dev/kyle/mcias/clients/go`; package `mciasgoclient`
|
||||
- Typed errors: `MciasAuthError`, `MciasForbiddenError`, `MciasNotFoundError`,
|
||||
`MciasInputError`, `MciasConflictError`, `MciasServerError`
|
||||
- TLS 1.2+ enforced via `tls.Config{MinVersion: tls.VersionTLS12}`
|
||||
- Token state guarded by `sync.RWMutex` for concurrent safety
|
||||
- JSON decoded with `DisallowUnknownFields` on all responses
|
||||
- 25 tests in `client_test.go`; all pass with `go test -race`
|
||||
|
||||
**clients/rust/** — Rust async client library
|
||||
- Crate: `mcias-client`; tokio async, reqwest + rustls-tls (no OpenSSL dep)
|
||||
- `MciasError` enum via `thiserror`; `Arc<RwLock<Option<String>>>` for token
|
||||
- 23 integration tests using `wiremock`; `cargo clippy -- -D warnings` clean
|
||||
|
||||
**clients/lisp/** — Common Lisp client library
|
||||
- ASDF system `mcias-client`; HTTP via dexador, JSON via yason
|
||||
- CLOS class `mcias-client`; plain functions for all operations
|
||||
- Conditions: `mcias-error` base + 6 typed subclasses
|
||||
- Mock server: Hunchentoot `mock-dispatcher` subclass (port 0, random per test)
|
||||
- 37 fiveam checks; all pass on SBCL 2.6.1
|
||||
- Fixed: yason decodes JSON `false` as `:false`; `validate-token` normalises
|
||||
to `t`/`nil` before returning
|
||||
|
||||
**clients/python/** — Python 3.11+ client library
|
||||
- Package `mcias_client` (setuptools, pyproject.toml); dep: `httpx >= 0.27`
|
||||
- `Client` context manager; `py.typed` marker; all symbols fully annotated
|
||||
- Dataclasses: `Account`, `PublicKey`, `PGCreds`
|
||||
- 32 pytest tests using `respx` mock transport; `mypy --strict` clean; `ruff` clean
|
||||
**NOTE:** The client libraries described in ARCHITECTURE.md §19 were designed
|
||||
but never committed to the repository. The `clients/` directory does not exist.
|
||||
Only `test/mock/mockserver.go` was implemented. The designs remain in
|
||||
ARCHITECTURE.md for future implementation.
|
||||
|
||||
**test/mock/mockserver.go** — Go in-memory mock server
|
||||
- `Server` struct with `sync.RWMutex`; used by Go client integration test
|
||||
- `Server` struct with `sync.RWMutex`; used for Go integration tests
|
||||
- `NewServer()`, `AddAccount()`, `ServeHTTP()` for httptest.Server use
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user