diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/admission_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/admission_test.go index dfdb5b70a54..6425813c1f2 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/admission_test.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/admission_test.go @@ -265,10 +265,10 @@ var _ Validator = &fakeValidator{} type fakeValidator struct { validationFilter, auditAnnotationFilter, messageFilter *fakeFilter - ValidateFunc func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) ValidateResult + ValidateFunc func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult } -func (f *fakeValidator) RegisterDefinition(definition *v1alpha1.ValidatingAdmissionPolicy, validateFunc func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) ValidateResult) { +func (f *fakeValidator) RegisterDefinition(definition *v1alpha1.ValidatingAdmissionPolicy, validateFunc func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult) { //Key must be something that we can decipher from the inputs to Validate so using message which will be on the validationCondition object of evalResult var key string if len(definition.Spec.Validations) > 0 { @@ -285,8 +285,8 @@ func (f *fakeValidator) RegisterDefinition(definition *v1alpha1.ValidatingAdmiss validatorMap[key] = f } -func (f *fakeValidator) Validate(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) ValidateResult { - return f.ValidateFunc(ctx, versionedAttr, versionedParams, runtimeCELCostBudget) +func (f *fakeValidator) Validate(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { + return f.ValidateFunc(ctx, versionedAttr, versionedParams, runtimeCELCostBudget, authz) } var _ Matcher = &fakeMatcher{} @@ -419,7 +419,7 @@ func setupTestCommon(t *testing.T, compiler cel.FilterCompiler, matcher Matcher, // Override compiler used by controller for tests controller = handler.evaluator.(*celAdmissionController) controller.policyController.filterCompiler = compiler - controller.policyController.newValidator = func(validationFilter cel.Filter, celMatcher matchconditions.Matcher, auditAnnotationFilter, messageFilter cel.Filter, fail *admissionRegistrationv1.FailurePolicyType, authorizer authorizer.Authorizer) Validator { + controller.policyController.newValidator = func(validationFilter cel.Filter, celMatcher matchconditions.Matcher, auditAnnotationFilter, messageFilter cel.Filter, fail *admissionRegistrationv1.FailurePolicyType) Validator { f := validationFilter.(*fakeFilter) v := validatorMap[f.keyId] v.validationFilter = f @@ -770,7 +770,7 @@ func TestBasicPolicyDefinitionFailure(t *testing.T) { } }) - validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) ValidateResult { + validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { return ValidateResult{ Decisions: []PolicyDecision{ { @@ -840,7 +840,7 @@ func TestDefinitionDoesntMatch(t *testing.T) { } }) - validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) ValidateResult { + validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { return ValidateResult{ Decisions: []PolicyDecision{ { @@ -953,7 +953,7 @@ func TestReconfigureBinding(t *testing.T) { } }) - validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) ValidateResult { + validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { return ValidateResult{ Decisions: []PolicyDecision{ { @@ -1063,7 +1063,7 @@ func TestRemoveDefinition(t *testing.T) { } }) - validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) ValidateResult { + validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { return ValidateResult{ Decisions: []PolicyDecision{ { @@ -1132,7 +1132,7 @@ func TestRemoveBinding(t *testing.T) { } }) - validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) ValidateResult { + validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { return ValidateResult{ Decisions: []PolicyDecision{ { @@ -1242,7 +1242,7 @@ func TestInvalidParamSourceInstanceName(t *testing.T) { } }) - validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) ValidateResult { + validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { return ValidateResult{ Decisions: []PolicyDecision{ { @@ -1310,7 +1310,7 @@ func TestEmptyParamSource(t *testing.T) { } }) - validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) ValidateResult { + validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { return ValidateResult{ Decisions: []PolicyDecision{ { @@ -1412,7 +1412,7 @@ func TestMultiplePoliciesSharedParamType(t *testing.T) { } }) - validator1.RegisterDefinition(&policy1, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) ValidateResult { + validator1.RegisterDefinition(&policy1, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { evaluations1.Add(1) return ValidateResult{ Decisions: []PolicyDecision{ @@ -1431,7 +1431,7 @@ func TestMultiplePoliciesSharedParamType(t *testing.T) { } }) - validator2.RegisterDefinition(&policy2, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) ValidateResult { + validator2.RegisterDefinition(&policy2, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { evaluations2.Add(1) return ValidateResult{ Decisions: []PolicyDecision{ @@ -1541,7 +1541,7 @@ func TestNativeTypeParam(t *testing.T) { } }) - validator.RegisterDefinition(&nativeTypeParamPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) ValidateResult { + validator.RegisterDefinition(&nativeTypeParamPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { evaluations.Add(1) if _, ok := versionedParams.(*v1.ConfigMap); ok { return ValidateResult{ @@ -1623,7 +1623,7 @@ func TestAuditValidationAction(t *testing.T) { } }) - validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) ValidateResult { + validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { return ValidateResult{ Decisions: []PolicyDecision{ { @@ -1694,7 +1694,7 @@ func TestWarnValidationAction(t *testing.T) { } }) - validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) ValidateResult { + validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { return ValidateResult{ Decisions: []PolicyDecision{ { @@ -1753,7 +1753,7 @@ func TestAllValidationActions(t *testing.T) { } }) - validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) ValidateResult { + validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { return ValidateResult{ Decisions: []PolicyDecision{ { @@ -1824,7 +1824,7 @@ func TestAuditAnnotations(t *testing.T) { } }) - validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) ValidateResult { + validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { o, err := meta.Accessor(versionedParams) if err != nil { t.Fatal(err) diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/caching_authorizer.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/caching_authorizer.go new file mode 100644 index 00000000000..a295cb30dc0 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/caching_authorizer.go @@ -0,0 +1,133 @@ +/* +Copyright 2023 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package validatingadmissionpolicy + +import ( + "context" + "encoding/json" + "sort" + "strings" + + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/apiserver/pkg/authorization/authorizer" +) + +type authzResult struct { + authorized authorizer.Decision + reason string + err error +} + +type cachingAuthorizer struct { + authorizer authorizer.Authorizer + decisions map[string]authzResult +} + +func newCachingAuthorizer(in authorizer.Authorizer) authorizer.Authorizer { + return &cachingAuthorizer{ + authorizer: in, + decisions: make(map[string]authzResult), + } +} + +// The attribute accessors known to cache key construction. If this fails to compile, the cache +// implementation may need to be updated. +var _ authorizer.Attributes = (interface { + GetUser() user.Info + GetVerb() string + IsReadOnly() bool + GetNamespace() string + GetResource() string + GetSubresource() string + GetName() string + GetAPIGroup() string + GetAPIVersion() string + IsResourceRequest() bool + GetPath() string +})(nil) + +// The user info accessors known to cache key construction. If this fails to compile, the cache +// implementation may need to be updated. +var _ user.Info = (interface { + GetName() string + GetUID() string + GetGroups() []string + GetExtra() map[string][]string +})(nil) + +// Authorize returns an authorization decision by delegating to another Authorizer. If an equivalent +// check has already been performed, a cached result is returned. Not safe for concurrent use. +func (ca *cachingAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) { + serializableAttributes := authorizer.AttributesRecord{ + Verb: a.GetVerb(), + Namespace: a.GetNamespace(), + APIGroup: a.GetAPIGroup(), + APIVersion: a.GetAPIVersion(), + Resource: a.GetResource(), + Subresource: a.GetSubresource(), + Name: a.GetName(), + ResourceRequest: a.IsResourceRequest(), + Path: a.GetPath(), + } + + if u := a.GetUser(); u != nil { + di := &user.DefaultInfo{ + Name: u.GetName(), + UID: u.GetUID(), + } + + // Differently-ordered groups or extras could cause otherwise-equivalent checks to + // have distinct cache keys. + if groups := u.GetGroups(); len(groups) > 0 { + di.Groups = make([]string, len(groups)) + copy(di.Groups, groups) + sort.Strings(di.Groups) + } + + if extra := u.GetExtra(); len(extra) > 0 { + di.Extra = make(map[string][]string, len(extra)) + for k, vs := range extra { + vdupe := make([]string, len(vs)) + copy(vdupe, vs) + sort.Strings(vdupe) + di.Extra[k] = vdupe + } + } + + serializableAttributes.User = di + } + + var b strings.Builder + if err := json.NewEncoder(&b).Encode(serializableAttributes); err != nil { + return authorizer.DecisionNoOpinion, "", err + } + key := b.String() + + if cached, ok := ca.decisions[key]; ok { + return cached.authorized, cached.reason, cached.err + } + + authorized, reason, err := ca.authorizer.Authorize(ctx, a) + + ca.decisions[key] = authzResult{ + authorized: authorized, + reason: reason, + err: err, + } + + return authorized, reason, err +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/caching_authorizer_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/caching_authorizer_test.go new file mode 100644 index 00000000000..9cef35fe4af --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/caching_authorizer_test.go @@ -0,0 +1,251 @@ +/* +Copyright 2023 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package validatingadmissionpolicy + +import ( + "context" + "fmt" + "testing" + + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/apiserver/pkg/authorization/authorizer" +) + +func TestCachingAuthorizer(t *testing.T) { + type result struct { + decision authorizer.Decision + reason string + error error + } + + type invocation struct { + attributes authorizer.Attributes + expected result + } + + for _, tc := range []struct { + name string + calls []invocation + backend []result + }{ + { + name: "hit", + calls: []invocation{ + { + attributes: authorizer.AttributesRecord{Name: "test name"}, + expected: result{ + decision: authorizer.DecisionAllow, + reason: "test reason", + error: fmt.Errorf("test error"), + }, + }, + { + attributes: authorizer.AttributesRecord{Name: "test name"}, + expected: result{ + decision: authorizer.DecisionAllow, + reason: "test reason", + error: fmt.Errorf("test error"), + }, + }, + }, + backend: []result{ + { + decision: authorizer.DecisionAllow, + reason: "test reason", + error: fmt.Errorf("test error"), + }, + }, + }, + { + name: "hit with differently-ordered groups", + calls: []invocation{ + { + attributes: authorizer.AttributesRecord{ + User: &user.DefaultInfo{ + Groups: []string{"a", "b", "c"}, + }, + }, + expected: result{ + decision: authorizer.DecisionAllow, + reason: "test reason", + error: fmt.Errorf("test error"), + }, + }, + { + attributes: authorizer.AttributesRecord{ + User: &user.DefaultInfo{ + Groups: []string{"c", "b", "a"}, + }, + }, + expected: result{ + decision: authorizer.DecisionAllow, + reason: "test reason", + error: fmt.Errorf("test error"), + }, + }, + }, + backend: []result{ + { + decision: authorizer.DecisionAllow, + reason: "test reason", + error: fmt.Errorf("test error"), + }, + }, + }, + { + name: "hit with differently-ordered extra", + calls: []invocation{ + { + attributes: authorizer.AttributesRecord{ + User: &user.DefaultInfo{ + Extra: map[string][]string{ + "k": {"a", "b", "c"}, + }, + }, + }, + expected: result{ + decision: authorizer.DecisionAllow, + reason: "test reason", + error: fmt.Errorf("test error"), + }, + }, + { + attributes: authorizer.AttributesRecord{ + User: &user.DefaultInfo{ + Extra: map[string][]string{ + "k": {"c", "b", "a"}, + }, + }, + }, + expected: result{ + decision: authorizer.DecisionAllow, + reason: "test reason", + error: fmt.Errorf("test error"), + }, + }, + }, + backend: []result{ + { + decision: authorizer.DecisionAllow, + reason: "test reason", + error: fmt.Errorf("test error"), + }, + }, + }, + { + name: "miss due to different name", + calls: []invocation{ + { + attributes: authorizer.AttributesRecord{Name: "alpha"}, + expected: result{ + decision: authorizer.DecisionAllow, + reason: "test reason alpha", + error: fmt.Errorf("test error alpha"), + }, + }, + { + attributes: authorizer.AttributesRecord{Name: "beta"}, + expected: result{ + decision: authorizer.DecisionDeny, + reason: "test reason beta", + error: fmt.Errorf("test error beta"), + }, + }, + }, + backend: []result{ + { + decision: authorizer.DecisionAllow, + reason: "test reason alpha", + error: fmt.Errorf("test error alpha"), + }, + { + decision: authorizer.DecisionDeny, + reason: "test reason beta", + error: fmt.Errorf("test error beta"), + }, + }, + }, + { + name: "miss due to different user", + calls: []invocation{ + { + attributes: authorizer.AttributesRecord{ + User: &user.DefaultInfo{Name: "alpha"}, + }, + expected: result{ + decision: authorizer.DecisionAllow, + reason: "test reason alpha", + error: fmt.Errorf("test error alpha"), + }, + }, + { + attributes: authorizer.AttributesRecord{ + User: &user.DefaultInfo{Name: "beta"}, + }, + expected: result{ + decision: authorizer.DecisionDeny, + reason: "test reason beta", + error: fmt.Errorf("test error beta"), + }, + }, + }, + backend: []result{ + { + decision: authorizer.DecisionAllow, + reason: "test reason alpha", + error: fmt.Errorf("test error alpha"), + }, + { + decision: authorizer.DecisionDeny, + reason: "test reason beta", + error: fmt.Errorf("test error beta"), + }, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + var misses int + frontend := newCachingAuthorizer(func() authorizer.Authorizer { + return authorizer.AuthorizerFunc(func(_ context.Context, attributes authorizer.Attributes) (authorizer.Decision, string, error) { + if misses >= len(tc.backend) { + t.Fatalf("got more than expected %d backend invocations", len(tc.backend)) + } + result := tc.backend[misses] + misses++ + return result.decision, result.reason, result.error + }) + }()) + + for i, invocation := range tc.calls { + decision, reason, err := frontend.Authorize(context.TODO(), invocation.attributes) + if decision != invocation.expected.decision { + t.Errorf("(call %d of %d) expected decision %v, got %v", i+1, len(tc.calls), invocation.expected.decision, decision) + } + if reason != invocation.expected.reason { + t.Errorf("(call %d of %d) expected reason %q, got %q", i+1, len(tc.calls), invocation.expected.reason, reason) + } + if err.Error() != invocation.expected.error.Error() { + t.Errorf("(call %d of %d) expected error %q, got %q", i+1, len(tc.calls), invocation.expected.error.Error(), err.Error()) + } + } + + if len(tc.backend) > misses { + t.Errorf("expected %d backend invocations, got %d", len(tc.backend), misses) + } + }) + } +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/controller.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/controller.go index 3f6af530530..ab5cd9434ca 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/controller.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/controller.go @@ -67,6 +67,8 @@ type celAdmissionController struct { // A snapshot of the current policy configuration is synced with this field // asynchronously definitions atomic.Value + + authz authorizer.Authorizer } // Everything someone might need to validate a single ValidatingPolicyDefinition @@ -147,8 +149,8 @@ func NewAdmissionController( informerFactory.Admissionregistration().V1alpha1().ValidatingAdmissionPolicies().Informer()), generic.NewInformer[*v1alpha1.ValidatingAdmissionPolicyBinding]( informerFactory.Admissionregistration().V1alpha1().ValidatingAdmissionPolicyBindings().Informer()), - authz, ), + authz: authz, } } @@ -236,6 +238,8 @@ func (c *celAdmissionController) Validate( } policyDatas := c.definitions.Load().([]policyData) + authz := newCachingAuthorizer(c.authz) + for _, definitionInfo := range policyDatas { definition := definitionInfo.lastReconciledValue matches, matchKind, err := c.policyController.matcher.DefinitionMatches(a, o, definition) @@ -336,7 +340,7 @@ func (c *celAdmissionController) Validate( versionedAttr = va } - validationResult := bindingInfo.validator.Validate(ctx, versionedAttr, param, celconfig.RuntimeCELCostBudget) + validationResult := bindingInfo.validator.Validate(ctx, versionedAttr, param, celconfig.RuntimeCELCostBudget, authz) for i, decision := range validationResult.Decisions { switch decision.Action { diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/controller_reconcile.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/controller_reconcile.go index f1c91020252..21bbbd54064 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/controller_reconcile.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/controller_reconcile.go @@ -36,7 +36,6 @@ import ( "k8s.io/apiserver/pkg/admission/plugin/cel" "k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/internal/generic" "k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions" - "k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/apiserver/pkg/cel/environment" "k8s.io/client-go/dynamic" "k8s.io/client-go/dynamic/dynamicinformer" @@ -96,11 +95,9 @@ type policyController struct { definitionsToBindings map[namespacedName]sets.Set[namespacedName] client kubernetes.Interface - - authz authorizer.Authorizer } -type newValidator func(validationFilter cel.Filter, celMatcher matchconditions.Matcher, auditAnnotationFilter, messageFilter cel.Filter, failurePolicy *v1.FailurePolicyType, authorizer authorizer.Authorizer) Validator +type newValidator func(validationFilter cel.Filter, celMatcher matchconditions.Matcher, auditAnnotationFilter, messageFilter cel.Filter, failurePolicy *v1.FailurePolicyType) Validator func newPolicyController( restMapper meta.RESTMapper, @@ -111,7 +108,6 @@ func newPolicyController( matcher Matcher, policiesInformer generic.Informer[*v1alpha1.ValidatingAdmissionPolicy], bindingsInformer generic.Informer[*v1alpha1.ValidatingAdmissionPolicyBinding], - authz authorizer.Authorizer, ) *policyController { res := &policyController{} *res = policyController{ @@ -142,7 +138,6 @@ func newPolicyController( restMapper: restMapper, dynamicClient: dynamicClient, client: client, - authz: authz, } return res } @@ -512,7 +507,7 @@ func (c *policyController) latestPolicyData() []policyData { for i := range matchConditions { matchExpressionAccessors[i] = (*matchconditions.MatchCondition)(&matchConditions[i]) } - matcher = matchconditions.NewMatcher(c.filterCompiler.Compile(matchExpressionAccessors, optionalVars, environment.StoredExpressions), c.authz, failurePolicy, "validatingadmissionpolicy", definitionInfo.lastReconciledValue.Name) + matcher = matchconditions.NewMatcher(c.filterCompiler.Compile(matchExpressionAccessors, optionalVars, environment.StoredExpressions), failurePolicy, "validatingadmissionpolicy", definitionInfo.lastReconciledValue.Name) } bindingInfo.validator = c.newValidator( c.filterCompiler.Compile(convertv1alpha1Validations(definitionInfo.lastReconciledValue.Spec.Validations), optionalVars, environment.StoredExpressions), @@ -520,7 +515,6 @@ func (c *policyController) latestPolicyData() []policyData { c.filterCompiler.Compile(convertv1alpha1AuditAnnotations(definitionInfo.lastReconciledValue.Spec.AuditAnnotations), optionalVars, environment.StoredExpressions), c.filterCompiler.Compile(convertV1Alpha1MessageExpressions(definitionInfo.lastReconciledValue.Spec.Validations), expressionOptionalVars, environment.StoredExpressions), failurePolicy, - c.authz, ) } bindingInfos = append(bindingInfos, *bindingInfo) diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/interface.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/interface.go index 0f84152e8b4..135148b9e1e 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/interface.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/interface.go @@ -27,6 +27,7 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/admission/plugin/cel" + "k8s.io/apiserver/pkg/authorization/authorizer" ) var _ cel.ExpressionAccessor = &ValidationCondition{} @@ -85,5 +86,5 @@ type ValidateResult struct { type Validator interface { // Validate is used to take cel evaluations and convert into decisions // runtimeCELCostBudget was added for testing purpose only. Callers should always use const RuntimeCELCostBudget from k8s.io/apiserver/pkg/apis/cel/config.go as input. - Validate(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) ValidateResult + Validate(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult } diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/validator.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/validator.go index 448750c9199..65854764f84 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/validator.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/validator.go @@ -42,17 +42,15 @@ type validator struct { auditAnnotationFilter cel.Filter messageFilter cel.Filter failPolicy *v1.FailurePolicyType - authorizer authorizer.Authorizer } -func NewValidator(validationFilter cel.Filter, celMatcher matchconditions.Matcher, auditAnnotationFilter, messageFilter cel.Filter, failPolicy *v1.FailurePolicyType, authorizer authorizer.Authorizer) Validator { +func NewValidator(validationFilter cel.Filter, celMatcher matchconditions.Matcher, auditAnnotationFilter, messageFilter cel.Filter, failPolicy *v1.FailurePolicyType) Validator { return &validator{ celMatcher: celMatcher, validationFilter: validationFilter, auditAnnotationFilter: auditAnnotationFilter, messageFilter: messageFilter, failPolicy: failPolicy, - authorizer: authorizer, } } @@ -72,7 +70,7 @@ func auditAnnotationEvaluationForError(f v1.FailurePolicyType) PolicyAuditAnnota // Validate takes a list of Evaluation and a failure policy and converts them into actionable PolicyDecisions // runtimeCELCostBudget was added for testing purpose only. Callers should always use const RuntimeCELCostBudget from k8s.io/apiserver/pkg/apis/cel/config.go as input. -func (v *validator) Validate(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) ValidateResult { +func (v *validator) Validate(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { var f v1.FailurePolicyType if v.failPolicy == nil { f = v1.Fail @@ -81,7 +79,7 @@ func (v *validator) Validate(ctx context.Context, versionedAttr *admission.Versi } if v.celMatcher != nil { - matchResults := v.celMatcher.Match(ctx, versionedAttr, versionedParams) + matchResults := v.celMatcher.Match(ctx, versionedAttr, versionedParams, authz) if matchResults.Error != nil { return ValidateResult{ Decisions: []PolicyDecision{ @@ -100,7 +98,7 @@ func (v *validator) Validate(ctx context.Context, versionedAttr *admission.Versi } } - optionalVars := cel.OptionalVariableBindings{VersionedParams: versionedParams, Authorizer: v.authorizer} + optionalVars := cel.OptionalVariableBindings{VersionedParams: versionedParams, Authorizer: authz} expressionOptionalVars := cel.OptionalVariableBindings{VersionedParams: versionedParams} admissionRequest := cel.CreateAdmissionRequest(versionedAttr.Attributes) evalResults, remainingBudget, err := v.validationFilter.ForInput(ctx, versionedAttr, admissionRequest, optionalVars, runtimeCELCostBudget) diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/validator_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/validator_test.go index 69a55e5cc12..55f3ef8627f 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/validator_test.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/validator_test.go @@ -35,6 +35,7 @@ import ( "k8s.io/apiserver/pkg/admission/plugin/cel" "k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions" celconfig "k8s.io/apiserver/pkg/apis/cel" + "k8s.io/apiserver/pkg/authorization/authorizer" apiservercel "k8s.io/apiserver/pkg/cel" "k8s.io/apiserver/pkg/cel/environment" ) @@ -70,7 +71,7 @@ type fakeCELMatcher struct { matches bool } -func (f *fakeCELMatcher) Match(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object) matchconditions.MatchResult { +func (f *fakeCELMatcher) Match(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, authz authorizer.Authorizer) matchconditions.MatchResult { return matchconditions.MatchResult{Matches: f.matches, FailedConditionName: "placeholder", Error: f.error} } @@ -891,7 +892,7 @@ func TestValidate(t *testing.T) { if tc.costBudget != 0 { budget = tc.costBudget } - validateResult := v.Validate(ctx, fakeVersionedAttr, nil, budget) + validateResult := v.Validate(ctx, fakeVersionedAttr, nil, budget, nil) require.Equal(t, len(validateResult.Decisions), len(tc.policyDecision)) @@ -943,7 +944,7 @@ func TestContextCanceled(t *testing.T) { } ctx, cancel := context.WithCancel(context.TODO()) cancel() - validationResult := v.Validate(ctx, fakeVersionedAttr, nil, celconfig.RuntimeCELCostBudget) + validationResult := v.Validate(ctx, fakeVersionedAttr, nil, celconfig.RuntimeCELCostBudget, nil) if len(validationResult.Decisions) != 1 || !strings.Contains(validationResult.Decisions[0].Message, "operation interrupted") { t.Errorf("Expected 'operation interrupted' but got %v", validationResult.Decisions) } diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/accessors.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/accessors.go index b95af270d7a..6c48418de61 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/accessors.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/accessors.go @@ -26,7 +26,6 @@ import ( "k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions" "k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/namespace" "k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/object" - "k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/apiserver/pkg/cel/environment" webhookutil "k8s.io/apiserver/pkg/util/webhook" "k8s.io/client-go/rest" @@ -49,7 +48,7 @@ type WebhookAccessor interface { GetRESTClient(clientManager *webhookutil.ClientManager) (*rest.RESTClient, error) // GetCompiledMatcher gets the compiled matcher object - GetCompiledMatcher(compiler cel.FilterCompiler, authorizer authorizer.Authorizer) matchconditions.Matcher + GetCompiledMatcher(compiler cel.FilterCompiler) matchconditions.Matcher // GetName gets the webhook Name field. Note that the name is scoped to the webhook // configuration and does not provide a globally unique identity, if a unique identity is @@ -125,7 +124,7 @@ func (m *mutatingWebhookAccessor) GetRESTClient(clientManager *webhookutil.Clien } // TODO: graduation to beta: resolve the fact that we rebuild ALL items whenever ANY config changes in NewMutatingWebhookConfigurationManager and NewValidatingWebhookConfigurationManager ... now that we're doing CEL compilation, we probably want to avoid that -func (m *mutatingWebhookAccessor) GetCompiledMatcher(compiler cel.FilterCompiler, authorizer authorizer.Authorizer) matchconditions.Matcher { +func (m *mutatingWebhookAccessor) GetCompiledMatcher(compiler cel.FilterCompiler) matchconditions.Matcher { m.compileMatcher.Do(func() { expressions := make([]cel.ExpressionAccessor, len(m.MutatingWebhook.MatchConditions)) for i, matchCondition := range m.MutatingWebhook.MatchConditions { @@ -141,7 +140,7 @@ func (m *mutatingWebhookAccessor) GetCompiledMatcher(compiler cel.FilterCompiler HasAuthorizer: true, }, environment.StoredExpressions, - ), authorizer, m.FailurePolicy, "validating", m.Name) + ), m.FailurePolicy, "validating", m.Name) }) return m.compiledMatcher } @@ -253,7 +252,7 @@ func (v *validatingWebhookAccessor) GetRESTClient(clientManager *webhookutil.Cli return v.client, v.clientErr } -func (v *validatingWebhookAccessor) GetCompiledMatcher(compiler cel.FilterCompiler, authorizer authorizer.Authorizer) matchconditions.Matcher { +func (v *validatingWebhookAccessor) GetCompiledMatcher(compiler cel.FilterCompiler) matchconditions.Matcher { v.compileMatcher.Do(func() { expressions := make([]cel.ExpressionAccessor, len(v.ValidatingWebhook.MatchConditions)) for i, matchCondition := range v.ValidatingWebhook.MatchConditions { @@ -269,7 +268,7 @@ func (v *validatingWebhookAccessor) GetCompiledMatcher(compiler cel.FilterCompil HasAuthorizer: true, }, environment.StoredExpressions, - ), authorizer, v.FailurePolicy, "validating", v.Name) + ), v.FailurePolicy, "validating", v.Name) }) return v.compiledMatcher } diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic/webhook.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic/webhook.go index ad5ec18e24f..f5c6eec137f 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic/webhook.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic/webhook.go @@ -225,8 +225,8 @@ func (a *Webhook) ShouldCallHook(ctx context.Context, h webhook.WebhookAccessor, return nil, apierrors.NewInternalError(err) } - matcher := h.GetCompiledMatcher(a.filterCompiler, a.authorizer) - matchResult := matcher.Match(ctx, versionedAttr, nil) + matcher := h.GetCompiledMatcher(a.filterCompiler) + matchResult := matcher.Match(ctx, versionedAttr, nil, a.authorizer) if matchResult.Error != nil { klog.Warningf("Failed evaluating match conditions, failing closed %v: %v", h.GetName(), matchResult.Error) diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic/webhook_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic/webhook_test.go index 64595cca202..6c12323862d 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic/webhook_test.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic/webhook_test.go @@ -54,7 +54,7 @@ type fakeMatcher struct { matchResult bool } -func (f *fakeMatcher) Match(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object) matchconditions.MatchResult { +func (f *fakeMatcher) Match(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, authz authorizer.Authorizer) matchconditions.MatchResult { if f.throwError != nil { return matchconditions.MatchResult{ Matches: true, @@ -76,7 +76,7 @@ type fakeWebhookAccessor struct { matchResult bool } -func (f *fakeWebhookAccessor) GetCompiledMatcher(compiler cel.FilterCompiler, authorizer authorizer.Authorizer) matchconditions.Matcher { +func (f *fakeWebhookAccessor) GetCompiledMatcher(compiler cel.FilterCompiler) matchconditions.Matcher { return &fakeMatcher{ throwError: f.throwError, matchResult: f.matchResult, diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions/interface.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions/interface.go index 09468655bd0..094a019d1f9 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions/interface.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions/interface.go @@ -21,6 +21,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/admission" + "k8s.io/apiserver/pkg/authorization/authorizer" ) type MatchResult struct { @@ -32,5 +33,5 @@ type MatchResult struct { // Matcher contains logic for converting Evaluations to bool of matches or does not match type Matcher interface { // Match is used to take cel evaluations and convert into decisions - Match(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object) MatchResult + Match(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, authz authorizer.Authorizer) MatchResult } diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions/matcher.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions/matcher.go index 09a500dd39c..90a8f90b2e3 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions/matcher.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions/matcher.go @@ -53,13 +53,12 @@ var _ Matcher = &matcher{} // matcher evaluates compiled cel expressions and determines if they match the given request or not type matcher struct { filter celplugin.Filter - authorizer authorizer.Authorizer failPolicy v1.FailurePolicyType matcherType string objectName string } -func NewMatcher(filter celplugin.Filter, authorizer authorizer.Authorizer, failPolicy *v1.FailurePolicyType, matcherType, objectName string) Matcher { +func NewMatcher(filter celplugin.Filter, failPolicy *v1.FailurePolicyType, matcherType, objectName string) Matcher { var f v1.FailurePolicyType if failPolicy == nil { f = v1.Fail @@ -68,17 +67,16 @@ func NewMatcher(filter celplugin.Filter, authorizer authorizer.Authorizer, failP } return &matcher{ filter: filter, - authorizer: authorizer, failPolicy: f, matcherType: matcherType, objectName: objectName, } } -func (m *matcher) Match(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object) MatchResult { +func (m *matcher) Match(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, authz authorizer.Authorizer) MatchResult { evalResults, _, err := m.filter.ForInput(ctx, versionedAttr, celplugin.CreateAdmissionRequest(versionedAttr.Attributes), celplugin.OptionalVariableBindings{ VersionedParams: versionedParams, - Authorizer: m.authorizer, + Authorizer: authz, }, celconfig.RuntimeCELCostBudgetMatchConditions) if err != nil { diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions/matcher_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions/matcher_test.go index 2ba83cbf4ef..30f500cf557 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions/matcher_test.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions/matcher_test.go @@ -334,9 +334,9 @@ func TestMatch(t *testing.T) { m := NewMatcher(&fakeCelFilter{ evaluations: tc.evaluations, throwError: tc.throwError, - }, nil, tc.failPolicy, "test", "testhook") + }, tc.failPolicy, "test", "testhook") ctx := context.TODO() - matchResult := m.Match(ctx, fakeVersionedAttr, nil) + matchResult := m.Match(ctx, fakeVersionedAttr, nil, nil) if matchResult.Error != nil { if len(tc.expectError) == 0 { diff --git a/test/integration/apiserver/cel/validatingadmissionpolicy_test.go b/test/integration/apiserver/cel/validatingadmissionpolicy_test.go index 60cbde9bcd7..83948704d69 100644 --- a/test/integration/apiserver/cel/validatingadmissionpolicy_test.go +++ b/test/integration/apiserver/cel/validatingadmissionpolicy_test.go @@ -18,12 +18,16 @@ package cel import ( "context" + "encoding/json" "fmt" + "net/http" + "net/http/httptest" "os" "strconv" "strings" "sync" "testing" + "text/template" "time" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" @@ -36,12 +40,15 @@ import ( "k8s.io/client-go/rest" featuregatetesting "k8s.io/component-base/featuregate/testing" + "k8s.io/kubernetes/cmd/kube-apiserver/app/options" apiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing" + authzmodes "k8s.io/kubernetes/pkg/kubeapiserver/authorizer/modes" "k8s.io/kubernetes/test/integration/authutil" "k8s.io/kubernetes/test/integration/etcd" "k8s.io/kubernetes/test/integration/framework" "k8s.io/kubernetes/test/utils" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" @@ -51,13 +58,11 @@ import ( "k8s.io/client-go/dynamic" clientset "k8s.io/client-go/kubernetes" - v1 "k8s.io/api/core/v1" - rbacv1 "k8s.io/api/rbac/v1" - admissionregistrationv1 "k8s.io/api/admissionregistration/v1" admissionregistrationv1alpha1 "k8s.io/api/admissionregistration/v1alpha1" - - apierrors "k8s.io/apimachinery/pkg/api/errors" + authorizationv1 "k8s.io/api/authorization/v1" + v1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" ) // Test_ValidateNamespace_NoParams tests a ValidatingAdmissionPolicy that validates creation of a Namespace with no params. @@ -2451,15 +2456,15 @@ func withWaitReadyConstraintAndExpression(policy *admissionregistrationv1alpha1. return policy } -func createAndWaitReady(t *testing.T, client *clientset.Clientset, binding *admissionregistrationv1alpha1.ValidatingAdmissionPolicyBinding, matchLabels map[string]string) error { +func createAndWaitReady(t *testing.T, client clientset.Interface, binding *admissionregistrationv1alpha1.ValidatingAdmissionPolicyBinding, matchLabels map[string]string) error { return createAndWaitReadyNamespaced(t, client, binding, matchLabels, "default") } -func createAndWaitReadyNamespaced(t *testing.T, client *clientset.Clientset, binding *admissionregistrationv1alpha1.ValidatingAdmissionPolicyBinding, matchLabels map[string]string, ns string) error { +func createAndWaitReadyNamespaced(t *testing.T, client clientset.Interface, binding *admissionregistrationv1alpha1.ValidatingAdmissionPolicyBinding, matchLabels map[string]string, ns string) error { return createAndWaitReadyNamespacedWithWarnHandler(t, client, binding, matchLabels, ns, newWarningHandler()) } -func createAndWaitReadyNamespacedWithWarnHandler(t *testing.T, client *clientset.Clientset, binding *admissionregistrationv1alpha1.ValidatingAdmissionPolicyBinding, matchLabels map[string]string, ns string, handler *warningHandler) error { +func createAndWaitReadyNamespacedWithWarnHandler(t *testing.T, client clientset.Interface, binding *admissionregistrationv1alpha1.ValidatingAdmissionPolicyBinding, matchLabels map[string]string, ns string, handler *warningHandler) error { marker := &v1.Endpoints{ObjectMeta: metav1.ObjectMeta{Name: "test-marker", Namespace: ns, Labels: matchLabels}} defer func() { err := client.CoreV1().Endpoints(ns).Delete(context.TODO(), marker.Name, metav1.DeleteOptions{}) @@ -3022,3 +3027,167 @@ func toHasLengthOf(n int) func(warnings []admissionregistrationv1alpha1.Expressi } } } + +func TestAuthorizationDecisionCaching(t *testing.T) { + for _, tc := range []struct { + name string + validations []admissionregistrationv1alpha1.Validation + }{ + { + name: "hit", + validations: []admissionregistrationv1alpha1.Validation{ + { + Expression: "authorizer.requestResource.check('test').reason() == authorizer.requestResource.check('test').reason()", + }, + }, + }, + { + name: "miss", + validations: []admissionregistrationv1alpha1.Validation{ + { + Expression: "authorizer.requestResource.subresource('a').check('test').reason() == '1'", + }, + { + Expression: "authorizer.requestResource.subresource('b').check('test').reason() == '2'", + }, + { + Expression: "authorizer.requestResource.subresource('c').check('test').reason() == '3'", + }, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ValidatingAdmissionPolicy, true)() + + ctx, cancel := context.WithCancel(context.TODO()) + defer cancel() + + var nChecks int + webhook := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var review authorizationv1.SubjectAccessReview + if err := json.NewDecoder(r.Body).Decode(&review); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + } + + review.Status.Allowed = true + if review.Spec.ResourceAttributes.Verb == "test" { + nChecks++ + review.Status.Reason = fmt.Sprintf("%d", nChecks) + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(review); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + })) + defer webhook.Close() + + kcfd, err := os.CreateTemp("", "kubeconfig-") + if err != nil { + t.Fatal(err) + } + func() { + defer kcfd.Close() + tmpl, err := template.New("kubeconfig").Parse(` +apiVersion: v1 +kind: Config +clusters: + - name: test-authz-service + cluster: + server: {{ .Server }} +users: + - name: test-api-server +current-context: webhook +contexts: +- context: + cluster: test-authz-service + user: test-api-server + name: webhook +`) + if err != nil { + t.Fatal(err) + } + err = tmpl.Execute(kcfd, struct { + Server string + }{ + Server: webhook.URL, + }) + if err != nil { + t.Fatal(err) + } + }() + + client, config, teardown := framework.StartTestServer(ctx, t, framework.TestServerSetup{ + ModifyServerRunOptions: func(options *options.ServerRunOptions) { + options.Admission.GenericAdmission.EnablePlugins = append(options.Admission.GenericAdmission.EnablePlugins, "ValidatingAdmissionPolicy") + options.APIEnablement.RuntimeConfig.Set("api/all=true") + + options.Authorization.Modes = []string{authzmodes.ModeWebhook} + options.Authorization.WebhookConfigFile = kcfd.Name() + options.Authorization.WebhookVersion = "v1" + // Bypass webhook cache to observe the policy plugin's cache behavior. + options.Authorization.WebhookCacheAuthorizedTTL = 0 + options.Authorization.WebhookCacheUnauthorizedTTL = 0 + }, + }) + defer teardown() + + policy := &admissionregistrationv1alpha1.ValidatingAdmissionPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-authorization-decision-caching-policy", + }, + Spec: admissionregistrationv1alpha1.ValidatingAdmissionPolicySpec{ + MatchConstraints: &admissionregistrationv1alpha1.MatchResources{ + ResourceRules: []admissionregistrationv1alpha1.NamedRuleWithOperations{ + { + ResourceNames: []string{"test-authorization-decision-caching-namespace"}, + RuleWithOperations: admissionregistrationv1alpha1.RuleWithOperations{ + Operations: []admissionregistrationv1.OperationType{ + admissionregistrationv1.Create, + }, + Rule: admissionregistrationv1.Rule{ + APIGroups: []string{""}, + APIVersions: []string{"v1"}, + Resources: []string{"namespaces"}, + }, + }, + }, + }, + }, + Validations: tc.validations, + }, + } + + policy, err = client.AdmissionregistrationV1alpha1().ValidatingAdmissionPolicies().Create(ctx, withWaitReadyConstraintAndExpression(policy), metav1.CreateOptions{}) + if err != nil { + t.Fatal(err) + } + + if err := createAndWaitReady(t, client, makeBinding(policy.Name+"-binding", policy.Name, ""), nil); err != nil { + t.Fatal(err) + } + + config = rest.CopyConfig(config) + config.Impersonate = rest.ImpersonationConfig{ + UserName: "alice", + UID: "1234", + } + client, err = clientset.NewForConfig(config) + if err != nil { + t.Fatal(err) + } + + if _, err := client.CoreV1().Namespaces().Create( + ctx, + &v1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-authorization-decision-caching-namespace", + }, + }, + metav1.CreateOptions{}, + ); err != nil { + t.Fatal(err) + } + }) + } +}