Merge pull request #116443 from benluddy/secondary-authz-decision-caching

Cache authz decisions within the scope of validating policy admission.
This commit is contained in:
Kubernetes Prow Robot 2023-07-11 12:41:11 -07:00 committed by GitHub
commit 6ffca50136
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 614 additions and 65 deletions

View File

@ -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)

View File

@ -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
}

View File

@ -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)
}
})
}
}

View File

@ -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 {

View File

@ -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)

View File

@ -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
}

View File

@ -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)

View File

@ -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)
}

View File

@ -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
}

View File

@ -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)

View File

@ -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,

View File

@ -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
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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)
}
})
}
}