From 13a9a4721a855dec2b2315b33678feb026d2c897 Mon Sep 17 00:00:00 2001 From: Jim Bugwadia Date: Fri, 4 Dec 2020 12:05:24 -0800 Subject: [PATCH] wildcard label and annotation keys validate patterns (#1360) --- pkg/engine/utils.go | 45 +-------- pkg/engine/validate/pattern.go | 2 +- pkg/engine/validate/validate.go | 28 ++++-- pkg/engine/validate/validate_test.go | 54 +++++++++++ pkg/engine/validation.go | 3 +- pkg/engine/wildcards/wildcards.go | 131 +++++++++++++++++++++++++++ 6 files changed, 208 insertions(+), 55 deletions(-) create mode 100644 pkg/engine/wildcards/wildcards.go diff --git a/pkg/engine/utils.go b/pkg/engine/utils.go index 2d0a4f95bf..aa115daae0 100644 --- a/pkg/engine/utils.go +++ b/pkg/engine/utils.go @@ -7,6 +7,7 @@ import ( "github.com/go-logr/logr" kyverno "github.com/kyverno/kyverno/pkg/api/kyverno/v1" "github.com/kyverno/kyverno/pkg/engine/context" + "github.com/kyverno/kyverno/pkg/engine/wildcards" "github.com/kyverno/kyverno/pkg/resourcecache" "github.com/kyverno/kyverno/pkg/utils" "github.com/minio/minio/pkg/wildcard" @@ -76,7 +77,7 @@ func checkAnnotations(annotations map[string]string, resourceAnnotations map[str } func checkSelector(labelSelector *metav1.LabelSelector, resourceLabels map[string]string) (bool, error) { - replaceWildcardsInSelector(labelSelector, resourceLabels) + wildcards.ReplaceInSelector(labelSelector, resourceLabels) selector, err := metav1.LabelSelectorAsSelector(labelSelector) if err != nil { log.Log.Error(err, "failed to build label selector") @@ -90,48 +91,6 @@ func checkSelector(labelSelector *metav1.LabelSelector, resourceLabels map[strin return false, nil } -// replaceWildcardsInSelector replaces label selector keys and values containing -// wildcard characters with matching keys and values from the resource labels. -func replaceWildcardsInSelector(labelSelector *metav1.LabelSelector, resourceLabels map[string]string) { - result := map[string]string{} - for k, v := range labelSelector.MatchLabels { - if containsWildcards(k) || containsWildcards(v) { - matchK, matchV := expandWildcards(k, v, resourceLabels) - result[matchK] = matchV - } else { - result[k] = v - } - } - - labelSelector.MatchLabels = result -} - -func containsWildcards(s string) bool { - return strings.Contains(s, "*") || strings.Contains(s, "?") -} - -func expandWildcards(k, v string, labels map[string]string) (key string, val string) { - for k1, v1 := range labels { - if wildcard.Match(k, k1) { - if wildcard.Match(v, v1) { - return k1, v1 - } - } - } - - k = replaceWildCardChars(k) - v = replaceWildCardChars(v) - return k, v -} - -// replaceWildCardChars will replace '*' and '?' characters which are not -// supported by Kubernetes with a '0'. -func replaceWildCardChars(s string) string { - s = strings.Replace(s, "*", "0", -1) - s = strings.Replace(s, "?", "0", -1) - return s -} - // doesResourceMatchConditionBlock filters the resource with defined conditions // for a match / exclude block, it has the following attributes: // ResourceDescription: diff --git a/pkg/engine/validate/pattern.go b/pkg/engine/validate/pattern.go index f1f988d55c..a08f7d4e26 100644 --- a/pkg/engine/validate/pattern.go +++ b/pkg/engine/validate/pattern.go @@ -46,7 +46,7 @@ func ValidateValueWithPattern(log logr.Logger, value, pattern interface{}) bool return validateValueWithMapPattern(log, value, typedPattern) case []interface{}: // TODO: check if this is ever called? - log.Info("arrays as patterns is not supported") + log.Info("arrays are not supported as patterns") return false default: log.Info("Unknown type", "type", fmt.Sprintf("%T", typedPattern), "value", typedPattern) diff --git a/pkg/engine/validate/validate.go b/pkg/engine/validate/validate.go index 6d12524dbb..3d0072ec61 100644 --- a/pkg/engine/validate/validate.go +++ b/pkg/engine/validate/validate.go @@ -3,6 +3,7 @@ package validate import ( "errors" "fmt" + "github.com/kyverno/kyverno/pkg/engine/wildcards" "path" "reflect" "strconv" @@ -20,10 +21,10 @@ import ( func ValidateResourceWithPattern(log logr.Logger, resource, pattern interface{}) (string, error) { // newAnchorMap - to check anchor key has values ac := common.NewAnchorMap() - path, err := validateResourceElement(log, resource, pattern, pattern, "/", ac) + elemPath, err := validateResourceElement(log, resource, pattern, pattern, "/", ac) if err != nil { if !ac.IsAnchorError() { - return path, err + return elemPath, err } } @@ -65,6 +66,7 @@ func validateResourceElement(log logr.Logger, resourceElement, patternElement, o } } } + if !ValidateValueWithPattern(log, resourceElement, patternElement) { return path, fmt.Errorf("Validation rule failed at '%s' to validate value '%v' with pattern '%v'", path, resourceElement, patternElement) } @@ -79,6 +81,10 @@ func validateResourceElement(log logr.Logger, resourceElement, patternElement, o // If validateResourceElement detects map element inside resource and pattern trees, it goes to validateMap // For each element of the map we must detect the type again, so we pass these elements to validateResourceElement func validateMap(log logr.Logger, resourceMap, patternMap map[string]interface{}, origPattern interface{}, path string, ac *common.AnchorKey) (string, error) { + + // + patternMap = wildcards.ExpandInMetadata(patternMap, resourceMap) + // check if there is anchor in pattern // Phase 1 : Evaluate all the anchors // Phase 2 : Evaluate non-anchors @@ -86,6 +92,7 @@ func validateMap(log logr.Logger, resourceMap, patternMap map[string]interface{} // Evaluate anchors for key, patternElement := range anchors { + // get handler for each pattern in the pattern // - Conditional // - Existence @@ -104,6 +111,7 @@ func validateMap(log logr.Logger, resourceMap, patternMap map[string]interface{} return handlerPath, err } } + // If anchor fails then succeed validate and skip further validation of recursion if ac.AnchorError != nil { return "", nil @@ -133,18 +141,18 @@ func validateArray(log logr.Logger, resourceArray, patternArray []interface{}, o case map[string]interface{}: // This is special case, because maps in arrays can have anchors that must be // processed with the special way affecting the entire array - path, err := validateArrayOfMaps(log, resourceArray, typedPatternElement, originPattern, path, ac) + elemPath, err := validateArrayOfMaps(log, resourceArray, typedPatternElement, originPattern, path, ac) if err != nil { - return path, err + return elemPath, err } default: // In all other cases - detect type and handle each array element with validateResourceElement if len(resourceArray) >= len(patternArray) { for i, patternElement := range patternArray { currentPath := path + strconv.Itoa(i) + "/" - path, err := validateResourceElement(log, resourceArray[i], patternElement, originPattern, currentPath, ac) + elemPath, err := validateResourceElement(log, resourceArray[i], patternElement, originPattern, currentPath, ac) if err != nil { - return path, err + return elemPath, err } } } else { @@ -167,7 +175,7 @@ func actualizePattern(log logr.Logger, origPattern interface{}, referencePattern } // Check for variables // substitute it from Context - // remove abosolute path + // remove absolute path // {{ }} // value := actualPath := formAbsolutePath(referencePattern, absolutePath) @@ -260,12 +268,12 @@ func getValueFromPattern(log logr.Logger, patternMap map[string]interface{}, key } } - path := "" + elemPath := "" for _, elem := range keys { - path = "/" + elem + path + elemPath = "/" + elem + elemPath } - return nil, fmt.Errorf("No value found for specified reference: %s", path) + return nil, fmt.Errorf("No value found for specified reference: %s", elemPath) } // validateArrayOfMaps gets anchors from pattern array map element, applies anchors logic diff --git a/pkg/engine/validate/validate_test.go b/pkg/engine/validate/validate_test.go index b70e92f15d..048e0449ba 100644 --- a/pkg/engine/validate/validate_test.go +++ b/pkg/engine/validate/validate_test.go @@ -1356,3 +1356,57 @@ func TestValidateMapElement_OneElementInArrayNotPass(t *testing.T) { assert.Equal(t, path, "/0/object/0/key2/") assert.Assert(t, err != nil) } + +func TestValidateMapWildcardKeys(t *testing.T) { + pattern := []byte(`{"metadata" : {"annotations": {"test/*": "value1"}}}`) + resource := []byte(`{"metadata" : {"annotations": {"test/bar": "value1"}}}`) + testValidationPattern(t, pattern, resource, "", true) + + pattern = []byte(`{"metadata" : {"annotations": {"test/b??": "v*"}}}`) + resource = []byte(`{"metadata" : {"annotations": {"test/bar": "value1"}}}`) + testValidationPattern(t, pattern, resource, "", true) + + pattern = []byte(`{}`) + resource = []byte(`{"metadata" : {"annotations": {"test/bar": "value1"}}}`) + testValidationPattern(t, pattern, resource, "", true) + + pattern = []byte(`{"metadata" : {"annotations": {"test/b??": "v*"}}}`) + resource = []byte(`{"metadata" : {"labels": {"test/bar": "value1"}}}`) + testValidationPattern(t, pattern, resource, "/metadata/annotations/", false) + + pattern = []byte(`{"metadata" : {"labels": {"*/test": "foo"}}}`) + resource = []byte(`{"metadata" : {"labels": {"foo/test": "foo"}}}`) + testValidationPattern(t, pattern, resource, "", true) + + pattern = []byte(`{"metadata" : {"labels": {"foo/123*": "bar"}}}`) + resource = []byte(`{"metadata" : {"labels": {"foo/12?": "bar", "foo/123": "bar"}}}`) + testValidationPattern(t, pattern, resource, "", true) + + pattern = []byte(`{"metadata" : {"labels": {"foo/123*": "bar"}}}`) + resource = []byte(`{"metadata" : {"labels": {"foo/12?": "bar", "foo/123": "bar2"}}}`) + testValidationPattern(t, pattern, resource, "/metadata/labels/foo/123*/", false) + + pattern = []byte(`{"metadata" : {"labels": {"foo/1*": "bar", "foo/4*": "bar2"}}}`) + resource = []byte(`{"metadata" : {"labels": {"foo/123": "bar", "foo/456": "bar2"}}}`) + testValidationPattern(t, pattern, resource, "", true) + + pattern = []byte(`{"metadata" : {"labels": {"foo/1*": "bar", "foo/4*": "bar2"}}}`) + resource = []byte(`{"metadata" : {"labels": {"foo/123": "bar"}}}`) + testValidationPattern(t, pattern, resource, "/metadata/labels/foo/4*/", false) +} + +func testValidationPattern(t *testing.T, patternBytes []byte, resourceBytes []byte, path string, nilErr bool) { + var pattern, resource interface{} + err := json.Unmarshal(patternBytes, &pattern) + assert.NilError(t, err) + err = json.Unmarshal(resourceBytes, &resource) + assert.NilError(t, err) + + p, err := validateResourceElement(log.Log, resource, pattern, pattern, "/", common.NewAnchorMap()) + assert.Equal(t, p, path) + if nilErr { + assert.NilError(t, err) + } else { + assert.Assert(t, err != nil) + } +} diff --git a/pkg/engine/validation.go b/pkg/engine/validation.go index fa6a2f55af..4d2894a980 100644 --- a/pkg/engine/validation.go +++ b/pkg/engine/validation.go @@ -178,6 +178,7 @@ func validateResource(log logr.Logger, ctx context.EvalInterface, policy kyverno log.V(4).Info("resource fails the match description", "reason", err.Error()) continue } + // add configmap json data to context if err := AddResourceToContext(log, rule.Context, resCache, jsonContext); err != nil { log.V(4).Info("cannot add configmaps to context", "reason", err.Error()) @@ -278,7 +279,7 @@ func validatePatterns(log logr.Logger, ctx context.EvalInterface, resource unstr rule.Validation.Message, rule.Name, path) return resp } - // rule application successful + logger.V(4).Info("successfully processed rule") resp.Success = true resp.Message = fmt.Sprintf("Validation rule '%s' succeeded.", rule.Name) diff --git a/pkg/engine/wildcards/wildcards.go b/pkg/engine/wildcards/wildcards.go new file mode 100644 index 0000000000..872f5a2010 --- /dev/null +++ b/pkg/engine/wildcards/wildcards.go @@ -0,0 +1,131 @@ +package wildcards + +import ( + commonAnchor "github.com/kyverno/kyverno/pkg/engine/anchor/common" + "github.com/minio/minio/pkg/wildcard" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "strings" +) + +// ReplaceInSelector replaces label selector keys and values containing +// wildcard characters with matching keys and values from the resource labels. +func ReplaceInSelector(labelSelector *metav1.LabelSelector, resourceLabels map[string]string) { + result := replaceWildcardsInMap(labelSelector.MatchLabels, resourceLabels) + labelSelector.MatchLabels = result +} + +func replaceWildcardsInMap(patternMap map[string]string, resourceMap map[string]string) map[string]string { + result := map[string]string{} + for k, v := range patternMap { + if hasWildcards(k) || hasWildcards(v) { + matchK, matchV := expandWildcards(k, v, resourceMap, true) + result[matchK] = matchV + } else { + result[k] = v + } + } + + return result +} + +func hasWildcards(s string) bool { + return strings.Contains(s, "*") || strings.Contains(s, "?") +} + +func expandWildcards(k, v string, resourceMap map[string]string, replace bool) (key string, val string) { + for k1, v1 := range resourceMap { + if wildcard.Match(k, k1) { + if wildcard.Match(v, v1) { + return k1, v1 + } + } + } + + if replace { + k = replaceWildCardChars(k) + v = replaceWildCardChars(v) + } + + return k, v +} + +// replaceWildCardChars will replace '*' and '?' characters which are not +// supported by Kubernetes with a '0'. +func replaceWildCardChars(s string) string { + s = strings.Replace(s, "*", "0", -1) + s = strings.Replace(s, "?", "0", -1) + return s +} + +// ExpandInMetadata substitutes wildcard characters in map keys for metadata.labels and +// metadata.annotations that are present in a validation pattern. Values are not substituted +// here, as they are evaluated separately while processing the validation pattern. +func ExpandInMetadata(patternMap, resourceMap map[string]interface{}) map[string]interface{} { + + patternMetadata := patternMap["metadata"] + if patternMetadata == nil { + return patternMap + } + + resourceMetadata := resourceMap["metadata"] + if resourceMetadata == nil { + return patternMap + } + + metadata := patternMetadata.(map[string]interface{}) + labels := expandWildcardsInTag("labels", patternMetadata, resourceMetadata) + if labels != nil { + metadata["labels"] = labels + } + + annotations := expandWildcardsInTag("annotations", patternMetadata, resourceMetadata) + if annotations != nil { + metadata["annotations"] = annotations + } + + return patternMap +} + +func expandWildcardsInTag(tag string, patternMetadata, resourceMetadata interface{}) map[string]interface{} { + patternData := getValueAsStringMap(tag, patternMetadata) + if patternData == nil { + return nil + } + + resourceData := getValueAsStringMap(tag, resourceMetadata) + if resourceData == nil { + return nil + } + + results := map[string]interface{}{} + for k, v := range patternData { + if hasWildcards(k) { + newKey := commonAnchor.RemoveAnchor(k) + matchK, _ := expandWildcards(newKey, v, resourceData, false) + matchK = strings.Replace(k, newKey, matchK, 1) + results[matchK] = v + } else { + results[k] = v + } + } + + return results +} + +func getValueAsStringMap(key string, dataMap interface{}) map[string]string { + if dataMap == nil { + return nil + } + + val := dataMap.(map[string]interface{})[key] + if val == nil { + return nil + } + + result := map[string]string{} + for k, v := range val.(map[string]interface{}) { + result[k] = v.(string) + } + + return result +}