From 04b26c4697c8896074dea137a5a5fa29c3c8ae2d Mon Sep 17 00:00:00 2001 From: Cici Huang <8658046+cici37@users.noreply.github.com> Date: Fri, 14 Jul 2023 17:53:08 -0700 Subject: [PATCH] ValidatingAdmissionPolicy: support namespace access (#118267) * Support namespace access from cel expression in validatingadmissionpolicy. * Whitelist the exposed fields in namespace object and add test * better handling of cluster-scoped resources. * [API REVIEW] namespaceObject in Expression doc. * compatibility with composition. * generated: ./hack/update-codegen.sh && ./hack/update-openapi-spec.sh * workaround namespace of namespace is unexpectedly set. * basic test coverage for namespaceObject. --------- Co-authored-by: Jiahui Feng Kubernetes-commit: 13172cba5c0e1c6a076dbda4aeebbccaf658c7f1 --- go.mod | 4 +- go.sum | 4 +- pkg/admission/plugin/cel/compile.go | 54 ++++++++++++ pkg/admission/plugin/cel/compile_test.go | 40 +++++++++ pkg/admission/plugin/cel/composition.go | 5 +- pkg/admission/plugin/cel/composition_test.go | 2 +- pkg/admission/plugin/cel/filter.go | 39 ++++++++- pkg/admission/plugin/cel/filter_test.go | 85 ++++++++++++++++++- pkg/admission/plugin/cel/interface.go | 3 +- .../admission_test.go | 42 ++++----- .../validatingadmissionpolicy/controller.go | 20 ++++- .../validatingadmissionpolicy/interface.go | 7 +- .../validatingadmissionpolicy/matcher.go | 5 ++ .../matching/matching.go | 5 ++ .../validatingadmissionpolicy/typechecking.go | 5 ++ .../validatingadmissionpolicy/validator.go | 12 ++- .../validator_test.go | 7 +- .../plugin/webhook/matchconditions/matcher.go | 2 +- .../webhook/matchconditions/matcher_test.go | 4 +- .../webhook/predicates/namespace/matcher.go | 6 ++ 20 files changed, 309 insertions(+), 42 deletions(-) diff --git a/go.mod b/go.mod index ffced3d5a..d78184a91 100644 --- a/go.mod +++ b/go.mod @@ -41,7 +41,7 @@ require ( google.golang.org/protobuf v1.30.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/square/go-jose.v2 v2.6.0 - k8s.io/api v0.0.0-20230714211713-ac1defa44e72 + k8s.io/api v0.0.0-20230715005308-48d7a4d6b0f6 k8s.io/apimachinery v0.0.0-20230714211010-7924d2c22746 k8s.io/client-go v0.0.0-20230714212436-f19b40cda940 k8s.io/component-base v0.0.0-20230714213649-faf645bcb8bf @@ -125,7 +125,7 @@ require ( ) replace ( - k8s.io/api => k8s.io/api v0.0.0-20230714211713-ac1defa44e72 + k8s.io/api => k8s.io/api v0.0.0-20230715005308-48d7a4d6b0f6 k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20230714211010-7924d2c22746 k8s.io/client-go => k8s.io/client-go v0.0.0-20230714212436-f19b40cda940 k8s.io/component-base => k8s.io/component-base v0.0.0-20230714213649-faf645bcb8bf diff --git a/go.sum b/go.sum index de04d922d..0af921408 100644 --- a/go.sum +++ b/go.sum @@ -668,8 +668,8 @@ 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-20230714211713-ac1defa44e72 h1:VuW3Hv7ZC0+Mf7MwYisaDUAczwTtd0Q+puDnQMCIXYo= -k8s.io/api v0.0.0-20230714211713-ac1defa44e72/go.mod h1:nHelHGU5nRmkT223Jbg1sZZjj9DvhsKLsEekrET50A4= +k8s.io/api v0.0.0-20230715005308-48d7a4d6b0f6 h1:Vyx1tzAt+Z9+Lr1vyjxRu3Oqw2E5UFqQSOWo7ObFvsY= +k8s.io/api v0.0.0-20230715005308-48d7a4d6b0f6/go.mod h1:nHelHGU5nRmkT223Jbg1sZZjj9DvhsKLsEekrET50A4= k8s.io/apimachinery v0.0.0-20230714211010-7924d2c22746 h1:ZRR1OH9l7t2q4sW0wB74g7ZDDSAWvAuO/jAcSHt7FVk= k8s.io/apimachinery v0.0.0-20230714211010-7924d2c22746/go.mod h1:82hjKsW08vcDFh8wS9BtEee9UTcm0IlvyBnJ/zawF6E= k8s.io/client-go v0.0.0-20230714212436-f19b40cda940 h1:EUg/9w1/iqWVzXNXjS2UvB02elWNuW4gTPG3cFlhIEo= diff --git a/pkg/admission/plugin/cel/compile.go b/pkg/admission/plugin/cel/compile.go index 8128b9504..25ee108ea 100644 --- a/pkg/admission/plugin/cel/compile.go +++ b/pkg/admission/plugin/cel/compile.go @@ -33,6 +33,7 @@ const ( OldObjectVarName = "oldObject" ParamsVarName = "params" RequestVarName = "request" + NamespaceVarName = "namespaceObject" AuthorizerVarName = "authorizer" RequestResourceAuthorizerVarName = "authorizer.requestResource" VariableVarName = "variables" @@ -85,6 +86,56 @@ func BuildRequestType() *apiservercel.DeclType { )) } +// BuildNamespaceType generates a DeclType for Namespace. +// Certain nested fields in Namespace (e.g. managedFields, ownerReferences etc.) are omitted in the generated DeclType +// by design. +func BuildNamespaceType() *apiservercel.DeclType { + field := func(name string, declType *apiservercel.DeclType, required bool) *apiservercel.DeclField { + return apiservercel.NewDeclField(name, declType, required, nil, nil) + } + fields := func(fields ...*apiservercel.DeclField) map[string]*apiservercel.DeclField { + result := make(map[string]*apiservercel.DeclField, len(fields)) + for _, f := range fields { + result[f.Name] = f + } + return result + } + + specType := apiservercel.NewObjectType("kubernetes.NamespaceSpec", fields( + field("finalizers", apiservercel.NewListType(apiservercel.StringType, -1), true), + )) + conditionType := apiservercel.NewObjectType("kubernetes.NamespaceCondition", fields( + field("status", apiservercel.StringType, true), + field("type", apiservercel.StringType, true), + field("lastTransitionTime", apiservercel.TimestampType, true), + field("message", apiservercel.StringType, true), + field("reason", apiservercel.StringType, true), + )) + statusType := apiservercel.NewObjectType("kubernetes.NamespaceStatus", fields( + field("conditions", apiservercel.NewListType(conditionType, -1), true), + field("phase", apiservercel.StringType, true), + )) + metadataType := apiservercel.NewObjectType("kubernetes.NamespaceMetadata", fields( + field("name", apiservercel.StringType, true), + field("generateName", apiservercel.StringType, true), + field("namespace", apiservercel.StringType, true), + field("labels", apiservercel.NewMapType(apiservercel.StringType, apiservercel.StringType, -1), true), + field("annotations", apiservercel.NewMapType(apiservercel.StringType, apiservercel.StringType, -1), true), + field("UID", apiservercel.StringType, true), + field("creationTimestamp", apiservercel.TimestampType, true), + field("deletionGracePeriodSeconds", apiservercel.IntType, true), + field("deletionTimestamp", apiservercel.TimestampType, true), + field("generation", apiservercel.IntType, true), + field("resourceVersion", apiservercel.StringType, true), + field("finalizers", apiservercel.NewListType(apiservercel.StringType, -1), true), + )) + return apiservercel.NewObjectType("kubernetes.Namespace", fields( + field("metadata", metadataType, true), + field("spec", specType, true), + field("status", statusType, true), + )) +} + // CompilationResult represents a compiled validations expression. type CompilationResult struct { Program cel.Program @@ -168,6 +219,7 @@ func (c compiler) CompileCELExpression(expressionAccessor ExpressionAccessor, op func mustBuildEnvs(baseEnv *environment.EnvSet) variableDeclEnvs { requestType := BuildRequestType() + namespaceType := BuildNamespaceType() envs := make(variableDeclEnvs, 4) // since the number of variable combinations is small, pre-build a environment for each for _, hasParams := range []bool{false, true} { for _, hasAuthorizer := range []bool{false, true} { @@ -183,6 +235,7 @@ func mustBuildEnvs(baseEnv *environment.EnvSet) variableDeclEnvs { envOpts = append(envOpts, cel.Variable(ObjectVarName, cel.DynType), cel.Variable(OldObjectVarName, cel.DynType), + cel.Variable(NamespaceVarName, namespaceType.CelType()), cel.Variable(RequestVarName, requestType.CelType())) extended, err := baseEnv.Extend( @@ -192,6 +245,7 @@ func mustBuildEnvs(baseEnv *environment.EnvSet) variableDeclEnvs { IntroducedVersion: version.MajorMinor(1, 0), EnvOptions: envOpts, DeclTypes: []*apiservercel.DeclType{ + namespaceType, requestType, }, }, diff --git a/pkg/admission/plugin/cel/compile_test.go b/pkg/admission/plugin/cel/compile_test.go index 9b9cd73a8..5caf82858 100644 --- a/pkg/admission/plugin/cel/compile_test.go +++ b/pkg/admission/plugin/cel/compile_test.go @@ -49,6 +49,11 @@ func TestCompileValidatingPolicyExpression(t *testing.T) { expressions: []string{"object.foo < params.x"}, hasParams: true, }, + { + name: "namespaceObject", + expressions: []string{"namespaceObject.metadata.name.startsWith('test')"}, + hasParams: true, + }, { name: "without params", errorExpressions: map[string]string{"object.foo < params.x": "undeclared reference to 'params'"}, @@ -135,6 +140,41 @@ func TestCompileValidatingPolicyExpression(t *testing.T) { }, envType: environment.NewExpressions, }, + { + name: "valid namespaceObject", + expressions: []string{ + "namespaceObject.metadata != null", + "namespaceObject.metadata.name == 'test'", + "namespaceObject.metadata.generateName == 'test'", + "namespaceObject.metadata.namespace == 'testns'", + "'test' in namespaceObject.metadata.labels", + "'test' in namespaceObject.metadata.annotations", + "namespaceObject.metadata.UID == '12345'", + "type(namespaceObject.metadata.creationTimestamp) == google.protobuf.Timestamp", + "type(namespaceObject.metadata.deletionTimestamp) == google.protobuf.Timestamp", + "namespaceObject.metadata.deletionGracePeriodSeconds == 5", + "namespaceObject.metadata.generation == 2", + "namespaceObject.metadata.resourceVersion == 'v1'", + "namespaceObject.metadata.finalizers[0] == 'testEnv'", + "namespaceObject.spec.finalizers[0] == 'testEnv'", + "namespaceObject.status.phase == 'Active'", + "namespaceObject.status.conditions[0].status == 'True'", + "namespaceObject.status.conditions[0].type == 'NamespaceDeletionDiscoveryFailure'", + "type(namespaceObject.status.conditions[0].lastTransitionTime) == google.protobuf.Timestamp", + "namespaceObject.status.conditions[0].message == 'Unknow'", + "namespaceObject.status.conditions[0].reason == 'Invalid'", + }, + }, + { + name: "invalid namespaceObject", + errorExpressions: map[string]string{ + "namespaceObject.foo1 == 'nope'": "undefined field 'foo1'", + "namespaceObject.metadata.foo2 == 'nope'": "undefined field 'foo2'", + "namespaceObject.spec.foo3 == 'nope'": "undefined field 'foo3'", + "namespaceObject.status.foo4 == 'nope'": "undefined field 'foo4'", + "namespaceObject.status.conditions[0].foo5 == 'nope'": "undefined field 'foo5'", + }, + }, } // Include the test library, which includes the test() function in the storage environment during test diff --git a/pkg/admission/plugin/cel/composition.go b/pkg/admission/plugin/cel/composition.go index afdf4d367..38b80a304 100644 --- a/pkg/admission/plugin/cel/composition.go +++ b/pkg/admission/plugin/cel/composition.go @@ -25,6 +25,7 @@ import ( "github.com/google/cel-go/common/types/ref" v1 "k8s.io/api/admission/v1" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/util/version" "k8s.io/apiserver/pkg/admission" apiservercel "k8s.io/apiserver/pkg/cel" @@ -149,9 +150,9 @@ func (c *compositionContext) Variables(activation any) ref.Val { return lazyMap } -func (f *CompositedFilter) ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *v1.AdmissionRequest, optionalVars OptionalVariableBindings, runtimeCELCostBudget int64) ([]EvaluationResult, int64, error) { +func (f *CompositedFilter) ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *v1.AdmissionRequest, optionalVars OptionalVariableBindings, namespace *corev1.Namespace, runtimeCELCostBudget int64) ([]EvaluationResult, int64, error) { ctx = f.compositionEnv.CreateContext(ctx) - return f.Filter.ForInput(ctx, versionedAttr, request, optionalVars, runtimeCELCostBudget) + return f.Filter.ForInput(ctx, versionedAttr, request, optionalVars, namespace, runtimeCELCostBudget) } func (c *compositionContext) reportCost(cost int64) { diff --git a/pkg/admission/plugin/cel/composition_test.go b/pkg/admission/plugin/cel/composition_test.go index 08ec6c522..2ae483e69 100644 --- a/pkg/admission/plugin/cel/composition_test.go +++ b/pkg/admission/plugin/cel/composition_test.go @@ -141,7 +141,7 @@ func TestCompositedPolicies(t *testing.T) { if costBudget == 0 { costBudget = celconfig.RuntimeCELCostBudget } - result, _, err := f.ForInput(context.Background(), versionedAttr, CreateAdmissionRequest(versionedAttr.Attributes), optionalVars, costBudget) + result, _, err := f.ForInput(context.Background(), versionedAttr, CreateAdmissionRequest(versionedAttr.Attributes), optionalVars, nil, costBudget) if !tc.expectErr && err != nil { t.Fatalf("failed evaluation: %v", err) } diff --git a/pkg/admission/plugin/cel/filter.go b/pkg/admission/plugin/cel/filter.go index bb586cbf9..9cce78e46 100644 --- a/pkg/admission/plugin/cel/filter.go +++ b/pkg/admission/plugin/cel/filter.go @@ -27,6 +27,7 @@ import ( admissionv1 "k8s.io/api/admission/v1" authenticationv1 "k8s.io/api/authentication/v1" + v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -46,7 +47,7 @@ func NewFilterCompiler(env *environment.EnvSet) FilterCompiler { } type evaluationActivation struct { - object, oldObject, params, request, authorizer, requestResourceAuthorizer, variables interface{} + object, oldObject, params, request, namespace, authorizer, requestResourceAuthorizer, variables interface{} } // ResolveName returns a value from the activation by qualified name, or false if the name @@ -61,6 +62,8 @@ func (a *evaluationActivation) ResolveName(name string) (interface{}, bool) { return a.params, true // params may be null case RequestVarName: return a.request, true + case NamespaceVarName: + return a.namespace, true case AuthorizerVarName: return a.authorizer, a.authorizer != nil case RequestResourceAuthorizerVarName: @@ -126,7 +129,7 @@ func objectToResolveVal(r runtime.Object) (interface{}, error) { // ForInput evaluates the compiled CEL expressions converting them into CELEvaluations // errors per evaluation are returned on the Evaluation object // 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 (f *filter) ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *admissionv1.AdmissionRequest, inputs OptionalVariableBindings, runtimeCELCostBudget int64) ([]EvaluationResult, int64, error) { +func (f *filter) ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *admissionv1.AdmissionRequest, inputs OptionalVariableBindings, namespace *v1.Namespace, runtimeCELCostBudget int64) ([]EvaluationResult, int64, error) { // TODO: replace unstructured with ref.Val for CEL variables when native type support is available evaluations := make([]EvaluationResult, len(f.compilationResults)) var err error @@ -156,11 +159,16 @@ func (f *filter) ForInput(ctx context.Context, versionedAttr *admission.Versione if err != nil { return nil, -1, err } + namespaceVal, err := objectToResolveVal(namespace) + if err != nil { + return nil, -1, err + } va := &evaluationActivation{ object: objectVal, oldObject: oldObjectVal, params: paramsVal, request: requestVal.Object, + namespace: namespaceVal, authorizer: authorizerVal, requestResourceAuthorizer: requestResourceAuthorizerVal, } @@ -307,6 +315,33 @@ func CreateAdmissionRequest(attr admission.Attributes) *admissionv1.AdmissionReq } } +// CreateNamespaceObject creates a Namespace object that is suitable for the CEL evaluation. +// If the namespace is nil, CreateNamespaceObject returns nil +func CreateNamespaceObject(namespace *v1.Namespace) *v1.Namespace { + if namespace == nil { + return nil + } + + return &v1.Namespace{ + Status: namespace.Status, + Spec: namespace.Spec, + ObjectMeta: metav1.ObjectMeta{ + Name: namespace.Name, + GenerateName: namespace.GenerateName, + Namespace: namespace.Namespace, + UID: namespace.UID, + ResourceVersion: namespace.ResourceVersion, + Generation: namespace.Generation, + CreationTimestamp: namespace.CreationTimestamp, + DeletionTimestamp: namespace.DeletionTimestamp, + DeletionGracePeriodSeconds: namespace.DeletionGracePeriodSeconds, + Labels: namespace.Labels, + Annotations: namespace.Annotations, + Finalizers: namespace.Finalizers, + }, + } +} + // CompilationErrors returns a list of all the errors from the compilation of the evaluator func (e *filter) CompilationErrors() []error { compilationErrors := []error{} diff --git a/pkg/admission/plugin/cel/filter_test.go b/pkg/admission/plugin/cel/filter_test.go index a3cc88ebd..011d0606a 100644 --- a/pkg/admission/plugin/cel/filter_test.go +++ b/pkg/admission/plugin/cel/filter_test.go @@ -149,6 +149,28 @@ func TestFilter(t *testing.T) { }, } + nsObject := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Labels: map[string]string{ + "env": "test", + "foo": "demo", + }, + Annotations: map[string]string{ + "annotation1": "testAnnotation1", + }, + Finalizers: []string{"f1"}, + }, + Spec: corev1.NamespaceSpec{ + Finalizers: []corev1.FinalizerName{ + corev1.FinalizerKubernetes, + }, + }, + Status: corev1.NamespaceStatus{ + Phase: corev1.NamespaceActive, + }, + } + var nilUnstructured *unstructured.Unstructured cases := []struct { name string @@ -159,6 +181,7 @@ func TestFilter(t *testing.T) { hasParamKind bool authorizer authorizer.Authorizer testPerCallLimit uint64 + namespaceObject *corev1.Namespace }{ { name: "valid syntax for object", @@ -674,6 +697,64 @@ func TestFilter(t *testing.T) { params: crdParams, testPerCallLimit: 1, }, + { + name: "test namespaceObject", + validations: []ExpressionAccessor{ + &condition{ + Expression: "namespaceObject.metadata.name == 'test'", + }, + &condition{ + Expression: "'env' in namespaceObject.metadata.labels && namespaceObject.metadata.labels.env == 'test'", + }, + &condition{ + Expression: "('fake' in namespaceObject.metadata.labels) && namespaceObject.metadata.labels.fake == 'test'", + }, + &condition{ + Expression: "namespaceObject.spec.finalizers[0] == 'kubernetes'", + }, + &condition{ + Expression: "namespaceObject.status.phase == 'Active'", + }, + &condition{ + Expression: "size(namespaceObject.metadata.managedFields) == 1", + }, + &condition{ + Expression: "size(namespaceObject.metadata.ownerReferences) == 1", + }, + &condition{ + Expression: "'env' in namespaceObject.metadata.annotations", + }, + }, + attributes: newValidAttribute(&podObject, false), + results: []EvaluationResult{ + { + EvalResult: celtypes.True, + }, + { + EvalResult: celtypes.True, + }, + { + EvalResult: celtypes.False, + }, + { + EvalResult: celtypes.True, + }, + { + EvalResult: celtypes.True, + }, + { + Error: errors.New("undefined field 'managedFields'"), + }, + { + Error: errors.New("undefined field 'ownerReferences'"), + }, + { + EvalResult: celtypes.False, + }, + }, + hasParamKind: false, + namespaceObject: nsObject, + }, } for _, tc := range cases { @@ -706,7 +787,7 @@ func TestFilter(t *testing.T) { optionalVars := OptionalVariableBindings{VersionedParams: tc.params, Authorizer: tc.authorizer} ctx := context.TODO() - evalResults, _, err := f.ForInput(ctx, versionedAttr, CreateAdmissionRequest(versionedAttr.Attributes), optionalVars, celconfig.RuntimeCELCostBudget) + evalResults, _, err := f.ForInput(ctx, versionedAttr, CreateAdmissionRequest(versionedAttr.Attributes), optionalVars, CreateNamespaceObject(tc.namespaceObject), celconfig.RuntimeCELCostBudget) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -852,7 +933,7 @@ func TestRuntimeCELCostBudget(t *testing.T) { } optionalVars := OptionalVariableBindings{VersionedParams: tc.params, Authorizer: tc.authorizer} ctx := context.TODO() - evalResults, remaining, err := f.ForInput(ctx, versionedAttr, CreateAdmissionRequest(versionedAttr.Attributes), optionalVars, tc.testRuntimeCELCostBudget) + evalResults, remaining, err := f.ForInput(ctx, versionedAttr, CreateAdmissionRequest(versionedAttr.Attributes), optionalVars, nil, tc.testRuntimeCELCostBudget) if tc.exceedBudget && err == nil { t.Errorf("Expected RuntimeCELCostBudge to be exceeded but got nil") } diff --git a/pkg/admission/plugin/cel/interface.go b/pkg/admission/plugin/cel/interface.go index 5bc708cf5..c9f4e6336 100644 --- a/pkg/admission/plugin/cel/interface.go +++ b/pkg/admission/plugin/cel/interface.go @@ -24,6 +24,7 @@ import ( "github.com/google/cel-go/common/types/ref" v1 "k8s.io/api/admission/v1" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/authorization/authorizer" @@ -87,7 +88,7 @@ type Filter interface { // ForInput converts compiled CEL-typed values into evaluated CEL-typed value. // runtimeCELCostBudget was added for testing purpose only. Callers should always use const RuntimeCELCostBudget from k8s.io/apiserver/pkg/apis/cel/config.go as input. // If cost budget is calculated, the filter should return the remaining budget. - ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *v1.AdmissionRequest, optionalVars OptionalVariableBindings, runtimeCELCostBudget int64) ([]EvaluationResult, int64, error) + ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *v1.AdmissionRequest, optionalVars OptionalVariableBindings, namespace *corev1.Namespace, runtimeCELCostBudget int64) ([]EvaluationResult, int64, error) // CompilationErrors returns a list of errors from the compilation of the evaluator CompilationErrors() []error diff --git a/pkg/admission/plugin/validatingadmissionpolicy/admission_test.go b/pkg/admission/plugin/validatingadmissionpolicy/admission_test.go index 6425813c1..8c382be99 100644 --- a/pkg/admission/plugin/validatingadmissionpolicy/admission_test.go +++ b/pkg/admission/plugin/validatingadmissionpolicy/admission_test.go @@ -253,7 +253,7 @@ type fakeFilter struct { keyId string } -func (f *fakeFilter) ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *admissionv1.AdmissionRequest, inputs cel.OptionalVariableBindings, runtimeCELCostBudget int64) ([]cel.EvaluationResult, int64, error) { +func (f *fakeFilter) ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *admissionv1.AdmissionRequest, inputs cel.OptionalVariableBindings, namespace *v1.Namespace, runtimeCELCostBudget int64) ([]cel.EvaluationResult, int64, error) { return []cel.EvaluationResult{}, 0, nil } @@ -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, authz authorizer.Authorizer) ValidateResult + ValidateFunc func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, 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, authz authorizer.Authorizer) ValidateResult) { +func (f *fakeValidator) RegisterDefinition(definition *v1alpha1.ValidatingAdmissionPolicy, validateFunc func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, 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, authz authorizer.Authorizer) ValidateResult { - return f.ValidateFunc(ctx, versionedAttr, versionedParams, runtimeCELCostBudget, authz) +func (f *fakeValidator) Validate(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { + return f.ValidateFunc(ctx, versionedAttr, versionedParams, namespace, runtimeCELCostBudget, authz) } var _ Matcher = &fakeMatcher{} @@ -295,6 +295,10 @@ func (f *fakeMatcher) ValidateInitialization() error { return nil } +func (f *fakeMatcher) GetNamespace(name string) (*v1.Namespace, error) { + return nil, nil +} + type fakeMatcher struct { DefaultMatch bool DefinitionMatchFuncs map[namespacedName]func(*v1alpha1.ValidatingAdmissionPolicy, admission.Attributes) bool @@ -770,7 +774,7 @@ func TestBasicPolicyDefinitionFailure(t *testing.T) { } }) - validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { + validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { return ValidateResult{ Decisions: []PolicyDecision{ { @@ -840,7 +844,7 @@ func TestDefinitionDoesntMatch(t *testing.T) { } }) - validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { + validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { return ValidateResult{ Decisions: []PolicyDecision{ { @@ -953,7 +957,7 @@ func TestReconfigureBinding(t *testing.T) { } }) - validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { + validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { return ValidateResult{ Decisions: []PolicyDecision{ { @@ -1063,7 +1067,7 @@ func TestRemoveDefinition(t *testing.T) { } }) - validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { + validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { return ValidateResult{ Decisions: []PolicyDecision{ { @@ -1132,7 +1136,7 @@ func TestRemoveBinding(t *testing.T) { } }) - validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { + validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { return ValidateResult{ Decisions: []PolicyDecision{ { @@ -1242,7 +1246,7 @@ func TestInvalidParamSourceInstanceName(t *testing.T) { } }) - validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { + validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { return ValidateResult{ Decisions: []PolicyDecision{ { @@ -1310,7 +1314,7 @@ func TestEmptyParamSource(t *testing.T) { } }) - validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { + validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { return ValidateResult{ Decisions: []PolicyDecision{ { @@ -1412,7 +1416,7 @@ func TestMultiplePoliciesSharedParamType(t *testing.T) { } }) - validator1.RegisterDefinition(&policy1, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { + validator1.RegisterDefinition(&policy1, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { evaluations1.Add(1) return ValidateResult{ Decisions: []PolicyDecision{ @@ -1431,7 +1435,7 @@ func TestMultiplePoliciesSharedParamType(t *testing.T) { } }) - validator2.RegisterDefinition(&policy2, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { + validator2.RegisterDefinition(&policy2, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { evaluations2.Add(1) return ValidateResult{ Decisions: []PolicyDecision{ @@ -1541,7 +1545,7 @@ func TestNativeTypeParam(t *testing.T) { } }) - validator.RegisterDefinition(&nativeTypeParamPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { + validator.RegisterDefinition(&nativeTypeParamPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { evaluations.Add(1) if _, ok := versionedParams.(*v1.ConfigMap); ok { return ValidateResult{ @@ -1623,7 +1627,7 @@ func TestAuditValidationAction(t *testing.T) { } }) - validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { + validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { return ValidateResult{ Decisions: []PolicyDecision{ { @@ -1694,7 +1698,7 @@ func TestWarnValidationAction(t *testing.T) { } }) - validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { + validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { return ValidateResult{ Decisions: []PolicyDecision{ { @@ -1753,7 +1757,7 @@ func TestAllValidationActions(t *testing.T) { } }) - validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { + validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { return ValidateResult{ Decisions: []PolicyDecision{ { @@ -1824,7 +1828,7 @@ func TestAuditAnnotations(t *testing.T) { } }) - validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { + validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { o, err := meta.Accessor(versionedParams) if err != nil { t.Fatal(err) diff --git a/pkg/admission/plugin/validatingadmissionpolicy/controller.go b/pkg/admission/plugin/validatingadmissionpolicy/controller.go index 4df61ed1c..f0ca17438 100644 --- a/pkg/admission/plugin/validatingadmissionpolicy/controller.go +++ b/pkg/admission/plugin/validatingadmissionpolicy/controller.go @@ -26,6 +26,7 @@ import ( "time" "k8s.io/api/admissionregistration/v1alpha1" + v1 "k8s.io/api/core/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -319,6 +320,23 @@ func (c *celAdmissionController) Validate( continue } } + var namespace *v1.Namespace + namespaceName := a.GetNamespace() + + // Special case, the namespace object has the namespace of itself (maybe a bug). + // unset it if the incoming object is a namespace + if gvk := a.GetKind(); gvk.Kind == "Namespace" && gvk.Version == "v1" && gvk.Group == "" { + namespaceName = "" + } + + // if it is cluster scoped, namespaceName will be empty + // Otherwise, get the Namespace resource. + if namespaceName != "" { + namespace, err = c.policyController.matcher.GetNamespace(namespaceName) + if err != nil { + return err + } + } if versionedAttr == nil { va, err := admission.NewVersionedAttributes(a, matchKind, o) @@ -330,7 +348,7 @@ func (c *celAdmissionController) Validate( versionedAttr = va } - validationResult := bindingInfo.validator.Validate(ctx, versionedAttr, param, celconfig.RuntimeCELCostBudget, authz) + validationResult := bindingInfo.validator.Validate(ctx, versionedAttr, param, namespace, celconfig.RuntimeCELCostBudget, authz) for i, decision := range validationResult.Decisions { switch decision.Action { diff --git a/pkg/admission/plugin/validatingadmissionpolicy/interface.go b/pkg/admission/plugin/validatingadmissionpolicy/interface.go index 750295d36..28c5a0dad 100644 --- a/pkg/admission/plugin/validatingadmissionpolicy/interface.go +++ b/pkg/admission/plugin/validatingadmissionpolicy/interface.go @@ -22,6 +22,7 @@ import ( celgo "github.com/google/cel-go/cel" "k8s.io/api/admissionregistration/v1alpha1" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -90,6 +91,10 @@ type Matcher interface { // BindingMatches says whether this policy definition matches the provided admission // resource request BindingMatches(a admission.Attributes, o admission.ObjectInterfaces, definition *v1alpha1.ValidatingAdmissionPolicyBinding) (bool, error) + + // GetNamespace retrieves the Namespace resource by the given name. The name may be empty, in which case + // GetNamespace must return nil, nil + GetNamespace(name string) (*corev1.Namespace, error) } // ValidateResult defines the result of a Validator.Validate operation. @@ -104,5 +109,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, authz authorizer.Authorizer) ValidateResult + Validate(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *corev1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult } diff --git a/pkg/admission/plugin/validatingadmissionpolicy/matcher.go b/pkg/admission/plugin/validatingadmissionpolicy/matcher.go index a659a99f1..5ed3d17b1 100644 --- a/pkg/admission/plugin/validatingadmissionpolicy/matcher.go +++ b/pkg/admission/plugin/validatingadmissionpolicy/matcher.go @@ -18,6 +18,7 @@ package validatingadmissionpolicy import ( "k8s.io/api/admissionregistration/v1alpha1" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime/schema" @@ -76,3 +77,7 @@ func (c *matcher) BindingMatches(a admission.Attributes, o admission.ObjectInter isMatch, _, err := c.Matcher.Matches(a, o, &criteria) return isMatch, err } + +func (c *matcher) GetNamespace(name string) (*corev1.Namespace, error) { + return c.Matcher.GetNamespace(name) +} diff --git a/pkg/admission/plugin/validatingadmissionpolicy/matching/matching.go b/pkg/admission/plugin/validatingadmissionpolicy/matching/matching.go index c4f7e64af..a97adb171 100644 --- a/pkg/admission/plugin/validatingadmissionpolicy/matching/matching.go +++ b/pkg/admission/plugin/validatingadmissionpolicy/matching/matching.go @@ -21,6 +21,7 @@ import ( v1 "k8s.io/api/admissionregistration/v1" "k8s.io/api/admissionregistration/v1alpha1" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/admission" "k8s.io/client-go/kubernetes" @@ -44,6 +45,10 @@ type Matcher struct { objectMatcher *object.Matcher } +func (m *Matcher) GetNamespace(name string) (*corev1.Namespace, error) { + return m.namespaceMatcher.GetNamespace(name) +} + // NewMatcher initialize the matcher with dependencies requires func NewMatcher( namespaceLister listersv1.NamespaceLister, diff --git a/pkg/admission/plugin/validatingadmissionpolicy/typechecking.go b/pkg/admission/plugin/validatingadmissionpolicy/typechecking.go index 21c454ceb..8857933c3 100644 --- a/pkg/admission/plugin/validatingadmissionpolicy/typechecking.go +++ b/pkg/admission/plugin/validatingadmissionpolicy/typechecking.go @@ -347,10 +347,15 @@ func sortGVKList(list []schema.GroupVersionKind) []schema.GroupVersionKind { func buildEnv(hasParams bool, hasAuthorizer bool, types typeOverwrite) (*cel.Env, error) { baseEnv := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()) requestType := plugincel.BuildRequestType() + namespaceType := plugincel.BuildNamespaceType() var varOpts []cel.EnvOption var declTypes []*apiservercel.DeclType + // namespace, hand-crafted type + declTypes = append(declTypes, namespaceType) + varOpts = append(varOpts, createVariableOpts(namespaceType, plugincel.NamespaceVarName)...) + // request, hand-crafted type declTypes = append(declTypes, requestType) varOpts = append(varOpts, createVariableOpts(requestType, plugincel.RequestVarName)...) diff --git a/pkg/admission/plugin/validatingadmissionpolicy/validator.go b/pkg/admission/plugin/validatingadmissionpolicy/validator.go index 65854764f..b21e6495b 100644 --- a/pkg/admission/plugin/validatingadmissionpolicy/validator.go +++ b/pkg/admission/plugin/validatingadmissionpolicy/validator.go @@ -24,6 +24,7 @@ import ( celtypes "github.com/google/cel-go/common/types" v1 "k8s.io/api/admissionregistration/v1" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/admission" @@ -70,7 +71,8 @@ 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, authz authorizer.Authorizer) ValidateResult { + +func (v *validator) Validate(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *corev1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult { var f v1.FailurePolicyType if v.failPolicy == nil { f = v1.Fail @@ -101,7 +103,9 @@ func (v *validator) Validate(ctx context.Context, versionedAttr *admission.Versi 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) + // Decide which fields are exposed + ns := cel.CreateNamespaceObject(namespace) + evalResults, remainingBudget, err := v.validationFilter.ForInput(ctx, versionedAttr, admissionRequest, optionalVars, ns, runtimeCELCostBudget) if err != nil { return ValidateResult{ Decisions: []PolicyDecision{ @@ -114,7 +118,7 @@ func (v *validator) Validate(ctx context.Context, versionedAttr *admission.Versi } } decisions := make([]PolicyDecision, len(evalResults)) - messageResults, _, err := v.messageFilter.ForInput(ctx, versionedAttr, admissionRequest, expressionOptionalVars, remainingBudget) + messageResults, _, err := v.messageFilter.ForInput(ctx, versionedAttr, admissionRequest, expressionOptionalVars, ns, remainingBudget) for i, evalResult := range evalResults { var decision = &decisions[i] // TODO: move this to generics @@ -191,7 +195,7 @@ func (v *validator) Validate(ctx context.Context, versionedAttr *admission.Versi } options := cel.OptionalVariableBindings{VersionedParams: versionedParams} - auditAnnotationEvalResults, _, err := v.auditAnnotationFilter.ForInput(ctx, versionedAttr, cel.CreateAdmissionRequest(versionedAttr.Attributes), options, runtimeCELCostBudget) + auditAnnotationEvalResults, _, err := v.auditAnnotationFilter.ForInput(ctx, versionedAttr, cel.CreateAdmissionRequest(versionedAttr.Attributes), options, namespace, runtimeCELCostBudget) if err != nil { return ValidateResult{ Decisions: []PolicyDecision{ diff --git a/pkg/admission/plugin/validatingadmissionpolicy/validator_test.go b/pkg/admission/plugin/validatingadmissionpolicy/validator_test.go index 55f3ef862..ab9c12c72 100644 --- a/pkg/admission/plugin/validatingadmissionpolicy/validator_test.go +++ b/pkg/admission/plugin/validatingadmissionpolicy/validator_test.go @@ -28,6 +28,7 @@ import ( admissionv1 "k8s.io/api/admission/v1" v1 "k8s.io/api/admissionregistration/v1" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -47,7 +48,7 @@ type fakeCelFilter struct { throwError bool } -func (f *fakeCelFilter) ForInput(_ context.Context, _ *admission.VersionedAttributes, _ *admissionv1.AdmissionRequest, _ cel.OptionalVariableBindings, costBudget int64) ([]cel.EvaluationResult, int64, error) { +func (f *fakeCelFilter) ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *admissionv1.AdmissionRequest, optionalVars cel.OptionalVariableBindings, namespace *corev1.Namespace, costBudget int64) ([]cel.EvaluationResult, int64, error) { if costBudget <= 0 { // this filter will cost 1, so cost = 0 means fail. return nil, -1, &apiservercel.Error{ Type: apiservercel.ErrorTypeInvalid, @@ -892,7 +893,7 @@ func TestValidate(t *testing.T) { if tc.costBudget != 0 { budget = tc.costBudget } - validateResult := v.Validate(ctx, fakeVersionedAttr, nil, budget, nil) + validateResult := v.Validate(ctx, fakeVersionedAttr, nil, nil, budget, nil) require.Equal(t, len(validateResult.Decisions), len(tc.policyDecision)) @@ -944,7 +945,7 @@ func TestContextCanceled(t *testing.T) { } ctx, cancel := context.WithCancel(context.TODO()) cancel() - validationResult := v.Validate(ctx, fakeVersionedAttr, nil, celconfig.RuntimeCELCostBudget, nil) + validationResult := v.Validate(ctx, fakeVersionedAttr, nil, nil, celconfig.RuntimeCELCostBudget, nil) if len(validationResult.Decisions) != 1 || !strings.Contains(validationResult.Decisions[0].Message, "operation interrupted") { t.Errorf("Expected 'operation interrupted' but got %v", validationResult.Decisions) } diff --git a/pkg/admission/plugin/webhook/matchconditions/matcher.go b/pkg/admission/plugin/webhook/matchconditions/matcher.go index 62aeadaf4..e9c3fb8f7 100644 --- a/pkg/admission/plugin/webhook/matchconditions/matcher.go +++ b/pkg/admission/plugin/webhook/matchconditions/matcher.go @@ -81,7 +81,7 @@ func (m *matcher) Match(ctx context.Context, versionedAttr *admission.VersionedA evalResults, _, err := m.filter.ForInput(ctx, versionedAttr, celplugin.CreateAdmissionRequest(versionedAttr.Attributes), celplugin.OptionalVariableBindings{ VersionedParams: versionedParams, Authorizer: authz, - }, celconfig.RuntimeCELCostBudgetMatchConditions) + }, nil, celconfig.RuntimeCELCostBudgetMatchConditions) if err != nil { admissionmetrics.Metrics.ObserveMatchConditionEvaluationTime(ctx, time.Since(t), m.objectName, m.matcherKind, m.matcherType, string(versionedAttr.GetOperation())) diff --git a/pkg/admission/plugin/webhook/matchconditions/matcher_test.go b/pkg/admission/plugin/webhook/matchconditions/matcher_test.go index 76ed88c3e..20d9aebfb 100644 --- a/pkg/admission/plugin/webhook/matchconditions/matcher_test.go +++ b/pkg/admission/plugin/webhook/matchconditions/matcher_test.go @@ -22,6 +22,8 @@ import ( "strings" "testing" + api "k8s.io/api/core/v1" + v1 "k8s.io/api/admissionregistration/v1" celtypes "github.com/google/cel-go/common/types" @@ -40,7 +42,7 @@ type fakeCelFilter struct { throwError bool } -func (f *fakeCelFilter) ForInput(context.Context, *admission.VersionedAttributes, *admissionv1.AdmissionRequest, cel.OptionalVariableBindings, int64) ([]cel.EvaluationResult, int64, error) { +func (f *fakeCelFilter) ForInput(context.Context, *admission.VersionedAttributes, *admissionv1.AdmissionRequest, cel.OptionalVariableBindings, *api.Namespace, int64) ([]cel.EvaluationResult, int64, error) { if f.throwError { return nil, 0, errors.New("test error") } diff --git a/pkg/admission/plugin/webhook/predicates/namespace/matcher.go b/pkg/admission/plugin/webhook/predicates/namespace/matcher.go index 459e3f5df..6427bc674 100644 --- a/pkg/admission/plugin/webhook/predicates/namespace/matcher.go +++ b/pkg/admission/plugin/webhook/predicates/namespace/matcher.go @@ -20,6 +20,8 @@ import ( "context" "fmt" + v1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -42,6 +44,10 @@ type Matcher struct { Client clientset.Interface } +func (m *Matcher) GetNamespace(name string) (*v1.Namespace, error) { + return m.NamespaceLister.Get(name) +} + // Validate checks if the Matcher has a NamespaceLister and Client. func (m *Matcher) Validate() error { var errs []error