1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2025-03-28 02:18:15 +00:00

feat: support namespaceObject variable in CEL expressions (#8071)

* feat: support namespaceObject variable in CEL expressions

Signed-off-by: Mariam Fahmy <mariam.fahmy@nirmata.com>

* fix a bug

Signed-off-by: Mariam Fahmy <mariam.fahmy@nirmata.com>

---------

Signed-off-by: Mariam Fahmy <mariam.fahmy@nirmata.com>
Co-authored-by: shuting <shuting@nirmata.com>
This commit is contained in:
Mariam Fahmy 2023-08-21 11:04:59 +03:00 committed by GitHub
parent ce4beb0e92
commit 96adc301e5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 140 additions and 19 deletions

View file

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

View file

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

View file

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

View file

@ -0,0 +1,6 @@
apiVersion: kuttl.dev/v1beta1
kind: TestStep
apply:
- ns.yaml
assert:
- ns.yaml

View file

@ -0,0 +1,6 @@
apiVersion: kuttl.dev/v1beta1
kind: TestStep
apply:
- policy.yaml
assert:
- policy-assert.yaml

View file

@ -0,0 +1,8 @@
apiVersion: kuttl.dev/v1beta1
kind: TestStep
apply:
- file: statefulset-pass.yaml
shouldFail: false
- file: statefulset-fail.yaml
shouldFail: true

View file

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

View file

@ -0,0 +1,9 @@
apiVersion: v1
kind: Namespace
metadata:
name: production
---
apiVersion: v1
kind: Namespace
metadata:
name: testing

View file

@ -0,0 +1,9 @@
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: check-statefulset-namespace
status:
conditions:
- reason: Succeeded
status: "True"
type: Ready

View file

@ -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."

View file

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

View file

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