1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2025-03-13 19:28:55 +00:00

fix: add policy validation for ValidationFailureActionOverride field (#4784)

Signed-off-by: ansalamdaniel <ansalam.daniel@infracloud.io>
This commit is contained in:
ansalamdaniel 2022-10-06 11:46:12 +05:30 committed by GitHub
parent f659f7791c
commit 27de93a3d2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 715 additions and 3 deletions

View file

@ -123,6 +123,14 @@ func Validate(policy kyvernov1.PolicyInterface, client dclient.Interface, mock b
if errs := policy.Validate(clusterResources); len(errs) != 0 {
return nil, errs.ToAggregate()
}
if !namespaced {
err := validateNamespaces(spec, specPath.Child("validationFailureActionOverrides"))
if err != nil {
return nil, err
}
}
rules := autogen.ComputeRules(policy)
rulesPath := specPath.Child("rules")
for i, rule := range rules {
@ -1071,3 +1079,61 @@ func validateKinds(kinds []string, mock bool, client dclient.Interface, p kyvern
}
return nil
}
func validateWildcardsWithNamespaces(enforce, audit, enforceW, auditW []string) error {
pat, ns, notOk := utils.CheckWildcardNamespaces(auditW, enforce)
if notOk {
return fmt.Errorf("wildcard pattern '%s' matches with namespace '%s'", pat, ns)
}
pat, ns, notOk = utils.CheckWildcardNamespaces(enforceW, audit)
if notOk {
return fmt.Errorf("wildcard pattern '%s' matches with namespace '%s'", pat, ns)
}
pat1, pat2, notOk := utils.CheckWildcardNamespaces(auditW, enforceW)
if notOk {
return fmt.Errorf("wildcard pattern '%s' conflicts with the pattern '%s'", pat1, pat2)
}
pat1, pat2, notOk = utils.CheckWildcardNamespaces(enforceW, auditW)
if notOk {
return fmt.Errorf("wildcard pattern '%s' conflicts with the pattern '%s'", pat1, pat2)
}
return nil
}
func validateNamespaces(s *kyvernov1.Spec, path *field.Path) error {
action := map[string]sets.String{
string(kyvernov1.Enforce): sets.NewString(),
string(kyvernov1.Audit): sets.NewString(),
"enforceW": sets.NewString(),
"auditW": sets.NewString(),
}
for i, vfa := range s.ValidationFailureActionOverrides {
patternList, nsList := utils.SeperateWildcards(vfa.Namespaces)
if vfa.Action == kyvernov1.Audit {
if action[string(kyvernov1.Enforce)].HasAny(nsList...) {
return fmt.Errorf("conflicting namespaces found in path: %s: %s", path.Index(i).Child("namespaces").String(),
strings.Join(action[string(kyvernov1.Enforce)].Intersection(sets.NewString(nsList...)).List(), ", "))
}
action["auditW"].Insert(patternList...)
} else if vfa.Action == kyvernov1.Enforce {
if action[string(kyvernov1.Audit)].HasAny(nsList...) {
return fmt.Errorf("conflicting namespaces found in path: %s: %s", path.Index(i).Child("namespaces").String(),
strings.Join(action[string(kyvernov1.Audit)].Intersection(sets.NewString(nsList...)).List(), ", "))
}
action["enforceW"].Insert(patternList...)
}
action[string(vfa.Action)].Insert(nsList...)
err := validateWildcardsWithNamespaces(action[string(kyvernov1.Enforce)].List(),
action[string(kyvernov1.Audit)].List(), action["enforceW"].List(), action["auditW"].List())
if err != nil {
return fmt.Errorf("path: %s: %s", path.Index(i).Child("namespaces").String(), err.Error())
}
}
return nil
}

View file

@ -2,15 +2,17 @@ package policy
import (
"encoding/json"
"errors"
"fmt"
"testing"
kyverno "github.com/kyverno/kyverno/api/kyverno/v1"
"github.com/kyverno/kyverno/pkg/logging"
"github.com/kyverno/kyverno/pkg/openapi"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
"gotest.tools/assert"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/util/validation/field"
)
func Test_Validate_ResourceDescription_Empty(t *testing.T) {
@ -1032,6 +1034,7 @@ func Test_Validate_ApiCall(t *testing.T) {
}
}
}
func Test_Wildcards_Kind(t *testing.T) {
rawPolicy := []byte(`
{
@ -1442,3 +1445,434 @@ func Test_PodControllerAutoGenExclusion_None_Policy(t *testing.T) {
}
assert.NilError(t, err)
}
func Test_ValidateNamespace(t *testing.T) {
testcases := []struct {
description string
spec *kyverno.Spec
expectedError error
}{
{
description: "tc1",
spec: &kyverno.Spec{
ValidationFailureAction: kyverno.Enforce,
ValidationFailureActionOverrides: []kyverno.ValidationFailureActionOverride{
{
Action: kyverno.Enforce,
Namespaces: []string{
"default",
"test",
},
},
{
Action: kyverno.Audit,
Namespaces: []string{
"default",
},
},
},
Rules: []kyverno.Rule{
{
Name: "require-labels",
MatchResources: kyverno.MatchResources{ResourceDescription: kyverno.ResourceDescription{Kinds: []string{"Pod"}}},
Validation: kyverno.Validation{
Message: "label 'app.kubernetes.io/name' is required",
RawPattern: &apiextv1.JSON{Raw: []byte(`"metadata": {"lables": {"app.kubernetes.io/name": "?*"}}`)},
},
},
},
},
expectedError: errors.New("conflicting namespaces found in path: spec.validationFailureActionOverrides[1].namespaces: default"),
},
{
description: "tc2",
spec: &kyverno.Spec{
ValidationFailureAction: kyverno.Enforce,
ValidationFailureActionOverrides: []kyverno.ValidationFailureActionOverride{
{
Action: kyverno.Enforce,
Namespaces: []string{
"default",
"test",
},
},
{
Action: kyverno.Audit,
Namespaces: []string{
"default",
},
},
},
Rules: []kyverno.Rule{
{
Name: "require-labels",
MatchResources: kyverno.MatchResources{ResourceDescription: kyverno.ResourceDescription{Kinds: []string{"Pod"}}},
Mutation: kyverno.Mutation{
RawPatchStrategicMerge: &apiextv1.JSON{Raw: []byte(`"metadata": {"labels": {"app-name": "{{request.object.metadata.name}}"}}`)},
},
},
},
},
expectedError: errors.New("conflicting namespaces found in path: spec.validationFailureActionOverrides[1].namespaces: default"),
},
{
description: "tc3",
spec: &kyverno.Spec{
ValidationFailureAction: kyverno.Enforce,
ValidationFailureActionOverrides: []kyverno.ValidationFailureActionOverride{
{
Action: kyverno.Enforce,
Namespaces: []string{
"default*",
"test",
},
},
{
Action: kyverno.Audit,
Namespaces: []string{
"default",
},
},
},
Rules: []kyverno.Rule{
{
Name: "require-labels",
MatchResources: kyverno.MatchResources{ResourceDescription: kyverno.ResourceDescription{Kinds: []string{"Pod"}}},
Validation: kyverno.Validation{
Message: "label 'app.kubernetes.io/name' is required",
RawPattern: &apiextv1.JSON{Raw: []byte(`"metadata": {"lables": {"app.kubernetes.io/name": "?*"}}`)},
},
},
},
},
expectedError: errors.New("path: spec.validationFailureActionOverrides[1].namespaces: wildcard pattern 'default*' matches with namespace 'default'"),
},
{
description: "tc4",
spec: &kyverno.Spec{
ValidationFailureAction: kyverno.Enforce,
ValidationFailureActionOverrides: []kyverno.ValidationFailureActionOverride{
{
Action: kyverno.Enforce,
Namespaces: []string{
"default",
"test",
},
},
{
Action: kyverno.Audit,
Namespaces: []string{
"*",
},
},
},
Rules: []kyverno.Rule{
{
Name: "require-labels",
MatchResources: kyverno.MatchResources{ResourceDescription: kyverno.ResourceDescription{Kinds: []string{"Pod"}}},
Validation: kyverno.Validation{
Message: "label 'app.kubernetes.io/name' is required",
RawPattern: &apiextv1.JSON{Raw: []byte(`"metadata": {"lables": {"app.kubernetes.io/name": "?*"}}`)},
},
},
},
},
expectedError: errors.New("path: spec.validationFailureActionOverrides[1].namespaces: wildcard pattern '*' matches with namespace 'default'"),
},
{
description: "tc5",
spec: &kyverno.Spec{
ValidationFailureAction: kyverno.Enforce,
ValidationFailureActionOverrides: []kyverno.ValidationFailureActionOverride{
{
Action: kyverno.Enforce,
Namespaces: []string{
"default",
"test",
},
},
{
Action: kyverno.Audit,
Namespaces: []string{
"?*",
},
},
},
Rules: []kyverno.Rule{
{
Name: "require-labels",
MatchResources: kyverno.MatchResources{ResourceDescription: kyverno.ResourceDescription{Kinds: []string{"Pod"}}},
Validation: kyverno.Validation{
Message: "label 'app.kubernetes.io/name' is required",
RawPattern: &apiextv1.JSON{Raw: []byte(`"metadata": {"lables": {"app.kubernetes.io/name": "?*"}}`)},
},
},
},
},
expectedError: errors.New("path: spec.validationFailureActionOverrides[1].namespaces: wildcard pattern '?*' matches with namespace 'default'"),
},
{
description: "tc6",
spec: &kyverno.Spec{
ValidationFailureAction: kyverno.Enforce,
ValidationFailureActionOverrides: []kyverno.ValidationFailureActionOverride{
{
Action: kyverno.Enforce,
Namespaces: []string{
"default?",
"test",
},
},
{
Action: kyverno.Audit,
Namespaces: []string{
"default1",
},
},
},
Rules: []kyverno.Rule{
{
Name: "require-labels",
MatchResources: kyverno.MatchResources{ResourceDescription: kyverno.ResourceDescription{Kinds: []string{"Pod"}}},
Validation: kyverno.Validation{
Message: "label 'app.kubernetes.io/name' is required",
RawPattern: &apiextv1.JSON{Raw: []byte(`"metadata": {"lables": {"app.kubernetes.io/name": "?*"}}`)},
},
},
},
},
expectedError: errors.New("path: spec.validationFailureActionOverrides[1].namespaces: wildcard pattern 'default?' matches with namespace 'default1'"),
},
{
description: "tc7",
spec: &kyverno.Spec{
ValidationFailureAction: kyverno.Enforce,
ValidationFailureActionOverrides: []kyverno.ValidationFailureActionOverride{
{
Action: kyverno.Enforce,
Namespaces: []string{
"default*",
"test",
},
},
{
Action: kyverno.Audit,
Namespaces: []string{
"?*",
},
},
},
Rules: []kyverno.Rule{
{
Name: "require-labels",
MatchResources: kyverno.MatchResources{ResourceDescription: kyverno.ResourceDescription{Kinds: []string{"Pod"}}},
Validation: kyverno.Validation{
Message: "label 'app.kubernetes.io/name' is required",
RawPattern: &apiextv1.JSON{Raw: []byte(`"metadata": {"lables": {"app.kubernetes.io/name": "?*"}}`)},
},
},
},
},
expectedError: errors.New("path: spec.validationFailureActionOverrides[1].namespaces: wildcard pattern '?*' matches with namespace 'test'"),
},
{
description: "tc8",
spec: &kyverno.Spec{
ValidationFailureAction: kyverno.Enforce,
ValidationFailureActionOverrides: []kyverno.ValidationFailureActionOverride{
{
Action: kyverno.Enforce,
Namespaces: []string{
"*",
},
},
{
Action: kyverno.Audit,
Namespaces: []string{
"?*",
},
},
},
Rules: []kyverno.Rule{
{
Name: "require-labels",
MatchResources: kyverno.MatchResources{ResourceDescription: kyverno.ResourceDescription{Kinds: []string{"Pod"}}},
Validation: kyverno.Validation{
Message: "label 'app.kubernetes.io/name' is required",
RawPattern: &apiextv1.JSON{Raw: []byte(`"metadata": {"lables": {"app.kubernetes.io/name": "?*"}}`)},
},
},
},
},
expectedError: errors.New("path: spec.validationFailureActionOverrides[1].namespaces: wildcard pattern '?*' conflicts with the pattern '*'"),
},
{
description: "tc9",
spec: &kyverno.Spec{
ValidationFailureAction: kyverno.Enforce,
ValidationFailureActionOverrides: []kyverno.ValidationFailureActionOverride{
{
Action: kyverno.Enforce,
Namespaces: []string{
"default*",
"test",
},
},
{
Action: kyverno.Audit,
Namespaces: []string{
"default",
"test*",
},
},
},
Rules: []kyverno.Rule{
{
Name: "require-labels",
MatchResources: kyverno.MatchResources{ResourceDescription: kyverno.ResourceDescription{Kinds: []string{"Pod"}}},
Validation: kyverno.Validation{
Message: "label 'app.kubernetes.io/name' is required",
RawPattern: &apiextv1.JSON{Raw: []byte(`"metadata": {"lables": {"app.kubernetes.io/name": "?*"}}`)},
},
},
},
},
expectedError: errors.New("path: spec.validationFailureActionOverrides[1].namespaces: wildcard pattern 'test*' matches with namespace 'test'"),
},
{
description: "tc10",
spec: &kyverno.Spec{
ValidationFailureAction: kyverno.Enforce,
ValidationFailureActionOverrides: []kyverno.ValidationFailureActionOverride{
{
Action: kyverno.Enforce,
Namespaces: []string{
"*efault",
"test",
},
},
{
Action: kyverno.Audit,
Namespaces: []string{
"default",
},
},
},
Rules: []kyverno.Rule{
{
Name: "require-labels",
MatchResources: kyverno.MatchResources{ResourceDescription: kyverno.ResourceDescription{Kinds: []string{"Pod"}}},
Validation: kyverno.Validation{
Message: "label 'app.kubernetes.io/name' is required",
RawPattern: &apiextv1.JSON{Raw: []byte(`"metadata": {"lables": {"app.kubernetes.io/name": "?*"}}`)},
},
},
},
},
expectedError: errors.New("path: spec.validationFailureActionOverrides[1].namespaces: wildcard pattern '*efault' matches with namespace 'default'"),
},
{
description: "tc11",
spec: &kyverno.Spec{
ValidationFailureAction: kyverno.Enforce,
ValidationFailureActionOverrides: []kyverno.ValidationFailureActionOverride{
{
Action: kyverno.Enforce,
Namespaces: []string{
"default-*",
"test",
},
},
{
Action: kyverno.Audit,
Namespaces: []string{
"default",
},
},
},
Rules: []kyverno.Rule{
{
Name: "require-labels",
MatchResources: kyverno.MatchResources{ResourceDescription: kyverno.ResourceDescription{Kinds: []string{"Pod"}}},
Validation: kyverno.Validation{
Message: "label 'app.kubernetes.io/name' is required",
RawPattern: &apiextv1.JSON{Raw: []byte(`"metadata": {"lables": {"app.kubernetes.io/name": "?*"}}`)},
},
},
},
},
},
{
description: "tc12",
spec: &kyverno.Spec{
ValidationFailureAction: kyverno.Enforce,
ValidationFailureActionOverrides: []kyverno.ValidationFailureActionOverride{
{
Action: kyverno.Enforce,
Namespaces: []string{
"default*?",
},
},
{
Action: kyverno.Audit,
Namespaces: []string{
"default",
"test*",
},
},
},
Rules: []kyverno.Rule{
{
Name: "require-labels",
MatchResources: kyverno.MatchResources{ResourceDescription: kyverno.ResourceDescription{Kinds: []string{"Pod"}}},
Validation: kyverno.Validation{
Message: "label 'app.kubernetes.io/name' is required",
RawPattern: &apiextv1.JSON{Raw: []byte(`"metadata": {"lables": {"app.kubernetes.io/name": "?*"}}`)},
},
},
},
},
},
{
description: "tc13",
spec: &kyverno.Spec{
ValidationFailureAction: kyverno.Enforce,
ValidationFailureActionOverrides: []kyverno.ValidationFailureActionOverride{
{
Action: kyverno.Enforce,
Namespaces: []string{
"default?",
},
},
{
Action: kyverno.Audit,
Namespaces: []string{
"default",
},
},
},
Rules: []kyverno.Rule{
{
Name: "require-labels",
MatchResources: kyverno.MatchResources{ResourceDescription: kyverno.ResourceDescription{Kinds: []string{"Pod"}}},
Validation: kyverno.Validation{
Message: "label 'app.kubernetes.io/name' is required",
RawPattern: &apiextv1.JSON{Raw: []byte(`"metadata": {"lables": {"app.kubernetes.io/name": "?*"}}`)},
},
},
},
},
},
}
for _, tc := range testcases {
t.Run(tc.description, func(t *testing.T) {
err := validateNamespaces(tc.spec, field.NewPath("spec").Child("validationFailureActionOverrides"))
if tc.expectedError != nil {
assert.Error(t, err, tc.expectedError.Error())
} else {
assert.NilError(t, err)
}
})
}
}

View file

@ -358,3 +358,38 @@ func OverrideRuntimeErrorHandler() {
}
}
}
func SeperateWildcards(l []string) (lw []string, rl []string) {
for _, val := range l {
if wildcard.ContainsWildcard(val) {
lw = append(lw, val)
} else {
rl = append(rl, val)
}
}
return lw, rl
}
func CheckWildcardNamespaces(patterns []string, ns []string) (string, string, bool) {
for _, n := range ns {
pat, element, boolval := containsNamespaceWithStringReturn(patterns, n)
if boolval {
return pat, element, true
}
}
return "", "", false
}
func containsWithStringReturn(list []string, element string, fn func(string, string) bool) (string, string, bool) {
for _, e := range list {
if fn(e, element) {
return e, element, true
}
}
return "", "", false
}
// containsNamespaceWithStringReturn check if namespace satisfies any list of pattern(regex)
func containsNamespaceWithStringReturn(patterns []string, ns string) (string, string, bool) {
return containsWithStringReturn(patterns, ns, comparePatterns)
}

View file

@ -202,5 +202,182 @@ func Test_ConvertResource(t *testing.T) {
assert.Assert(t, resource.GetNamespace() == test.expectedNamespace)
break
}
}
func Test_SeperateWildcards(t *testing.T) {
testcases := []struct {
description string
inputList []string
expList1 []string
expList2 []string
}{
{
description: "tc1",
inputList: []string{"test*", "default", "default1", "hello"},
expList1: []string{"test*"},
expList2: []string{"default", "default1", "hello"},
},
{
description: "tc2",
inputList: []string{"test*", "default*", "default1?", "hello?"},
expList1: []string{"test*", "default*", "default1?", "hello?"},
expList2: nil,
},
{
description: "tc3",
inputList: []string{"test", "default", "default1", "hello"},
expList1: nil,
expList2: []string{"test", "default", "default1", "hello"},
},
{
description: "tc4",
inputList: nil,
expList1: nil,
expList2: nil,
},
}
for _, tc := range testcases {
t.Run(tc.description, func(t *testing.T) {
list1, list2 := SeperateWildcards(tc.inputList)
assert.DeepEqual(t, list1, tc.expList1)
assert.DeepEqual(t, list2, tc.expList2)
})
}
}
func Test_CheckWildcardNamespaces(t *testing.T) {
testcases := []struct {
description string
inputPatterns []string
inputNs []string
expString1 string
expString2 string
expBool bool
}{
{
description: "tc1",
inputPatterns: []string{"default*", "test*"},
inputNs: []string{"default", "default1"},
expString1: "default*",
expString2: "default",
expBool: true,
},
{
description: "tc2",
inputPatterns: []string{"test*"},
inputNs: []string{"default1", "test"},
expString1: "test*",
expString2: "test",
expBool: true,
},
{
description: "tc3",
inputPatterns: []string{"*"},
inputNs: []string{"default1", "test"},
expString1: "*",
expString2: "default1",
expBool: true,
},
{
description: "tc4",
inputPatterns: []string{"a*"},
inputNs: []string{"default1", "test"},
expString1: "",
expString2: "",
expBool: false,
},
{
description: "tc5",
inputPatterns: nil,
inputNs: []string{"default1", "test"},
expString1: "",
expString2: "",
expBool: false,
},
{
description: "tc6",
inputPatterns: []string{"*"},
inputNs: nil,
expString1: "",
expString2: "",
expBool: false,
},
{
description: "tc7",
inputPatterns: nil,
inputNs: nil,
expString1: "",
expString2: "",
expBool: false,
},
}
for _, tc := range testcases {
t.Run(tc.description, func(t *testing.T) {
str1, str2, actualBool := CheckWildcardNamespaces(tc.inputPatterns, tc.inputNs)
assert.Equal(t, str1, tc.expString1)
assert.Equal(t, str2, tc.expString2)
assert.Equal(t, actualBool, tc.expBool)
})
}
}
func Test_containsNamespaceWithStringReturn(t *testing.T) {
testcases := []struct {
description string
inputPattern []string
inputNs string
expStr1 string
expStr2 string
expBool bool
}{
{
description: "tc1",
inputPattern: []string{"default*"},
inputNs: "default",
expStr1: "default*",
expStr2: "default",
expBool: true,
},
{
description: "tc2",
inputPattern: []string{"*"},
inputNs: "default",
expStr1: "*",
expStr2: "default",
expBool: true,
},
{
description: "tc3",
inputPattern: []string{"*"},
inputNs: "default",
expStr1: "*",
expStr2: "default",
expBool: true,
},
{
description: "tc4",
inputPattern: nil,
inputNs: "default",
expStr1: "",
expStr2: "",
expBool: false,
},
{
description: "tc5",
inputPattern: nil,
inputNs: "",
expStr1: "",
expStr2: "",
expBool: false,
},
}
for _, tc := range testcases {
t.Run(tc.description, func(t *testing.T) {
str1, str2, actualBool := containsNamespaceWithStringReturn(tc.inputPattern, tc.inputNs)
assert.Equal(t, str1, tc.expStr1)
assert.Equal(t, str2, tc.expStr2)
assert.Equal(t, actualBool, tc.expBool)
})
}
}