Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 80 additions & 2 deletions internal/evaluator/evaluator.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ import (
admissionv1 "k8s.io/api/admission/v1"
admissionregv1 "k8s.io/api/admissionregistration/v1"
admissionv1beta1 "k8s.io/api/admissionregistration/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
plugin "k8s.io/apiserver/pkg/admission/plugin/cel"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer"
Expand Down Expand Up @@ -113,6 +115,7 @@ type TestCase interface {
// EvaluateTest evaluates a policy against a test case and returns whether it passed.
func (e *Evaluator) EvaluateTest(
mutatingPolicy *admissionv1beta1.MutatingAdmissionPolicy,
mutatingBinding *admissionv1beta1.MutatingAdmissionPolicyBinding,
validatingPolicy *admissionregv1.ValidatingAdmissionPolicy,
validatingBinding *admissionregv1.ValidatingAdmissionPolicyBinding,
testCase TestCase,
Expand All @@ -135,7 +138,7 @@ func (e *Evaluator) EvaluateTest(
}

// Evaluate policy
evalResult, err := e.evaluatePolicy(mutatingPolicy, validatingPolicy, validatingBinding, testCase)
evalResult, err := e.evaluatePolicy(mutatingPolicy, mutatingBinding, validatingPolicy, validatingBinding, testCase)
if err != nil {
return &TestResult{
Passed: false,
Expand Down Expand Up @@ -236,6 +239,7 @@ func getDiff(expected, actual string) string {
// evaluatePolicy evaluates the appropriate policy (mutating or validating) and returns the result.
func (e *Evaluator) evaluatePolicy(
mutatingPolicy *admissionv1beta1.MutatingAdmissionPolicy,
mutatingBinding *admissionv1beta1.MutatingAdmissionPolicyBinding,
validatingPolicy *admissionregv1.ValidatingAdmissionPolicy,
validatingBinding *admissionregv1.ValidatingAdmissionPolicyBinding,
testCase TestCase,
Expand All @@ -250,6 +254,7 @@ func (e *Evaluator) evaluatePolicy(
case mutatingPolicy != nil:
return e.EvaluateMutating(
mutatingPolicy,
mutatingBinding,
testCase.GetRequest(),
testCase.GetObject(),
testCase.GetOldObject(),
Expand Down Expand Up @@ -566,6 +571,7 @@ type TestOutcome struct {
// EvaluateMutating evaluates a MutatingAdmissionPolicy against an admission request.
func (e *Evaluator) EvaluateMutating(
policy *admissionv1beta1.MutatingAdmissionPolicy,
binding *admissionv1beta1.MutatingAdmissionPolicyBinding,
request *admissionv1.AdmissionRequest,
object *unstructured.Unstructured,
oldObject *unstructured.Unstructured,
Expand All @@ -574,6 +580,14 @@ func (e *Evaluator) EvaluateMutating(
authorizer authorizer.Authorizer,
userInfo user.Info,
) (*EvaluationResult, error) {
// Evaluate binding's namespaceSelector if present
if matched, err := e.matchesNamespaceSelectorV1Beta1(binding, namespaceObj); err != nil {
return nil, fmt.Errorf("evaluate namespace selector: %w", err)
} else if !matched {
// Namespace selector doesn't match, policy doesn't apply
return &EvaluationResult{Allowed: true}, nil
}

requestMap, err := convertAdmissionRequest(request)
if err != nil {
return nil, fmt.Errorf("convert admission request: %w", err)
Expand Down Expand Up @@ -693,7 +707,7 @@ func (e *Evaluator) applyMutations(
}

// EvaluateValidating evaluates a ValidatingAdmissionPolicy against an admission request.
func (e *Evaluator) EvaluateValidating(
func (e *Evaluator) EvaluateValidating( //nolint:cyclop // Complexity is inherent in evaluating all aspects of a validating policy
policy *admissionregv1.ValidatingAdmissionPolicy,
binding *admissionregv1.ValidatingAdmissionPolicyBinding,
request *admissionv1.AdmissionRequest,
Expand All @@ -704,6 +718,14 @@ func (e *Evaluator) EvaluateValidating(
authorizer authorizer.Authorizer,
userInfo user.Info,
) (*EvaluationResult, error) {
// Evaluate binding's namespaceSelector if present
if matched, err := e.matchesNamespaceSelector(binding, namespaceObj); err != nil {
return nil, fmt.Errorf("evaluate namespace selector: %w", err)
} else if !matched {
// Namespace selector doesn't match, policy doesn't apply
return &EvaluationResult{Allowed: true}, nil
}

// Convert admission request
requestMap, err := convertAdmissionRequest(request)
if err != nil {
Expand Down Expand Up @@ -754,6 +776,62 @@ func (e *Evaluator) EvaluateValidating(
}, nil
}

// matchesNamespaceSelectorByLabelSelector checks if the namespace object's labels match the given label selector.
// Returns true if the selector is nil, empty, or matches the namespace labels.
func matchesNamespaceSelectorByLabelSelector(
labelSelector *metav1.LabelSelector,
namespaceObj *unstructured.Unstructured,
) (bool, error) {
if labelSelector == nil {
return true, nil
}

// Convert LabelSelector to labels.Selector
selector, err := metav1.LabelSelectorAsSelector(labelSelector)
if err != nil {
return false, fmt.Errorf("parse namespace selector: %w", err)
}

// Empty selector matches everything
if selector.Empty() {
return true, nil
}

// No namespace object provided - can't evaluate selector
if namespaceObj == nil {
return true, nil
}

// Check if namespace labels match the selector
return selector.Matches(labels.Set(namespaceObj.GetLabels())), nil
}

// matchesNamespaceSelector checks if the namespace object's labels match the binding's namespace selector.
// Returns true if the selector matches (policy should be evaluated), false otherwise.
func (e *Evaluator) matchesNamespaceSelector(
binding *admissionregv1.ValidatingAdmissionPolicyBinding,
namespaceObj *unstructured.Unstructured,
) (bool, error) {
if binding == nil || binding.Spec.MatchResources == nil {
return true, nil
}

return matchesNamespaceSelectorByLabelSelector(binding.Spec.MatchResources.NamespaceSelector, namespaceObj)
}

// matchesNamespaceSelectorV1Beta1 checks if the namespace object's labels match the binding's namespace selector.
// Returns true if the selector matches (policy should be evaluated), false otherwise.
func (e *Evaluator) matchesNamespaceSelectorV1Beta1(
binding *admissionv1beta1.MutatingAdmissionPolicyBinding,
namespaceObj *unstructured.Unstructured,
) (bool, error) {
if binding == nil || binding.Spec.MatchResources == nil {
return true, nil
}

return matchesNamespaceSelectorByLabelSelector(binding.Spec.MatchResources.NamespaceSelector, namespaceObj)
}

// evaluateMatchConditions evaluates all match conditions and returns true if all match.
func (e *Evaluator) evaluateMatchConditions(conditions []admissionregv1.MatchCondition, vars map[string]any) (bool, error) {
for _, condition := range conditions {
Expand Down
2 changes: 1 addition & 1 deletion internal/evaluator/evaluator_authorizer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ func runMutatingTest(t *testing.T, policy *admissionv1beta1.MutatingAdmissionPol

userInfo := MockUserInfo(username, groups)

result, err := evaluator.EvaluateMutating(policy, request, object, nil, nil, nil, auth, userInfo)
result, err := evaluator.EvaluateMutating(policy, nil, request, object, nil, nil, nil, auth, userInfo)
if err != nil {
t.Fatalf("EvaluateMutating() error = %v", err)
}
Expand Down
2 changes: 1 addition & 1 deletion internal/evaluator/evaluator_params_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ func TestEvaluateMutating_WithParams(t *testing.T) {
Operation: admissionv1.Create,
}

result, err := evaluator.EvaluateMutating(tc.policy, request, tc.object, nil, tc.params, nil, nil, nil)
result, err := evaluator.EvaluateMutating(tc.policy, nil, request, tc.object, nil, tc.params, nil, nil, nil)
if err != nil {
t.Fatalf("EvaluateMutating() error = %v", err)
}
Expand Down
5 changes: 3 additions & 2 deletions internal/evaluator/evaluator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -696,7 +696,7 @@ func TestEvaluateMutating(t *testing.T) {
Operation: admissionv1.Create,
}

result, err := evaluator.EvaluateMutating(tc.policy, request, tc.object, tc.oldObject, nil, nil, nil, nil)
result, err := evaluator.EvaluateMutating(tc.policy, nil, request, tc.object, tc.oldObject, nil, nil, nil, nil)

if tc.expectedError {
if err == nil {
Expand Down Expand Up @@ -1558,6 +1558,7 @@ func TestEvaluator_EvaluateTest(t *testing.T) {
tests := []struct {
name string
mutatingPolicy *admissionv1beta1.MutatingAdmissionPolicy
mutatingBinding *admissionv1beta1.MutatingAdmissionPolicyBinding
validatingPolicy *admissionregv1.ValidatingAdmissionPolicy
validatingBinding *admissionregv1.ValidatingAdmissionPolicyBinding
testCase MockTestCase
Expand Down Expand Up @@ -1827,7 +1828,7 @@ func TestEvaluator_EvaluateTest(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()

result := evaluator.EvaluateTest(tc.mutatingPolicy, tc.validatingPolicy, tc.validatingBinding, tc.testCase)
result := evaluator.EvaluateTest(tc.mutatingPolicy, tc.mutatingBinding, tc.validatingPolicy, tc.validatingBinding, tc.testCase)

if result.Passed != tc.wantPassed {
t.Errorf("EvaluateTest() Passed = %v, want %v. Message: %s", result.Passed, tc.wantPassed, result.Message)
Expand Down
27 changes: 18 additions & 9 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ func runSuite(eval *evaluator.Evaluator, rep *reporter.Reporter, suite *loader.T
for _, test := range suite.Tests {
suiteRep.StartTest(test.Name)

mutatingPolicy, validatingPolicy, validatingBinding := findPolicies(suite, test.PolicyName)
mutatingPolicy, mutatingBinding, validatingPolicy, validatingBinding := findPolicies(suite, test.PolicyName)

if mutatingPolicy == nil && validatingPolicy == nil {
suiteRep.ReportFail(test.Name, fmt.Sprintf("policy %q not found", test.PolicyName))
Expand All @@ -146,24 +146,33 @@ func runSuite(eval *evaluator.Evaluator, rep *reporter.Reporter, suite *loader.T
}

// Evaluate test
result := eval.EvaluateTest(mutatingPolicy, validatingPolicy, validatingBinding, test)
result := eval.EvaluateTest(mutatingPolicy, mutatingBinding, validatingPolicy, validatingBinding, test)

suiteRep.ReportResult(test.Name, result)
}

return nil
}

func findPolicies(suite *loader.TestSuite, policyName string) (*admissionv1beta1.MutatingAdmissionPolicy, *admissionregv1.ValidatingAdmissionPolicy, *admissionregv1.ValidatingAdmissionPolicyBinding) {
var mutatingPolicy *admissionv1beta1.MutatingAdmissionPolicy

var validatingPolicy *admissionregv1.ValidatingAdmissionPolicy

var validatingBinding *admissionregv1.ValidatingAdmissionPolicyBinding
func findPolicies(suite *loader.TestSuite, policyName string) (*admissionv1beta1.MutatingAdmissionPolicy, *admissionv1beta1.MutatingAdmissionPolicyBinding, *admissionregv1.ValidatingAdmissionPolicy, *admissionregv1.ValidatingAdmissionPolicyBinding) {
var (
mutatingPolicy *admissionv1beta1.MutatingAdmissionPolicy
mutatingBinding *admissionv1beta1.MutatingAdmissionPolicyBinding
validatingPolicy *admissionregv1.ValidatingAdmissionPolicy
validatingBinding *admissionregv1.ValidatingAdmissionPolicyBinding
)

for _, policy := range suite.MutatingPolicies {
if policy.Name == policyName {
mutatingPolicy = policy
// Find matching binding
for _, binding := range suite.MutatingBindings {
if binding.Spec.PolicyName == policy.Name {
mutatingBinding = binding

break
}
}

break
}
Expand All @@ -187,7 +196,7 @@ func findPolicies(suite *loader.TestSuite, policyName string) (*admissionv1beta1
}
}

return mutatingPolicy, validatingPolicy, validatingBinding
return mutatingPolicy, mutatingBinding, validatingPolicy, validatingBinding
}

func getVersion() string {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
apiVersion: admissionregistration.k8s.io/v1beta1
kind: MutatingAdmissionPolicyBinding
metadata:
name: namespace-selector-binding-mutating-test-binding
spec:
policyName: namespace-selector-binding-mutating-test
matchResources:
namespaceSelector:
matchExpressions:
- key: environment
operator: In
values: ["prod"]

Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
apiVersion: admissionregistration.k8s.io/v1beta1
kind: MutatingAdmissionPolicy
metadata:
name: namespace-selector-binding-mutating-test
spec:
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["configmaps"]
mutations:
- patchType: ApplyConfiguration
applyConfiguration:
expression: |
Object{
metadata: Object.metadata{
labels: {"mutated": "true"}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: test-config
data:
key: value

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
operation: CREATE
namespaceObject:
apiVersion: v1
kind: Namespace
metadata:
name: dev-namespace
labels:
environment: dev

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: test-config
data:
key: value

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
operation: CREATE
namespaceObject:
apiVersion: v1
kind: Namespace
metadata:
name: unlabeled-namespace
labels: {}

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: test-config
labels:
mutated: "true"
data:
key: value

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: test-config
data:
key: value

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
operation: CREATE
namespaceObject:
apiVersion: v1
kind: Namespace
metadata:
name: prod-namespace
labels:
environment: prod

Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
name: namespace-selector-binding-test-binding
spec:
policyName: namespace-selector-binding-test
validationActions: [Deny]
matchResources:
namespaceSelector:
matchExpressions:
- key: environment
operator: In
values: ["prod"]

Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: namespace-selector-binding-test
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["configmaps"]
validations:
- expression: "false"
message: "This policy always denies - used to test namespace selector filtering"
reason: Forbidden

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: test-config
data:
key: value

Loading