Files
mcias/internal/policy/policy.go
Kyle Isom 052d3ed1b8 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.
2026-03-11 23:24:03 -07:00

142 lines
5.3 KiB
Go

// 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"`
}