mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-09 12:07:47 +00:00
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:
commit
6ffca50136
@ -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)
|
||||
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user