From 96adc301e5850cd1db90a806c685440a9f20d040 Mon Sep 17 00:00:00 2001 From: Mariam Fahmy Date: Mon, 21 Aug 2023 11:04:59 +0300 Subject: [PATCH] feat: support namespaceObject variable in CEL expressions (#8071) * feat: support namespaceObject variable in CEL expressions Signed-off-by: Mariam Fahmy * fix a bug Signed-off-by: Mariam Fahmy --------- Signed-off-by: Mariam Fahmy Co-authored-by: shuting --- pkg/engine/adapters/dclient.go | 6 +++ pkg/engine/api/client.go | 3 ++ .../handlers/validation/validate_cel.go | 50 ++++++++++++------- .../check-statefulset-namespace/01-ns.yaml | 6 +++ .../02-policy.yaml | 6 +++ .../03-statefulset.yaml | 8 +++ .../cel/check-statefulset-namespace/README.md | 7 +++ .../cel/check-statefulset-namespace/ns.yaml | 9 ++++ .../policy-assert.yaml | 9 ++++ .../check-statefulset-namespace/policy.yaml | 19 +++++++ .../statefulset-fail.yaml | 18 +++++++ .../statefulset-pass.yaml | 18 +++++++ 12 files changed, 140 insertions(+), 19 deletions(-) create mode 100644 test/conformance/kuttl/validate/clusterpolicy/standard/cel/check-statefulset-namespace/01-ns.yaml create mode 100644 test/conformance/kuttl/validate/clusterpolicy/standard/cel/check-statefulset-namespace/02-policy.yaml create mode 100644 test/conformance/kuttl/validate/clusterpolicy/standard/cel/check-statefulset-namespace/03-statefulset.yaml create mode 100644 test/conformance/kuttl/validate/clusterpolicy/standard/cel/check-statefulset-namespace/README.md create mode 100644 test/conformance/kuttl/validate/clusterpolicy/standard/cel/check-statefulset-namespace/ns.yaml create mode 100644 test/conformance/kuttl/validate/clusterpolicy/standard/cel/check-statefulset-namespace/policy-assert.yaml create mode 100644 test/conformance/kuttl/validate/clusterpolicy/standard/cel/check-statefulset-namespace/policy.yaml create mode 100644 test/conformance/kuttl/validate/clusterpolicy/standard/cel/check-statefulset-namespace/statefulset-fail.yaml create mode 100644 test/conformance/kuttl/validate/clusterpolicy/standard/cel/check-statefulset-namespace/statefulset-pass.yaml diff --git a/pkg/engine/adapters/dclient.go b/pkg/engine/adapters/dclient.go index 2ca901e4e2..ff076d4550 100644 --- a/pkg/engine/adapters/dclient.go +++ b/pkg/engine/adapters/dclient.go @@ -7,6 +7,8 @@ import ( "github.com/kyverno/kyverno/pkg/auth" "github.com/kyverno/kyverno/pkg/clients/dclient" engineapi "github.com/kyverno/kyverno/pkg/engine/api" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) @@ -44,6 +46,10 @@ func (a *dclientAdapter) GetResource(ctx context.Context, apiVersion, kind, name return a.client.GetResource(ctx, apiVersion, kind, namespace, name, subresources...) } +func (a *dclientAdapter) GetNamespace(ctx context.Context, name string, opts metav1.GetOptions) (*corev1.Namespace, error) { + return a.client.GetKubeClient().CoreV1().Namespaces().Get(ctx, name, opts) +} + func (a *dclientAdapter) CanI(ctx context.Context, kind, namespace, verb, subresource, user string) (bool, error) { canI := auth.NewCanI(a.client.Discovery(), a.client.GetKubeClient().AuthorizationV1().SubjectAccessReviews(), kind, namespace, verb, subresource, user) ok, err := canI.RunAccessCheck(ctx) diff --git a/pkg/engine/api/client.go b/pkg/engine/api/client.go index 70a4bdf43d..c13cf8906f 100644 --- a/pkg/engine/api/client.go +++ b/pkg/engine/api/client.go @@ -7,6 +7,8 @@ import ( "github.com/google/go-containerregistry/pkg/authn" gcrremote "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/sigstore/cosign/v2/pkg/oci/remote" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) @@ -29,6 +31,7 @@ type AuthClient interface { type ResourceClient interface { GetResource(ctx context.Context, apiVersion, kind, namespace, name string, subresources ...string) (*unstructured.Unstructured, error) GetResources(ctx context.Context, group, version, kind, subresource, namespace, name string) ([]Resource, error) + GetNamespace(ctx context.Context, name string, opts metav1.GetOptions) (*corev1.Namespace, error) } type Client interface { diff --git a/pkg/engine/handlers/validation/validate_cel.go b/pkg/engine/handlers/validation/validate_cel.go index 525b8958b0..73011f0f68 100644 --- a/pkg/engine/handlers/validation/validate_cel.go +++ b/pkg/engine/handlers/validation/validate_cel.go @@ -9,6 +9,8 @@ import ( engineapi "github.com/kyverno/kyverno/pkg/engine/api" "github.com/kyverno/kyverno/pkg/engine/handlers" engineutils "github.com/kyverno/kyverno/pkg/engine/utils" + corev1 "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" "k8s.io/apimachinery/pkg/runtime/schema" @@ -43,10 +45,13 @@ func (h validateCELHandler) Process( return resource, nil } - oldResource := policyContext.OldResource() gvr := schema.GroupVersionResource(policyContext.RequestResource()) + gvk := resource.GroupVersionKind() + namespaceName := resource.GetNamespace() + resourceName := resource.GetName() var object, oldObject, versionedParams runtime.Object + oldResource := policyContext.OldResource() object = resource.DeepCopyObject() if oldResource.Object == nil { oldObject = nil @@ -59,9 +64,8 @@ func (h validateCELHandler) Process( validations := rule.Validation.CEL.Expressions auditAnnotations := rule.Validation.CEL.AuditAnnotations - // Get the parameter resource + // get the parameter resource if exists hasParam := rule.Validation.CEL.HasParam() - if hasParam { paramKind := rule.Validation.CEL.GetParamKind() paramRef := rule.Validation.CEL.GetParamRef() @@ -84,6 +88,7 @@ func (h validateCELHandler) Process( versionedParams = paramResource.DeepCopyObject() } + // extract CEL expressions from validate.cel.expressions for _, cel := range validations { condition := &validatingadmissionpolicy.ValidationCondition{ Expression: cel.Expression, @@ -98,6 +103,7 @@ func (h validateCELHandler) Process( messageExpressions = append(messageExpressions, messageCondition) } + // extract CEL expressions from rule.celPreconditions for _, condition := range rule.CELPreconditions { matchCondition := &matchconditions.MatchCondition{ Name: condition.Name, @@ -107,6 +113,7 @@ func (h validateCELHandler) Process( matchExpressions = append(matchExpressions, matchCondition) } + // extract CEL expressions from validate.cel.auditAnnotations for _, auditAnnotation := range auditAnnotations { auditCondition := &validatingadmissionpolicy.AuditAnnotationCondition{ Key: auditAnnotation.Key, @@ -116,6 +123,7 @@ func (h validateCELHandler) Process( auditExpressions = append(auditExpressions, auditCondition) } + // compile CEL expressions compositedCompiler, err := cel.NewCompositedCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion())) if err != nil { return resource, handlers.WithError(rule, engineapi.Validation, "Error while creating composited compiler", err) @@ -125,25 +133,29 @@ func (h validateCELHandler) Process( auditAnnotationFilter := compositedCompiler.Compile(auditExpressions, cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: false}, environment.StoredExpressions) matchConditionFilter := compositedCompiler.Compile(matchExpressions, cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: false}, environment.StoredExpressions) + // newMatcher will be used to check if the incoming resource matches the CEL preconditions newMatcher := matchconditions.NewMatcher(matchConditionFilter, nil, "", "", "") - validator := validatingadmissionpolicy.NewValidator(filter, newMatcher, auditAnnotationFilter, messageExpressionfilter, nil) - - admissionAttributes := admission.NewAttributesRecord( - object, - oldObject, - resource.GroupVersionKind(), - resource.GetNamespace(), - resource.GetName(), - gvr, - "", - admission.Operation(policyContext.Operation()), - nil, - false, - nil, - ) + admissionAttributes := admission.NewAttributesRecord(object, oldObject, gvk, namespaceName, resourceName, gvr, "", admission.Operation(policyContext.Operation()), nil, false, nil) versionedAttr, _ := admission.NewVersionedAttributes(admissionAttributes, admissionAttributes.GetKind(), nil) - validateResult := validator.Validate(ctx, gvr, versionedAttr, versionedParams, nil, celconfig.RuntimeCELCostBudget, nil) + + var namespace *corev1.Namespace + // Special case, the namespace object has the namespace of itself. + // unset it if the incoming object is a namespace + if gvk.Kind == "Namespace" && gvk.Version == "v1" && gvk.Group == "" { + namespaceName = "" + } + if namespaceName != "" { + namespace, err = h.client.GetNamespace(ctx, namespaceName, metav1.GetOptions{}) + if err != nil { + return resource, handlers.WithResponses( + engineapi.RuleError(rule.Name, engineapi.Validation, "Error getting the resource's namespace", err), + ) + } + } + + // validate the incoming object against the rule + validateResult := validator.Validate(ctx, gvr, versionedAttr, versionedParams, namespace, celconfig.RuntimeCELCostBudget, nil) for _, decision := range validateResult.Decisions { switch decision.Action { diff --git a/test/conformance/kuttl/validate/clusterpolicy/standard/cel/check-statefulset-namespace/01-ns.yaml b/test/conformance/kuttl/validate/clusterpolicy/standard/cel/check-statefulset-namespace/01-ns.yaml new file mode 100644 index 0000000000..1427e92e17 --- /dev/null +++ b/test/conformance/kuttl/validate/clusterpolicy/standard/cel/check-statefulset-namespace/01-ns.yaml @@ -0,0 +1,6 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +apply: +- ns.yaml +assert: +- ns.yaml diff --git a/test/conformance/kuttl/validate/clusterpolicy/standard/cel/check-statefulset-namespace/02-policy.yaml b/test/conformance/kuttl/validate/clusterpolicy/standard/cel/check-statefulset-namespace/02-policy.yaml new file mode 100644 index 0000000000..b088ed7601 --- /dev/null +++ b/test/conformance/kuttl/validate/clusterpolicy/standard/cel/check-statefulset-namespace/02-policy.yaml @@ -0,0 +1,6 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +apply: +- policy.yaml +assert: +- policy-assert.yaml diff --git a/test/conformance/kuttl/validate/clusterpolicy/standard/cel/check-statefulset-namespace/03-statefulset.yaml b/test/conformance/kuttl/validate/clusterpolicy/standard/cel/check-statefulset-namespace/03-statefulset.yaml new file mode 100644 index 0000000000..ac2a4ca5ca --- /dev/null +++ b/test/conformance/kuttl/validate/clusterpolicy/standard/cel/check-statefulset-namespace/03-statefulset.yaml @@ -0,0 +1,8 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +apply: +- file: statefulset-pass.yaml + shouldFail: false +- file: statefulset-fail.yaml + shouldFail: true + \ No newline at end of file diff --git a/test/conformance/kuttl/validate/clusterpolicy/standard/cel/check-statefulset-namespace/README.md b/test/conformance/kuttl/validate/clusterpolicy/standard/cel/check-statefulset-namespace/README.md new file mode 100644 index 0000000000..78b4ea6bb9 --- /dev/null +++ b/test/conformance/kuttl/validate/clusterpolicy/standard/cel/check-statefulset-namespace/README.md @@ -0,0 +1,7 @@ +## Description + +This test creates a policy that uses CEL expressions to check if the statefulset is created in the `production` namespace or not. + +## Expected Behavior + +The statefulset `bad-statefulset` is blocked, and the statefulset `good-statefulset` is created. diff --git a/test/conformance/kuttl/validate/clusterpolicy/standard/cel/check-statefulset-namespace/ns.yaml b/test/conformance/kuttl/validate/clusterpolicy/standard/cel/check-statefulset-namespace/ns.yaml new file mode 100644 index 0000000000..83e1993da7 --- /dev/null +++ b/test/conformance/kuttl/validate/clusterpolicy/standard/cel/check-statefulset-namespace/ns.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: production +--- +apiVersion: v1 +kind: Namespace +metadata: + name: testing diff --git a/test/conformance/kuttl/validate/clusterpolicy/standard/cel/check-statefulset-namespace/policy-assert.yaml b/test/conformance/kuttl/validate/clusterpolicy/standard/cel/check-statefulset-namespace/policy-assert.yaml new file mode 100644 index 0000000000..d721c304a9 --- /dev/null +++ b/test/conformance/kuttl/validate/clusterpolicy/standard/cel/check-statefulset-namespace/policy-assert.yaml @@ -0,0 +1,9 @@ +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: check-statefulset-namespace +status: + conditions: + - reason: Succeeded + status: "True" + type: Ready diff --git a/test/conformance/kuttl/validate/clusterpolicy/standard/cel/check-statefulset-namespace/policy.yaml b/test/conformance/kuttl/validate/clusterpolicy/standard/cel/check-statefulset-namespace/policy.yaml new file mode 100644 index 0000000000..259b0b8008 --- /dev/null +++ b/test/conformance/kuttl/validate/clusterpolicy/standard/cel/check-statefulset-namespace/policy.yaml @@ -0,0 +1,19 @@ +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: check-statefulset-namespace +spec: + validationFailureAction: Enforce + background: false + rules: + - name: statefulset-namespace + match: + any: + - resources: + kinds: + - StatefulSet + validate: + cel: + expressions: + - expression: "namespaceObject.metadata.name == 'production'" + message: "The StatefulSet must be created in the 'production' namespace." diff --git a/test/conformance/kuttl/validate/clusterpolicy/standard/cel/check-statefulset-namespace/statefulset-fail.yaml b/test/conformance/kuttl/validate/clusterpolicy/standard/cel/check-statefulset-namespace/statefulset-fail.yaml new file mode 100644 index 0000000000..90c08772c8 --- /dev/null +++ b/test/conformance/kuttl/validate/clusterpolicy/standard/cel/check-statefulset-namespace/statefulset-fail.yaml @@ -0,0 +1,18 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: bad-statefulset + namespace: testing +spec: + replicas: 1 + selector: + matchLabels: + app: app + template: + metadata: + labels: + app: app + spec: + containers: + - name: container2 + image: nginx diff --git a/test/conformance/kuttl/validate/clusterpolicy/standard/cel/check-statefulset-namespace/statefulset-pass.yaml b/test/conformance/kuttl/validate/clusterpolicy/standard/cel/check-statefulset-namespace/statefulset-pass.yaml new file mode 100644 index 0000000000..1f6b372ff1 --- /dev/null +++ b/test/conformance/kuttl/validate/clusterpolicy/standard/cel/check-statefulset-namespace/statefulset-pass.yaml @@ -0,0 +1,18 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: good-statefulset + namespace: production +spec: + replicas: 1 + selector: + matchLabels: + app: app + template: + metadata: + labels: + app: app + spec: + containers: + - name: container2 + image: nginx