From 51254b2d5a27cf8ba858cbf08865374c9227dfab Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Charles-Edouard=20Br=C3=A9t=C3=A9ch=C3=A9?=
 <charled.breteche@gmail.com>
Date: Tue, 22 Mar 2022 14:17:51 +0100
Subject: [PATCH] refactor: ResourceDescription validation (#3446)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Charles-Edouard Brétéché <charled.breteche@gmail.com>
---
 api/kyverno/v1/resource_description_test.go  | 31 ++++++++
 api/kyverno/v1/resource_description_types.go | 13 +++
 api/kyverno/v1/utils.go                      | 19 +++++
 pkg/policy/validate.go                       | 84 --------------------
 pkg/policy/validate_test.go                  | 38 ---------
 5 files changed, 63 insertions(+), 122 deletions(-)

diff --git a/api/kyverno/v1/resource_description_test.go b/api/kyverno/v1/resource_description_test.go
index 4875681028..41e7ce9e72 100644
--- a/api/kyverno/v1/resource_description_test.go
+++ b/api/kyverno/v1/resource_description_test.go
@@ -4,6 +4,7 @@ import (
 	"testing"
 
 	"gotest.tools/assert"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/util/validation/field"
 )
 
@@ -17,6 +18,36 @@ func Test_ResourceDescription(t *testing.T) {
 		name:       "valid",
 		namespaced: true,
 		subject:    ResourceDescription{},
+	}, {
+		name:       "name-names",
+		namespaced: true,
+		subject: ResourceDescription{
+			Name:  "foo",
+			Names: []string{"bar", "baz"},
+		},
+		errors: []string{
+			`dummy: Invalid value: v1.ResourceDescription{Kinds:[]string(nil), Name:"foo", Names:[]string{"bar", "baz"}, Namespaces:[]string(nil), Annotations:map[string]string(nil), Selector:(*v1.LabelSelector)(nil), NamespaceSelector:(*v1.LabelSelector)(nil)}: Both name and names can not be specified together`,
+		},
+	}, {
+		name:       "selector",
+		namespaced: true,
+		subject: ResourceDescription{
+			Selector: &metav1.LabelSelector{
+				MatchLabels: map[string]string{
+					"app.type": "prod",
+				},
+			},
+		},
+	}, {
+		name:       "bad-selector",
+		namespaced: true,
+		subject: ResourceDescription{
+			Kinds:    []string{"Deployment"},
+			Selector: &metav1.LabelSelector{},
+		},
+		errors: []string{
+			`dummy.selector: Invalid value: v1.LabelSelector{MatchLabels:map[string]string(nil), MatchExpressions:[]v1.LabelSelectorRequirement(nil)}: The requirements are not specified in selector`,
+		},
 	}, {
 		name:       "namespaces",
 		namespaced: true,
diff --git a/api/kyverno/v1/resource_description_types.go b/api/kyverno/v1/resource_description_types.go
index 0752d38004..3f8d66bac7 100644
--- a/api/kyverno/v1/resource_description_types.go
+++ b/api/kyverno/v1/resource_description_types.go
@@ -55,6 +55,19 @@ type ResourceDescription struct {
 // Validate implements programmatic validation
 func (r *ResourceDescription) Validate(path *field.Path, namespaced bool, clusterResources sets.String) field.ErrorList {
 	var errs field.ErrorList
+	if r.Name != "" && len(r.Names) > 0 {
+		errs = append(errs, field.Invalid(path, r, "Both name and names can not be specified together"))
+	}
+	if r.Selector != nil && !labelSelectorContainsWildcard(r.Selector) {
+		if selector, err := metav1.LabelSelectorAsSelector(r.Selector); err != nil {
+			errs = append(errs, field.Invalid(path.Child("selector"), r.Selector, err.Error()))
+		} else {
+			requirements, _ := selector.Requirements()
+			if len(requirements) == 0 {
+				errs = append(errs, field.Invalid(path.Child("selector"), r.Selector, "The requirements are not specified in selector"))
+			}
+		}
+	}
 	if namespaced {
 		if len(r.Namespaces) > 0 {
 			errs = append(errs, field.Forbidden(path.Child("namespaces"), "Filtering namespaces not allowed in namespaced policies"))
diff --git a/api/kyverno/v1/utils.go b/api/kyverno/v1/utils.go
index 76dcb60677..f86488eb3d 100755
--- a/api/kyverno/v1/utils.go
+++ b/api/kyverno/v1/utils.go
@@ -1,8 +1,11 @@
 package v1
 
 import (
+	"strings"
+
 	"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
 	apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/util/validation/field"
 	log "sigs.k8s.io/controller-runtime/pkg/log"
 )
@@ -36,6 +39,22 @@ func ValidatePolicyName(path *field.Path, name string) field.ErrorList {
 	return errs
 }
 
+func labelSelectorContainsWildcard(v *metav1.LabelSelector) bool {
+	for k, v := range v.MatchLabels {
+		if isWildcardPresent(k) || isWildcardPresent(v) {
+			return true
+		}
+	}
+	return false
+}
+
+func isWildcardPresent(v string) bool {
+	if strings.Contains(v, "*") || strings.Contains(v, "?") {
+		return true
+	}
+	return false
+}
+
 // ViolatedRule stores the information regarding the rule.
 type ViolatedRule struct {
 	// Name specifies violated rule name.
diff --git a/pkg/policy/validate.go b/pkg/policy/validate.go
index 063f1c1cb6..e358f85260 100644
--- a/pkg/policy/validate.go
+++ b/pkg/policy/validate.go
@@ -916,27 +916,6 @@ func validateResources(path *field.Path, rule kyverno.Rule) (string, error) {
 		}
 	}
 
-	if len(rule.ExcludeResources.Any) > 0 {
-		for _, rmr := range rule.ExcludeResources.Any {
-			// exclude resources
-			if path, err := validateExcludeResourceDescription(rmr.ResourceDescription); err != nil {
-				return fmt.Sprintf("exclude.resources.%s", path), err
-			}
-		}
-	} else if len(rule.ExcludeResources.All) > 0 {
-		for _, rmr := range rule.ExcludeResources.All {
-			// exclude resources
-			if path, err := validateExcludeResourceDescription(rmr.ResourceDescription); err != nil {
-				return fmt.Sprintf("exclude.resources.%s", path), err
-			}
-		}
-	} else {
-		// exclude resources
-		if path, err := validateExcludeResourceDescription(rule.ExcludeResources.ResourceDescription); err != nil {
-			return fmt.Sprintf("exclude.resources.%s", path), err
-		}
-	}
-
 	//validating the values present under validate.preconditions, if they exist
 	if target := rule.GetAnyAllConditions(); target != nil {
 		if path, err := validateConditions(target, "preconditions"); err != nil {
@@ -1216,72 +1195,9 @@ func validateMatchedResourceDescription(rd kyverno.ResourceDescription) (string,
 		return "", fmt.Errorf("match resources not specified")
 	}
 
-	if rd.Name != "" && len(rd.Names) > 0 {
-		return "", fmt.Errorf("both name and names can not be specified together")
-	}
-
-	if err := validateResourceDescription(rd); err != nil {
-		return "match", err
-	}
-
 	return "", nil
 }
 
-func validateExcludeResourceDescription(rd kyverno.ResourceDescription) (string, error) {
-	if reflect.DeepEqual(rd, kyverno.ResourceDescription{}) {
-		// exclude is not mandatory
-		return "", nil
-	}
-
-	if rd.Name != "" && len(rd.Names) > 0 {
-		return "", fmt.Errorf("both name and names can not be specified together")
-	}
-
-	if err := validateResourceDescription(rd); err != nil {
-		return "exclude", err
-	}
-	return "", nil
-}
-
-// validateResourceDescription returns error if selector is invalid
-// field type is checked through openapi
-func validateResourceDescription(rd kyverno.ResourceDescription) error {
-	if rd.Selector != nil {
-		if labelSelectorContainsWildcard(rd.Selector) {
-			return nil
-		}
-
-		selector, err := metav1.LabelSelectorAsSelector(rd.Selector)
-		if err != nil {
-			return err
-		}
-		requirements, _ := selector.Requirements()
-		if len(requirements) == 0 {
-			return errors.New("the requirements are not specified in selector")
-		}
-	}
-	return nil
-}
-
-func labelSelectorContainsWildcard(v *metav1.LabelSelector) bool {
-	for k, v := range v.MatchLabels {
-		if isWildcardPresent(k) {
-			return true
-		}
-		if isWildcardPresent(v) {
-			return true
-		}
-	}
-	return false
-}
-
-func isWildcardPresent(v string) bool {
-	if strings.Contains(v, "*") || strings.Contains(v, "?") {
-		return true
-	}
-	return false
-}
-
 // checkClusterResourceInMatchAndExclude returns false if namespaced ClusterPolicy contains cluster wide resources in
 // Match and Exclude block
 func checkClusterResourceInMatchAndExclude(rule kyverno.Rule, clusterResources sets.String, mock bool, res []*metav1.APIResourceList) error {
diff --git a/pkg/policy/validate_test.go b/pkg/policy/validate_test.go
index 286eb21928..dae2f53524 100644
--- a/pkg/policy/validate_test.go
+++ b/pkg/policy/validate_test.go
@@ -261,44 +261,6 @@ func Test_Validate_PreconditionsValuesList_KeyRequestOperation_UnknownItem(t *te
 	assert.Assert(t, err != nil)
 }
 
-func Test_Validate_ResourceDescription_MissingKindsOnExclude(t *testing.T) {
-	var err error
-	excludeResourcedescirption := []byte(`
-	{
-		"selector": {
-		   "matchLabels": {
-			  "app.type": "prod"
-		   }
-		}
-	 }`)
-
-	var rd kyverno.ResourceDescription
-	err = json.Unmarshal(excludeResourcedescirption, &rd)
-	assert.NilError(t, err)
-
-	_, err = validateExcludeResourceDescription(rd)
-	assert.NilError(t, err)
-}
-
-func Test_Validate_ResourceDescription_InvalidSelector(t *testing.T) {
-	rawResourcedescirption := []byte(`
-	{
-		"kinds": [
-		   "Deployment"
-		],
-		"selector": {
-		   "app.type": "prod"
-		}
-	 }`)
-
-	var rd kyverno.ResourceDescription
-	err := json.Unmarshal(rawResourcedescirption, &rd)
-	assert.NilError(t, err)
-
-	err = validateResourceDescription(rd)
-	assert.Assert(t, err != nil)
-}
-
 func Test_Validate_Policy(t *testing.T) {
 	rawPolicy := []byte(`
 	{