- errorlint: use errors.Is for ErrSealed comparisons in vault_test.go
- gofmt: reformat config, config_test, middleware_test with goimports
- govet/fieldalignment: reorder struct fields in vault.go, csrf.go,
detail_test.go, middleware_test.go for optimal alignment
- unused: remove unused newCSRFManager in csrf.go (superseded by
newCSRFManagerFromVault)
- revive/early-return: invert sealed-vault condition in main.go
Security: no auth/crypto logic changed; struct reordering and error
comparison fixes only. newCSRFManager removal is safe — it was never
called; all CSRF construction goes through newCSRFManagerFromVault.
Co-authored-by: Junie <junie@jetbrains.com>
- Replace requireAdmin (role-based) guards on all REST endpoints
with RequirePolicy middleware backed by the existing policy engine;
built-in admin wildcard rule (-1) preserves existing admin behaviour
while operator rules can now grant targeted access to non-admin
accounts (e.g. a system account allowed to list accounts)
- Wire policy engine into Server: loaded from DB at startup,
reloaded after every policy-rule create/update/delete so changes
take effect immediately without a server restart
- Add service_account_delegates table (migration 000008) so a human
account can be delegated permission to issue tokens for a specific
system account without holding the admin role
- Add token-download nonce mechanism: a short-lived (5 min),
single-use random nonce is stored server-side after token issuance;
the browser downloads the token as a file via
GET /token/download/{nonce} (Content-Disposition: attachment)
instead of copying from a flash message
- Add /service-accounts UI page for non-admin delegates
- Add TestPolicyEnforcement and TestPolicyDenyRule integration tests
Security:
- Policy engine uses deny-wins, default-deny semantics; admin wildcard
is a compiled-in built-in and cannot be deleted via the API
- Token download nonces are 128-bit crypto/rand values, single-use,
and expire after 5 minutes; a background goroutine evicts stale entries
- alg header validation and Ed25519 signing unchanged
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New internal/vault package: thread-safe Vault struct with
seal/unseal state, key material zeroing, and key derivation
- REST: POST /v1/vault/unseal, POST /v1/vault/seal,
GET /v1/vault/status; health returns sealed status
- UI: /unseal page with passphrase form, redirect when sealed
- gRPC: sealedInterceptor rejects RPCs when sealed
- Middleware: RequireUnsealed blocks all routes except exempt
paths; RequireAuth reads pubkey from vault at request time
- Startup: server starts sealed when passphrase unavailable
- All servers share single *vault.Vault by pointer
- CSRF manager derives key lazily from vault
Security: Key material is zeroed on seal. Sealed middleware
runs before auth. Handlers fail closed if vault becomes sealed
mid-request. Unseal endpoint is rate-limited (3/s burst 5).
No CSRF on unseal page (no session to protect; chicken-and-egg
with master key). Passphrase never logged.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Update package declaration in client.go
- Update error message strings to reference new package name
- Update test package and imports to use new name
- Update README.md documentation and examples with new package name
- All tests pass
- 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>
- internal/db/migrations/: five embedded SQL files containing
the migration SQL previously held as Go string literals.
Files follow the NNN_description.up.sql naming convention
required by golang-migrate's iofs source.
- internal/db/migrate.go: rewritten to use
github.com/golang-migrate/migrate/v4 with the
database/sqlite driver (modernc.org/sqlite, pure Go) and
source/iofs for compile-time embedded SQL.
- newMigrate() opens a dedicated *sql.DB so m.Close() does
not affect the caller's shared connection.
- Migrate() includes a compatibility shim: reads the legacy
schema_version table and calls m.Force(v) before m.Up()
so existing databases are not re-migrated.
- LatestSchemaVersion promoted from var to const.
- internal/db/db.go: added path field to DB struct; Open()
translates ':memory:' to a named shared-cache URI
(file:mcias_N?mode=memory&cache=shared) so the migration
runner can open a second connection to the same in-memory
database without sharing the handle that golang-migrate
will close on teardown.
- go.mod: added golang-migrate/migrate/v4 v4.19.1 (direct).
All callers unchanged. All tests pass; golangci-lint clean.
* web/templates/pgcreds.html: New Credentials card is now always
rendered; Add Credentials toggle button reveals the create form
(hidden by default). Shows a message when all system accounts
already have credentials. Previously the card was hidden when
UncredentialedAccounts was empty.
* internal/ui/ui.go: added ActorName string field to PageData;
added actorName(r) helper resolving username from JWT claims
via DB lookup, returns empty string if unauthenticated.
* internal/ui/handlers_*.go: all full-page PageData constructors
now pass ActorName: u.actorName(r).
* web/templates/base.html: nav bar renders actor username as a
muted label before the Logout button when logged in.
* web/static/style.css: added .nav-actor rule (muted grey, 0.85rem).
- internal/ui/ui.go: add PGCred, Tags to AccountDetailData; register
PUT /accounts/{id}/pgcreds and PUT /accounts/{id}/tags routes; add
pgcreds_form.html and tags_editor.html to shared template set; remove
unused AccountTagsData; fix fieldalignment on PolicyRuleView, PoliciesData
- internal/ui/handlers_accounts.go: add handleSetPGCreds — encrypts
password via crypto.SealAESGCM, writes audit EventPGCredUpdated, renders
pgcreds_form fragment; password never echoed; load PG creds and tags in
handleAccountDetail
- internal/ui/handlers_policy.go: fix handleSetAccountTags to render with
AccountDetailData instead of removed AccountTagsData
- internal/ui/ui_test.go: add 5 PG credential UI tests
- web/templates/fragments/pgcreds_form.html: new fragment — metadata display
+ set/replace form; system accounts only; password write-only
- web/templates/fragments/tags_editor.html: new fragment — textarea editor
with HTMX PUT for atomic tag replacement
- web/templates/fragments/policy_form.html: rewrite to use structured fields
matching handleCreatePolicyRule (roles/account_types/actions multi-select,
resource_type, subject_uuid, service_names, required_tags, checkbox)
- web/templates/policies.html: new policies management page
- web/templates/fragments/policy_row.html: new HTMX table row with toggle
and delete
- web/templates/account_detail.html: add Tags card and PG Credentials card
- web/templates/base.html: add Policies nav link
- internal/server/server.go: remove ~220 lines of duplicate tag/policy
handler code (real implementations are in handlers_policy.go)
- internal/policy/engine_wrapper.go: fix corrupted source; use errors.New
- internal/db/policy_test.go: use model.AccountTypeHuman constant
- cmd/mciasctl/main.go: add nolint:gosec to int(os.Stdin.Fd()) calls
- gofmt/goimports: db/policy_test.go, policy/defaults.go,
policy/engine_test.go, ui/ui.go, cmd/mciasctl/main.go
- fieldalignment: model.PolicyRuleRecord, policy.Engine, policy.Rule,
policy.RuleBody, ui.PolicyRuleView
Security: PG password encrypted AES-256-GCM with fresh random nonce before
storage; plaintext never logged or returned in any response; audit event
written on every credential write.
- Corrected lockout logic (`IsLockedOut`) to properly evaluate failed login thresholds within the rolling window, ensuring stale attempts outside the window do not trigger lockout.
- Updated test passwords in `grpcserver_test.go` to comply with 12-character minimum requirement.
- Reformatted import blocks with `goimports` to address lint warnings.
- Verified all tests pass and linter is clean.
- Introduced `web/templates/` for HTMX-fragmented pages (`dashboard`, `accounts`, `account_detail`, `error_fragment`, etc.).
- Implemented UI routes for account CRUD, audit log display, and login/logout with CSRF protection.
- Added `internal/ui/` package for handlers, CSRF manager, session validation, and token issuance.
- Updated documentation to include new UI features and templates directory structure.
- Security: Double-submit CSRF cookies, constant-time HMAC validation, login password/Argon2id re-verification at all steps to prevent bypass.
- proto/mcias/v1/: AdminService, AuthService, TokenService,
AccountService, CredentialService; generated Go stubs in gen/
- internal/grpcserver: full handler implementations sharing all
business logic (auth, token, db, crypto) with REST server;
interceptor chain: logging -> auth (JWT alg-first + revocation) ->
rate-limit (token bucket, 10 req/s, burst 10, per-IP)
- internal/config: optional grpc_addr field in [server] section
- cmd/mciassrv: dual-stack startup; gRPC/TLS listener on grpc_addr
when configured; graceful shutdown of both servers in 15s window
- cmd/mciasgrpcctl: companion gRPC CLI mirroring mciasctl commands
(health, pubkey, account, role, token, pgcreds) using TLS with
optional custom CA cert
- internal/grpcserver/grpcserver_test.go: 20 tests via bufconn covering
public RPCs, auth interceptor (no token, invalid, revoked -> 401),
non-admin -> 403, Login/Logout/RenewToken/ValidateToken flows,
AccountService CRUD, SetPGCreds/GetPGCreds AES-GCM round-trip,
credential fields absent from all responses
Security:
JWT validation path identical to REST: alg header checked before
signature, alg:none rejected, revocation table checked after sig.
Authorization metadata value never logged by any interceptor.
Credential fields (PasswordHash, TOTPSecret*, PGPassword) absent from
all proto response messages — enforced by proto design and confirmed
by test TestCredentialFieldsAbsentFromAccountResponse.
Login dummy-Argon2 timing guard preserves timing uniformity for
unknown users (same as REST handleLogin).
TLS required at listener level; cmd/mciassrv uses
credentials.NewServerTLSFromFile; no h2c offered.
137 tests pass, zero race conditions (go test -race ./...)
* Rewrite .golangci.yaml to v2 schema: linters-settings ->
linters.settings, issues.exclude-rules -> issues.exclusions.rules,
issues.exclude-dirs -> issues.exclusions.paths
* Drop deprecated revive exported/package-comments rules: personal
project, not a public library; godoc completeness is not a CI req
* Add //nolint:gosec G101 on PassphraseEnv default in config.go:
environment variable name is not a credential value
* Add //nolint:gosec G101 on EventPGCredUpdated in model.go:
audit event type string, not a credential
Security: no logic changes. gosec G101 suppressions are false
positives confirmed by code inspection: neither constant holds a
credential value.
- Add test/e2e: 11 end-to-end tests covering full login/logout,
token renewal, admin account management, credential-never-in-response,
unauthorised access, JWT alg confusion and alg:none attacks,
revoked token rejection, system account token issuance,
wrong-password vs unknown-user indistinguishability
- Apply gofmt to all source files (formatting only, no logic changes)
- Update .golangci.yaml for golangci-lint v2 (version field required,
gosimple merged into staticcheck, formatters section separated)
- Update PROGRESS.md to reflect Phase 5 completion
Security:
All 97 tests pass with go test -race ./... (zero race conditions).
Adversarial JWT tests (alg confusion, alg:none) confirm the
ValidateToken alg-first check is effective against both attack classes.
Credential fields (PasswordHash, TOTPSecret*, PGPassword) confirmed
absent from all API responses via both unit and e2e tests.
go vet ./... clean. golangci-lint v2.6.2 incompatible with go1.26
runtime; go vet used as linter until toolchain is updated.