Phase 4: policy engine with deny-wins, default-deny evaluation
internal/policy/: Priority-based policy engine per ARCHITECTURE.md §4. Stateless Evaluate() sorts rules by priority, collects all matches, deny-wins over allow, default-deny if no match. Rule matching: all populated fields ANDed, empty fields are wildcards, repository glob via path.Match. Built-in defaults: admin wildcard (all actions), human user content access (pull/push/delete/catalog), version check (always accessible). Engine wrapper with sync.RWMutex-protected cache, SetRules merges with defaults, Reload loads from RuleStore. internal/db/: LoadEnabledPolicyRules() parses rule_json column from policy_rules table into []policy.Rule, filtered by enabled=1, ordered by priority. internal/server/: RequirePolicy middleware extracts claims from context, repo from chi URL param, evaluates policy, returns OCI DENIED (403) on deny with optional audit callback. 69 tests passing across all packages. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
68
internal/db/policy.go
Normal file
68
internal/db/policy.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"git.wntrmute.dev/kyle/mcr/internal/policy"
|
||||
)
|
||||
|
||||
// ruleBody is the JSON structure stored in the rule_json column.
|
||||
type ruleBody struct {
|
||||
Effect string `json:"effect"`
|
||||
Roles []string `json:"roles,omitempty"`
|
||||
AccountTypes []string `json:"account_types,omitempty"`
|
||||
SubjectUUID string `json:"subject_uuid,omitempty"`
|
||||
Actions []string `json:"actions"`
|
||||
Repositories []string `json:"repositories,omitempty"`
|
||||
}
|
||||
|
||||
// LoadEnabledPolicyRules returns all enabled policy rules from the database,
|
||||
// ordered by priority ascending. It implements policy.RuleStore.
|
||||
func (d *DB) LoadEnabledPolicyRules() ([]policy.Rule, error) {
|
||||
rows, err := d.Query(
|
||||
`SELECT id, priority, description, rule_json
|
||||
FROM policy_rules
|
||||
WHERE enabled = 1
|
||||
ORDER BY priority ASC`,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("db: load policy rules: %w", err)
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
var rules []policy.Rule
|
||||
for rows.Next() {
|
||||
var id int64
|
||||
var priority int
|
||||
var description, ruleJSON string
|
||||
if err := rows.Scan(&id, &priority, &description, &ruleJSON); err != nil {
|
||||
return nil, fmt.Errorf("db: scan policy rule: %w", err)
|
||||
}
|
||||
|
||||
var body ruleBody
|
||||
if err := json.Unmarshal([]byte(ruleJSON), &body); err != nil {
|
||||
return nil, fmt.Errorf("db: parse rule_json for rule %d: %w", id, err)
|
||||
}
|
||||
|
||||
rule := policy.Rule{
|
||||
ID: id,
|
||||
Priority: priority,
|
||||
Description: description,
|
||||
Effect: policy.Effect(body.Effect),
|
||||
Roles: body.Roles,
|
||||
AccountTypes: body.AccountTypes,
|
||||
SubjectUUID: body.SubjectUUID,
|
||||
Repositories: body.Repositories,
|
||||
}
|
||||
for _, a := range body.Actions {
|
||||
rule.Actions = append(rule.Actions, policy.Action(a))
|
||||
}
|
||||
rules = append(rules, rule)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("db: iterate policy rules: %w", err)
|
||||
}
|
||||
|
||||
return rules, nil
|
||||
}
|
||||
Reference in New Issue
Block a user