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>
163 lines
3.6 KiB
Go
163 lines
3.6 KiB
Go
package policy
|
|
|
|
import "testing"
|
|
|
|
// fakeRuleStore implements RuleStore for testing.
|
|
type fakeRuleStore struct {
|
|
rules []Rule
|
|
err error
|
|
}
|
|
|
|
func (f *fakeRuleStore) LoadEnabledPolicyRules() ([]Rule, error) {
|
|
return f.rules, f.err
|
|
}
|
|
|
|
func TestEngineDefaultsOnly(t *testing.T) {
|
|
e := NewEngine()
|
|
|
|
// Admin should be allowed.
|
|
effect, _ := e.Evaluate(PolicyInput{
|
|
Subject: "admin-uuid",
|
|
AccountType: "human",
|
|
Roles: []string{"admin"},
|
|
Action: ActionPush,
|
|
Repository: "myapp",
|
|
})
|
|
if effect != Allow {
|
|
t.Fatalf("admin push with defaults: got %s, want allow", effect)
|
|
}
|
|
|
|
// System account should be denied.
|
|
effect, _ = e.Evaluate(PolicyInput{
|
|
Subject: "system-uuid",
|
|
AccountType: "system",
|
|
Action: ActionPull,
|
|
Repository: "myapp",
|
|
})
|
|
if effect != Deny {
|
|
t.Fatalf("system pull with defaults: got %s, want deny", effect)
|
|
}
|
|
}
|
|
|
|
func TestEngineWithCustomRules(t *testing.T) {
|
|
e := NewEngine()
|
|
e.SetRules([]Rule{
|
|
{
|
|
ID: 1,
|
|
Priority: 50,
|
|
Effect: Allow,
|
|
SubjectUUID: "ci-uuid",
|
|
Actions: []Action{ActionPull, ActionPush},
|
|
},
|
|
})
|
|
|
|
// System account with matching rule should be allowed.
|
|
effect, _ := e.Evaluate(PolicyInput{
|
|
Subject: "ci-uuid",
|
|
AccountType: "system",
|
|
Action: ActionPull,
|
|
Repository: "myapp",
|
|
})
|
|
if effect != Allow {
|
|
t.Fatalf("ci pull with custom rule: got %s, want allow", effect)
|
|
}
|
|
|
|
// Different subject should still be denied.
|
|
effect, _ = e.Evaluate(PolicyInput{
|
|
Subject: "other-uuid",
|
|
AccountType: "system",
|
|
Action: ActionPull,
|
|
Repository: "myapp",
|
|
})
|
|
if effect != Deny {
|
|
t.Fatalf("other pull: got %s, want deny", effect)
|
|
}
|
|
}
|
|
|
|
func TestEngineReload(t *testing.T) {
|
|
e := NewEngine()
|
|
|
|
store := &fakeRuleStore{
|
|
rules: []Rule{
|
|
{
|
|
ID: 1,
|
|
Priority: 50,
|
|
Effect: Allow,
|
|
SubjectUUID: "ci-uuid",
|
|
Actions: []Action{ActionPull},
|
|
},
|
|
},
|
|
}
|
|
if err := e.Reload(store); err != nil {
|
|
t.Fatalf("Reload: %v", err)
|
|
}
|
|
|
|
// ci-uuid should now be allowed.
|
|
effect, _ := e.Evaluate(PolicyInput{
|
|
Subject: "ci-uuid",
|
|
AccountType: "system",
|
|
Action: ActionPull,
|
|
Repository: "myapp",
|
|
})
|
|
if effect != Allow {
|
|
t.Fatalf("ci pull after reload: got %s, want allow", effect)
|
|
}
|
|
|
|
// Reload again with new rules (simulating DB change).
|
|
store.rules = []Rule{
|
|
{
|
|
ID: 2,
|
|
Priority: 50,
|
|
Effect: Allow,
|
|
SubjectUUID: "deploy-uuid",
|
|
Actions: []Action{ActionPull},
|
|
},
|
|
}
|
|
if err := e.Reload(store); err != nil {
|
|
t.Fatalf("Reload (second): %v", err)
|
|
}
|
|
|
|
// ci-uuid should now be denied (old rule gone).
|
|
effect, _ = e.Evaluate(PolicyInput{
|
|
Subject: "ci-uuid",
|
|
AccountType: "system",
|
|
Action: ActionPull,
|
|
Repository: "myapp",
|
|
})
|
|
if effect != Deny {
|
|
t.Fatalf("ci pull after second reload: got %s, want deny", effect)
|
|
}
|
|
|
|
// deploy-uuid should now be allowed.
|
|
effect, _ = e.Evaluate(PolicyInput{
|
|
Subject: "deploy-uuid",
|
|
AccountType: "system",
|
|
Action: ActionPull,
|
|
Repository: "myapp",
|
|
})
|
|
if effect != Allow {
|
|
t.Fatalf("deploy pull after reload: got %s, want allow", effect)
|
|
}
|
|
}
|
|
|
|
func TestEngineReloadDisabledExcluded(t *testing.T) {
|
|
e := NewEngine()
|
|
|
|
// Store returns no rules (all disabled or none exist).
|
|
store := &fakeRuleStore{rules: nil}
|
|
if err := e.Reload(store); err != nil {
|
|
t.Fatalf("Reload: %v", err)
|
|
}
|
|
|
|
// No custom rules, so system account should be denied.
|
|
effect, _ := e.Evaluate(PolicyInput{
|
|
Subject: "ci-uuid",
|
|
AccountType: "system",
|
|
Action: ActionPull,
|
|
Repository: "myapp",
|
|
})
|
|
if effect != Deny {
|
|
t.Fatalf("system pull with no custom rules: got %s, want deny", effect)
|
|
}
|
|
}
|