diff --git a/pkg/engine/context/context.go b/pkg/engine/context/context.go index 2389e0c26d..db03c1ab4c 100644 --- a/pkg/engine/context/context.go +++ b/pkg/engine/context/context.go @@ -99,6 +99,16 @@ func (ctx *Context) AddJSON(dataRaw []byte) error { return nil } +// AddJSON merges json data +func (ctx *Context) AddJSONObject(jsonData interface{}) error { + jsonBytes, err := json.Marshal(jsonData) + if err != nil { + return err + } + + return ctx.AddJSON(jsonBytes) +} + // AddRequest adds an admission request to context func (ctx *Context) AddRequest(request *v1beta1.AdmissionRequest) error { modifiedResource := struct { diff --git a/pkg/engine/policyContext.go b/pkg/engine/policyContext.go index dd23dad881..1d1c97f4b2 100644 --- a/pkg/engine/policyContext.go +++ b/pkg/engine/policyContext.go @@ -20,6 +20,9 @@ type PolicyContext struct { // OldResource is the prior resource for an update, or nil OldResource unstructured.Unstructured + // Element is set when the context is used for processing a foreach loop + Element unstructured.Unstructured + // AdmissionInfo contains the admission request information AdmissionInfo kyverno.RequestInfo diff --git a/pkg/engine/validate/validate.go b/pkg/engine/validate/validate.go index e041b71c52..b896437b9e 100644 --- a/pkg/engine/validate/validate.go +++ b/pkg/engine/validate/validate.go @@ -37,7 +37,7 @@ func MatchPattern(logger logr.Logger, resource, pattern interface{}) error { // if conditional or global anchors report errors, the rule does not apply to the resource if common.IsConditionalAnchorError(err.Error()) || common.IsGlobalAnchorError(err.Error()) { logger.V(3).Info("skipping resource as anchor does not apply", "msg", ac.AnchorError.Error()) - return &PatternError{nil, "", true} + return &PatternError{err, "", true} } // check if an anchor defined in the policy rule is missing in the resource @@ -49,7 +49,7 @@ func MatchPattern(logger logr.Logger, resource, pattern interface{}) error { return &PatternError{err, elemPath, false} } - return &PatternError{nil, "", false} + return nil } // validateResourceElement detects the element type (map, array, nil, string, int, bool, float) diff --git a/pkg/engine/validation.go b/pkg/engine/validation.go index cc33757c76..8d64ef89ce 100644 --- a/pkg/engine/validation.go +++ b/pkg/engine/validation.go @@ -278,13 +278,17 @@ func addElementToContext(ctx *PolicyContext, e interface{}) error { return err } + jsonData := map[string]interface{}{ + "element": data, + } + + if err := ctx.JSONContext.AddJSONObject(jsonData); err != nil { + return errors.Wrapf(err, "failed to add element (%v) to JSON context", e) + } + u := unstructured.Unstructured{} u.SetUnstructuredContent(data) - ctx.NewResource = u - - if err := ctx.JSONContext.AddResourceAsObject(e); err != nil { - return errors.Wrapf(err, "failed to add resource (%v) to JSON context", e) - } + ctx.Element = u return nil } @@ -375,12 +379,17 @@ func (v *validator) getDenyMessage(deny bool) string { } func (v *validator) validateResourceWithRule() *response.RuleResponse { - if reflect.DeepEqual(v.ctx.OldResource, unstructured.Unstructured{}) { + if !isEmptyUnstructured(&v.ctx.Element) { + resp := v.validatePatterns(v.ctx.Element) + return resp + } + + if !isEmptyUnstructured(&v.ctx.OldResource) { resp := v.validatePatterns(v.ctx.NewResource) return resp } - if reflect.DeepEqual(v.ctx.NewResource, unstructured.Unstructured{}) { + if isEmptyUnstructured(&v.ctx.NewResource) { v.log.V(3).Info("skipping validation on deleted resource") return nil } @@ -395,6 +404,18 @@ func (v *validator) validateResourceWithRule() *response.RuleResponse { return newResp } +func isEmptyUnstructured(u *unstructured.Unstructured) bool { + if u == nil { + return true + } + + if reflect.DeepEqual(*u, unstructured.Unstructured{}) { + return true + } + + return false +} + // matches checks if either the new or old resource satisfies the filter conditions defined in the rule func matches(logger logr.Logger, rule kyverno.Rule, ctx *PolicyContext) bool { err := MatchesResourceDescription(ctx.NewResource, rule, ctx.AdmissionInfo, ctx.ExcludeGroupRole, ctx.NamespaceLabels) @@ -525,9 +546,9 @@ func (v *validator) buildErrorMessage(err error, path string) string { return fmt.Sprintf("validation error: rule %s execution error: %s", v.rule.Name, err.Error()) } - msgRaw, err := variables.SubstituteAll(v.log, v.ctx.JSONContext, v.rule.Validation.Message) - if err != nil { - v.log.Info("failed to substitute variables in message: %v", err) + msgRaw, sErr := variables.SubstituteAll(v.log, v.ctx.JSONContext, v.rule.Validation.Message) + if sErr != nil { + v.log.Info("failed to substitute variables in message: %v", sErr) } msg := msgRaw.(string) diff --git a/pkg/engine/validation_test.go b/pkg/engine/validation_test.go index 259d3c1666..63b73feff7 100644 --- a/pkg/engine/validation_test.go +++ b/pkg/engine/validation_test.go @@ -2514,7 +2514,7 @@ func Test_foreach_container_deny_fail(t *testing.T) { "list": "request.object.spec.template.spec.containers", "deny": { "conditions": [ - {"key": "{{ regex_match('{{request.object.image}}', 'docker.io') }}", "operator": "Equals", "value": false} + {"key": "{{ regex_match('{{element.image}}', 'docker.io') }}", "operator": "Equals", "value": false} ] } } @@ -2550,7 +2550,7 @@ func Test_foreach_container_deny_success(t *testing.T) { "list": "request.object.spec.template.spec.containers", "deny": { "conditions": [ - {"key": "{{ regex_match('{{request.object.image}}', 'docker.io') }}", "operator": "Equals", "value": false} + {"key": "{{ regex_match('{{element.image}}', 'docker.io') }}", "operator": "Equals", "value": false} ] } } @@ -2623,14 +2623,14 @@ func Test_foreach_context_preconditions(t *testing.T) { "context": [{"name": "img", "configMap": {"name": "mycmap", "namespace": "default"}}], "preconditions": { "all": [ { - "key": "{{request.object.name}}", + "key": "{{element.name}}", "operator": "In", "value": ["podvalid"] } ]}, "deny": { "conditions": [ - {"key": "{{ request.object.image }}", "operator": "NotEquals", "value": "{{ img.data.{{ request.object.name }} }}"} + {"key": "{{ element.image }}", "operator": "NotEquals", "value": "{{ img.data.{{ element.name }} }}"} ] } } @@ -2687,14 +2687,14 @@ func Test_foreach_context_preconditions_fail(t *testing.T) { "context": [{"name": "img", "configMap": {"name": "mycmap", "namespace": "default"}}], "preconditions": { "all": [ { - "key": "{{request.object.name}}", + "key": "{{element.name}}", "operator": "In", "value": ["podvalid", "podinvalid"] } ]}, "deny": { "conditions": [ - {"key": "{{ request.object.image }}", "operator": "NotEquals", "value": "{{ img.data.{{ request.object.name }} }}"} + {"key": "{{ element.image }}", "operator": "NotEquals", "value": "{{ img.data.{{ element.name }} }}"} ] } } diff --git a/pkg/engine/variables/vars.go b/pkg/engine/variables/vars.go index 33e078d934..ab019ff951 100644 --- a/pkg/engine/variables/vars.go +++ b/pkg/engine/variables/vars.go @@ -270,6 +270,8 @@ func substituteVariablesIfAny(log logr.Logger, ctx context.EvalInterface, vr Var return data.Element, nil } + isDeleteRequest := isDeleteRequest(ctx) + vars := RegexVariables.FindAllString(value, -1) for len(vars) > 0 { originalPattern := value @@ -281,8 +283,7 @@ func substituteVariablesIfAny(log logr.Logger, ctx context.EvalInterface, vr Var variable = strings.Replace(variable, "@", fmt.Sprintf("request.object.%s", getJMESPath(data.Path)), -1) } - operation, err := ctx.Query("request.operation") - if err == nil && operation == "DELETE" { + if isDeleteRequest { variable = strings.ReplaceAll(variable, "request.object", "request.oldObject") } @@ -318,6 +319,15 @@ func substituteVariablesIfAny(log logr.Logger, ctx context.EvalInterface, vr Var }) } +func isDeleteRequest(ctx context.EvalInterface) bool { + operation, err := ctx.Query("request.operation") + if err == nil && operation == "DELETE" { + return true + } + + return false +} + // getJMESPath converts path to JMES format func getJMESPath(rawPath string) string { tokens := strings.Split(rawPath, "/")[3:] // skip empty element and two non-resource (like mutate.overlay) diff --git a/pkg/policy/actions.go b/pkg/policy/actions.go index bfd449d1e0..fa674817a3 100644 --- a/pkg/policy/actions.go +++ b/pkg/policy/actions.go @@ -21,7 +21,11 @@ type Validation interface { // - Mutate // - Validation // - Generate -func validateActions(idx int, rule kyverno.Rule, client *dclient.Client, mock bool) error { +func validateActions(idx int, rule *kyverno.Rule, client *dclient.Client, mock bool) error { + if rule == nil { + return nil + } + var checker Validation // Mutate @@ -34,7 +38,7 @@ func validateActions(idx int, rule kyverno.Rule, client *dclient.Client, mock bo // Validate if rule.HasValidate() { - checker = validate.NewValidateFactory(rule.Validation) + checker = validate.NewValidateFactory(&rule.Validation) if path, err := checker.Validate(); err != nil { return fmt.Errorf("path: spec.rules[%d].validate.%s.: %v", idx, path, err) } diff --git a/pkg/policy/validate.go b/pkg/policy/validate.go index b5835da1fe..b3160b19f2 100644 --- a/pkg/policy/validate.go +++ b/pkg/policy/validate.go @@ -148,7 +148,7 @@ func Validate(policy *kyverno.ClusterPolicy, client *dclient.Client, mock bool, // - Mutate // - Validate // - Generate - if err := validateActions(i, rule, client, mock); err != nil { + if err := validateActions(i, &rule, client, mock); err != nil { return err } diff --git a/pkg/policy/validate/validate.go b/pkg/policy/validate/validate.go index d04646bad1..664e879ce9 100644 --- a/pkg/policy/validate/validate.go +++ b/pkg/policy/validate/validate.go @@ -2,42 +2,43 @@ package validate import ( "fmt" + "strings" kyverno "github.com/kyverno/kyverno/pkg/api/kyverno/v1" commonAnchors "github.com/kyverno/kyverno/pkg/engine/anchor/common" "github.com/kyverno/kyverno/pkg/policy/common" ) -// Validate provides implementation to validate 'validate' rule +// Validate validates a 'validate' rule type Validate struct { // rule to hold 'validate' rule specifications - rule kyverno.Validation + rule *kyverno.Validation } //NewValidateFactory returns a new instance of Mutate validation checker -func NewValidateFactory(rule kyverno.Validation) *Validate { +func NewValidateFactory(rule *kyverno.Validation) *Validate { m := Validate{ rule: rule, } + return &m } //Validate validates the 'validate' rule func (v *Validate) Validate() (string, error) { - rule := v.rule - if err := v.validateOverlayPattern(); err != nil { + if err := v.validateElements(); err != nil { // no need to proceed ahead return "", err } - if rule.Pattern != nil { - if path, err := common.ValidatePattern(rule.Pattern, "/", []commonAnchors.IsAnchor{commonAnchors.IsConditionAnchor, commonAnchors.IsExistenceAnchor, commonAnchors.IsEqualityAnchor, commonAnchors.IsNegationAnchor, commonAnchors.IsGlobalAnchor}); err != nil { + if v.rule.Pattern != nil { + if path, err := common.ValidatePattern(v.rule.Pattern, "/", []commonAnchors.IsAnchor{commonAnchors.IsConditionAnchor, commonAnchors.IsExistenceAnchor, commonAnchors.IsEqualityAnchor, commonAnchors.IsNegationAnchor, commonAnchors.IsGlobalAnchor}); err != nil { return fmt.Sprintf("pattern.%s", path), err } } - if rule.AnyPattern != nil { - anyPattern, err := rule.DeserializeAnyPattern() + if v.rule.AnyPattern != nil { + anyPattern, err := v.rule.DeserializeAnyPattern() if err != nil { return "anyPattern", fmt.Errorf("failed to deserialize anyPattern, expect array: %v", err) } @@ -47,19 +48,92 @@ func (v *Validate) Validate() (string, error) { } } } + + if v.rule.ForEachValidation != nil { + if err := v.validateForEach(v.rule.ForEachValidation); err != nil { + return "", err + } + } + return "", nil } -// validateOverlayPattern checks one of pattern/anyPattern must exist -func (v *Validate) validateOverlayPattern() error { - rule := v.rule - if rule.Pattern == nil && rule.AnyPattern == nil && rule.Deny == nil { - return fmt.Errorf("pattern, anyPattern or deny must be specified") +func (v *Validate) validateElements() error { + count := validationElemCount(v.rule) + if count == 0 { + return fmt.Errorf("one of pattern, anyPattern, deny, foreach must be specified") } - if rule.Pattern != nil && rule.AnyPattern != nil { - return fmt.Errorf("only one operation allowed per validation rule(pattern or anyPattern)") + if count > 1 { + return fmt.Errorf("only one of pattern, anyPattern, deny, foreach can be specified") } return nil } + +func validationElemCount(v *kyverno.Validation) int { + if v == nil { + return 0 + } + + count := 0 + if v.Pattern != nil { + count++ + } + + if v.AnyPattern != nil { + count++ + } + + if v.Deny != nil { + count++ + } + + if v.ForEachValidation != nil { + count++ + } + + return count +} + +func (v *Validate) validateForEach(foreach *kyverno.ForEachValidation) error { + if foreach.List == "" { + return fmt.Errorf("foreach.list is required") + } + + if !strings.HasPrefix(foreach.List, "request.object") { + return fmt.Errorf("foreach.list must start with 'request.object' e.g. 'request.object.spec.containers'.") + } + + count := foreachElemCount(foreach) + if count == 0 { + return fmt.Errorf("one of pattern, anyPattern, deny must be specified") + } + + if count > 1 { + return fmt.Errorf("only one of pattern, anyPattern, deny can be specified") + } + + return nil +} + +func foreachElemCount(foreach *kyverno.ForEachValidation) int { + if foreach == nil { + return 0 + } + + count := 0 + if foreach.Pattern != nil { + count++ + } + + if foreach.AnyPattern != nil { + count++ + } + + if foreach.Deny != nil { + count++ + } + + return count +}