1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2024-12-15 17:51:20 +00:00

feat: support namespaced parameter resources for CEL expressions in Kyverno policies (#8084)

* feat: support namespaced parameter resources for CEL expressions in Kyverno policies

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

* fix lint issue

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

* fix

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

* fix kuttl test

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

---------

Signed-off-by: Mariam Fahmy <mariam.fahmy@nirmata.com>
This commit is contained in:
Mariam Fahmy 2023-08-28 17:43:09 +03:00 committed by GitHub
parent 0f9fe30c08
commit 94aa1f18c6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
56 changed files with 501 additions and 41 deletions

View file

@ -50,6 +50,21 @@ func (a *dclientAdapter) GetNamespace(ctx context.Context, name string, opts met
return a.client.GetKubeClient().CoreV1().Namespaces().Get(ctx, name, opts)
}
func (a *dclientAdapter) ListResource(ctx context.Context, apiVersion string, kind string, namespace string, lselector *metav1.LabelSelector) (*unstructured.UnstructuredList, error) {
return a.client.ListResource(ctx, apiVersion, kind, namespace, lselector)
}
func (a *dclientAdapter) IsNamespaced(group, version, kind string) (bool, error) {
gvrss, err := a.client.Discovery().FindResources(group, version, kind, "")
if err != nil {
return false, err
}
for _, apiResource := range gvrss {
return apiResource.Namespaced, nil
}
return false, nil
}
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

@ -30,8 +30,10 @@ type AuthClient interface {
type ResourceClient interface {
GetResource(ctx context.Context, apiVersion, kind, namespace, name string, subresources ...string) (*unstructured.Unstructured, error)
ListResource(ctx context.Context, apiVersion string, kind string, namespace string, lselector *metav1.LabelSelector) (*unstructured.UnstructuredList, 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)
IsNamespaced(group, version, kind string) (bool, error)
}
type Client interface {

View file

@ -10,6 +10,7 @@ import (
"github.com/kyverno/kyverno/pkg/engine/handlers"
engineutils "github.com/kyverno/kyverno/pkg/engine/utils"
celutils "github.com/kyverno/kyverno/pkg/utils/cel"
admissionregistrationv1alpha1 "k8s.io/api/admissionregistration/v1alpha1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@ -53,7 +54,7 @@ func (h validateCELHandler) Process(
object := resource.DeepCopyObject()
// in case of update request, set the oldObject to the current resource before it gets updated
var oldObject, versionedParams runtime.Object
var oldObject runtime.Object
oldResource := policyContext.OldResource()
if oldResource.Object == nil {
oldObject = nil
@ -70,31 +71,7 @@ func (h validateCELHandler) Process(
validations := rule.Validation.CEL.Expressions
auditAnnotations := rule.Validation.CEL.AuditAnnotations
// get the parameter resource if exists
if hasParam && h.client != nil {
paramKind := rule.Validation.CEL.GetParamKind()
paramRef := rule.Validation.CEL.GetParamRef()
apiVersion := paramKind.APIVersion
kind := paramKind.Kind
name := paramRef.Name
namespace := paramRef.Namespace
if namespace == "" {
namespace = "default"
}
paramResource, err := h.client.GetResource(ctx, apiVersion, kind, namespace, name, "")
if err != nil {
return resource, handlers.WithError(rule, engineapi.Validation, "Error while getting the parameterized resource", err)
}
versionedParams = paramResource.DeepCopyObject()
}
optionalVars := cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: false}
// compile CEL expressions
compiler, err := celutils.NewCompiler(validations, auditAnnotations, matchConditions, variables)
if err != nil {
@ -129,20 +106,39 @@ func (h validateCELHandler) Process(
admissionAttributes := admission.NewAttributesRecord(object, oldObject, gvk, namespaceName, resourceName, gvr, "", admission.Operation(policyContext.Operation()), nil, false, nil)
versionedAttr, _ := admission.NewVersionedAttributes(admissionAttributes, admissionAttributes.GetKind(), nil)
// validate the incoming object against the rule
validateResult := validator.Validate(ctx, gvr, versionedAttr, versionedParams, namespace, celconfig.RuntimeCELCostBudget, nil)
var validationResults []validatingadmissionpolicy.ValidateResult
if hasParam {
paramKind := rule.Validation.CEL.ParamKind
paramRef := rule.Validation.CEL.ParamRef
for _, decision := range validateResult.Decisions {
switch decision.Action {
case validatingadmissionpolicy.ActionAdmit:
if decision.Evaluation == validatingadmissionpolicy.EvalError {
params, err := collectParams(ctx, h.client, paramKind, paramRef, namespaceName)
if err != nil {
return resource, handlers.WithResponses(
engineapi.RuleError(rule.Name, engineapi.Validation, "error in parameterized resource", err),
)
}
for _, param := range params {
validationResults = append(validationResults, validator.Validate(ctx, gvr, versionedAttr, param, namespace, celconfig.RuntimeCELCostBudget, nil))
}
} else {
validationResults = append(validationResults, validator.Validate(ctx, gvr, versionedAttr, nil, namespace, celconfig.RuntimeCELCostBudget, nil))
}
for _, validationResult := range validationResults {
for _, decision := range validationResult.Decisions {
switch decision.Action {
case validatingadmissionpolicy.ActionAdmit:
if decision.Evaluation == validatingadmissionpolicy.EvalError {
return resource, handlers.WithResponses(
engineapi.RuleError(rule.Name, engineapi.Validation, decision.Message, nil),
)
}
case validatingadmissionpolicy.ActionDeny:
return resource, handlers.WithResponses(
engineapi.RuleError(rule.Name, engineapi.Validation, decision.Message, nil),
engineapi.RuleFail(rule.Name, engineapi.Validation, decision.Message),
)
}
case validatingadmissionpolicy.ActionDeny:
return resource, handlers.WithResponses(
engineapi.RuleFail(rule.Name, engineapi.Validation, decision.Message),
)
}
}
@ -151,3 +147,61 @@ func (h validateCELHandler) Process(
engineapi.RulePass(rule.Name, engineapi.Validation, msg),
)
}
func collectParams(ctx context.Context, client engineapi.Client, paramKind *admissionregistrationv1alpha1.ParamKind, paramRef *admissionregistrationv1alpha1.ParamRef, namespace string) ([]runtime.Object, error) {
var params []runtime.Object
apiVersion := paramKind.APIVersion
kind := paramKind.Kind
gv, err := schema.ParseGroupVersion(apiVersion)
if err != nil {
return nil, fmt.Errorf("can't parse the parameter resource group version")
}
// If `paramKind` is cluster-scoped, then paramRef.namespace MUST be unset.
// If `paramKind` is namespace-scoped, the namespace of the object being evaluated for admission will be used
// when paramRef.namespace is left unset.
var paramsNamespace string
isNamespaced, err := client.IsNamespaced(gv.Group, gv.Version, kind)
if err != nil {
return nil, err
}
// check if `paramKind` is namespace-scoped
if isNamespaced {
// set params namespace to the incoming object's namespace by default.
paramsNamespace = namespace
if paramRef.Namespace != "" {
paramsNamespace = paramRef.Namespace
} else if paramsNamespace == "" {
return nil, fmt.Errorf("can't use namespaced paramRef to match cluster-scoped resources")
}
} else {
// It isn't allowed to set namespace for cluster-scoped params
if paramRef.Namespace != "" {
return nil, fmt.Errorf("paramRef.namespace must not be provided for a cluster-scoped `paramKind`")
}
}
if paramRef.Name != "" {
param, err := client.GetResource(ctx, apiVersion, kind, paramsNamespace, paramRef.Name, "")
if err != nil {
return nil, err
}
return []runtime.Object{param}, nil
} else if paramRef.Selector != nil {
paramList, err := client.ListResource(ctx, apiVersion, kind, paramsNamespace, paramRef.Selector)
if err != nil {
return nil, err
}
for i := range paramList.Items {
params = append(params, &paramList.Items[i])
}
}
if len(params) == 0 && paramRef.ParameterNotFoundAction != nil && *paramRef.ParameterNotFoundAction == admissionregistrationv1alpha1.DenyAction {
return nil, fmt.Errorf("no params found")
}
return params, nil
}

View file

@ -90,8 +90,16 @@ func (v *Validate) Validate(ctx context.Context) (string, error) {
}
if v.rule.CEL.ParamRef != nil {
if v.rule.CEL.ParamRef.Name == "" {
return "", fmt.Errorf("cel.paramRef.name is required")
if v.rule.CEL.ParamRef.Name == "" && v.rule.CEL.ParamRef.Selector == nil {
return "", fmt.Errorf("one of cel.paramRef.name or cel.paramRef.selector must be set")
}
if v.rule.CEL.ParamRef.Name != "" && v.rule.CEL.ParamRef.Selector != nil {
return "", fmt.Errorf("one of cel.paramRef.name or cel.paramRef.selector must be set")
}
if v.rule.CEL.ParamRef.ParameterNotFoundAction == nil {
return "", fmt.Errorf("cel.paramRef.parameterNotFoundAction is required")
}
if v.rule.CEL.ParamKind == nil {

View file

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

View file

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

View file

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

View file

@ -0,0 +1,12 @@
## Description
This test validates the use of parameter resources in validate.cel subrule.
This test creates the following:
1. A cluster-scoped custom resource definition `NamespaceConstraint`
3. A policy that checks the namespace name using the parameter resource.
4. Two namespaces.
## Expected Behavior
The namespace `testing-ns` is blocked, and the namespace `production-ns` is created.

View file

@ -0,0 +1,4 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: namespaceconstraints.rules.example.com

View file

@ -0,0 +1,26 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: namespaceconstraints.rules.example.com
spec:
group: rules.example.com
names:
kind: NamespaceConstraint
plural: namespaceconstraints
scope: Cluster
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
apiVersion:
type: string
kind:
type: string
metadata:
type: object
name:
type: string

View file

@ -0,0 +1,5 @@
apiVersion: rules.example.com/v1
kind: NamespaceConstraint
metadata:
name: "namespace-constraint-test.example.com"
name: "production-ns"

View file

@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: testing-ns

View file

@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: production-ns

View file

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

View file

@ -0,0 +1,25 @@
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: check-namespace-name
spec:
validationFailureAction: Enforce
background: false
rules:
- name: namespace-name
match:
any:
- resources:
kinds:
- Namespace
validate:
cel:
paramKind:
apiVersion: rules.example.com/v1
kind: NamespaceConstraint
paramRef:
name: "namespace-constraint-test.example.com"
parameterNotFoundAction: "Deny"
expressions:
- expression: "object.metadata.name == params.name"
messageExpression: "'Namespace name must be ' + params.name"

View file

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

View file

@ -0,0 +1,6 @@
apiVersion: kuttl.dev/v1beta1
kind: TestStep
apply:
- nameConstraint.yaml
assert:
- nameConstraint.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,5 @@
apiVersion: kuttl.dev/v1beta1
kind: TestStep
apply:
- file: ns.yaml
shouldFail: true

View file

@ -0,0 +1,12 @@
## Description
This test validates the use of parameter resources in validate.cel subrule.
This test creates the following:
1. A namespaced custom resource definition `NameConstraint`
3. A policy that checks the namespace name using the parameter resource.
4. A namespace `testing`.
## Expected Behavior
Since the parameter resource is namespaced-scope and the policy matches cluster-scoped resource `Namespace`, therefore the creation of a namespace is blocked

View file

@ -0,0 +1,4 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: nameconstraints.rules.example.com

View file

@ -0,0 +1,26 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: nameconstraints.rules.example.com
spec:
group: rules.example.com
names:
kind: NameConstraint
plural: nameconstraints
scope: Namespaced
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
apiVersion:
type: string
kind:
type: string
metadata:
type: object
name:
type: string

View file

@ -0,0 +1,5 @@
apiVersion: rules.example.com/v1
kind: NameConstraint
metadata:
name: "name-constraint-test.example.com"
name: "default"

View file

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

View file

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

View file

@ -0,0 +1,25 @@
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: check-namespace-name
spec:
validationFailureAction: Enforce
background: false
rules:
- name: namespace-name
match:
any:
- resources:
kinds:
- Namespace
validate:
cel:
paramKind:
apiVersion: rules.example.com/v1
kind: NameConstraint
paramRef:
name: "name-constraint-test.example.com"
parameterNotFoundAction: "Deny"
expressions:
- expression: "object.metadata.name == params.name"
messageExpression: "'Namespace name must be ' + params.name"

View file

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

View file

@ -4,8 +4,8 @@ This test validates the use of parameter resources in validate.cel subrule.
This test creates the following:
1. A namespace `test-params`
2. A custom resource definition `ReplicaLimit`
3. A policy that checks the deployment replicas using the parameter resource.
2. A namespaced custom resource definition `ReplicaLimit`
3. A policy that checks the deployment replicas using the parameter resource. The `validate.cel.paramRef.namespace` is set.
4. Two deployments.
## Expected Behavior

View file

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

View file

@ -18,8 +18,9 @@ spec:
apiVersion: rules.example.com/v1
kind: ReplicaLimit
paramRef:
name: "replica-limit-test.example.com"
namespace: "test-params"
name: "replica-limit"
namespace: "testing"
parameterNotFoundAction: "Deny"
expressions:
- expression: "object.spec.replicas <= params.maxReplicas"
messageExpression: "'Deployment spec.replicas must be less than ' + string(params.maxReplicas)"

View file

@ -0,0 +1,6 @@
apiVersion: rules.example.com/v1
kind: ReplicaLimit
metadata:
name: "replica-limit"
namespace: testing
maxReplicas: 3

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:
- crd.yaml
assert:
- crd-assert.yaml

View file

@ -0,0 +1,6 @@
apiVersion: kuttl.dev/v1beta1
kind: TestStep
apply:
- replicaLimit.yaml
assert:
- replicaLimit.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,7 @@
apiVersion: kuttl.dev/v1beta1
kind: TestStep
apply:
- file: statefulset-pass.yaml
shouldFail: false
- file: statefulset-fail.yaml
shouldFail: true

View file

@ -0,0 +1,4 @@
apiVersion: kuttl.dev/v1beta1
kind: TestStep
commands:
- command: sleep 3

View file

@ -0,0 +1,13 @@
## Description
This test validates the use of parameter resources in validate.cel subrule.
This test creates the following:
1. A namespace `test-params`
2. A namespaced custom resource definition `ReplicaLimit`
3. A policy that checks the statefulset replicas using the parameter resource. The `validate.cel.paramRef.namespace` is unset so it is expected to retrieve the parameter resource from the statefulset's namespace
4. Two statefulsets.
## Expected Behavior
The statefulset `statefulset-fail` is blocked, and the statefulset `statefulset-pass` is created.

View file

@ -0,0 +1,4 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: replicalimits.rules.example.com

View file

@ -0,0 +1,26 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: replicalimits.rules.example.com
spec:
group: rules.example.com
names:
kind: ReplicaLimit
plural: replicalimits
scope: Namespaced
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
apiVersion:
type: string
kind:
type: string
metadata:
type: object
maxReplicas:
type: integer

View file

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

View file

@ -0,0 +1,25 @@
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: check-statefulset-replicas
spec:
validationFailureAction: Enforce
background: false
rules:
- name: statefulset-replicas
match:
any:
- resources:
kinds:
- StatefulSet
validate:
cel:
paramKind:
apiVersion: rules.example.com/v1
kind: ReplicaLimit
paramRef:
name: "replica-limit-test.example.com"
parameterNotFoundAction: "Deny"
expressions:
- expression: "object.spec.replicas <= params.maxReplicas"
messageExpression: "'StatefulSet spec.replicas must be less than ' + string(params.maxReplicas)"

View file

@ -0,0 +1,18 @@
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: statefulset-fail
namespace: test-params
spec:
replicas: 4
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: statefulset-pass
namespace: test-params
spec:
replicas: 2
selector:
matchLabels:
app: app
template:
metadata:
labels:
app: app
spec:
containers:
- name: container2
image: nginx