- Web UI admin password reset now enforces admin role server-side (was cookie-auth + CSRF only; any logged-in user could previously reset any account's password) - Added self-service password change UI at GET/PUT /profile: current_password + new_password + confirm_password; server-side equality check; lockout + Argon2id verification; revokes all other sessions on success - password_change_form.html fragment and profile.html page - Nav bar actor name now links to /profile - policy: ActionChangePassword + default rule -7 allowing human accounts to change their own password - openapi.yaml: built-in rules count updated to -7 Migration recovery: - mciasdb schema force --version N: new subcommand to clear dirty migration state without running SQL (break-glass) - schema subcommands bypass auto-migration on open so the tool stays usable when the database is dirty - Migrate(): shim no longer overrides schema_migrations when it already has an entry; duplicate-column error on the latest migration is force-cleaned and treated as success (handles columns added outside the runner) Security: - Admin role is now validated in handleAdminResetPassword before any DB access; non-admin receives 403 - handleSelfChangePassword follows identical lockout + constant-time Argon2id path as the REST self-service handler; current password required to prevent token-theft account takeover Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
143 lines
5.4 KiB
Go
143 lines
5.4 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
|
|
ActionChangePassword Action = "auth:change_password" // 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"`
|
|
}
|