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:
parent
ce4beb0e92
commit
96adc301e5
12 changed files with 140 additions and 19 deletions
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
apiVersion: kuttl.dev/v1beta1
|
||||
kind: TestStep
|
||||
apply:
|
||||
- ns.yaml
|
||||
assert:
|
||||
- ns.yaml
|
|
@ -0,0 +1,6 @@
|
|||
apiVersion: kuttl.dev/v1beta1
|
||||
kind: TestStep
|
||||
apply:
|
||||
- policy.yaml
|
||||
assert:
|
||||
- policy-assert.yaml
|
|
@ -0,0 +1,8 @@
|
|||
apiVersion: kuttl.dev/v1beta1
|
||||
kind: TestStep
|
||||
apply:
|
||||
- file: statefulset-pass.yaml
|
||||
shouldFail: false
|
||||
- file: statefulset-fail.yaml
|
||||
shouldFail: true
|
||||
|
|
@ -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.
|
|
@ -0,0 +1,9 @@
|
|||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: production
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: testing
|
|
@ -0,0 +1,9 @@
|
|||
apiVersion: kyverno.io/v1
|
||||
kind: ClusterPolicy
|
||||
metadata:
|
||||
name: check-statefulset-namespace
|
||||
status:
|
||||
conditions:
|
||||
- reason: Succeeded
|
||||
status: "True"
|
||||
type: Ready
|
|
@ -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."
|
|
@ -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
|
|
@ -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
|
Loading…
Add table
Reference in a new issue