diff --git a/pkg/engine/anchor.go b/pkg/engine/anchor.go index e3d604007a..f459e9805b 100644 --- a/pkg/engine/anchor.go +++ b/pkg/engine/anchor.go @@ -5,6 +5,7 @@ import ( "strconv" "github.com/golang/glog" + "github.com/nirmata/kyverno/pkg/engine/anchor" ) //ValidationHandler for element processes @@ -15,19 +16,20 @@ type ValidationHandler interface { //CreateElementHandler factory to process elements func CreateElementHandler(element string, pattern interface{}, path string) ValidationHandler { switch { - case isConditionAnchor(element): + case anchor.IsConditionAnchor(element): return NewConditionAnchorHandler(element, pattern, path) - case isExistanceAnchor(element): + case anchor.IsExistanceAnchor(element): return NewExistanceHandler(element, pattern, path) - case isEqualityAnchor(element): + case anchor.IsEqualityAnchor(element): return NewEqualityHandler(element, pattern, path) - case isNegationAnchor(element): + case anchor.IsNegationAnchor(element): return NewNegationHandler(element, pattern, path) default: return NewDefaultHandler(element, pattern, path) } } +//NewNegationHandler returns instance of negation handler func NewNegationHandler(anchor string, pattern interface{}, path string) ValidationHandler { return NegationHandler{ anchor: anchor, @@ -56,6 +58,7 @@ func (nh NegationHandler) Handle(resourceMap map[string]interface{}, originPatte return "", nil } +//NewEqualityHandler returens instance of equality handler func NewEqualityHandler(anchor string, pattern interface{}, path string) ValidationHandler { return EqualityHandler{ anchor: anchor, @@ -217,7 +220,7 @@ func getAnchorsResourcesFromMap(patternMap map[string]interface{}) (map[string]i anchors := map[string]interface{}{} resources := map[string]interface{}{} for key, value := range patternMap { - if isConditionAnchor(key) || isExistanceAnchor(key) || isEqualityAnchor(key) || isNegationAnchor(key) { + if anchor.IsConditionAnchor(key) || anchor.IsExistanceAnchor(key) || anchor.IsEqualityAnchor(key) || anchor.IsNegationAnchor(key) { anchors[key] = value continue } diff --git a/pkg/engine/anchor/utils.go b/pkg/engine/anchor/utils.go new file mode 100644 index 0000000000..b1075b7541 --- /dev/null +++ b/pkg/engine/anchor/utils.go @@ -0,0 +1,54 @@ +package anchor + +// Anchor function type +type IsAnchor func(str string) bool + +func IsConditionAnchor(str string) bool { + if len(str) < 2 { + return false + } + + return (str[0] == '(' && str[len(str)-1] == ')') +} + +func IsNegationAnchor(str string) bool { + left := "X(" + right := ")" + if len(str) < len(left)+len(right) { + return false + } + //TODO: trim spaces ? + return (str[:len(left)] == left && str[len(str)-len(right):] == right) +} + +func IsAddingAnchor(key string) bool { + const left = "+(" + const right = ")" + + if len(key) < len(left)+len(right) { + return false + } + + return left == key[:len(left)] && right == key[len(key)-len(right):] +} + +func IsEqualityAnchor(str string) bool { + left := "=(" + right := ")" + if len(str) < len(left)+len(right) { + return false + } + //TODO: trim spaces ? + return (str[:len(left)] == left && str[len(str)-len(right):] == right) +} + +func IsExistanceAnchor(str string) bool { + left := "^(" + right := ")" + + if len(str) < len(left)+len(right) { + return false + } + + return (str[:len(left)] == left && str[len(str)-len(right):] == right) +} diff --git a/pkg/engine/anchor/utils_test.go b/pkg/engine/anchor/utils_test.go new file mode 100644 index 0000000000..8397294258 --- /dev/null +++ b/pkg/engine/anchor/utils_test.go @@ -0,0 +1,58 @@ +package anchor + +import ( + "testing" + + "gotest.tools/assert" +) + +func TestWrappedWithParentheses_StringIsWrappedWithParentheses(t *testing.T) { + str := "(something)" + assert.Assert(t, IsConditionAnchor(str)) +} + +func TestWrappedWithParentheses_StringHasOnlyParentheses(t *testing.T) { + str := "()" + assert.Assert(t, IsConditionAnchor(str)) +} + +func TestWrappedWithParentheses_StringHasNoParentheses(t *testing.T) { + str := "something" + assert.Assert(t, !IsConditionAnchor(str)) +} + +func TestWrappedWithParentheses_StringHasLeftParentheses(t *testing.T) { + str := "(something" + assert.Assert(t, !IsConditionAnchor(str)) +} + +func TestWrappedWithParentheses_StringHasRightParentheses(t *testing.T) { + str := "something)" + assert.Assert(t, !IsConditionAnchor(str)) +} + +func TestWrappedWithParentheses_StringParenthesesInside(t *testing.T) { + str := "so)m(et(hin)g" + assert.Assert(t, !IsConditionAnchor(str)) +} + +func TestWrappedWithParentheses_Empty(t *testing.T) { + str := "" + assert.Assert(t, !IsConditionAnchor(str)) +} + +func TestIsExistanceAnchor_Yes(t *testing.T) { + assert.Assert(t, IsExistanceAnchor("^(abc)")) +} + +func TestIsExistanceAnchor_NoRightBracket(t *testing.T) { + assert.Assert(t, !IsExistanceAnchor("^(abc")) +} + +func TestIsExistanceAnchor_OnlyHat(t *testing.T) { + assert.Assert(t, !IsExistanceAnchor("^abc")) +} + +func TestIsExistanceAnchor_ConditionAnchor(t *testing.T) { + assert.Assert(t, !IsExistanceAnchor("(abc)")) +} diff --git a/pkg/engine/generation.go b/pkg/engine/generation.go index 5e24426d47..91bfc451db 100644 --- a/pkg/engine/generation.go +++ b/pkg/engine/generation.go @@ -38,7 +38,7 @@ func Generate(client *client.Client, policy kyverno.ClusterPolicy, ns unstructur response.PolicyResponse.RulesAppliedCount++ } for _, rule := range policy.Spec.Rules { - if rule.Generation == (kyverno.Generation{}) { + if !rule.HasGenerate() { continue } glog.V(4).Infof("applying policy %s generate rule %s on resource %s/%s/%s", policy.Name, rule.Name, ns.GetKind(), ns.GetNamespace(), ns.GetName()) diff --git a/pkg/engine/mutation.go b/pkg/engine/mutation.go index 30bdbdf9cc..fbf4504cc8 100644 --- a/pkg/engine/mutation.go +++ b/pkg/engine/mutation.go @@ -37,7 +37,7 @@ func Mutate(policy kyverno.ClusterPolicy, resource unstructured.Unstructured) (r for _, rule := range policy.Spec.Rules { //TODO: to be checked before calling the resources as well - if reflect.DeepEqual(rule.Mutation, kyverno.Mutation{}) { + if !rule.HasMutate() { continue } // check if the resource satisfies the filter conditions defined in the rule diff --git a/pkg/engine/overlay.go b/pkg/engine/overlay.go index 8a99f07252..82a3950241 100644 --- a/pkg/engine/overlay.go +++ b/pkg/engine/overlay.go @@ -14,6 +14,7 @@ import ( jsonpatch "github.com/evanphx/json-patch" kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1alpha1" + "github.com/nirmata/kyverno/pkg/engine/anchor" ) // processOverlay processes validation patterns on the resource @@ -148,7 +149,7 @@ func applyOverlayToMap(resourceMap, overlayMap map[string]interface{}, path stri for key, value := range overlayMap { // skip anchor element because it has condition, not // the value that must replace resource value - if isConditionAnchor(key) { + if anchor.IsConditionAnchor(key) { continue } @@ -156,7 +157,7 @@ func applyOverlayToMap(resourceMap, overlayMap map[string]interface{}, path stri currentPath := path + noAnchorKey + "/" resourcePart, ok := resourceMap[noAnchorKey] - if ok && !isAddingAnchor(key) { + if ok && !anchor.IsAddingAnchor(key) { // Key exists - go down through the overlay and resource trees patches, err := applyOverlay(resourcePart, value, currentPath) if err != nil { diff --git a/pkg/engine/overlayCondition.go b/pkg/engine/overlayCondition.go index c08128d699..e2aee0226e 100755 --- a/pkg/engine/overlayCondition.go +++ b/pkg/engine/overlayCondition.go @@ -4,6 +4,7 @@ import ( "reflect" "github.com/golang/glog" + "github.com/nirmata/kyverno/pkg/engine/anchor" ) func meetConditions(resource, overlay interface{}) bool { @@ -51,7 +52,7 @@ func checkConditionOnMap(resourceMap, overlayMap map[string]interface{}) bool { for key, value := range overlayMap { resourcePart, ok := resourceMap[key] - if ok && !isAddingAnchor(key) { + if ok && !anchor.IsAddingAnchor(key) { if !meetConditions(resourcePart, value) { return false } diff --git a/pkg/engine/policy/utils.go b/pkg/engine/policy/utils.go index 175ef61488..52d83d0df4 100644 --- a/pkg/engine/policy/utils.go +++ b/pkg/engine/policy/utils.go @@ -1,26 +1,5 @@ package policy -import ( - "errors" - "fmt" - "strings" -) - -// joinErrs joins the list of error into single error -// adds a new line between errors -func joinErrs(errs []error) error { - if len(errs) == 0 { - return nil - } - - res := "\n" - for _, err := range errs { - res = fmt.Sprintf(res + err.Error() + "\n") - } - - return errors.New(res) -} - //Contains Check if strint is contained in a list of string func containString(list []string, element string) bool { for _, e := range list { @@ -30,73 +9,3 @@ func containString(list []string, element string) bool { } return false } - -// hasExistingAnchor checks if str has existing anchor -// strip anchor if necessary -func hasExistingAnchor(str string) (bool, string) { - left := "^(" - right := ")" - - if len(str) < len(left)+len(right) { - return false, str - } - - return (str[:len(left)] == left && str[len(str)-len(right):] == right), str[len(left) : len(str)-len(right)] -} - -// hasValidAnchors checks str has the valid anchor -// mutate: (), +() -// validate: (), ^(), =(), X() -// generate: none -// invalid anchors: ~(),!() -func hasValidAnchors(anchors []anchor, str string) (bool, string) { - if wrappedWithAttributes(str) { - return mustWrapWithAnchors(anchors, str) - } - - return true, str -} - -// mustWrapWithAnchors validates str must wrap with -// at least one given anchor -func mustWrapWithAnchors(anchors []anchor, str string) (bool, string) { - for _, a := range anchors { - if str[:len(a.left)] == a.left && str[len(str)-len(a.right):] == a.right { - return true, str[len(a.left) : len(str)-len(a.right)] - } - } - - return false, str -} - -func wrappedWithAttributes(str string) bool { - if len(str) < 2 { - return false - } - - if (str[0] == '(' && str[len(str)-1] == ')') || - (str[0] == '^' || str[0] == '+' || str[0] == '=' || str[0] == 'X' || str[0] == '!' || str[0] == '~') && - (str[1] == '(' && str[len(str)-1] == ')') { - return true - } - - return false -} - -func joinAnchors(anchorPatterns []anchor) string { - var res []string - for _, a := range anchorPatterns { - res = append(res, a.left+a.right) - } - - return strings.Join(res, " || ") -} - -func hasNegationAnchor(str string) (bool, string) { - left := "X(" - right := ")" - if len(str) < len(left)+len(right) { - return false, str - } - return (str[:len(left)] == left && str[len(str)-len(right):] == right), str[len(left) : len(str)-len(right)] -} diff --git a/pkg/engine/policy/validate.go b/pkg/engine/policy/validate.go index 7de3225525..c38f8acb56 100644 --- a/pkg/engine/policy/validate.go +++ b/pkg/engine/policy/validate.go @@ -4,89 +4,84 @@ import ( "errors" "fmt" "reflect" + "regexp" "strconv" - "github.com/golang/glog" kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1alpha1" + "github.com/nirmata/kyverno/pkg/engine/anchor" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -type anchor struct { - left string - right string -} - -var ( - conditionalAnchor = anchor{left: "(", right: ")"} - existingAnchor = anchor{left: "^(", right: ")"} - equalityAnchor = anchor{left: "=(", right: ")"} - plusAnchor = anchor{left: "+(", right: ")"} - negationAnchor = anchor{left: "X(", right: ")"} -) - +// Validate does some initial check to verify some conditions +// - One operation per rule +// - ResourceDescription mandatory checks func Validate(p kyverno.ClusterPolicy) error { - var errs []error - - if err := validateUniqueRuleName(p); err != nil { - errs = append(errs, fmt.Errorf("- Invalid Policy '%s':", p.Name)) - errs = append(errs, err) + if path, err := validateUniqueRuleName(p); err != nil { + return fmt.Errorf("path: spec.%s: %v", path, err) } - for _, rule := range p.Spec.Rules { - if ruleErrs := validateRule(rule); len(ruleErrs) != 0 { - errs = append(errs, fmt.Errorf("- invalid rule '%s':", rule.Name)) - errs = append(errs, ruleErrs...) + for i, rule := range p.Spec.Rules { + // only one type of rule is allowed per rule + if err := validateRuleType(rule); err != nil { + return fmt.Errorf("path: spec.rules[%d]: %v", i, err) } - } - return joinErrs(errs) -} - -// ValidateUniqueRuleName checks if the rule names are unique across a policy -func validateUniqueRuleName(p kyverno.ClusterPolicy) error { - var ruleNames []string - - for _, rule := range p.Spec.Rules { - if containString(ruleNames, rule.Name) { - return fmt.Errorf(`duplicate rule name: '%s'`, rule.Name) + // validate resource description + if path, err := validateResources(rule); err != nil { + return fmt.Errorf("path: spec.rules[%d].%s: %v", i, path, err) + } + // validate rule types + // only one type of rule is allowed per rule + if err := validateRuleType(rule); err != nil { + // as there are more than 1 operation in rule, not need to evaluate it further + return fmt.Errorf("path: spec.rules[%d]: %v", i, err) + } + // Operation Validation + // Mutation + if rule.HasMutate() { + if path, err := validateMutation(rule.Mutation); err != nil { + return fmt.Errorf("path: spec.rules[%d].mutate.%s.: %v", i, path, err) + } + } + // Validation + if rule.HasValidate() { + if path, err := validateValidation(rule.Validation); err != nil { + return fmt.Errorf("path: spec.rules[%d].validate.%s.: %v", i, path, err) + } + } + // Generation + if rule.HasGenerate() { + if path, err := validateGeneration(rule.Generation); err != nil { + return fmt.Errorf("path: spec.rules[%d].generate.%s.: %v", i, path, err) + } } - ruleNames = append(ruleNames, rule.Name) } return nil } -// Validate checks if rule is not empty and all substructures are valid -func validateRule(r kyverno.Rule) []error { - var errs []error - - // only one type of rule is allowed per rule - if err := validateRuleType(r); err != nil { - errs = append(errs, err) +func validateResources(rule kyverno.Rule) (string, error) { + // matched resources + if path, err := validateMatchedResourceDescription(rule.MatchResources.ResourceDescription); err != nil { + return fmt.Sprintf("resources.%s", path), err } - - // validate resource description block - if err := validateMatchedResourceDescription(r.MatchResources.ResourceDescription); err != nil { - errs = append(errs, fmt.Errorf("error in match block, %v", err)) + // exclude resources + if path, err := validateExcludeResourceDescription(rule.ExcludeResources.ResourceDescription); err != nil { + return fmt.Sprintf("resources.%s", path), err } + return "", nil +} - if err := validateResourceDescription(r.ExcludeResources.ResourceDescription); err != nil { - errs = append(errs, fmt.Errorf("error in exclude block, %v", err)) +// ValidateUniqueRuleName checks if the rule names are unique across a policy +func validateUniqueRuleName(p kyverno.ClusterPolicy) (string, error) { + var ruleNames []string + + for i, rule := range p.Spec.Rules { + if containString(ruleNames, rule.Name) { + return fmt.Sprintf("rule[%d]", i), fmt.Errorf(`duplicate rule name: '%s'`, rule.Name) + } + ruleNames = append(ruleNames, rule.Name) } - - // validate anchors on mutate - if mErrs := validateMutation(r.Mutation); len(mErrs) != 0 { - errs = append(errs, mErrs...) - } - - if vErrs := validateValidation(r.Validation); len(vErrs) != 0 { - errs = append(errs, vErrs...) - } - - if err := validateGeneration(r.Generation); err != nil { - errs = append(errs, err) - } - - return errs + return "", nil } // validateRuleType checks only one type of rule is defined per rule @@ -104,7 +99,7 @@ func validateRuleType(r kyverno.Rule) error { }() if operationCount == 0 { - return fmt.Errorf("no operation defined in the rule '%s'.(supported operations: mutation,validation,generation,query)", r.Name) + return fmt.Errorf("no operation defined in the rule '%s'.(supported operations: mutation,validation,generation)", r.Name) } else if operationCount != 1 { return fmt.Errorf("multiple operations defined in the rule '%s', only one type of operation is allowed per rule", r.Name) } @@ -116,16 +111,31 @@ func validateRuleType(r kyverno.Rule) error { // Returns error if // - kinds is empty array in matched resource block, i.e. kinds: [] // - selector is invalid -func validateMatchedResourceDescription(rd kyverno.ResourceDescription) error { +func validateMatchedResourceDescription(rd kyverno.ResourceDescription) (string, error) { if reflect.DeepEqual(rd, kyverno.ResourceDescription{}) { - return nil + return "", fmt.Errorf("match resources not specified") } if len(rd.Kinds) == 0 { - return errors.New("field Kind is not specified") + return "match", fmt.Errorf("kind is mandatory") } - return validateResourceDescription(rd) + 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 err := validateResourceDescription(rd); err != nil { + return "exclude", err + } + return "", nil } // validateResourceDescription returns error if selector is invalid @@ -144,22 +154,23 @@ func validateResourceDescription(rd kyverno.ResourceDescription) error { return nil } -func validateMutation(m kyverno.Mutation) []error { - var errs []error +func validateMutation(m kyverno.Mutation) (string, error) { + // JSON Patches if len(m.Patches) != 0 { - for _, patch := range m.Patches { - err := validatePatch(patch) - errs = append(errs, err) + for i, patch := range m.Patches { + if err := validatePatch(patch); err != nil { + return fmt.Sprintf("patch[%d]", i), err + } } } - + // Overlay if m.Overlay != nil { - _, err := validateAnchors([]anchor{conditionalAnchor, plusAnchor}, m.Overlay, "/") + path, err := validatePattern(m.Overlay, "/", []anchor.IsAnchor{anchor.IsConditionAnchor, anchor.IsAddingAnchor}) if err != nil { - errs = append(errs, err) + return path, err } } - return errs + return "", nil } // Validate if all mandatory PolicyPatch fields are set @@ -167,7 +178,6 @@ func validatePatch(pp kyverno.Patch) error { if pp.Path == "" { return errors.New("JSONPatch field 'path' is mandatory") } - if pp.Operation == "add" || pp.Operation == "replace" { if pp.Value == nil { return fmt.Errorf("JSONPatch field 'value' is mandatory for operation '%s'", pp.Operation) @@ -181,151 +191,154 @@ func validatePatch(pp kyverno.Patch) error { return fmt.Errorf("Unsupported JSONPatch operation '%s'", pp.Operation) } -func validateValidation(v kyverno.Validation) []error { - var errs []error - +func validateValidation(v kyverno.Validation) (string, error) { if err := validateOverlayPattern(v); err != nil { - errs = append(errs, err) + // no need to proceed ahead + return "", err } if v.Pattern != nil { - if _, err := validateAnchors([]anchor{conditionalAnchor, existingAnchor, equalityAnchor, negationAnchor}, v.Pattern, "/"); err != nil { - errs = append(errs, err) + if path, err := validatePattern(v.Pattern, "/", []anchor.IsAnchor{anchor.IsConditionAnchor, anchor.IsExistanceAnchor, anchor.IsEqualityAnchor, anchor.IsNegationAnchor}); err != nil { + return fmt.Sprintf("pattern.%s", path), err } } if len(v.AnyPattern) != 0 { - for _, p := range v.AnyPattern { - if _, err := validateAnchors([]anchor{conditionalAnchor, existingAnchor, equalityAnchor, negationAnchor}, p, "/"); err != nil { - errs = append(errs, err) + for i, pattern := range v.AnyPattern { + if path, err := validatePattern(pattern, "/", []anchor.IsAnchor{anchor.IsConditionAnchor, anchor.IsExistanceAnchor, anchor.IsEqualityAnchor, anchor.IsNegationAnchor}); err != nil { + return fmt.Sprintf("anyPattern[%d].%s", i, path), err } } } - - return errs + return "", nil } // validateOverlayPattern checks one of pattern/anyPattern must exist func validateOverlayPattern(v kyverno.Validation) error { - if reflect.DeepEqual(v, kyverno.Validation{}) { - return nil - } - if v.Pattern == nil && len(v.AnyPattern) == 0 { - return fmt.Errorf("neither pattern nor anyPattern found") + return fmt.Errorf("a pattern or anyPattern must be specified") } if v.Pattern != nil && len(v.AnyPattern) != 0 { - return fmt.Errorf("either pattern or anyPattern is allowed") + return fmt.Errorf("only one operation allowed per validation rule(pattern or anyPattern)") } return nil } // Validate returns error if generator is configured incompletely -func validateGeneration(gen kyverno.Generation) error { - if reflect.DeepEqual(gen, kyverno.Generation{}) { - return nil - } +func validateGeneration(gen kyverno.Generation) (string, error) { if gen.Data == nil && gen.Clone == (kyverno.CloneFrom{}) { - return fmt.Errorf("neither data nor clone (source) of %s is specified", gen.Kind) + return "", fmt.Errorf("clone or data are required") } if gen.Data != nil && gen.Clone != (kyverno.CloneFrom{}) { - return fmt.Errorf("both data nor clone (source) of %s are specified", gen.Kind) + return "", fmt.Errorf("only one operation allowed per generate rule(data or clone)") } - - if gen.Data != nil { - if _, err := validateAnchors(nil, gen.Data, "/"); err != nil { - return fmt.Errorf("anchors are not allowed on generate pattern data: %v", err) - } + // check kind is non empty + // check name is non empty + if gen.Name == "" { + return "name", fmt.Errorf("name cannot be empty") + } + if gen.Kind == "" { + return "kind", fmt.Errorf("kind cannot be empty") } - if !reflect.DeepEqual(gen.Clone, kyverno.CloneFrom{}) { - if _, err := validateAnchors(nil, gen.Clone, ""); err != nil { - return fmt.Errorf("invalid character found on pattern clone: %v", err) + if path, err := validateClone(gen.Clone); err != nil { + return fmt.Sprintf("clone.%s", path), err } } - - return nil + if gen.Data != nil { + //TODO: is this required ?? as anchors can only be on pattern and not resource + // we can add this check by not sure if its needed here + if path, err := validatePattern(gen.Data, "/", []anchor.IsAnchor{}); err != nil { + return fmt.Sprintf("data.%s", path), fmt.Errorf("anchors not supported on generate resoruces: %v", err) + } + } + return "", nil } -// validateAnchors validates: -// 1. existing acnchor must define on array -// 2. anchors in mutation must be one of: (), +() -// 3. anchors in validate must be one of: (), ^(), =(), X() -// 4. no anchor is allowed in generate -func validateAnchors(anchorPatterns []anchor, pattern interface{}, path string) (string, error) { - switch typedPattern := pattern.(type) { +func validateClone(c kyverno.CloneFrom) (string, error) { + if c.Name == "" { + return "name", fmt.Errorf("name cannot be empty") + } + if c.Namespace == "" { + return "namespace", fmt.Errorf("namespace cannot be empty") + } + return "", nil +} + +func validatePattern(patternElement interface{}, path string, supportedAnchors []anchor.IsAnchor) (string, error) { + switch typedPatternElement := patternElement.(type) { case map[string]interface{}: - return validateAnchorsOnMap(anchorPatterns, typedPattern, path) + return validateMap(typedPatternElement, path, supportedAnchors) case []interface{}: - return validateAnchorsOnArray(anchorPatterns, typedPattern, path) + return validateArray(typedPatternElement, path, supportedAnchors) case string, float64, int, int64, bool, nil: - // check on type string - if checkedPattern := reflect.ValueOf(pattern); checkedPattern.Kind() == reflect.String { - if hasAnchor, str := hasExistingAnchor(checkedPattern.String()); hasAnchor { - return path, fmt.Errorf("existing anchor at %s must be of type array, found: %v", path+str, checkedPattern.Kind()) - } - } - // return nil on all other cases - return "", nil - case interface{}: - // special case for generate clone, as it is a struct - if clone, ok := pattern.(kyverno.CloneFrom); ok { - return "", validateAnchorsOnCloneFrom(nil, clone) - } + //TODO? check operator return "", nil default: - glog.V(4).Infof("Pattern contains unknown type %T. Path: %s", pattern, path) - return path, fmt.Errorf("pattern contains unknown type, path: %s", path) + return path, fmt.Errorf("Validation rule failed at '%s', pattern contains unknown type", path) } + return "", nil } -func validateAnchorsOnCloneFrom(anchorPatterns []anchor, pattern kyverno.CloneFrom) error { - // namespace and name are required fields - // if wrapped with invalid character, this field is empty during unmarshaling - if pattern.Namespace == "" { - return errors.New("namespace is requried") - } - - if pattern.Name == "" { - return errors.New("name is requried") - } - - return nil -} - -func validateAnchorsOnMap(anchorPatterns []anchor, pattern map[string]interface{}, path string) (string, error) { - for key, patternElement := range pattern { - if valid, str := hasValidAnchors(anchorPatterns, key); !valid { - return path, fmt.Errorf("invalid anchor found at %s, expect: %s", path+str, joinAnchors(anchorPatterns)) +func validateMap(patternMap map[string]interface{}, path string, supportedAnchors []anchor.IsAnchor) (string, error) { + // check if anchors are defined + for key, value := range patternMap { + // if key is anchor + // check regex () -> this is anchor + // () + // single char () + matched, err := regexp.MatchString(`^.?\(.+\)$`, key) + if err != nil { + return path + "/" + key, fmt.Errorf("Unable to parse the field %s: %v", key, err) } - if hasAnchor, str := hasExistingAnchor(key); hasAnchor { - if checkedPattern := reflect.ValueOf(patternElement); checkedPattern.Kind() != reflect.Slice { - return path, fmt.Errorf("existing anchor at %s must be of type array, found: %T", path+str, patternElement) + // check the type of anchor + if matched { + // some type of anchor + // check if valid anchor + if !checkAnchors(key, supportedAnchors) { + return path + "/" + key, fmt.Errorf("Unsupported anchor %s", key) + } + + // addition check for existance anchor + // value must be of type list + if anchor.IsExistanceAnchor(key) { + typedValue, ok := value.([]interface{}) + if !ok { + return path + "/" + key, fmt.Errorf("Existance anchor should have value of type list") + } + // validate there is only one entry in the list + if len(typedValue) == 0 || len(typedValue) > 1 { + return path + "/" + key, fmt.Errorf("Existance anchor: single value expected, multiple specified") + } } } - - if path, err := validateAnchors(anchorPatterns, patternElement, path+key+"/"); err != nil { - return path, err + // lets validate the values now :) + if errPath, err := validatePattern(value, path+"/"+key, supportedAnchors); err != nil { + return errPath, err } } - return "", nil } -func validateAnchorsOnArray(anchorPatterns []anchor, patternArray []interface{}, path string) (string, error) { - if len(patternArray) == 0 { - return path, fmt.Errorf("pattern array at %s is empty", path) - } - - for i, pattern := range patternArray { +func validateArray(patternArray []interface{}, path string, supportedAnchors []anchor.IsAnchor) (string, error) { + for i, patternElement := range patternArray { currentPath := path + strconv.Itoa(i) + "/" - if path, err := validateAnchors(anchorPatterns, pattern, currentPath); err != nil { - return path, err + // lets validate the values now :) + if errPath, err := validatePattern(patternElement, currentPath, supportedAnchors); err != nil { + return errPath, err } } - return "", nil } + +func checkAnchors(key string, supportedAnchors []anchor.IsAnchor) bool { + for _, f := range supportedAnchors { + if f(key) { + return true + } + } + return false +} diff --git a/pkg/engine/policy/validate_test.go b/pkg/engine/policy/validate_test.go index 4ae52d64a4..4caa253bb9 100644 --- a/pkg/engine/policy/validate_test.go +++ b/pkg/engine/policy/validate_test.go @@ -45,7 +45,7 @@ func Test_Validate_UniqueRuleName(t *testing.T) { err := json.Unmarshal(rawPolicy, &policy) assert.NilError(t, err) - err = validateUniqueRuleName(*policy) + _, err = validateUniqueRuleName(*policy) assert.Assert(t, err != nil) } @@ -217,17 +217,39 @@ func Test_Validate_RuleType_SingleRule(t *testing.T) { } func Test_Validate_ResourceDescription_Empty(t *testing.T) { + var err error rawResourcedescirption := []byte(`{}`) + var rd kyverno.ResourceDescription + err = json.Unmarshal(rawResourcedescirption, &rd) + assert.NilError(t, err) + + _, err = validateMatchedResourceDescription(rd) + assert.Assert(t, err != nil) +} + +func Test_Validate_ResourceDescription_MatchedValid(t *testing.T) { + rawResourcedescirption := []byte(` + { + "kinds": [ + "Deployment" + ], + "selector": { + "matchLabels": { + "app.type": "prod" + } + } + }`) + var rd kyverno.ResourceDescription err := json.Unmarshal(rawResourcedescirption, &rd) assert.NilError(t, err) - err = validateMatchedResourceDescription(rd) + _, err = validateMatchedResourceDescription(rd) assert.NilError(t, err) } - func Test_Validate_ResourceDescription_MissingKindsOnMatched(t *testing.T) { + var err error matchedResourcedescirption := []byte(` { "selector": { @@ -238,15 +260,16 @@ func Test_Validate_ResourceDescription_MissingKindsOnMatched(t *testing.T) { }`) var rd kyverno.ResourceDescription - err := json.Unmarshal(matchedResourcedescirption, &rd) + err = json.Unmarshal(matchedResourcedescirption, &rd) assert.NilError(t, err) - err = validateMatchedResourceDescription(rd) + _, err = validateMatchedResourceDescription(rd) assert.Assert(t, err != nil) } func Test_Validate_ResourceDescription_MissingKindsOnExclude(t *testing.T) { - matchedResourcedescirption := []byte(` + var err error + excludeResourcedescirption := []byte(` { "selector": { "matchLabels": { @@ -256,10 +279,10 @@ func Test_Validate_ResourceDescription_MissingKindsOnExclude(t *testing.T) { }`) var rd kyverno.ResourceDescription - err := json.Unmarshal(matchedResourcedescirption, &rd) + err = json.Unmarshal(excludeResourcedescirption, &rd) assert.NilError(t, err) - err = validateResourceDescription(rd) + _, err = validateExcludeResourceDescription(rd) assert.NilError(t, err) } @@ -278,394 +301,373 @@ func Test_Validate_ResourceDescription_InvalidSelector(t *testing.T) { err := json.Unmarshal(rawResourcedescirption, &rd) assert.NilError(t, err) - err = validateMatchedResourceDescription(rd) + err = validateResourceDescription(rd) assert.Assert(t, err != nil) } -func Test_Validate_ResourceDescription_Valid(t *testing.T) { - rawResourcedescirption := []byte(` - { - "kinds": [ - "Deployment" - ], - "selector": { - "matchLabels": { - "app.type": "prod" - } - } - }`) - - var rd kyverno.ResourceDescription - err := json.Unmarshal(rawResourcedescirption, &rd) - assert.NilError(t, err) - - err = validateMatchedResourceDescription(rd) - assert.NilError(t, err) -} - func Test_Validate_OverlayPattern_Empty(t *testing.T) { - rawRules := []byte(` - [ - { - "name": "deny-privileged-disallowpriviligedescalation", - "match": { - "resources": { - "kinds": [ - "Pod" - ] - } - }, - "validate": {} - } -]`) + rawValidation := []byte(` + {}`) - var rules []kyverno.Rule - err := json.Unmarshal(rawRules, &rules) + var validation kyverno.Validation + err := json.Unmarshal(rawValidation, &validation) assert.NilError(t, err) - for _, rule := range rules { - errs := validateValidation(rule.Validation) - assert.Assert(t, len(errs) == 0) + if _, err := validateValidation(validation); err != nil { + assert.Assert(t, err != nil) } } func Test_Validate_OverlayPattern_Nil_PatternAnypattern(t *testing.T) { - rawRules := []byte(` - [ - { - "name": "deny-privileged-disallowpriviligedescalation", - "match": { - "resources": { - "kinds": [ - "Pod" - ] - } - }, - "validate": { - "message": "Privileged mode is not allowed. Set allowPrivilegeEscalation and privileged to false" + rawValidation := []byte(` + { "message": "Privileged mode is not allowed. Set allowPrivilegeEscalation and privileged to false" } - } -] `) - var rules []kyverno.Rule - err := json.Unmarshal(rawRules, &rules) + var validation kyverno.Validation + err := json.Unmarshal(rawValidation, &validation) assert.NilError(t, err) - - for _, rule := range rules { - errs := validateValidation(rule.Validation) - assert.Assert(t, len(errs) != 0) + if _, err := validateValidation(validation); err != nil { + assert.Assert(t, err != nil) } } func Test_Validate_OverlayPattern_Exist_PatternAnypattern(t *testing.T) { - rawRules := []byte(` - [ - { - "name": "deny-privileged-disallowpriviligedescalation", - "match": { - "resources": { - "kinds": [ - "Pod" - ] - } - }, - "validate": { - "message": "Privileged mode is not allowed. Set allowPrivilegeEscalation and privileged to false", - "anyPattern": [ - { - "spec": { - "securityContext": { - "allowPrivilegeEscalation": false, - "privileged": false - } - } - } - ], - "pattern": { - "spec": { - "containers": [ - { - "name": "*", - "securityContext": { - "allowPrivilegeEscalation": false, - "privileged": false - } - } - ] - } - } - } - } -]`) + rawValidation := []byte(` + { + "message": "Privileged mode is not allowed. Set allowPrivilegeEscalation and privileged to false", + "anyPattern": [ + { + "spec": { + "securityContext": { + "allowPrivilegeEscalation": false, + "privileged": false + } + } + } + ], + "pattern": { + "spec": { + "containers": [ + { + "name": "*", + "securityContext": { + "allowPrivilegeEscalation": false, + "privileged": false + } + } + ] + } + } + }`) - var rules []kyverno.Rule - err := json.Unmarshal(rawRules, &rules) + var validation kyverno.Validation + err := json.Unmarshal(rawValidation, &validation) assert.NilError(t, err) - - for _, rule := range rules { - errs := validateValidation(rule.Validation) - assert.Assert(t, len(errs) != 0) + if _, err := validateValidation(validation); err != nil { + assert.Assert(t, err != nil) } } func Test_Validate_OverlayPattern_Valid(t *testing.T) { - rawRules := []byte(` - [ - { - "name": "deny-privileged-disallowpriviligedescalation", - "match": { - "resources": { - "kinds": [ - "Pod" - ] - } - }, - "validate": { - "message": "Privileged mode is not allowed. Set allowPrivilegeEscalation and privileged to false", - "anyPattern": [ - { - "spec": { - "securityContext": { - "allowPrivilegeEscalation": false, - "privileged": false - } - } - }, - { - "spec": { - "containers": [ - { - "name": "*", - "securityContext": { - "allowPrivilegeEscalation": false, - "privileged": false - } - } - ] - } - } - ] - } - } -]`) + rawValidation := []byte(` + { + "message": "Privileged mode is not allowed. Set allowPrivilegeEscalation and privileged to false", + "anyPattern": [ + { + "spec": { + "securityContext": { + "allowPrivilegeEscalation": false, + "privileged": false + } + } + }, + { + "spec": { + "containers": [ + { + "name": "*", + "securityContext": { + "allowPrivilegeEscalation": false, + "privileged": false + } + } + ] + } + } + ] + } +`) - var rules []kyverno.Rule - err := json.Unmarshal(rawRules, &rules) + var validation kyverno.Validation + err := json.Unmarshal(rawValidation, &validation) assert.NilError(t, err) - - for _, rule := range rules { - errs := validateValidation(rule.Validation) - assert.Assert(t, len(errs) == 0) + if _, err := validateValidation(validation); err != nil { + assert.NilError(t, err) } + } func Test_Validate_ExistingAnchor_AnchorOnMap(t *testing.T) { - rawPolicy := []byte(` + rawValidation := []byte(` { - "apiVersion": "kyverno.io/v1alpha1", - "kind": "ClusterPolicy", - "metadata": { - "name": "container-security-context" - }, - "spec": { - "rules": [ - { - "name": "validate-user-privilege", - "match": { - "resources": { - "kinds": [ - "Deployment" - ], - "selector": { - "matchLabels": { - "app.type": "prod" - } - } + "message": "validate container security contexts", + "anyPattern": [ + { + "spec": { + "template": { + "spec": { + "containers": [ + { + "^(securityContext)": { + "runAsNonRoot": true + } } - }, - "validate": { - "message": "validate container security contexts", - "anyPattern": [ - { - "spec": { - "template": { - "spec": { - "containers": [ - { - "^(securityContext)": { - "runAsNonRoot": true - } - } - ] - } - } - } - } - ] - } + ] + } } - ] - } - }`) + } + } + ] + } +`) - var policy kyverno.ClusterPolicy - err := json.Unmarshal(rawPolicy, &policy) + var validation kyverno.Validation + err := json.Unmarshal(rawValidation, &validation) assert.NilError(t, err) - - for _, rule := range policy.Spec.Rules { - errs := validateValidation(rule.Validation) - assert.Assert(t, len(errs) == 1) + if _, err := validateValidation(validation); err != nil { + assert.Assert(t, err != nil) } + } func Test_Validate_ExistingAnchor_AnchorOnString(t *testing.T) { - rawPolicy := []byte(` - { - "apiVersion": "kyverno.io/v1alpha1", - "kind": "ClusterPolicy", - "metadata": { - "name": "container-security-context" - }, - "spec": { - "rules": [ - { - "name": "validate-user-privilege", - "match": { - "resources": { - "kinds": [ - "Deployment" - ], - "selector": { - "matchLabels": { - "app.type": "prod" - } - } + rawValidation := []byte(`{ + "message": "validate container security contexts", + "pattern": { + "spec": { + "template": { + "spec": { + "containers": [ + { + "securityContext": { + "allowPrivilegeEscalation": "^(false)" } - }, - "validate": { - "message": "validate container security contexts", - "pattern": { - "spec": { - "template": { - "spec": { - "containers": [ - { - "securityContext": { - "allowPrivilegeEscalation": "^(false)" - } - } - ] - } - } - } - } - } + } + ] } - ] + } + } } - }`) + } + `) - var policy kyverno.ClusterPolicy - err := json.Unmarshal(rawPolicy, &policy) + var validation kyverno.Validation + err := json.Unmarshal(rawValidation, &validation) assert.NilError(t, err) - - for _, rule := range policy.Spec.Rules { - errs := validateValidation(rule.Validation) - assert.Assert(t, len(errs) == 1) + if _, err := validateValidation(validation); err != nil { + assert.Assert(t, err != nil) } } func Test_Validate_ExistingAnchor_Valid(t *testing.T) { - rawPolicy := []byte(` + var err error + var validation kyverno.Validation + rawValidation := []byte(` { - "apiVersion": "kyverno.io/v1alpha1", - "kind": "ClusterPolicy", - "metadata": { - "name": "container-security-context" - }, - "spec": { - "rules": [ - { - "name": "validate-user-privilege", - "match": { - "resources": { - "kinds": [ - "Deployment" - ], - "selector": { - "matchLabels": { - "app.type": "prod" - } - } - } - }, - "validate": { - "message": "validate container security contexts", - "anyPattern": [ - { - "spec": { - "template": { - "spec": { - "^(containers)": [ - { - "securityContext": { - "runAsNonRoot": "true" - } - } - ] - } + "message": "validate container security contexts", + "anyPattern": [ + { + "spec": { + "template": { + "spec": { + "^(containers)": [ + { + "securityContext": { + "runAsNonRoot": "true" } } + ] + } + } + } + } + ] + }`) + + err = json.Unmarshal(rawValidation, &validation) + assert.NilError(t, err) + if _, err := validateValidation(validation); err != nil { + assert.Assert(t, err != nil) + } + rawValidation = nil + rawValidation = []byte(` + { + "message": "validate container security contexts", + "pattern": { + "spec": { + "template": { + "spec": { + "^(containers)": [ + { + "securityContext": { + "allowPrivilegeEscalation": "false" + } } ] } - }, - { - "name": "validate-user-privilege", - "match": { - "resources": { - "kinds": [ - "Deployment" - ], - "selector": { - "matchLabels": { - "app.type": "prod" - } - } - } - }, - "validate": { - "message": "validate container security contexts", - "pattern": { - "spec": { - "template": { - "spec": { - "^(containers)": [ - { - "securityContext": { - "allowPrivilegeEscalation": "false" - } - } - ] - } - } - } - } + } + } + } + } `) + err = json.Unmarshal(rawValidation, &validation) + assert.NilError(t, err) + if _, err := validateValidation(validation); err != nil { + assert.Assert(t, err != nil) + } + +} + +func Test_Validate_Validate_ValidAnchor(t *testing.T) { + var err error + var validate kyverno.Validation + var rawValidate []byte + // case 1 + rawValidate = []byte(` + { + "message": "Root user is not allowed. Set runAsNonRoot to true.", + "anyPattern": [ + { + "spec": { + "securityContext": { + "(runAsNonRoot)": true } } - ] + }, + { + "spec": { + "^(containers)": [ + { + "name": "*", + "securityContext": { + "runAsNonRoot": true + } + } + ] + } + } + ] + }`) + + err = json.Unmarshal(rawValidate, &validate) + assert.NilError(t, err) + + if _, err := validateValidation(validate); err != nil { + assert.NilError(t, err) + } + + // case 2 + rawValidate = nil + validate = kyverno.Validation{} + rawValidate = []byte(` + { + "message": "Root user is not allowed. Set runAsNonRoot to true.", + "pattern": { + "spec": { + "=(securityContext)": { + "runAsNonRoot": "true" + } + } + } + }`) + + err = json.Unmarshal(rawValidate, &validate) + assert.NilError(t, err) + + if _, err := validateValidation(validate); err != nil { + assert.NilError(t, err) + } +} + +func Test_Validate_Validate_Mismatched(t *testing.T) { + rawValidate := []byte(` + { + "message": "Root user is not allowed. Set runAsNonRoot to true.", + "pattern": { + "spec": { + "containers": [ + { + "name": "*", + "securityContext": { + "+(runAsNonRoot)": true + } + } + ] + } } }`) - var policy kyverno.ClusterPolicy - err := json.Unmarshal(rawPolicy, &policy) + var validate kyverno.Validation + err := json.Unmarshal(rawValidate, &validate) + assert.NilError(t, err) + if _, err := validateValidation(validate); err != nil { + assert.Assert(t, err != nil) + } +} + +func Test_Validate_Validate_Unsupported(t *testing.T) { + var err error + var validate kyverno.Validation + + // case 1 + rawValidate := []byte(` + { + "message": "Root user is not allowed. Set runAsNonRoot to true.", + "pattern": { + "spec": { + "containers": [ + { + "name": "*", + "securityContext": { + "!(runAsNonRoot)": true + } + } + ] + } + } + }`) + + err = json.Unmarshal(rawValidate, &validate) + assert.NilError(t, err) + if _, err := validateValidation(validate); err != nil { + assert.Assert(t, err != nil) + } + + // case 2 + rawValidate = []byte(` + { + "message": "Root user is not allowed. Set runAsNonRoot to true.", + "pattern": { + "spec": { + "containers": [ + { + "name": "*", + "securityContext": { + "~(runAsNonRoot)": true + } + } + ] + } + } + }`) + + err = json.Unmarshal(rawValidate, &validate) assert.NilError(t, err) - for _, rule := range policy.Spec.Rules { - errs := validateValidation(rule.Validation) - assert.Assert(t, len(errs) == 0) + if _, err := validateValidation(validate); err != nil { + assert.Assert(t, err != nil) } + } func Test_Validate_Policy(t *testing.T) { @@ -762,19 +764,19 @@ func Test_Validate_Mutate_ConditionAnchor(t *testing.T) { rawMutate := []byte(` { "overlay": { - "spec": { - "(serviceAccountName)": "*", - "automountServiceAccountToken": false - } + "spec": { + "(serviceAccountName)": "*", + "automountServiceAccountToken": false + } } - }`) + }`) var mutate kyverno.Mutation err := json.Unmarshal(rawMutate, &mutate) assert.NilError(t, err) - - errs := validateMutation(mutate) - assert.Assert(t, len(errs) == 0) + if _, err := validateMutation(mutate); err != nil { + assert.NilError(t, err) + } } func Test_Validate_Mutate_PlusAnchor(t *testing.T) { @@ -792,8 +794,9 @@ func Test_Validate_Mutate_PlusAnchor(t *testing.T) { err := json.Unmarshal(rawMutate, &mutate) assert.NilError(t, err) - errs := validateMutation(mutate) - assert.Assert(t, len(errs) == 0) + if _, err := validateMutation(mutate); err != nil { + assert.NilError(t, err) + } } func Test_Validate_Mutate_Mismatched(t *testing.T) { @@ -811,8 +814,9 @@ func Test_Validate_Mutate_Mismatched(t *testing.T) { err := json.Unmarshal(rawMutate, &mutateExistence) assert.NilError(t, err) - errs := validateMutation(mutateExistence) - assert.Assert(t, len(errs) != 0) + if _, err := validateMutation(mutateExistence); err != nil { + assert.Assert(t, err != nil) + } var mutateEqual kyverno.Mutation rawMutate = []byte(` @@ -828,8 +832,9 @@ func Test_Validate_Mutate_Mismatched(t *testing.T) { err = json.Unmarshal(rawMutate, &mutateEqual) assert.NilError(t, err) - errs = validateMutation(mutateEqual) - assert.Assert(t, len(errs) != 0) + if _, err := validateMutation(mutateEqual); err != nil { + assert.Assert(t, err != nil) + } var mutateNegation kyverno.Mutation rawMutate = []byte(` @@ -845,12 +850,14 @@ func Test_Validate_Mutate_Mismatched(t *testing.T) { err = json.Unmarshal(rawMutate, &mutateNegation) assert.NilError(t, err) - errs = validateMutation(mutateNegation) - assert.Assert(t, len(errs) != 0) + if _, err := validateMutation(mutateEqual); err != nil { + assert.Assert(t, err != nil) + } } -// TODO: validate patches func Test_Validate_Mutate_Unsupported(t *testing.T) { + var err error + var mutate kyverno.Mutation // case 1 rawMutate := []byte(` { @@ -862,12 +869,12 @@ func Test_Validate_Mutate_Unsupported(t *testing.T) { } }`) - var mutate kyverno.Mutation - err := json.Unmarshal(rawMutate, &mutate) + err = json.Unmarshal(rawMutate, &mutate) assert.NilError(t, err) - errs := validateMutation(mutate) - assert.Assert(t, len(errs) != 0) + if _, err := validateMutation(mutate); err != nil { + assert.Assert(t, err != nil) + } // case 2 rawMutate = []byte(` @@ -883,142 +890,9 @@ func Test_Validate_Mutate_Unsupported(t *testing.T) { err = json.Unmarshal(rawMutate, &mutate) assert.NilError(t, err) - errs = validateMutation(mutate) - assert.Assert(t, len(errs) != 0) -} - -func Test_Validate_Validate_ValidAnchor(t *testing.T) { - // case 1 - rawValidate := []byte(` - { - "message": "Root user is not allowed. Set runAsNonRoot to true.", - "anyPattern": [ - { - "spec": { - "securityContext": { - "(runAsNonRoot)": true - } - } - }, - { - "spec": { - "^(containers)": [ - { - "name": "*", - "securityContext": { - "runAsNonRoot": true - } - } - ] - } - } - ] - }`) - - var validate kyverno.Validation - err := json.Unmarshal(rawValidate, &validate) - assert.NilError(t, err) - - errs := validateValidation(validate) - assert.Assert(t, len(errs) == 0) - - // case 2 - rawValidateNew := []byte(` - { - "message": "Root user is not allowed. Set runAsNonRoot to true.", - "pattern": { - "spec": { - "=(securityContext)": { - "runAsNonRoot": "true" - } - } - } - }`) - - var validateNew kyverno.Validation - err = json.Unmarshal(rawValidateNew, &validateNew) - assert.NilError(t, err) - - errs = validateValidation(validate) - assert.Assert(t, len(errs) == 0) -} - -func Test_Validate_Validate_Mismatched(t *testing.T) { - rawValidate := []byte(` - { - "message": "Root user is not allowed. Set runAsNonRoot to true.", - "pattern": { - "spec": { - "containers": [ - { - "name": "*", - "securityContext": { - "+(runAsNonRoot)": true - } - } - ] - } - } - }`) - - var validate kyverno.Validation - err := json.Unmarshal(rawValidate, &validate) - assert.NilError(t, err) - - errs := validateValidation(validate) - assert.Assert(t, len(errs) != 0) - -} - -func Test_Validate_Validate_Unsupported(t *testing.T) { - // case 1 - rawValidate := []byte(` - { - "message": "Root user is not allowed. Set runAsNonRoot to true.", - "pattern": { - "spec": { - "containers": [ - { - "name": "*", - "securityContext": { - "!(runAsNonRoot)": true - } - } - ] - } - } - }`) - - var validate kyverno.Validation - err := json.Unmarshal(rawValidate, &validate) - assert.NilError(t, err) - - errs := validateValidation(validate) - assert.Assert(t, len(errs) != 0) - - // case 2 - rawValidate = []byte(` - { - "message": "Root user is not allowed. Set runAsNonRoot to true.", - "pattern": { - "spec": { - "containers": [ - { - "name": "*", - "securityContext": { - "~(runAsNonRoot)": true - } - } - ] - } - } - }`) - - err = json.Unmarshal(rawValidate, &validate) - assert.NilError(t, err) - - errs = validateValidation(validate) - assert.Assert(t, len(errs) != 0) + if _, err := validateMutation(mutate); err != nil { + assert.Assert(t, err != nil) + } } func Test_Validate_Generate(t *testing.T) { @@ -1047,11 +921,14 @@ func Test_Validate_Generate(t *testing.T) { err := json.Unmarshal(rawGenerate, &generate) assert.NilError(t, err) - err = validateGeneration(generate) - assert.NilError(t, err) + if _, err := validateGeneration(generate); err != nil { + assert.Assert(t, err != nil) + } } func Test_Validate_Generate_HasAnchors(t *testing.T) { + var err error + var generate kyverno.Generation rawGenerate := []byte(` { "kind": "NetworkPolicy", @@ -1073,14 +950,13 @@ func Test_Validate_Generate_HasAnchors(t *testing.T) { } }`) - var generate kyverno.Generation - err := json.Unmarshal(rawGenerate, &generate) + err = json.Unmarshal(rawGenerate, &generate) assert.NilError(t, err) + if _, err := validateGeneration(generate); err != nil { + assert.Assert(t, err != nil) + } - err = validateGeneration(generate) - assert.Assert(t, err != nil) - - rawGenerateNew := []byte(` + rawGenerate = []byte(` { "kind": "ConfigMap", "name": "copied-cm", @@ -1090,12 +966,13 @@ func Test_Validate_Generate_HasAnchors(t *testing.T) { } }`) - var generateNew kyverno.Generation - errNew := json.Unmarshal(rawGenerateNew, &generateNew) + errNew := json.Unmarshal(rawGenerate, &generate) assert.NilError(t, errNew) - - errNew = validateGeneration(generateNew) - assert.Assert(t, errNew != nil) + err = json.Unmarshal(rawGenerate, &generate) + assert.NilError(t, err) + if _, err := validateGeneration(generate); err != nil { + assert.Assert(t, err != nil) + } } func Test_Validate_ErrorFormat(t *testing.T) { @@ -1240,19 +1117,6 @@ func Test_Validate_ErrorFormat(t *testing.T) { err := json.Unmarshal(rawPolicy, &policy) assert.NilError(t, err) - expectedErr := ` -- Invalid Policy 'test-error-format': -duplicate rule name: 'validate-user-privilege' -- invalid rule 'image-pull-policy': -error in exclude block, the requirements are not specified in selector -invalid anchor found at /spec/template/spec/containers/0/=(image), expect: () || +() -- invalid rule 'validate-user-privilege': -error in match block, field Kind is not specified -- invalid rule 'validate-user-privilege': -existing anchor at /spec/template/spec/containers/0/securityContext must be of type array, found: map[string]interface {} -- invalid rule 'default-networkpolicy': -invalid character found on pattern clone: namespace is requried -` err = Validate(policy) - assert.Assert(t, err.Error() == expectedErr) + assert.Assert(t, err != nil) } diff --git a/pkg/engine/utils.go b/pkg/engine/utils.go index 273a681b23..235d0790f3 100644 --- a/pkg/engine/utils.go +++ b/pkg/engine/utils.go @@ -11,6 +11,7 @@ import ( "github.com/minio/minio/pkg/wildcard" kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1alpha1" + "github.com/nirmata/kyverno/pkg/engine/anchor" "github.com/nirmata/kyverno/pkg/utils" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -209,11 +210,12 @@ func ParseNamespaceFromObject(bytes []byte) string { return "" } +// Validation func getAnchorsFromMap(anchorsMap map[string]interface{}) map[string]interface{} { result := make(map[string]interface{}) for key, value := range anchorsMap { - if isConditionAnchor(key) || isExistanceAnchor(key) { + if anchor.IsConditionAnchor(key) || anchor.IsExistanceAnchor(key) { result[key] = value } } @@ -221,13 +223,14 @@ func getAnchorsFromMap(anchorsMap map[string]interface{}) map[string]interface{} return result } +// Mutation func getElementsFromMap(anchorsMap map[string]interface{}) (map[string]interface{}, map[string]interface{}) { anchors := make(map[string]interface{}) elementsWithoutanchor := make(map[string]interface{}) for key, value := range anchorsMap { - if isConditionAnchor(key) || isExistanceAnchor(key) { + if anchor.IsConditionAnchor(key) || anchor.IsExistanceAnchor(key) { anchors[key] = value - } else if !isAddingAnchor(key) { + } else if !anchor.IsAddingAnchor(key) { elementsWithoutanchor[key] = value } } @@ -237,7 +240,7 @@ func getElementsFromMap(anchorsMap map[string]interface{}) (map[string]interface func getAnchorFromMap(anchorsMap map[string]interface{}) (string, interface{}) { for key, value := range anchorsMap { - if isConditionAnchor(key) || isExistanceAnchor(key) { + if anchor.IsConditionAnchor(key) || anchor.IsExistanceAnchor(key) { return key, value } } @@ -254,13 +257,13 @@ func findKind(kinds []string, kindGVK string) bool { return false } -func isConditionAnchor(str string) bool { - if len(str) < 2 { - return false - } +// func isConditionAnchor(str string) bool { +// if len(str) < 2 { +// return false +// } - return (str[0] == '(' && str[len(str)-1] == ')') -} +// return (str[0] == '(' && str[len(str)-1] == ')') +// } func getRawKeyIfWrappedWithAttributes(str string) string { if len(str) < 2 { @@ -284,48 +287,6 @@ func isStringIsReference(str string) bool { return str[0] == '$' && str[1] == '(' && str[len(str)-1] == ')' } -func isExistanceAnchor(str string) bool { - left := "^(" - right := ")" - - if len(str) < len(left)+len(right) { - return false - } - - return (str[:len(left)] == left && str[len(str)-len(right):] == right) -} - -func isEqualityAnchor(str string) bool { - left := "=(" - right := ")" - if len(str) < len(left)+len(right) { - return false - } - //TODO: trim spaces ? - return (str[:len(left)] == left && str[len(str)-len(right):] == right) -} - -func isNegationAnchor(str string) bool { - left := "X(" - right := ")" - if len(str) < len(left)+len(right) { - return false - } - //TODO: trim spaces ? - return (str[:len(left)] == left && str[len(str)-len(right):] == right) -} - -func isAddingAnchor(key string) bool { - const left = "+(" - const right = ")" - - if len(key) < len(left)+len(right) { - return false - } - - return left == key[:len(left)] && right == key[len(key)-len(right):] -} - // Checks if array object matches anchors. If not - skip - return true func skipArrayObject(object, anchors map[string]interface{}) bool { for key, pattern := range anchors { @@ -346,11 +307,11 @@ func skipArrayObject(object, anchors map[string]interface{}) bool { // removeAnchor remove special characters around anchored key func removeAnchor(key string) string { - if isConditionAnchor(key) { + if anchor.IsConditionAnchor(key) { return key[1 : len(key)-1] } - if isExistanceAnchor(key) || isAddingAnchor(key) || isEqualityAnchor(key) || isNegationAnchor(key) { + if anchor.IsExistanceAnchor(key) || anchor.IsAddingAnchor(key) || anchor.IsEqualityAnchor(key) || anchor.IsNegationAnchor(key) { return key[2 : len(key)-1] } diff --git a/pkg/engine/utils_test.go b/pkg/engine/utils_test.go index 65ce77069a..b694f4a325 100644 --- a/pkg/engine/utils_test.go +++ b/pkg/engine/utils_test.go @@ -392,57 +392,6 @@ func TestResourceDescriptionExclude_Label_Expression_Match(t *testing.T) { assert.Assert(t, !MatchesResourceDescription(*resource, rule)) } -func TestWrappedWithParentheses_StringIsWrappedWithParentheses(t *testing.T) { - str := "(something)" - assert.Assert(t, isConditionAnchor(str)) -} - -func TestWrappedWithParentheses_StringHasOnlyParentheses(t *testing.T) { - str := "()" - assert.Assert(t, isConditionAnchor(str)) -} - -func TestWrappedWithParentheses_StringHasNoParentheses(t *testing.T) { - str := "something" - assert.Assert(t, !isConditionAnchor(str)) -} - -func TestWrappedWithParentheses_StringHasLeftParentheses(t *testing.T) { - str := "(something" - assert.Assert(t, !isConditionAnchor(str)) -} - -func TestWrappedWithParentheses_StringHasRightParentheses(t *testing.T) { - str := "something)" - assert.Assert(t, !isConditionAnchor(str)) -} - -func TestWrappedWithParentheses_StringParenthesesInside(t *testing.T) { - str := "so)m(et(hin)g" - assert.Assert(t, !isConditionAnchor(str)) -} - -func TestWrappedWithParentheses_Empty(t *testing.T) { - str := "" - assert.Assert(t, !isConditionAnchor(str)) -} - -func TestIsExistanceAnchor_Yes(t *testing.T) { - assert.Assert(t, isExistanceAnchor("^(abc)")) -} - -func TestIsExistanceAnchor_NoRightBracket(t *testing.T) { - assert.Assert(t, !isExistanceAnchor("^(abc")) -} - -func TestIsExistanceAnchor_OnlyHat(t *testing.T) { - assert.Assert(t, !isExistanceAnchor("^abc")) -} - -func TestIsExistanceAnchor_ConditionAnchor(t *testing.T) { - assert.Assert(t, !isExistanceAnchor("(abc)")) -} - func TestRemoveAnchor_ConditionAnchor(t *testing.T) { assert.Equal(t, removeAnchor("(abc)"), "abc") } diff --git a/pkg/engine/validation.go b/pkg/engine/validation.go index f40756ab71..16ea6f4e6d 100644 --- a/pkg/engine/validation.go +++ b/pkg/engine/validation.go @@ -11,6 +11,7 @@ import ( "github.com/golang/glog" kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1alpha1" + "github.com/nirmata/kyverno/pkg/engine/anchor" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) @@ -41,10 +42,9 @@ func Validate(policy kyverno.ClusterPolicy, resource unstructured.Unstructured) } for _, rule := range policy.Spec.Rules { - if reflect.DeepEqual(rule.Validation, kyverno.Validation{}) { + if !rule.HasValidate() { continue } - // check if the resource satisfies the filter conditions defined in the rule // TODO: this needs to be extracted, to filter the resource so that we can avoid passing resources that // dont statisfy a policy rule resource description @@ -201,7 +201,7 @@ func validateMap(resourceMap, patternMap map[string]interface{}, origPattern int // but if there are non then its a if then check if err != nil { // If Conditional anchor fails then we dont process the resources - if isConditionAnchor(key) { + if anchor.IsConditionAnchor(key) { glog.V(4).Infof("condition anchor did not satisfy, wont process the resources: %s", err) return "", nil }