- 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.
142 lines
5.3 KiB
Go
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"`
|
|
}
|