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:
326
openapi.yaml
326
openapi.yaml
@@ -118,6 +118,103 @@ components:
|
||||
description: JSON blob with event-specific metadata. Never contains credentials.
|
||||
example: '{"jti":"f47ac10b-..."}'
|
||||
|
||||
TagsResponse:
|
||||
type: object
|
||||
required: [tags]
|
||||
properties:
|
||||
tags:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Current tag list for the account.
|
||||
example: ["env:production", "svc:payments-api"]
|
||||
|
||||
RuleBody:
|
||||
type: object
|
||||
required: [effect]
|
||||
description: |
|
||||
The match conditions and effect of a policy rule. All fields except
|
||||
`effect` are optional; an omitted field acts as a wildcard.
|
||||
properties:
|
||||
effect:
|
||||
type: string
|
||||
enum: [allow, deny]
|
||||
example: allow
|
||||
roles:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Subject must have at least one of these roles.
|
||||
example: ["svc:payments-api"]
|
||||
account_types:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
enum: [human, system]
|
||||
description: Subject account type must be one of these.
|
||||
example: ["system"]
|
||||
subject_uuid:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Match only this specific subject UUID.
|
||||
example: 550e8400-e29b-41d4-a716-446655440000
|
||||
actions:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: |
|
||||
One of the defined action constants, e.g. `pgcreds:read`,
|
||||
`accounts:list`. Subject action must be in this list.
|
||||
example: ["pgcreds:read"]
|
||||
resource_type:
|
||||
type: string
|
||||
description: Resource type the rule applies to.
|
||||
example: pgcreds
|
||||
owner_matches_subject:
|
||||
type: boolean
|
||||
description: Resource owner UUID must equal the subject UUID.
|
||||
example: true
|
||||
service_names:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Resource service name must be one of these.
|
||||
example: ["payments-api"]
|
||||
required_tags:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Resource must have ALL of these tags.
|
||||
example: ["env:staging"]
|
||||
|
||||
PolicyRule:
|
||||
type: object
|
||||
required: [id, priority, description, rule, enabled, created_at, updated_at]
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
example: 1
|
||||
priority:
|
||||
type: integer
|
||||
description: Lower number = evaluated first.
|
||||
example: 100
|
||||
description:
|
||||
type: string
|
||||
example: Allow payments-api to read its own pgcreds
|
||||
rule:
|
||||
$ref: "#/components/schemas/RuleBody"
|
||||
enabled:
|
||||
type: boolean
|
||||
example: true
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
example: "2026-03-11T09:00:00Z"
|
||||
updated_at:
|
||||
type: string
|
||||
format: date-time
|
||||
example: "2026-03-11T09:00:00Z"
|
||||
|
||||
PGCreds:
|
||||
type: object
|
||||
required: [host, port, database, username, password]
|
||||
@@ -948,6 +1045,233 @@ paths:
|
||||
"403":
|
||||
$ref: "#/components/responses/Forbidden"
|
||||
|
||||
/v1/accounts/{id}/tags:
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
example: 550e8400-e29b-41d4-a716-446655440000
|
||||
|
||||
get:
|
||||
summary: Get account tags (admin)
|
||||
description: |
|
||||
Return the current tag set for an account. Tags are used by the policy
|
||||
engine for machine/service gating (e.g. `env:production`,
|
||||
`svc:payments-api`).
|
||||
operationId: getAccountTags
|
||||
tags: [Admin — Policy]
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
"200":
|
||||
description: Tag list.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/TagsResponse"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"403":
|
||||
$ref: "#/components/responses/Forbidden"
|
||||
"404":
|
||||
$ref: "#/components/responses/NotFound"
|
||||
|
||||
put:
|
||||
summary: Set account tags (admin)
|
||||
description: |
|
||||
Replace the account's full tag set atomically. Pass an empty array to
|
||||
clear all tags. Changes take effect immediately for new policy
|
||||
evaluations; no token renewal is required.
|
||||
operationId: setAccountTags
|
||||
tags: [Admin — Policy]
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [tags]
|
||||
properties:
|
||||
tags:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
example: ["env:production", "svc:payments-api"]
|
||||
responses:
|
||||
"200":
|
||||
description: Updated tag list.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/TagsResponse"
|
||||
"400":
|
||||
$ref: "#/components/responses/BadRequest"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"403":
|
||||
$ref: "#/components/responses/Forbidden"
|
||||
"404":
|
||||
$ref: "#/components/responses/NotFound"
|
||||
|
||||
/v1/policy/rules:
|
||||
get:
|
||||
summary: List policy rules (admin)
|
||||
description: |
|
||||
Return all operator-defined policy rules ordered by priority (ascending).
|
||||
Built-in default rules (IDs -1 to -6) are not included.
|
||||
operationId: listPolicyRules
|
||||
tags: [Admin — Policy]
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
"200":
|
||||
description: Array of policy rules.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/PolicyRule"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"403":
|
||||
$ref: "#/components/responses/Forbidden"
|
||||
|
||||
post:
|
||||
summary: Create policy rule (admin)
|
||||
description: |
|
||||
Create a new operator policy rule. Rules are evaluated in priority order
|
||||
(lower number = evaluated first, default 100). Deny-wins: if any matching
|
||||
rule has effect `deny`, access is denied regardless of allow rules.
|
||||
operationId: createPolicyRule
|
||||
tags: [Admin — Policy]
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [description, rule]
|
||||
properties:
|
||||
description:
|
||||
type: string
|
||||
example: Allow payments-api to read its own pgcreds
|
||||
priority:
|
||||
type: integer
|
||||
description: Evaluation priority. Lower = first. Default 100.
|
||||
example: 50
|
||||
rule:
|
||||
$ref: "#/components/schemas/RuleBody"
|
||||
responses:
|
||||
"201":
|
||||
description: Rule created.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/PolicyRule"
|
||||
"400":
|
||||
$ref: "#/components/responses/BadRequest"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"403":
|
||||
$ref: "#/components/responses/Forbidden"
|
||||
|
||||
/v1/policy/rules/{id}:
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
example: 1
|
||||
|
||||
get:
|
||||
summary: Get policy rule (admin)
|
||||
operationId: getPolicyRule
|
||||
tags: [Admin — Policy]
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
"200":
|
||||
description: Policy rule.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/PolicyRule"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"403":
|
||||
$ref: "#/components/responses/Forbidden"
|
||||
"404":
|
||||
$ref: "#/components/responses/NotFound"
|
||||
|
||||
patch:
|
||||
summary: Update policy rule (admin)
|
||||
description: |
|
||||
Update one or more fields of an existing policy rule. All fields are
|
||||
optional; omitted fields are left unchanged.
|
||||
operationId: updatePolicyRule
|
||||
tags: [Admin — Policy]
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
description:
|
||||
type: string
|
||||
example: Updated description
|
||||
priority:
|
||||
type: integer
|
||||
example: 75
|
||||
enabled:
|
||||
type: boolean
|
||||
example: false
|
||||
rule:
|
||||
$ref: "#/components/schemas/RuleBody"
|
||||
responses:
|
||||
"200":
|
||||
description: Updated rule.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/PolicyRule"
|
||||
"400":
|
||||
$ref: "#/components/responses/BadRequest"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"403":
|
||||
$ref: "#/components/responses/Forbidden"
|
||||
"404":
|
||||
$ref: "#/components/responses/NotFound"
|
||||
|
||||
delete:
|
||||
summary: Delete policy rule (admin)
|
||||
description: Permanently delete a policy rule. This action cannot be undone.
|
||||
operationId: deletePolicyRule
|
||||
tags: [Admin — Policy]
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
"204":
|
||||
description: Rule deleted.
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"403":
|
||||
$ref: "#/components/responses/Forbidden"
|
||||
"404":
|
||||
$ref: "#/components/responses/NotFound"
|
||||
|
||||
tags:
|
||||
- name: Public
|
||||
description: No authentication required.
|
||||
@@ -963,3 +1287,5 @@ tags:
|
||||
description: Requires admin role.
|
||||
- name: Admin — Audit
|
||||
description: Requires admin role.
|
||||
- name: Admin — Policy
|
||||
description: Requires admin role. Manage policy rules and account tags.
|
||||
|
||||
Reference in New Issue
Block a user