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:
159
PROGRESS.md
159
PROGRESS.md
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user