Add PG creds + policy/tags UI; fix lint and build

- 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.
This commit is contained in:
2026-03-11 23:24:03 -07:00
parent 5a8698e199
commit 052d3ed1b8
27 changed files with 3609 additions and 10 deletions

View File

@@ -2,9 +2,73 @@
Source of truth for current development state.
---
All phases complete. 137 Go server tests + 25 Go client tests + 23 Rust client
tests + 37 Lisp client tests + 32 Python client tests pass. Zero race
conditions (go test -race ./...).
All phases complete. Tests: all packages pass `go test ./...`; `golangci-lint run ./...` clean.
### 2026-03-11 — Postgres Credentials UI + Policy/Tags UI completion
**internal/ui/**
- `handlers_accounts.go`: added `handleSetPGCreds` — validates form fields,
encrypts password via `crypto.SealAESGCM` with fresh nonce, calls
`db.WritePGCredentials`, writes `EventPGCredUpdated` audit entry, re-reads and
renders `pgcreds_form` fragment; password never echoed in response
- `handlers_accounts.go`: updated `handleAccountDetail` to load PG credentials
for system accounts (non-fatal on `ErrNotFound`) and account tags for all
accounts
- `handlers_policy.go`: fixed `handleSetAccountTags` to render with
`AccountDetailData` (removed `AccountTagsData`); field ordering fixed for
`fieldalignment` linter
- `ui.go`: added `PGCred *model.PGCredential` and `Tags []string` to
`AccountDetailData`; added `pgcreds_form.html` and `tags_editor.html` to
shared template set; registered `PUT /accounts/{id}/pgcreds` and
`PUT /accounts/{id}/tags` routes; removed unused `AccountTagsData` struct;
field alignment fixed on `PolicyRuleView`, `PoliciesData`, `AccountDetailData`
- `ui_test.go`: added 5 new PG credential tests:
`TestSetPGCredsRejectsHumanAccount`, `TestSetPGCredsStoresAndDisplaysMetadata`,
`TestSetPGCredsPasswordNotEchoed`, `TestSetPGCredsRequiresPassword`,
`TestAccountDetailShowsPGCredsSection`
**web/templates/**
- `fragments/pgcreds_form.html` (new): displays current credential metadata
(host:port, database, username, updated-at — no password); includes HTMX
`hx-put` form for set/replace; system accounts only
- `fragments/tags_editor.html` (new): newline-separated tag textarea with HTMX
`hx-put` for atomic replacement; uses `.Account.UUID` for URL
- `fragments/policy_form.html`: rewritten to use structured fields matching
`handleCreatePolicyRule` parser: `description`, `priority`, `effect` (select),
`roles`/`account_types`/`actions` (multi-select), `resource_type`, `subject_uuid`,
`service_names`, `required_tags`, `owner_matches_subject` (checkbox)
- `policies.html` (new): policies management page with create-form toggle and
rules table (`id="policies-tbody"`)
- `fragments/policy_row.html` (new): HTMX table row with enable/disable toggle
(`hx-patch`) and delete button (`hx-delete`)
- `account_detail.html`: added Tags card (all accounts) and Postgres Credentials
card (system accounts only)
- `base.html`: added Policies nav link
**internal/server/server.go**
- Removed ~220 lines of duplicate tag and policy handler code that had been
inadvertently added; all real implementations live in `handlers_policy.go`
**internal/policy/engine_wrapper.go**
- Fixed corrupted source file (invisible character preventing `fmt` usage from
being recognized); rewrote to use `errors.New` for the denial error
**internal/db/policy_test.go**
- Fixed `CreateAccount` call using string literal `"human"``model.AccountTypeHuman`
**cmd/mciasctl/main.go**
- Added `//nolint:gosec` to three `int(os.Stdin.Fd())` conversions (safe:
uintptr == int on all target platforms; `term.ReadPassword` requires `int`)
**Linter fixes (all packages)**
- gofmt/goimports applied to `internal/db/policy_test.go`,
`internal/policy/defaults.go`, `internal/policy/engine_test.go`, `internal/ui/ui.go`
- fieldalignment fixed on `model.PolicyRuleRecord`, `policy.Engine`,
`policy.Rule`, `policy.RuleBody`, `ui.PolicyRuleView`
All tests pass (`go test ./...`); `golangci-lint run ./...` reports 0 issues.
---
- [x] Phase 0: Repository bootstrap (go.mod, .gitignore, docs)
- [x] Phase 1: Foundational packages (model, config, crypto, db)
- [x] Phase 2: Auth core (auth, token, middleware)
@@ -15,7 +79,96 @@ conditions (go test -race ./...).
- [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)
- [x] Phase 10: Policy engine — ABAC with machine/service gating
---
### 2026-03-11 — Phase 10: Policy engine (ABAC + machine/service gating)
**internal/policy/** (new package)
- `policy.go` — types: `Action`, `ResourceType`, `Effect`, `Resource`,
`PolicyInput`, `Rule`, `RuleBody`; 22 Action constants covering all API
operations
- `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
- `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*,
SystemOwn*, DenyWins, ServiceNameGating, MachineTagGating,
OwnerMatchesSubject, PriorityOrder, MultipleRequiredTags, AccountTypeGating
**internal/db/**
- `migrate.go`: migration id=4 — `account_tags` (account_id+tag PK, FK cascade)
and `policy_rules` (id, priority, description, rule_json, enabled,
created_by, timestamps) tables
- `tags.go` (new): `GetAccountTags`, `AddAccountTag`, `RemoveAccountTag`,
`SetAccountTags` (atomic DELETE+INSERT transaction); sorted alphabetically
- `policy.go` (new): `CreatePolicyRule`, `GetPolicyRule`, `ListPolicyRules`,
`UpdatePolicyRule`, `SetPolicyRuleEnabled`, `DeletePolicyRule`
- `tags_test.go`, `policy_test.go` (new): comprehensive DB-layer tests
**internal/model/**
- `PolicyRuleRecord` struct added
- New audit event constants: `EventTagAdded`, `EventTagRemoved`,
`EventPolicyRuleCreated`, `EventPolicyRuleUpdated`, `EventPolicyRuleDeleted`,
`EventPolicyDeny`
**internal/middleware/**
- `RequirePolicy` middleware: assembles `PolicyInput` from JWT claims +
`AccountTypeLookup` closure (DB-backed, avoids JWT schema change) +
`ResourceBuilder` closure; calls `engine.Evaluate`; logs deny via
`PolicyDenyLogger`
**internal/server/**
- New REST endpoints (all require admin):
- `GET/PUT /v1/accounts/{id}/tags`
- `GET/POST /v1/policy/rules`
- `GET/PATCH/DELETE /v1/policy/rules/{id}`
- `handlers_policy.go`: `handleGetTags`, `handleSetTags`, `handleListPolicyRules`,
`handleCreatePolicyRule`, `handleGetPolicyRule`, `handleUpdatePolicyRule`,
`handleDeletePolicyRule`, `policyRuleToResponse`, `loadPolicyRule`
**internal/ui/**
- `handlers_policy.go` (new): `handlePoliciesPage`, `handleCreatePolicyRule`,
`handleTogglePolicyRule`, `handleDeletePolicyRule`, `handleSetAccountTags`
- `ui.go`: registered 5 policy UI routes; added `PolicyRuleView`, `PoliciesData`,
`AccountTagsData` view types; added new fragment templates to shared set
**web/templates/**
- `policies.html` (new): policies management page
- `fragments/policy_row.html` (new): HTMX table row with enable/disable toggle
and delete button
- `fragments/policy_form.html` (new): create form with JSON textarea and action
reference chips
- `fragments/tags_editor.html` (new): newline-separated tag editor with HTMX
PUT for atomic replacement
- `account_detail.html`: added Tags card section using tags_editor fragment
- `base.html`: added Policies nav link
**cmd/mciasctl/**
- `policy` subcommands: `list`, `create -description STR -json FILE [-priority N]`,
`get -id ID`, `update -id ID [-priority N] [-enabled true|false]`,
`delete -id ID`
- `tag` subcommands: `list -id UUID`, `set -id UUID -tags tag1,tag2,...`
**openapi.yaml**
- New schemas: `TagsResponse`, `RuleBody`, `PolicyRule`
- New paths: `GET/PUT /v1/accounts/{id}/tags`,
`GET/POST /v1/policy/rules`, `GET/PATCH/DELETE /v1/policy/rules/{id}`
- New tag: `Admin — Policy`
**Design highlights:**
- Deny-wins + default-deny: explicit Deny beats any Allow; no match = Deny
- AccountType resolved via DB lookup (not JWT) to avoid breaking 29 IssueToken
call sites
- `RequirePolicy` wired alongside `RequireRole("admin")` for belt-and-suspenders
during migration; defaults reproduce current binary behavior exactly
- `policy.PolicyRecord` type avoids circular import between policy/db/model
All tests pass; `go test ./...` clean; `golangci-lint run ./...` clean.
### 2026-03-11 — Fix test failures and lockout logic
- `internal/db/accounts.go` (IsLockedOut): corrected window-expiry check from