Files
capsule/pkg/ruleengine/enforce_evaluator_test.go
T
Oliver BählerandGitHub f1b760a87a feat(rules): improve metadata enforcement and add ingress rules (#2050)
* feat: implement namespace metadata enforcement

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* feat: add ingress enforcment

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* feat: add ingress enforcment

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* feat: add ingress enforcment

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* feat: add ingress enforcment

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* feat: add ingress enforcment

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* feat: add ingress enforcment

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* feat: add ingress enforcment

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* feat: add ingress enforcment

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* feat: add ingress enforcment

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* feat: add ingress enforcment

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

* feat: add ingress enforcment

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>

---------

Signed-off-by: Oliver Baehler <oliver@sudo-i.net>
2026-07-22 22:24:54 +02:00

1349 lines
30 KiB
Go

// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package ruleengine
import (
"errors"
"strings"
"testing"
api "github.com/projectcapsule/capsule/pkg/api/rules"
)
type testObject struct {
Values []Value
}
type testRule struct {
Name string
ShouldMatch bool
MatchedValue any
Detail string
Err error
}
type enforceSpec struct {
action api.ActionType
items []testRule
}
type testFixture struct {
items map[*api.NamespaceRuleEnforceBody][]testRule
}
func TestEvaluateEnforce_ValidationErrors(t *testing.T) {
t.Parallel()
validFixture := newTestFixture()
validSet := validFixture.set("registry", nil)
tests := []struct {
name string
set Set[testRule, testObject]
wantErr string
}{
{
name: "empty set name",
set: Set[testRule, testObject]{
Values: validSet.Values,
Rules: validSet.Rules,
Matches: validSet.Matches,
},
wantErr: "rule set name is empty",
},
{
name: "nil values extractor",
set: Set[testRule, testObject]{
Name: "registry",
Rules: validSet.Rules,
Matches: validSet.Matches,
},
wantErr: "registry: values extractor is nil",
},
{
name: "nil rules extractor",
set: Set[testRule, testObject]{
Name: "registry",
Values: validSet.Values,
Matches: validSet.Matches,
},
wantErr: "registry: rules extractor is nil",
},
{
name: "nil matcher",
set: Set[testRule, testObject]{
Name: "registry",
Values: validSet.Values,
Rules: validSet.Rules,
},
wantErr: "registry: matcher is nil",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
evaluation, err := EvaluateEnforce(
testObject{Values: []Value{{Value: "harbor/app:1", Path: "spec.containers[0].image"}}},
[]*api.NamespaceRuleEnforceBody{{Action: api.ActionTypeAllow}},
tt.set,
)
if err == nil {
t.Fatalf("expected error, got nil")
}
if evaluation != nil {
t.Fatalf("expected nil evaluation on setup validation error, got %#v", evaluation)
}
if !strings.Contains(err.Error(), tt.wantErr) {
t.Fatalf("expected error containing %q, got %q", tt.wantErr, err.Error())
}
})
}
}
func TestEvaluateEnforce_RulesWithError(t *testing.T) {
t.Parallel()
wantErr := errors.New("invalid selector")
evaluation, err := EvaluateEnforce(
testObject{Values: []Value{{Value: "prod", Path: "metadata.labels[env]"}}},
[]*api.NamespaceRuleEnforceBody{{Action: api.ActionTypeAllow}},
Set[testRule, testObject]{
Name: "metadata labels",
Values: func(obj testObject) []Value {
return obj.Values
},
RulesWithError: func(*api.NamespaceRuleEnforceBody) ([]testRule, error) {
return nil, wantErr
},
Matches: func(testRule, Value) (Match, error) {
return Match{}, nil
},
},
)
if !errors.Is(err, wantErr) {
t.Fatalf("EvaluateEnforce() error = %v, want %v", err, wantErr)
}
if evaluation == nil {
t.Fatal("EvaluateEnforce() evaluation = nil, want partial evaluation")
}
}
func TestEvaluateEnforce_EmptyInputs(t *testing.T) {
t.Parallel()
tests := []struct {
name string
obj testObject
enforceSpecs []enforceSpec
includeNilBody bool
}{
{
name: "no values",
obj: testObject{},
enforceSpecs: []enforceSpec{
{
action: api.ActionTypeDeny,
items: []testRule{
{Name: "deny", ShouldMatch: true},
},
},
},
},
{
name: "empty value is skipped",
obj: testObject{
Values: []Value{
{Value: "", Path: "spec.value"},
},
},
enforceSpecs: []enforceSpec{
{
action: api.ActionTypeDeny,
items: []testRule{
{Name: "deny", ShouldMatch: true},
},
},
},
},
{
name: "nil enforce bodies",
obj: testObject{
Values: []Value{
{Value: "harbor/app:1", Path: "spec.containers[0].image"},
},
},
},
{
name: "empty enforce bodies",
obj: testObject{
Values: []Value{
{Value: "harbor/app:1", Path: "spec.containers[0].image"},
},
},
enforceSpecs: []enforceSpec{},
},
{
name: "nil enforce body is ignored",
obj: testObject{
Values: []Value{
{Value: "harbor/app:1", Path: "spec.containers[0].image"},
},
},
includeNilBody: true,
},
{
name: "enforce body without rule items is ignored",
obj: testObject{
Values: []Value{
{Value: "harbor/app:1", Path: "spec.containers[0].image"},
},
},
enforceSpecs: []enforceSpec{
{
action: api.ActionTypeDeny,
},
},
},
{
name: "non matching rule item is ignored",
obj: testObject{
Values: []Value{
{Value: "harbor/app:1", Path: "spec.containers[0].image"},
},
},
enforceSpecs: []enforceSpec{
{
action: api.ActionTypeDeny,
items: []testRule{
{Name: "deny", ShouldMatch: false},
},
},
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
fixture := newTestFixture()
enforceBodies := buildEnforceBodies(fixture, tt.enforceSpecs)
if tt.includeNilBody {
enforceBodies = append(enforceBodies, nil)
}
evaluation, err := EvaluateEnforce(tt.obj, enforceBodies, fixture.set("registry", nil))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
assertNoBlocking(t, evaluation)
assertNoFinal(t, evaluation)
if len(evaluation.Audits) != 0 {
t.Fatalf("expected no audits, got %d", len(evaluation.Audits))
}
})
}
}
func TestEvaluateEnforce_LastMatchingAllowDenyWins(t *testing.T) {
t.Parallel()
tests := []struct {
name string
enforceSpecs []enforceSpec
wantFinalAction api.ActionType
wantBlocking bool
wantMessage string
}{
{
name: "single allow allows",
enforceSpecs: []enforceSpec{
{
action: api.ActionTypeAllow,
items: []testRule{
{Name: "harbor/.*", ShouldMatch: true},
},
},
},
wantFinalAction: api.ActionTypeAllow,
wantMessage: `registry "harbor/app:1" at spec.containers[0].image is allowed by namespace rule: matched allowed rule harbor/.*`,
},
{
name: "single deny denies",
enforceSpecs: []enforceSpec{
{
action: api.ActionTypeDeny,
items: []testRule{
{Name: "harbor/blocked/.*", ShouldMatch: true},
},
},
},
wantFinalAction: api.ActionTypeDeny,
wantBlocking: true,
wantMessage: `registry "harbor/app:1" at spec.containers[0].image is denied by namespace rule: matched denied rule harbor/blocked/.*`,
},
{
name: "deny then allow allows",
enforceSpecs: []enforceSpec{
{
action: api.ActionTypeDeny,
items: []testRule{
{Name: "harbor/.*", ShouldMatch: true},
},
},
{
action: api.ActionTypeAllow,
items: []testRule{
{Name: "harbor/app:.*", ShouldMatch: true},
},
},
},
wantFinalAction: api.ActionTypeAllow,
wantMessage: `registry "harbor/app:1" at spec.containers[0].image is allowed by namespace rule: matched allowed rule harbor/app:.*`,
},
{
name: "allow then deny denies",
enforceSpecs: []enforceSpec{
{
action: api.ActionTypeAllow,
items: []testRule{
{Name: "harbor/.*", ShouldMatch: true},
},
},
{
action: api.ActionTypeDeny,
items: []testRule{
{Name: "harbor/app:.*", ShouldMatch: true},
},
},
},
wantFinalAction: api.ActionTypeDeny,
wantBlocking: true,
wantMessage: `registry "harbor/app:1" at spec.containers[0].image is denied by namespace rule: matched denied rule harbor/app:.*`,
},
{
name: "last matching rule wins while non matching later rules are ignored",
enforceSpecs: []enforceSpec{
{
action: api.ActionTypeDeny,
items: []testRule{
{Name: "first-deny", ShouldMatch: true},
},
},
{
action: api.ActionTypeAllow,
items: []testRule{
{Name: "allow", ShouldMatch: true},
},
},
{
action: api.ActionTypeDeny,
items: []testRule{
{Name: "later-non-matching-deny", ShouldMatch: false},
},
},
},
wantFinalAction: api.ActionTypeAllow,
wantMessage: `registry "harbor/app:1" at spec.containers[0].image is allowed by namespace rule: matched allowed rule allow`,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
fixture := newTestFixture()
evaluation, err := EvaluateEnforce(
testObject{
Values: []Value{
{Value: "harbor/app:1", Path: "spec.containers[0].image"},
},
},
buildEnforceBodies(fixture, tt.enforceSpecs),
fixture.set("registry", nil),
)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
assertFinalAction(t, evaluation, tt.wantFinalAction)
if tt.wantBlocking {
assertBlockingAction(t, evaluation, api.ActionTypeDeny)
} else {
assertNoBlocking(t, evaluation)
}
if evaluation.Final.Message != tt.wantMessage {
t.Fatalf("final message = %q, want %q", evaluation.Final.Message, tt.wantMessage)
}
})
}
}
func TestEvaluateEnforce_DefaultActionIsDeny(t *testing.T) {
t.Parallel()
fixture := newTestFixture()
evaluation, err := EvaluateEnforce(
testObject{
Values: []Value{
{Value: "harbor/app:1", Path: "spec.containers[0].image"},
},
},
buildEnforceBodies(fixture, []enforceSpec{
{
action: "",
items: []testRule{
{Name: "default-deny", ShouldMatch: true},
},
},
}),
fixture.set("registry", nil),
)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
assertBlockingAction(t, evaluation, api.ActionTypeDeny)
if evaluation.Blocking.Message != `registry "harbor/app:1" at spec.containers[0].image is denied by namespace rule: matched denied rule default-deny` {
t.Fatalf("blocking message = %q", evaluation.Blocking.Message)
}
}
func TestEvaluateEnforce_AllowListMissDenies(t *testing.T) {
t.Parallel()
fixture := newTestFixture()
evaluation, err := EvaluateEnforce(
testObject{
Values: []Value{
{Value: "docker.io/library/nginx:latest", Path: "spec.containers[0].image"},
},
},
buildEnforceBodies(fixture, []enforceSpec{
{
action: api.ActionTypeAllow,
items: []testRule{
{Name: "harbor/.*", ShouldMatch: false},
{Name: "registry.local/.*", ShouldMatch: false},
},
},
}),
fixture.set("registry", nil),
)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
assertBlockingAction(t, evaluation, api.ActionTypeDeny)
assertNoFinal(t, evaluation)
want := `registry "docker.io/library/nginx:latest" at spec.containers[0].image is not allowed by namespace rule: value did not match any allowed rule. Allowed registries: harbor/.*, registry.local/.*`
if evaluation.Blocking.Message != want {
t.Fatalf("blocking message = %q, want %q", evaluation.Blocking.Message, want)
}
}
func TestEvaluateEnforce_AllowMissWithoutRuleDescriptionsUsesBaseMessage(t *testing.T) {
t.Parallel()
fixture := newTestFixture()
set := fixture.set("registry", nil)
set.RuleDescription = nil
set.AllowedDescription = ""
evaluation, err := EvaluateEnforce(
testObject{
Values: []Value{
{Value: "docker.io/library/nginx:latest", Path: "spec.containers[0].image"},
},
},
buildEnforceBodies(fixture, []enforceSpec{
{
action: api.ActionTypeAllow,
items: []testRule{
{Name: "harbor/.*", ShouldMatch: false},
},
},
}),
set,
)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
assertBlockingAction(t, evaluation, api.ActionTypeDeny)
want := `registry "docker.io/library/nginx:latest" at spec.containers[0].image is not allowed by namespace rule`
if evaluation.Blocking.Message != want {
t.Fatalf("blocking message = %q, want %q", evaluation.Blocking.Message, want)
}
}
func TestEvaluateEnforce_AllowMissDescriptionsAreLimited(t *testing.T) {
t.Parallel()
fixture := newTestFixture()
items := make([]testRule, 0, 12)
for i := 0; i < 12; i++ {
items = append(items, testRule{
Name: "rule-" + string(rune('a'+i)),
ShouldMatch: false,
})
}
evaluation, err := EvaluateEnforce(
testObject{
Values: []Value{
{Value: "unmatched", Path: "spec.value"},
},
},
buildEnforceBodies(fixture, []enforceSpec{
{
action: api.ActionTypeAllow,
items: items,
},
}),
fixture.set("registry", nil),
)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
assertBlockingAction(t, evaluation, api.ActionTypeDeny)
msg := evaluation.Blocking.Message
for _, expected := range []string{
"rule-a",
"rule-j",
"and 2 more",
} {
if !strings.Contains(msg, expected) {
t.Fatalf("expected message %q to contain %q", msg, expected)
}
}
for _, unexpected := range []string{
"rule-k",
"rule-l",
} {
if strings.Contains(msg, unexpected) {
t.Fatalf("expected message %q not to contain %q", msg, unexpected)
}
}
}
func TestEvaluateEnforce_AuditIsObservational(t *testing.T) {
t.Parallel()
fixture := newTestFixture()
evaluation, err := EvaluateEnforce(
testObject{
Values: []Value{
{Value: "audit/app:1", Path: "spec.containers[0].image"},
},
},
buildEnforceBodies(fixture, []enforceSpec{
{
action: api.ActionTypeAudit,
items: []testRule{
{
Name: "audit/.*",
ShouldMatch: true,
Detail: `"audit/app:1" matched registry rule audit/.*`,
},
},
},
}),
fixture.set("registry", nil),
)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
assertNoBlocking(t, evaluation)
assertNoFinal(t, evaluation)
if len(evaluation.Audits) != 1 {
t.Fatalf("expected 1 audit, got %d", len(evaluation.Audits))
}
audit := evaluation.Audits[0]
if audit.Action != api.ActionTypeAudit {
t.Fatalf("audit action = %q, want %q", audit.Action, api.ActionTypeAudit)
}
if audit.MatchedRule != "audit/.*" {
t.Fatalf("audit matched rule = %q, want %q", audit.MatchedRule, "audit/.*")
}
if audit.MatchDetail != `"audit/app:1" matched registry rule audit/.*` {
t.Fatalf("audit match detail = %q", audit.MatchDetail)
}
want := `registry "audit/app:1" at spec.containers[0].image matched audit namespace rule: "audit/app:1" matched registry rule audit/.*`
if audit.Message != want {
t.Fatalf("audit message = %q, want %q", audit.Message, want)
}
}
func TestEvaluateEnforce_AuditDoesNotSatisfyAllowList(t *testing.T) {
t.Parallel()
fixture := newTestFixture()
evaluation, err := EvaluateEnforce(
testObject{
Values: []Value{
{Value: "audit/app:1", Path: "spec.containers[0].image"},
},
},
buildEnforceBodies(fixture, []enforceSpec{
{
action: api.ActionTypeAudit,
items: []testRule{
{Name: "audit/.*", ShouldMatch: true},
},
},
{
action: api.ActionTypeAllow,
items: []testRule{
{Name: "allowed/.*", ShouldMatch: false},
},
},
}),
fixture.set("registry", nil),
)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(evaluation.Audits) != 1 {
t.Fatalf("expected 1 audit, got %d", len(evaluation.Audits))
}
assertBlockingAction(t, evaluation, api.ActionTypeDeny)
assertNoFinal(t, evaluation)
if !strings.Contains(evaluation.Blocking.Message, "Allowed registries: allowed/.*") {
t.Fatalf("blocking message = %q", evaluation.Blocking.Message)
}
}
func TestEvaluateEnforce_MatchDetailOverridesMatchedRuleInMessage(t *testing.T) {
t.Parallel()
fixture := newTestFixture()
evaluation, err := EvaluateEnforce(
testObject{
Values: []Value{
{Value: "10.0.171.239", Path: "spec.loadBalancerIP"},
},
},
buildEnforceBodies(fixture, []enforceSpec{
{
action: api.ActionTypeDeny,
items: []testRule{
{
Name: "10.0.0.0/8",
ShouldMatch: true,
MatchedValue: "10.0.0.0/8",
Detail: "10.0.171.239 is contained in 10.0.0.0/8",
},
},
},
}),
fixture.set("loadBalancer CIDR", nil),
)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
assertBlockingAction(t, evaluation, api.ActionTypeDeny)
blocking := evaluation.Blocking
if blocking.MatchedRule != "10.0.0.0/8" {
t.Fatalf("matched rule = %q, want %q", blocking.MatchedRule, "10.0.0.0/8")
}
if blocking.MatchDetail != "10.0.171.239 is contained in 10.0.0.0/8" {
t.Fatalf("match detail = %q", blocking.MatchDetail)
}
want := `loadBalancer CIDR "10.0.171.239" at spec.loadBalancerIP is denied by namespace rule: 10.0.171.239 is contained in 10.0.0.0/8`
if blocking.Message != want {
t.Fatalf("blocking message = %q, want %q", blocking.Message, want)
}
}
func TestEvaluateEnforce_CustomMessageOverridesDefaultMessage(t *testing.T) {
t.Parallel()
fixture := newTestFixture()
set := fixture.set("registry", func(action api.ActionType, value Value, matched any) string {
return "custom message"
})
evaluation, err := EvaluateEnforce(
testObject{
Values: []Value{
{Value: "harbor/app:1", Path: "spec.containers[0].image"},
},
},
buildEnforceBodies(fixture, []enforceSpec{
{
action: api.ActionTypeDeny,
items: []testRule{
{Name: "harbor/.*", ShouldMatch: true},
},
},
}),
set,
)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
assertBlockingAction(t, evaluation, api.ActionTypeDeny)
if evaluation.Blocking.Message != "custom message" {
t.Fatalf("blocking message = %q, want custom message", evaluation.Blocking.Message)
}
}
func TestEvaluateEnforce_MatcherError(t *testing.T) {
t.Parallel()
fixture := newTestFixture()
matchErr := errors.New("invalid regex")
evaluation, err := EvaluateEnforce(
testObject{
Values: []Value{
{Value: "harbor/app:1", Path: "spec.containers[0].image"},
},
},
buildEnforceBodies(fixture, []enforceSpec{
{
action: api.ActionTypeDeny,
items: []testRule{
{
Name: "bad-rule",
ShouldMatch: true,
Err: matchErr,
},
},
},
}),
fixture.set("registry", nil),
)
if err == nil {
t.Fatalf("expected error, got nil")
}
if evaluation == nil {
t.Fatalf("expected non-nil evaluation after runtime matcher error")
}
if !strings.Contains(err.Error(), "registry: invalid rule: invalid regex") {
t.Fatalf("error = %q", err.Error())
}
}
func TestEvaluateEnforce_UnsupportedAction(t *testing.T) {
t.Parallel()
fixture := newTestFixture()
evaluation, err := EvaluateEnforce(
testObject{
Values: []Value{
{Value: "harbor/app:1", Path: "spec.containers[0].image"},
},
},
buildEnforceBodies(fixture, []enforceSpec{
{
action: api.ActionType("invalid"),
items: []testRule{
{Name: "harbor/.*", ShouldMatch: true},
},
},
}),
fixture.set("registry", nil),
)
if err == nil {
t.Fatalf("expected error, got nil")
}
if evaluation == nil {
t.Fatalf("expected non-nil evaluation after runtime action error")
}
if !strings.Contains(err.Error(), `registry: unsupported rule action "invalid"`) {
t.Fatalf("error = %q", err.Error())
}
}
func TestEvaluation_BlockingError(t *testing.T) {
t.Parallel()
if err := (*Evaluation)(nil).BlockingError(); err != nil {
t.Fatalf("nil evaluation BlockingError() = %v, want nil", err)
}
evaluation := &Evaluation{}
if err := evaluation.BlockingError(); err != nil {
t.Fatalf("empty evaluation BlockingError() = %v, want nil", err)
}
evaluation.Blocking = &Decision{
Message: "blocked by rule",
}
err := evaluation.BlockingError()
if err == nil {
t.Fatalf("expected blocking error, got nil")
}
if err.Error() != "blocked by rule" {
t.Fatalf("blocking error = %q, want %q", err.Error(), "blocked by rule")
}
}
func TestDecisionError_ErrorFallback(t *testing.T) {
t.Parallel()
tests := []struct {
name string
err *DecisionError
}{
{
name: "nil error",
err: nil,
},
{
name: "nil decision",
err: &DecisionError{},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if tt.err.Error() != "namespace rule decision denied request" {
t.Fatalf("Error() = %q", tt.err.Error())
}
})
}
}
func TestEvaluation_Append(t *testing.T) {
t.Parallel()
t.Run("nil receiver or nil other does nothing", func(t *testing.T) {
t.Parallel()
var evaluation *Evaluation
evaluation.Append(&Evaluation{})
nonNil := &Evaluation{}
nonNil.Append(nil)
assertNoBlocking(t, nonNil)
assertNoFinal(t, nonNil)
if len(nonNil.Audits) != 0 {
t.Fatalf("expected no audits, got %d", len(nonNil.Audits))
}
})
t.Run("appends audits and overrides final and blocking", func(t *testing.T) {
t.Parallel()
firstAudit := &Decision{Action: api.ActionTypeAudit, Message: "audit-1"}
secondAudit := &Decision{Action: api.ActionTypeAudit, Message: "audit-2"}
final := &Decision{Action: api.ActionTypeAllow, Message: "final"}
blocking := &Decision{Action: api.ActionTypeDeny, Message: "blocking"}
evaluation := &Evaluation{
Audits: []*Decision{
firstAudit,
},
}
evaluation.Append(&Evaluation{
Audits: []*Decision{
secondAudit,
},
Final: final,
Blocking: blocking,
})
if len(evaluation.Audits) != 2 {
t.Fatalf("audits = %d, want 2", len(evaluation.Audits))
}
if evaluation.Audits[0] != firstAudit {
t.Fatalf("first audit pointer was not preserved")
}
if evaluation.Audits[1] != secondAudit {
t.Fatalf("second audit pointer was not appended")
}
if evaluation.Final != final {
t.Fatalf("final was not updated")
}
if evaluation.Blocking != blocking {
t.Fatalf("blocking was not updated")
}
})
}
func TestEvaluateEnforce_SkipsEmptyExtractedValues(t *testing.T) {
t.Parallel()
type testCase struct {
name string
action api.ActionType
values []Value
rules []string
wantMatcherCalls int
wantBlocking bool
wantFinal bool
wantAudits int
wantBlockingPath string
}
tests := []testCase{
{
name: "empty value is skipped before deny evaluation",
action: api.ActionTypeDeny,
values: []Value{
{
Value: "",
Path: "spec.value",
},
},
rules: []string{
"",
},
wantMatcherCalls: 0,
wantBlocking: false,
wantFinal: false,
wantAudits: 0,
},
{
name: "empty value is skipped before allow miss",
action: api.ActionTypeAllow,
values: []Value{
{
Value: "",
Path: "spec.value",
},
},
rules: []string{
"allowed",
},
wantMatcherCalls: 0,
wantBlocking: false,
wantFinal: false,
wantAudits: 0,
},
{
name: "empty value is skipped before audit evaluation",
action: api.ActionTypeAudit,
values: []Value{
{
Value: "",
Path: "spec.value",
},
},
rules: []string{
"",
},
wantMatcherCalls: 0,
wantBlocking: false,
wantFinal: false,
wantAudits: 0,
},
{
name: "empty value is skipped but later non empty value is evaluated",
action: api.ActionTypeDeny,
values: []Value{
{
Value: "",
Path: "spec.empty",
},
{
Value: "deny",
Path: "spec.nonEmpty",
},
},
rules: []string{
"deny",
},
wantMatcherCalls: 1,
wantBlocking: true,
wantFinal: true,
wantAudits: 0,
wantBlockingPath: "spec.nonEmpty",
},
{
name: "whitespace value is not skipped",
action: api.ActionTypeDeny,
values: []Value{
{
Value: " ",
Path: "spec.value",
},
},
rules: []string{
" ",
},
wantMatcherCalls: 1,
wantBlocking: true,
wantFinal: true,
wantAudits: 0,
wantBlockingPath: "spec.value",
},
{
name: "empty values are skipped before non matching allow value triggers allow miss",
action: api.ActionTypeAllow,
values: []Value{
{
Value: "",
Path: "spec.empty",
},
{
Value: "actual",
Path: "spec.actual",
},
},
rules: []string{
"allowed",
},
wantMatcherCalls: 1,
wantBlocking: true,
wantFinal: false,
wantAudits: 0,
wantBlockingPath: "spec.actual",
},
{
name: "all empty values are skipped",
action: api.ActionTypeDeny,
values: []Value{
{
Value: "",
Path: "spec.first",
},
{
Value: "",
Path: "spec.second",
},
},
rules: []string{
"",
},
wantMatcherCalls: 0,
wantBlocking: false,
wantFinal: false,
wantAudits: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
matcherCalls := 0
evaluation, err := EvaluateEnforce(
struct{}{},
[]*api.NamespaceRuleEnforceBody{
{
Action: tt.action,
},
},
Set[string, struct{}]{
Name: "registry",
EventReason: "NamespaceRuleViolation",
Values: func(struct{}) []Value {
return tt.values
},
Rules: func(*api.NamespaceRuleEnforceBody) []string {
return tt.rules
},
Matches: func(rule string, value Value) (Match, error) {
matcherCalls++
return Match{
Matched: value.Value == rule,
MatchedValue: rule,
}, nil
},
RuleDescription: func(rule string) string {
return rule
},
},
)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if evaluation == nil {
t.Fatalf("expected evaluation")
}
if matcherCalls != tt.wantMatcherCalls {
t.Fatalf("expected %d matcher calls, got %d", tt.wantMatcherCalls, matcherCalls)
}
if got := evaluation.Blocking != nil; got != tt.wantBlocking {
t.Fatalf("expected blocking=%t, got %t: %#v", tt.wantBlocking, got, evaluation.Blocking)
}
if got := evaluation.Final != nil; got != tt.wantFinal {
t.Fatalf("expected final=%t, got %t: %#v", tt.wantFinal, got, evaluation.Final)
}
if len(evaluation.Audits) != tt.wantAudits {
t.Fatalf("expected %d audits, got %d", tt.wantAudits, len(evaluation.Audits))
}
if tt.wantBlockingPath != "" {
if evaluation.Blocking == nil {
t.Fatalf("expected blocking decision")
}
if evaluation.Blocking.Value.Path != tt.wantBlockingPath {
t.Fatalf("expected blocking path %q, got %q", tt.wantBlockingPath, evaluation.Blocking.Value.Path)
}
}
})
}
}
func TestMessageHelpers(t *testing.T) {
t.Parallel()
set := Set[testRule, testObject]{
Name: "registry",
AllowedDescription: "Allowed registries",
RuleDescription: func(rule testRule) string {
return rule.Name
},
}
t.Run("allowedLabel default", func(t *testing.T) {
t.Parallel()
defaultSet := Set[testRule, testObject]{}
if got := allowedLabel(defaultSet); got != "Allowed values" {
t.Fatalf("allowedLabel() = %q, want %q", got, "Allowed values")
}
})
t.Run("appendMatchContext uses detail before rule", func(t *testing.T) {
t.Parallel()
got := appendMatchContext("message", "rule", "detail", "matched rule")
if got != "message: detail" {
t.Fatalf("appendMatchContext() = %q, want %q", got, "message: detail")
}
})
t.Run("appendMatchContext uses rule when detail is empty", func(t *testing.T) {
t.Parallel()
got := appendMatchContext("message", "rule", "", "matched rule")
if got != "message: matched rule rule" {
t.Fatalf("appendMatchContext() = %q", got)
}
})
t.Run("appendMatchContext leaves message unchanged without context", func(t *testing.T) {
t.Parallel()
got := appendMatchContext("message", "", "", "matched rule")
if got != "message" {
t.Fatalf("appendMatchContext() = %q, want message", got)
}
})
t.Run("describeRules skips empty descriptions", func(t *testing.T) {
t.Parallel()
got := describeRules(set, []testRule{
{Name: "first"},
{Name: ""},
{Name: "second"},
})
if got != "first, second" {
t.Fatalf("describeRules() = %q, want %q", got, "first, second")
}
})
}
func newTestFixture() *testFixture {
return &testFixture{
items: map[*api.NamespaceRuleEnforceBody][]testRule{},
}
}
func (f *testFixture) set(
name string,
message func(api.ActionType, Value, any) string,
) Set[testRule, testObject] {
return Set[testRule, testObject]{
Name: name,
EventReason: "NamespaceRuleViolation",
AllowedDescription: "Allowed registries",
Values: func(obj testObject) []Value {
return obj.Values
},
Rules: func(enforce *api.NamespaceRuleEnforceBody) []testRule {
return f.items[enforce]
},
Matches: func(rule testRule, value Value) (Match, error) {
if rule.Err != nil {
return Match{}, rule.Err
}
if !rule.ShouldMatch {
return Match{}, nil
}
matchedValue := rule.MatchedValue
if matchedValue == nil {
matchedValue = rule.Name
}
return Match{
Matched: true,
MatchedValue: matchedValue,
Detail: rule.Detail,
}, nil
},
Message: message,
RuleDescription: func(rule testRule) string {
return rule.Name
},
}
}
func buildEnforceBodies(
fixture *testFixture,
specs []enforceSpec,
) []*api.NamespaceRuleEnforceBody {
if len(specs) == 0 {
return nil
}
out := make([]*api.NamespaceRuleEnforceBody, 0, len(specs))
for _, spec := range specs {
enforce := &api.NamespaceRuleEnforceBody{
Action: spec.action,
}
fixture.items[enforce] = spec.items
out = append(out, enforce)
}
return out
}
func assertBlockingAction(t *testing.T, evaluation *Evaluation, action api.ActionType) {
t.Helper()
if evaluation == nil {
t.Fatalf("expected evaluation, got nil")
}
if evaluation.Blocking == nil {
t.Fatalf("expected blocking decision, got nil")
}
if evaluation.Blocking.Action != action {
t.Fatalf("blocking action = %q, want %q", evaluation.Blocking.Action, action)
}
}
func assertNoBlocking(t *testing.T, evaluation *Evaluation) {
t.Helper()
if evaluation == nil {
t.Fatalf("expected evaluation, got nil")
}
if evaluation.Blocking != nil {
t.Fatalf("expected no blocking decision, got %#v", evaluation.Blocking)
}
}
func assertFinalAction(t *testing.T, evaluation *Evaluation, action api.ActionType) {
t.Helper()
if evaluation == nil {
t.Fatalf("expected evaluation, got nil")
}
if evaluation.Final == nil {
t.Fatalf("expected final decision, got nil")
}
if evaluation.Final.Action != action {
t.Fatalf("final action = %q, want %q", evaluation.Final.Action, action)
}
}
func assertNoFinal(t *testing.T, evaluation *Evaluation) {
t.Helper()
if evaluation == nil {
t.Fatalf("expected evaluation, got nil")
}
if evaluation.Final != nil {
t.Fatalf("expected no final decision, got %#v", evaluation.Final)
}
}