Custom match criteria (#116350)

* Add custom match conditions for CEL admission

This PR is based off of, and dependent on the following PR:

https://github.com/kubernetes/kubernetes/pull/116261

Signed-off-by: Max Smythe <smythe@google.com>

* run `make update`

Signed-off-by: Max Smythe <smythe@google.com>

* Fix unit tests

Signed-off-by: Max Smythe <smythe@google.com>

* Fix unit tests

Signed-off-by: Max Smythe <smythe@google.com>

* Update compatibility test data

Signed-off-by: Max Smythe <smythe@google.com>

* Revert "Update compatibility test data"

This reverts commit 312ba7f9e74e0ec4a7ac1f07bf575479c608af28.

* Allow params during validation; make match conditions optional

Signed-off-by: Max Smythe <smythe@google.com>

* Add conditional ignoring of matcher CEL expression validation on update

Signed-off-by: Max Smythe <smythe@google.com>

* Run codegen

Signed-off-by: Max Smythe <smythe@google.com>

* Add more validation tests

Signed-off-by: Max Smythe <smythe@google.com>

* Short-circuit CEL matcher when no matchers specified

Signed-off-by: Max Smythe <smythe@google.com>

* Run codegen

Signed-off-by: Max Smythe <smythe@google.com>

* Address review comments

Signed-off-by: Max Smythe <smythe@google.com>

---------

Signed-off-by: Max Smythe <smythe@google.com>

Kubernetes-commit: e5fd204c33e90a7e8f5a0ee70242f1296a5ec7af
This commit is contained in:
Max Smythe 2023-03-15 17:23:15 -07:00 committed by Kubernetes Publisher
parent 05d2078e68
commit 41adff8c93
6 changed files with 104 additions and 20 deletions

12
go.mod
View File

@ -42,12 +42,12 @@ require (
google.golang.org/protobuf v1.28.1
gopkg.in/natefinch/lumberjack.v2 v2.0.0
gopkg.in/square/go-jose.v2 v2.6.0
k8s.io/api v0.0.0-20230315055831-abe66f57fdb1
k8s.io/api v0.0.0-20230316002315-c80582ebe125
k8s.io/apimachinery v0.0.0-20230315054728-8d1258da8f38
k8s.io/client-go v0.0.0-20230315061912-38589731da69
k8s.io/client-go v0.0.0-20230316040718-4666344cbcd7
k8s.io/component-base v0.0.0-20230315065615-6b9bb8ecc3d0
k8s.io/klog/v2 v2.90.1
k8s.io/kms v0.0.0-20230315071541-54e6d3479bfc
k8s.io/kms v0.0.0-20230315071547-f5c193c64781
k8s.io/kube-openapi v0.0.0-20230308215209-15aac26d736a
k8s.io/utils v0.0.0-20230209194617-a36077c30491
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.1.1
@ -124,9 +124,9 @@ require (
)
replace (
k8s.io/api => k8s.io/api v0.0.0-20230315032826-0b4c449988b1
k8s.io/api => k8s.io/api v0.0.0-20230316002315-c80582ebe125
k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20230315054728-8d1258da8f38
k8s.io/client-go => k8s.io/client-go v0.0.0-20230315061912-38589731da69
k8s.io/client-go => k8s.io/client-go v0.0.0-20230316040718-4666344cbcd7
k8s.io/component-base => k8s.io/component-base v0.0.0-20230315065615-6b9bb8ecc3d0
k8s.io/kms => k8s.io/kms v0.0.0-20230315071541-54e6d3479bfc
k8s.io/kms => k8s.io/kms v0.0.0-20230315071547-f5c193c64781
)

12
go.sum
View File

@ -878,18 +878,18 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
k8s.io/api v0.0.0-20230315032826-0b4c449988b1 h1:wlCdY1kqV0RkfnfRr4mEZ3fGJ1VvLelr5Q2vCnCICIo=
k8s.io/api v0.0.0-20230315032826-0b4c449988b1/go.mod h1:aZ6MBt4NMLXSxkSKFkoDaP4hTutnZIvH5dCSpOis9g4=
k8s.io/api v0.0.0-20230316002315-c80582ebe125 h1:sNLUUpJNxIYmttU1YQIm4nhSD2jK3wOkSQVsqhlFh2A=
k8s.io/api v0.0.0-20230316002315-c80582ebe125/go.mod h1:aZ6MBt4NMLXSxkSKFkoDaP4hTutnZIvH5dCSpOis9g4=
k8s.io/apimachinery v0.0.0-20230315054728-8d1258da8f38 h1:n1qDRCTPAXwyXYg7eSpWDO9FdW79lwAQ9dAr1vETpn4=
k8s.io/apimachinery v0.0.0-20230315054728-8d1258da8f38/go.mod h1:5ikh59fK3AJ287GUvpUsryoMFtH9zj/ARfWCo3AyXTM=
k8s.io/client-go v0.0.0-20230315061912-38589731da69 h1:LnTXY9Akksk/aAmbebKIaC0doqk1aKbZQA3OoAd0BB0=
k8s.io/client-go v0.0.0-20230315061912-38589731da69/go.mod h1:b0alWGtfu+BI7XZwwdOHJIsr7aDjKf3ANThw8Sr+tw8=
k8s.io/client-go v0.0.0-20230316040718-4666344cbcd7 h1:94RqmF9IE9dqxNjr6CRIlO+lVabE46SzrOptrmHAiCc=
k8s.io/client-go v0.0.0-20230316040718-4666344cbcd7/go.mod h1:L61adAiamj+NzecNEOyWV9fYq4VVrOCcn2enXFUiJL8=
k8s.io/component-base v0.0.0-20230315065615-6b9bb8ecc3d0 h1:IjneP02MOB07PIP9+PQjKrOIZEZ5T7umR+GIZkU4h0U=
k8s.io/component-base v0.0.0-20230315065615-6b9bb8ecc3d0/go.mod h1:kTuptveA6tUMLMKnaq4AbIAAk7IcdhwkbljAV3JZRpM=
k8s.io/klog/v2 v2.90.1 h1:m4bYOKall2MmOiRaR1J+We67Do7vm9KiQVlT96lnHUw=
k8s.io/klog/v2 v2.90.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
k8s.io/kms v0.0.0-20230315071541-54e6d3479bfc h1:Es1pSsyPK+D9zkWXy4/VVQoZATfbQ5j8xZvKj3I3rmA=
k8s.io/kms v0.0.0-20230315071541-54e6d3479bfc/go.mod h1:JHGAns80GkaZvH3pP7YV53PCFo3eNnoD5jsQ2HL03TM=
k8s.io/kms v0.0.0-20230315071547-f5c193c64781 h1:LH9Z43UgXQkuRc1ImiT3F3LoOfj9Xz0r1BP94+uYpys=
k8s.io/kms v0.0.0-20230315071547-f5c193c64781/go.mod h1:9rk1ftD6YM/h1KCjaoIZ8lv1CYvaCcuo57ZIWqiHHGI=
k8s.io/kube-openapi v0.0.0-20230308215209-15aac26d736a h1:gmovKNur38vgoWfGtP5QOGNOA7ki4n6qNYoFAgMlNvg=
k8s.io/kube-openapi v0.0.0-20230308215209-15aac26d736a/go.mod h1:y5VtZWM9sHHc2ZodIH/6SHzXj+TPU5USoA8lcIeKEKY=
k8s.io/utils v0.0.0-20230209194617-a36077c30491 h1:r0BAOLElQnnFhE/ApUsg3iHdVYYPBjNSSOMowRZxxsY=

View File

@ -28,8 +28,6 @@ import (
celgo "github.com/google/cel-go/cel"
"github.com/stretchr/testify/require"
"k8s.io/klog/v2"
admissionv1 "k8s.io/api/admission/v1"
admissionRegistrationv1 "k8s.io/api/admissionregistration/v1"
"k8s.io/api/admissionregistration/v1alpha1"
@ -47,6 +45,7 @@ import (
"k8s.io/apiserver/pkg/admission/initializer"
"k8s.io/apiserver/pkg/admission/plugin/cel"
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/internal/generic"
"k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions"
auditinternal "k8s.io/apiserver/pkg/apis/audit"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/features"
@ -57,6 +56,7 @@ import (
clienttesting "k8s.io/client-go/testing"
"k8s.io/client-go/tools/cache"
"k8s.io/component-base/featuregate"
"k8s.io/klog/v2"
)
var (
@ -418,7 +418,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, 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, authorizer authorizer.Authorizer) Validator {
f := validationFilter.(*fakeFilter)
v := validatorMap[f.keyId]
v.validationFilter = f

View File

@ -35,6 +35,7 @@ import (
celmetrics "k8s.io/apiserver/pkg/admission/cel"
"k8s.io/apiserver/pkg/admission/plugin/cel"
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/internal/generic"
"k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions"
celconfig "k8s.io/apiserver/pkg/apis/cel"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/client-go/dynamic"
@ -99,7 +100,7 @@ type policyController struct {
authz authorizer.Authorizer
}
type newValidator func(validationFilter cel.Filter, auditAnnotationFilter cel.Filter, 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, authorizer authorizer.Authorizer) Validator
func newPolicyController(
restMapper meta.RESTMapper,
@ -503,11 +504,22 @@ func (c *policyController) latestPolicyData() []policyData {
}
optionalVars := cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: true}
expressionOptionalVars := cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: false}
failurePolicy := convertv1alpha1FailurePolicyTypeTov1FailurePolicyType(definitionInfo.lastReconciledValue.Spec.FailurePolicy)
var matcher matchconditions.Matcher = nil
matchConditions := definitionInfo.lastReconciledValue.Spec.MatchConditions
if len(matchConditions) > 0 {
matchExpressionAccessors := make([]cel.ExpressionAccessor, len(matchConditions))
for i := range matchConditions {
matchExpressionAccessors[i] = (*matchconditions.MatchCondition)(&matchConditions[i])
}
matcher = matchconditions.NewMatcher(c.filterCompiler.Compile(matchExpressionAccessors, optionalVars, celconfig.PerCallLimit), c.authz, failurePolicy, "validatingadmissionpolicy", definitionInfo.lastReconciledValue.Name)
}
bindingInfo.validator = c.newValidator(
c.filterCompiler.Compile(convertv1alpha1Validations(definitionInfo.lastReconciledValue.Spec.Validations), optionalVars, celconfig.PerCallLimit),
matcher,
c.filterCompiler.Compile(convertv1alpha1AuditAnnotations(definitionInfo.lastReconciledValue.Spec.AuditAnnotations), optionalVars, celconfig.PerCallLimit),
c.filterCompiler.Compile(convertV1Alpha1MessageExpressions(definitionInfo.lastReconciledValue.Spec.Validations), expressionOptionalVars, celconfig.PerCallLimit),
convertv1alpha1FailurePolicyTypeTov1FailurePolicyType(definitionInfo.lastReconciledValue.Spec.FailurePolicy),
failurePolicy,
c.authz,
)
}

View File

@ -28,6 +28,7 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/admission"
"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"
@ -36,6 +37,7 @@ import (
// validator implements the Validator interface
type validator struct {
celMatcher matchconditions.Matcher
validationFilter cel.Filter
auditAnnotationFilter cel.Filter
messageFilter cel.Filter
@ -43,8 +45,9 @@ type validator struct {
authorizer authorizer.Authorizer
}
func NewValidator(validationFilter, 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, authorizer authorizer.Authorizer) Validator {
return &validator{
celMatcher: celMatcher,
validationFilter: validationFilter,
auditAnnotationFilter: auditAnnotationFilter,
messageFilter: messageFilter,
@ -77,6 +80,26 @@ func (v *validator) Validate(ctx context.Context, versionedAttr *admission.Versi
f = *v.failPolicy
}
if v.celMatcher != nil {
matchResults := v.celMatcher.Match(ctx, versionedAttr, versionedParams)
if matchResults.Error != nil {
return ValidateResult{
Decisions: []PolicyDecision{
{
Action: policyDecisionActionForError(f),
Evaluation: EvalError,
Message: matchResults.Error.Error(),
},
},
}
}
// if preconditions are not met, then do not return any validations
if !matchResults.Matches {
return ValidateResult{}
}
}
optionalVars := cel.OptionalVariableBindings{VersionedParams: versionedParams, Authorizer: v.authorizer}
expressionOptionalVars := cel.OptionalVariableBindings{VersionedParams: versionedParams}
admissionRequest := cel.CreateAdmissionRequest(versionedAttr.Attributes)

View File

@ -23,16 +23,17 @@ import (
"strings"
"testing"
"github.com/stretchr/testify/require"
celtypes "github.com/google/cel-go/common/types"
"github.com/stretchr/testify/require"
admissionv1 "k8s.io/api/admission/v1"
v1 "k8s.io/api/admissionregistration/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/cel"
"k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions"
celconfig "k8s.io/apiserver/pkg/apis/cel"
apiservercel "k8s.io/apiserver/pkg/cel"
)
@ -61,6 +62,17 @@ func (f *fakeCelFilter) CompilationErrors() []error {
return []error{}
}
var _ matchconditions.Matcher = &fakeCELMatcher{}
type fakeCELMatcher struct {
error error
matches bool
}
func (f *fakeCELMatcher) Match(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object) matchconditions.MatchResult {
return matchconditions.MatchResult{Matches: f.matches, FailedConditionName: "placeholder", Error: f.error}
}
func TestValidate(t *testing.T) {
ignore := v1.Ignore
fail := v1.Fail
@ -74,6 +86,7 @@ func TestValidate(t *testing.T) {
cases := []struct {
name string
failPolicy *v1.FailurePolicyType
matcher matchconditions.Matcher
evaluations []cel.EvaluationResult
messageEvaluations []cel.EvaluationResult
auditEvaluations []cel.EvaluationResult
@ -819,11 +832,46 @@ func TestValidate(t *testing.T) {
},
costBudget: 1, // shared between expression and messageExpression, needs 1 + 1 = 2 in total
},
{
name: "no match surpresses failure",
matcher: &fakeCELMatcher{matches: false},
evaluations: []cel.EvaluationResult{
{
Error: errors.New("expected"),
ExpressionAccessor: &ValidationCondition{},
},
},
policyDecision: []PolicyDecision{},
failPolicy: &fail,
},
{
name: "match error => presumed match",
matcher: &fakeCELMatcher{matches: true, error: fmt.Errorf("test error")},
evaluations: []cel.EvaluationResult{
{
Error: errors.New("expected"),
ExpressionAccessor: &ValidationCondition{},
},
},
policyDecision: []PolicyDecision{
{
Action: ActionDeny,
},
},
failPolicy: &fail,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
var matcher matchconditions.Matcher
if tc.matcher == nil {
matcher = &fakeCELMatcher{matches: true}
} else {
matcher = tc.matcher
}
v := validator{
failPolicy: tc.failPolicy,
celMatcher: matcher,
validationFilter: &fakeCelFilter{
evaluations: tc.evaluations,
throwError: tc.throwError,
@ -884,6 +932,7 @@ func TestContextCanceled(t *testing.T) {
f := fc.Compile([]cel.ExpressionAccessor{&ValidationCondition{Expression: "[1,2,3,4,5,6,7,8,9,10].map(x, [1,2,3,4,5,6,7,8,9,10].map(y, x*y)) == []"}}, cel.OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false}, celconfig.PerCallLimit)
v := validator{
failPolicy: &fail,
celMatcher: &fakeCELMatcher{matches: true},
validationFilter: f,
messageFilter: f,
auditAnnotationFilter: &fakeCelFilter{