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:
141
internal/policy/policy.go
Normal file
141
internal/policy/policy.go
Normal file
@@ -0,0 +1,141 @@
|
||||
// Package policy implements an in-process, attribute-based authorization
|
||||
// policy engine for MCIAS. Evaluation is a pure function: given a PolicyInput
|
||||
// and a slice of Rules it returns an Effect (Allow or Deny) and the Rule that
|
||||
// produced the decision. The caller is responsible for assembling PolicyInput
|
||||
// from JWT claims and database lookups; the engine never touches the database.
|
||||
//
|
||||
// Evaluation order:
|
||||
// 1. Rules are sorted by Priority (ascending; lower = higher precedence).
|
||||
// 2. Deny-wins: the first matching Deny rule terminates evaluation.
|
||||
// 3. If no Deny matched, the first matching Allow rule permits the request.
|
||||
// 4. Default-deny: if no rule matches, the request is denied.
|
||||
package policy
|
||||
|
||||
// Action is a structured action identifier of the form "resource:verb".
|
||||
// Security: using typed constants prevents callers from passing arbitrary
|
||||
// strings, making it harder to accidentally bypass a policy check.
|
||||
type Action string
|
||||
|
||||
const (
|
||||
ActionListAccounts Action = "accounts:list"
|
||||
ActionCreateAccount Action = "accounts:create"
|
||||
ActionReadAccount Action = "accounts:read"
|
||||
ActionUpdateAccount Action = "accounts:update"
|
||||
ActionDeleteAccount Action = "accounts:delete"
|
||||
|
||||
ActionReadRoles Action = "roles:read"
|
||||
ActionWriteRoles Action = "roles:write"
|
||||
|
||||
ActionReadTags Action = "tags:read"
|
||||
ActionWriteTags Action = "tags:write"
|
||||
|
||||
ActionIssueToken Action = "tokens:issue"
|
||||
ActionRevokeToken Action = "tokens:revoke"
|
||||
ActionValidateToken Action = "tokens:validate" // public endpoint
|
||||
ActionRenewToken Action = "tokens:renew" // self-service
|
||||
|
||||
ActionReadPGCreds Action = "pgcreds:read"
|
||||
ActionWritePGCreds Action = "pgcreds:write"
|
||||
|
||||
ActionReadAudit Action = "audit:read"
|
||||
|
||||
ActionEnrollTOTP Action = "totp:enroll" // self-service
|
||||
ActionRemoveTOTP Action = "totp:remove" // admin
|
||||
|
||||
ActionLogin Action = "auth:login" // public
|
||||
ActionLogout Action = "auth:logout" // self-service
|
||||
|
||||
ActionListRules Action = "policy:list"
|
||||
ActionManageRules Action = "policy:manage"
|
||||
)
|
||||
|
||||
// ResourceType identifies what kind of object a request targets.
|
||||
type ResourceType string
|
||||
|
||||
const (
|
||||
ResourceAccount ResourceType = "account"
|
||||
ResourceToken ResourceType = "token"
|
||||
ResourcePGCreds ResourceType = "pgcreds"
|
||||
ResourceAuditLog ResourceType = "audit_log"
|
||||
ResourceTOTP ResourceType = "totp"
|
||||
ResourcePolicy ResourceType = "policy"
|
||||
)
|
||||
|
||||
// Effect is the outcome of policy evaluation.
|
||||
type Effect string
|
||||
|
||||
const (
|
||||
Allow Effect = "allow"
|
||||
Deny Effect = "deny"
|
||||
)
|
||||
|
||||
// Resource describes the object the principal is attempting to act on. Tags
|
||||
// are loaded from the account_tags table by the middleware before evaluation.
|
||||
type Resource struct {
|
||||
Type ResourceType
|
||||
|
||||
// OwnerUUID is the UUID of the account that owns this resource (e.g. the
|
||||
// system account whose pg_credentials are being requested). Empty when the
|
||||
// resource is not account-owned (e.g. an audit log listing).
|
||||
OwnerUUID string
|
||||
|
||||
// ServiceName is the username of the system account that owns the resource.
|
||||
// Used for service-name-based gating rules (ServiceNames field on Rule).
|
||||
ServiceName string
|
||||
|
||||
// Tags are the account_tags values on the target account. The engine
|
||||
// checks RequiredTags against this slice.
|
||||
Tags []string
|
||||
}
|
||||
|
||||
// PolicyInput is assembled by the middleware from JWT claims and the current
|
||||
// request context. The engine accepts a PolicyInput and a rule set; it never
|
||||
// queries the database directly.
|
||||
type PolicyInput struct {
|
||||
// Principal fields — from JWT claims
|
||||
Subject string // account UUID ("sub")
|
||||
AccountType string // "human" or "system"
|
||||
Roles []string // role strings from "roles" claim
|
||||
|
||||
Action Action
|
||||
Resource Resource
|
||||
}
|
||||
|
||||
// Rule is a single policy statement. All non-zero fields are AND-ed together
|
||||
// as match conditions. A zero/empty field is a wildcard.
|
||||
//
|
||||
// Security: rules from the database are decoded and merged with compiled-in
|
||||
// defaults before evaluation. Neither the JSON encoding nor the DB storage is
|
||||
// trusted to produce sensible rules; the engine validates each condition
|
||||
// independently using set membership — there is no string interpolation or
|
||||
// code execution involved.
|
||||
type Rule struct {
|
||||
Description string
|
||||
SubjectUUID string
|
||||
ResourceType ResourceType
|
||||
Effect Effect
|
||||
Roles []string
|
||||
AccountTypes []string
|
||||
Actions []Action
|
||||
ServiceNames []string
|
||||
RequiredTags []string
|
||||
ID int64
|
||||
Priority int
|
||||
OwnerMatchesSubject bool
|
||||
}
|
||||
|
||||
// RuleBody is the subset of Rule that is stored as JSON in the policy_rules
|
||||
// table. ID, Description, and Priority are stored as dedicated columns.
|
||||
// Security: the JSON blob is decoded into a RuleBody before being merged into
|
||||
// a full Rule, so the database cannot inject ID or Description values.
|
||||
type RuleBody struct {
|
||||
SubjectUUID string `json:"subject_uuid,omitempty"`
|
||||
ResourceType ResourceType `json:"resource_type,omitempty"`
|
||||
Effect Effect `json:"effect"`
|
||||
Roles []string `json:"roles,omitempty"`
|
||||
AccountTypes []string `json:"account_types,omitempty"`
|
||||
Actions []Action `json:"actions,omitempty"`
|
||||
ServiceNames []string `json:"service_names,omitempty"`
|
||||
RequiredTags []string `json:"required_tags,omitempty"`
|
||||
OwnerMatchesSubject bool `json:"owner_matches_subject,omitempty"`
|
||||
}
|
||||
Reference in New Issue
Block a user