diff --git a/api/kyverno/v1/clusterpolicy_types.go b/api/kyverno/v1/clusterpolicy_types.go index e5d5c1b662..78baa2f619 100644 --- a/api/kyverno/v1/clusterpolicy_types.go +++ b/api/kyverno/v1/clusterpolicy_types.go @@ -99,7 +99,7 @@ func (p *ClusterPolicy) IsReady() bool { func (p *ClusterPolicy) Validate(clusterResources sets.String) (errs field.ErrorList) { errs = append(errs, ValidateAutogenAnnotation(field.NewPath("metadata").Child("annotations"), p.GetAnnotations())...) errs = append(errs, ValidatePolicyName(field.NewPath("name"), p.Name)...) - errs = append(errs, p.Spec.Validate(field.NewPath("spec"), p.IsNamespaced(), clusterResources)...) + errs = append(errs, p.Spec.Validate(field.NewPath("spec"), p.IsNamespaced(), p.Namespace, clusterResources)...) return errs } diff --git a/api/kyverno/v1/policy_types.go b/api/kyverno/v1/policy_types.go index 1cbc11f473..9aef36fce3 100755 --- a/api/kyverno/v1/policy_types.go +++ b/api/kyverno/v1/policy_types.go @@ -100,7 +100,7 @@ func (p *Policy) IsReady() bool { func (p *Policy) Validate(clusterResources sets.String) (errs field.ErrorList) { errs = append(errs, ValidateAutogenAnnotation(field.NewPath("metadata").Child("annotations"), p.GetAnnotations())...) errs = append(errs, ValidatePolicyName(field.NewPath("name"), p.Name)...) - errs = append(errs, p.Spec.Validate(field.NewPath("spec"), p.IsNamespaced(), clusterResources)...) + errs = append(errs, p.Spec.Validate(field.NewPath("spec"), p.IsNamespaced(), p.Namespace, clusterResources)...) return errs } diff --git a/api/kyverno/v1/rule_test.go b/api/kyverno/v1/rule_test.go index 18212a9965..dd813a6d45 100644 --- a/api/kyverno/v1/rule_test.go +++ b/api/kyverno/v1/rule_test.go @@ -13,7 +13,7 @@ func Test_Validate_RuleType_EmptyRule(t *testing.T) { Name: "validate-user-privilege", } path := field.NewPath("dummy") - errs := subject.Validate(path, false, nil) + errs := subject.Validate(path, false, "", nil) assert.Equal(t, len(errs), 1) assert.Equal(t, errs[0].Field, "dummy") assert.Equal(t, errs[0].Type, field.ErrorTypeInvalid) @@ -90,7 +90,7 @@ func Test_Validate_RuleType_MultipleRule(t *testing.T) { assert.NilError(t, err) for _, rule := range policy.Spec.Rules { path := field.NewPath("dummy") - errs := rule.Validate(path, false, nil) + errs := rule.Validate(path, false, "", nil) assert.Assert(t, len(errs) != 0) } } @@ -145,7 +145,7 @@ func Test_Validate_RuleType_SingleRule(t *testing.T) { assert.NilError(t, err) for _, rule := range policy.Spec.Rules { path := field.NewPath("dummy") - errs := rule.Validate(path, false, nil) + errs := rule.Validate(path, false, "", nil) assert.Assert(t, len(errs) == 0) } } @@ -227,3 +227,163 @@ func Test_doesMatchExcludeConflict(t *testing.T) { } } } + +func Test_Validate_NamespacedPolicy_MutateRuleTargetNamespace(t *testing.T) { + path := field.NewPath("dummy") + testcases := []struct { + description string + rule []byte + errors func(r *Rule) field.ErrorList + }{ + { + description: "Invalid mutate rule target namespace", + rule: []byte(` + { + "name": "auto-rollout-on-config-change", + "match": { + "resources": { + "kinds": [ + "ConfigMap" + ] + } + }, + "mutate": { + "targets": [ + { + "apiVersion": "apps/v1", + "kind": "Deployment", + "namespace": "maddy" + }, + { + "apiVersion": "v1", + "kind": "Service", + "namespace": "praddy" + } + ], + "patchStrategicMerge": { + "metadata": { + "annotations": { + "kyverno/tls-changed:": "true" + } + } + } + } + }`), + errors: func(r *Rule) (errs field.ErrorList) { + return append(errs, + field.Invalid(path.Child("targets").Index(0).Child("namespace"), "maddy", "This field can be ignored or should have value of the namespace where the policy is being created"), + field.Invalid(path.Child("targets").Index(1).Child("namespace"), "praddy", "This field can be ignored or should have value of the namespace where the policy is being created")) + }, + }, + { + description: "Valid mutate rule target namespace", + rule: []byte(` + { + "name": "auto-rollout-on-config-change", + "match": { + "resources": { + "kinds": [ + "ConfigMap" + ] + } + }, + "mutate": { + "targets": [ + { + "apiVersion": "apps/v1", + "kind": "Deployment", + "namespace": "amritapuri" + }, + { + "apiVersion": "v1", + "kind": "Service", + "namespace": "amritapuri" + } + ], + "patchStrategicMerge": { + "metadata": { + "annotations": { + "kyverno/tls-changed:": "true" + } + } + } + } + }`), + }, + } + + for _, testcase := range testcases { + var rule Rule + err := json.Unmarshal(testcase.rule, &rule) + assert.NilError(t, err) + errs := rule.ValidateMutationRuleTargetNamespace(path, true, "amritapuri") + var expectedErrs field.ErrorList + if testcase.errors != nil { + expectedErrs = testcase.errors(&rule) + } + assert.Equal(t, len(errs), len(expectedErrs)) + for i := range errs { + assert.Equal(t, errs[i].Error(), expectedErrs[i].Error()) + } + } +} + +func Test_Validate_ClusterPolicy_MutateRuleTargetNamespace(t *testing.T) { + path := field.NewPath("dummy") + testcases := []struct { + description string + rule []byte + errors func(r *Rule) field.ErrorList + }{ + { + description: "Valid mutate rule target namespace", + rule: []byte(` + { + "name": "auto-rollout-on-config-change", + "match": { + "resources": { + "kinds": [ + "ConfigMap" + ] + } + }, + "mutate": { + "targets": [ + { + "apiVersion": "apps/v1", + "kind": "Deployment", + "namespace": "maddy" + }, + { + "apiVersion": "v1", + "kind": "Service", + "namespace": "praddy" + } + ], + "patchStrategicMerge": { + "metadata": { + "annotations": { + "kyverno/tls-changed:": "true" + } + } + } + } + }`), + }, + } + + for _, testcase := range testcases { + var rule Rule + err := json.Unmarshal(testcase.rule, &rule) + assert.NilError(t, err) + errs := rule.ValidateMutationRuleTargetNamespace(path, false, "") + var expectedErrs field.ErrorList + if testcase.errors != nil { + expectedErrs = testcase.errors(&rule) + } + assert.Equal(t, len(errs), len(expectedErrs)) + for i := range errs { + assert.Equal(t, errs[i].Error(), expectedErrs[i].Error()) + } + } +} diff --git a/api/kyverno/v1/rule_types.go b/api/kyverno/v1/rule_types.go index f2bb591f8f..ca3e9d3aa9 100644 --- a/api/kyverno/v1/rule_types.go +++ b/api/kyverno/v1/rule_types.go @@ -347,11 +347,24 @@ func (r *Rule) ValidateMatchExcludeConflict(path *field.Path) (errs field.ErrorL return append(errs, field.Invalid(path, r, "Rule is matching an empty set")) } +// ValidateMutationRuleTargetNamespace checks if the targets are scoped to the policy's namespace +func (r *Rule) ValidateMutationRuleTargetNamespace(path *field.Path, namespaced bool, policyNamespace string) (errs field.ErrorList) { + if r.HasMutate() && namespaced { + for idx, target := range r.Mutation.Targets { + if target.Namespace != "" && target.Namespace != policyNamespace { + errs = append(errs, field.Invalid(path.Child("targets").Index(idx).Child("namespace"), target.Namespace, "This field can be ignored or should have value of the namespace where the policy is being created")) + } + } + } + return errs +} + // Validate implements programmatic validation -func (r *Rule) Validate(path *field.Path, namespaced bool, clusterResources sets.String) (errs field.ErrorList) { +func (r *Rule) Validate(path *field.Path, namespaced bool, policyNamespace string, clusterResources sets.String) (errs field.ErrorList) { errs = append(errs, r.ValidateRuleType(path)...) errs = append(errs, r.ValidateMatchExcludeConflict(path)...) errs = append(errs, r.MatchResources.Validate(path.Child("match"), namespaced, clusterResources)...) errs = append(errs, r.ExcludeResources.Validate(path.Child("exclude"), namespaced, clusterResources)...) + errs = append(errs, r.ValidateMutationRuleTargetNamespace(path, namespaced, policyNamespace)...) return errs } diff --git a/api/kyverno/v1/spec_test.go b/api/kyverno/v1/spec_test.go index 6ff74ee4e7..24e103b7bb 100644 --- a/api/kyverno/v1/spec_test.go +++ b/api/kyverno/v1/spec_test.go @@ -43,7 +43,7 @@ func Test_Validate_UniqueRuleName(t *testing.T) { }}, } path := field.NewPath("dummy") - errs := subject.Validate(path, false, nil) + errs := subject.Validate(path, false, "", nil) assert.Equal(t, len(errs), 1) assert.Equal(t, errs[0].Field, "dummy.rules[1].name") assert.Equal(t, errs[0].Type, field.ErrorTypeInvalid) diff --git a/api/kyverno/v1/spec_types.go b/api/kyverno/v1/spec_types.go index 1c73449291..2133106871 100644 --- a/api/kyverno/v1/spec_types.go +++ b/api/kyverno/v1/spec_types.go @@ -233,17 +233,17 @@ func (s *Spec) ValidateRuleNames(path *field.Path) (errs field.ErrorList) { } // ValidateRules implements programmatic validation of Rules -func (s *Spec) ValidateRules(path *field.Path, namespaced bool, clusterResources sets.String) (errs field.ErrorList) { +func (s *Spec) ValidateRules(path *field.Path, namespaced bool, policyNamespace string, clusterResources sets.String) (errs field.ErrorList) { errs = append(errs, s.ValidateRuleNames(path)...) for i, rule := range s.Rules { - errs = append(errs, rule.Validate(path.Index(i), namespaced, clusterResources)...) + errs = append(errs, rule.Validate(path.Index(i), namespaced, policyNamespace, clusterResources)...) } return errs } // Validate implements programmatic validation -func (s *Spec) Validate(path *field.Path, namespaced bool, clusterResources sets.String) (errs field.ErrorList) { - errs = append(errs, s.ValidateRules(path.Child("rules"), namespaced, clusterResources)...) +func (s *Spec) Validate(path *field.Path, namespaced bool, policyNamespace string, clusterResources sets.String) (errs field.ErrorList) { + errs = append(errs, s.ValidateRules(path.Child("rules"), namespaced, policyNamespace, clusterResources)...) if namespaced && len(s.ValidationFailureActionOverrides) > 0 { errs = append(errs, field.Forbidden(path.Child("validationFailureActionOverrides"), "Use of validationFailureActionOverrides is supported only with ClusterPolicy")) } diff --git a/pkg/engine/loadtargets.go b/pkg/engine/loadtargets.go index d6ccc3fd45..c4b7f7b01f 100644 --- a/pkg/engine/loadtargets.go +++ b/pkg/engine/loadtargets.go @@ -68,6 +68,11 @@ func getTargets(target kyvernov1.ResourceSpec, ctx *PolicyContext, logger logr.L namespace := target.Namespace name := target.Name + // if it's namespaced policy, targets has to be loaded only from the policy's namespace + if ctx.Policy.IsNamespaced() { + namespace = ctx.Policy.GetNamespace() + } + if namespace != "" && name != "" && !wildcard.ContainsWildcard(namespace) && !wildcard.ContainsWildcard(name) { obj, err := ctx.Client.GetResource(target.APIVersion, target.Kind, namespace, name)