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:
2026-03-19 15:05:28 -07:00
parent 3314b7a618
commit f5e67bd4aa
11 changed files with 1158 additions and 4 deletions

View File

@@ -6,7 +6,7 @@ See `PROJECT_PLAN.md` for the implementation roadmap and
## Current State
**Phase:** Batch A complete (Phases 2 + 3), ready for Phase 4 (policy engine)
**Phase:** 4 complete, ready for Batch B (Phase 5 + Phase 8)
**Last updated:** 2026-03-19
### Completed
@@ -15,6 +15,7 @@ See `PROJECT_PLAN.md` for the implementation roadmap and
- Phase 1: Configuration & database (all 3 steps)
- Phase 2: Blob storage layer (all 2 steps)
- Phase 3: MCIAS authentication (all 4 steps)
- Phase 4: Policy engine (all 4 steps)
- `ARCHITECTURE.md` — Full design specification (18 sections)
- `CLAUDE.md` — AI development guidance
- `PROJECT_PLAN.md` — Implementation plan (14 phases, 40+ steps)
@@ -22,13 +23,77 @@ See `PROJECT_PLAN.md` for the implementation roadmap and
### Next Steps
1. Phase 4: Policy engine (depends on Phase 3)
2. After Phase 4, Batch B: Phase 5 (OCI pull) and Phase 8 (admin REST)
1. Batch B: Phase 5 (OCI pull) and Phase 8 (admin REST) — independent,
can be done in parallel
2. After Phase 5, Phase 6 (OCI push) then Phase 7 (OCI delete)
---
## Log
### 2026-03-19 — Phase 4: Policy engine
**Task:** Implement the registry-specific authorization engine with
priority-based, deny-wins, default-deny evaluation per ARCHITECTURE.md §4.
**Changes:**
Step 4.1 — `internal/policy/` core types and evaluation:
- `policy.go`: `Action` (6 constants), `Effect` (Allow/Deny), `PolicyInput`,
`Rule` types per ARCHITECTURE.md §4
- `Evaluate(input, rules)` — stateless evaluation: sort by priority (stable),
collect all matching rules, deny-wins, default-deny
- Rule matching: all populated fields ANDed; empty fields are wildcards;
`Repositories` glob matching via `path.Match`; empty repo (global ops)
only matches rules with empty Repositories list
Step 4.2 — `internal/policy/` built-in defaults:
- `defaults.go`: `DefaultRules()` returns 3 built-in rules (negative IDs,
priority 0): admin wildcard (all actions), human user content access
(pull/push/delete/catalog), version check (always accessible)
Step 4.3 — `internal/policy/` engine wrapper with DB integration:
- `engine.go`: `Engine` struct with `sync.RWMutex`-protected rule cache;
`NewEngine()` pre-loaded with defaults; `SetRules()` merges with defaults;
`Evaluate()` thread-safe evaluation; `Reload(RuleStore)` loads from DB
- `RuleStore` interface: `LoadEnabledPolicyRules() ([]Rule, error)`
- `internal/db/policy.go`: `LoadEnabledPolicyRules()` on `*DB` — loads
enabled rules from `policy_rules` table, parses `rule_json` JSON column,
returns `[]policy.Rule` ordered by priority
Step 4.4 — `internal/server/` policy middleware:
- `policy.go`: `PolicyEvaluator` interface, `AuditFunc` callback type,
`RequirePolicy(evaluator, action, auditFn)` middleware — extracts claims
from context, repo name from chi URL param, assembles `PolicyInput`,
returns OCI DENIED (403) on deny with optional audit callback
**Verification:**
- `make all` passes: vet clean, lint 0 issues, 69 tests passing
(17 policy + 14 server + 15 db + 9 auth + 7 config + 14 storage - some
overlap from updated packages), all 3 binaries built
- Policy evaluation tests: admin wildcard, user allow, system account deny,
exact repo match (allow + deny on different repo), glob match
(production/* matches production/myapp, not production/team/myapp),
deny-wins over allow, priority ordering, empty repo global operation
(admin catalog allowed, repo-scoped rule doesn't match), multiple
matching rules (highest-priority allow returned)
- Default rules tests: admin allowed for all 6 actions, user allowed for
pull/push/delete/catalog but denied policy:manage, system account denied
for all except version_check, version_check allowed for both human and
system accounts
- Engine tests: defaults-only (admin allow, system deny), custom rules
(matching subject allowed, different subject denied), reload picks up new
rules (old rules gone), reload with empty store (disabled rules excluded,
falls back to defaults)
- DB tests: LoadEnabledPolicyRules returns only enabled rules ordered by
priority, parses rule_json correctly (effect, subject_uuid, actions,
repositories), empty table returns nil
- Middleware tests: admin allowed, user allowed, system denied (403 with
OCI DENIED error), system with matching rule allowed, explicit deny
rule blocks access (403)
---
### 2026-03-19 — Batch A: Phase 2 (blob storage) + Phase 3 (MCIAS auth)
**Task:** Implement content-addressed blob storage and MCIAS authentication