diff --git a/pkg/engine/generation.go b/pkg/engine/generation.go index 450d437b52..01e3aa00ed 100644 --- a/pkg/engine/generation.go +++ b/pkg/engine/generation.go @@ -7,7 +7,6 @@ import ( client "github.com/nirmata/kube-policy/client" kubepolicy "github.com/nirmata/kube-policy/pkg/apis/policy/v1alpha1" - "github.com/nirmata/kube-policy/pkg/engine/mutation" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -30,7 +29,7 @@ func Generate(client *client.Client, logger *log.Logger, policy kubepolicy.Polic continue } - ok, err := mutation.ResourceMeetsRules(rawResource, rule.ResourceDescription, gvk) + ok, err := ResourceMeetsRules(rawResource, rule.ResourceDescription, gvk) if err != nil { logger.Printf("Rule has invalid data: rule number = %d, rule name = %s in policy %s, err: %v\n", i, rule.Name, policy.ObjectMeta.Name, err) continue @@ -60,7 +59,7 @@ func applyRuleGenerator(client *client.Client, rawResource []byte, generator *ku return fmt.Errorf("Generator for '%s' is invalid: %s", generator.Kind, err) } - namespaceName := mutation.ParseNameFromObject(rawResource) + namespaceName := ParseNameFromObject(rawResource) // Generate the resource switch gvk.Kind { case "configmap": diff --git a/pkg/engine/mutation.go b/pkg/engine/mutation.go index fc83006bba..ec946f10db 100644 --- a/pkg/engine/mutation.go +++ b/pkg/engine/mutation.go @@ -26,7 +26,7 @@ func Mutate(policy kubepolicy.Policy, rawResource []byte, gvk metav1.GroupVersio continue } - ok, err := mutation.ResourceMeetsRules(rawResource, rule.ResourceDescription, gvk) + ok, err := ResourceMeetsRules(rawResource, rule.ResourceDescription, gvk) if err != nil { log.Printf("Rule has invalid data: rule number = %d, rule name = %s in policy %s, err: %v\n", i, rule.Name, policy.ObjectMeta.Name, err) continue diff --git a/pkg/engine/mutation/utils_test.go b/pkg/engine/mutation/utils_test.go deleted file mode 100644 index f8473ae287..0000000000 --- a/pkg/engine/mutation/utils_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package mutation - -import ( - "testing" -) - -func assertEqDataImpl(t *testing.T, expected, actual []byte, formatModifier string) { - if len(expected) != len(actual) { - t.Errorf("len(expected) != len(actual): %d != %d\n1:"+formatModifier+"\n2:"+formatModifier, len(expected), len(actual), expected, actual) - return - } - - for idx, val := range actual { - if val != expected[idx] { - t.Errorf("Slices not equal at index %d:\n1:"+formatModifier+"\n2:"+formatModifier, idx, expected, actual) - } - } -} - -func assertEqData(t *testing.T, expected, actual []byte) { - assertEqDataImpl(t, expected, actual, "%x") -} - -func assertEqStringAndData(t *testing.T, str string, data []byte) { - assertEqDataImpl(t, []byte(str), data, "%s") -} diff --git a/pkg/engine/mutation/utils.go b/pkg/engine/utils.go similarity index 99% rename from pkg/engine/mutation/utils.go rename to pkg/engine/utils.go index ad433932be..f4ce55a8cb 100644 --- a/pkg/engine/mutation/utils.go +++ b/pkg/engine/utils.go @@ -1,4 +1,4 @@ -package mutation +package engine import ( "encoding/json" @@ -6,10 +6,46 @@ import ( "github.com/minio/minio/pkg/wildcard" kubepolicy "github.com/nirmata/kube-policy/pkg/apis/policy/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" ) +// ResourceMeetsRules checks requests kind, name and labels to fit the policy +func ResourceMeetsRules(resourceRaw []byte, description kubepolicy.ResourceDescription, gvk metav1.GroupVersionKind) (bool, error) { + if description.Kind != gvk.Kind { + return false, nil + } + + if resourceRaw != nil { + meta := ParseMetadataFromObject(resourceRaw) + name := ParseNameFromObject(resourceRaw) + + if description.Name != nil { + + if !wildcard.Match(*description.Name, name) { + return false, nil + } + } + + if description.Selector != nil { + selector, err := metav1.LabelSelectorAsSelector(description.Selector) + + if err != nil { + return false, err + } + + labelMap := ParseLabelsFromMetadata(meta) + + if !selector.Matches(labelMap) { + return false, nil + } + + } + } + return true, nil +} + func ParseMetadataFromObject(bytes []byte) map[string]interface{} { var objectJSON map[string]interface{} json.Unmarshal(bytes, &objectJSON) @@ -68,38 +104,3 @@ func ParseRegexPolicyResourceName(policyResourceName string) (string, bool) { } return strings.Trim(regex[1], " "), true } - -// ResourceMeetsRules checks requests kind, name and labels to fit the policy -func ResourceMeetsRules(resourceRaw []byte, description kubepolicy.ResourceDescription, gvk metav1.GroupVersionKind) (bool, error) { - if description.Kind != gvk.Kind { - return false, nil - } - - if resourceRaw != nil { - meta := ParseMetadataFromObject(resourceRaw) - name := ParseNameFromObject(resourceRaw) - - if description.Name != nil { - - if !wildcard.Match(*description.Name, name) { - return false, nil - } - } - - if description.Selector != nil { - selector, err := metav1.LabelSelectorAsSelector(description.Selector) - - if err != nil { - return false, err - } - - labelMap := ParseLabelsFromMetadata(meta) - - if !selector.Matches(labelMap) { - return false, nil - } - - } - } - return true, nil -} diff --git a/pkg/engine/validation.go b/pkg/engine/validation.go index c22005e99b..d5a4c62dac 100644 --- a/pkg/engine/validation.go +++ b/pkg/engine/validation.go @@ -5,11 +5,14 @@ import ( "fmt" "log" + "github.com/minio/minio/pkg/wildcard" + kubepolicy "github.com/nirmata/kube-policy/pkg/apis/policy/v1alpha1" - "github.com/nirmata/kube-policy/pkg/engine/mutation" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +// Validate handles validating admission request +// Checks the target resourse for rules defined in the policy func Validate(policy kubepolicy.Policy, rawResource []byte, gvk metav1.GroupVersionKind) bool { var resource interface{} json.Unmarshal(rawResource, &resource) @@ -28,7 +31,7 @@ func Validate(policy kubepolicy.Policy, rawResource []byte, gvk metav1.GroupVers continue } - ok, err := mutation.ResourceMeetsRules(rawResource, rule.ResourceDescription, gvk) + ok, err := ResourceMeetsRules(rawResource, rule.ResourceDescription, gvk) if err != nil { log.Printf("Rule has invalid data: rule number = %d, rule name = %s in policy %s, err: %v\n", i, rule.Name, policy.ObjectMeta.Name, err) continue @@ -43,60 +46,196 @@ func Validate(policy kubepolicy.Policy, rawResource []byte, gvk metav1.GroupVers continue } - if err := traverseAndValidate(resource, rule.Validation.Pattern); err != nil { - log.Printf("Validation with the rule %s has failed %s: %s\n", rule.Name, err.Error(), *rule.Validation.Message) + if !validateMap(resource, rule.Validation.Pattern) { + log.Printf("Validation with the rule %s has failed: %s\n", rule.Name, *rule.Validation.Message) allowed = false } else { - log.Printf("Validation rule %s is successful %s: %s\n", rule.Name, err.Error(), *rule.Validation.Message) + log.Printf("Validation rule %s is successful\n", rule.Name) } } return allowed } -func traverseAndValidate(resourcePart, patternPart interface{}) error { +func validateMap(resourcePart, patternPart interface{}) bool { + pattern := patternPart.(map[string]interface{}) + resource, ok := resourcePart.(map[string]interface{}) + + if !ok { + fmt.Printf("Validating error: expected Map, found %T\n", resourcePart) + return false + } + + for key, value := range pattern { + if wrappedWithParentheses(key) { + key = key[1 : len(key)-1] + } + + if !validateMapElement(resource[key], value) { + return false + } + } + + return true +} + +func validateArray(resourcePart, patternPart interface{}) bool { + patternArray := patternPart.([]interface{}) + resourceArray, ok := resourcePart.([]interface{}) + + if !ok { + fmt.Printf("Validating error: expected array, found %T\n", resourcePart) + return false + } + + switch pattern := patternArray[0].(type) { + case map[string]interface{}: + anchors, err := getAnchorsFromMap(pattern) + if err != nil { + fmt.Printf("Validating error: %v\n", err) + return false + } + + for _, value := range resourceArray { + resource, ok := value.(map[string]interface{}) + if !ok { + fmt.Printf("Validating error: expected Map, found %T\n", resourcePart) + return false + } + + if skipArrayObject(resource, anchors) { + continue + } + + if !validateMap(resource, pattern) { + return false + } + } + + return true + default: + for _, value := range resourceArray { + if !checkSingleValue(value, patternArray[0]) { + return false + } + } + } + + return true +} + +func validateMapElement(resourcePart, patternPart interface{}) bool { switch pattern := patternPart.(type) { case map[string]interface{}: dictionary, ok := resourcePart.(map[string]interface{}) if !ok { - return fmt.Errorf("Validating error: expected %T, found %T", patternPart, resourcePart) + fmt.Printf("Validating error: expected %T, found %T\n", patternPart, resourcePart) + return false } - var err error - for key, value := range pattern { - err = traverseAndValidate(dictionary[key], value) - } - return err - + return validateMap(dictionary, pattern) case []interface{}: array, ok := resourcePart.([]interface{}) if !ok { - return fmt.Errorf("Validating error: expected %T, found %T", patternPart, resourcePart) + fmt.Printf("Validating error: expected %T, found %T\n", patternPart, resourcePart) + return false } - var err error - for i, value := range pattern { - err = traverseAndValidate(array[i], value) - } - return err + return validateArray(array, pattern) case string: - str := resourcePart.(string) - if !checkForWildcard(str, pattern) { - return fmt.Errorf("Value %s has not passed wildcard check %s", str, pattern) + str, ok := resourcePart.(string) + + if !ok { + fmt.Printf("Validating error: expected %T, found %T\n", patternPart, resourcePart) + return false } + + return checkSingleValue(str, pattern) default: - return fmt.Errorf("Received unknown type: %T", patternPart) + fmt.Printf("Validating error: unknown type in map: %T\n", patternPart) + return false + } +} + +func getAnchorsFromMap(pattern map[string]interface{}) (map[string]interface{}, error) { + result := make(map[string]interface{}) + + for key, value := range pattern { + if wrappedWithParentheses(key) { + result[key] = value + } } - return nil + return result, nil +} + +func skipArrayObject(object, anchors map[string]interface{}) bool { + for key, pattern := range anchors { + key = key[1 : len(key)-1] + + value, ok := object[key] + if !ok { + return true + } + + if !checkSingleValue(value, pattern) { + return true + } + } + + return false +} + +func checkSingleValue(value, pattern interface{}) bool { + switch typedPattern := pattern.(type) { + case string: + switch typedValue := value.(type) { + case string: + return checkForWildcard(typedValue, typedPattern) + case float64: + return checkForOperator(typedValue, typedPattern) + case int: + return checkForOperator(float64(typedValue), typedPattern) + default: + fmt.Printf("Validating error: expected string or numerical type, found %T, pattern: %s\n", value, typedPattern) + return false + } + case float64: + num, ok := value.(float64) + if !ok { + fmt.Printf("Validating error: expected float, found %T\n", value) + return false + } + + return typedPattern == num + case int: + num, ok := value.(int) + if !ok { + fmt.Printf("Validating error: expected int, found %T\n", value) + return false + } + + return typedPattern == num + default: + fmt.Printf("Validating error: expected pattern (string or numerical type), found %T\n", pattern) + return false + } } func checkForWildcard(value, pattern string) bool { - return value == pattern + return wildcard.Match(pattern, value) } -func checkForOperator(value int, pattern string) bool { +func checkForOperator(value float64, pattern string) bool { return true } + +func wrappedWithParentheses(str string) bool { + if len(str) < 2 { + return false + } + + return (str[0] == '(' && str[len(str)-1] == ')') +} diff --git a/pkg/engine/validation_test.go b/pkg/engine/validation_test.go new file mode 100644 index 0000000000..5302fd6988 --- /dev/null +++ b/pkg/engine/validation_test.go @@ -0,0 +1,234 @@ +package engine + +import ( + "encoding/json" + "testing" + + "gotest.tools/assert" +) + +func TestWrappedWithParentheses_StringIsWrappedWithParentheses(t *testing.T) { + str := "(something)" + assert.Assert(t, wrappedWithParentheses(str)) +} + +func TestWrappedWithParentheses_StringHasOnlyParentheses(t *testing.T) { + str := "()" + assert.Assert(t, wrappedWithParentheses(str)) +} + +func TestWrappedWithParentheses_StringHasNoParentheses(t *testing.T) { + str := "something" + assert.Assert(t, !wrappedWithParentheses(str)) +} + +func TestWrappedWithParentheses_StringHasLeftParentheses(t *testing.T) { + str := "(something" + assert.Assert(t, !wrappedWithParentheses(str)) +} + +func TestWrappedWithParentheses_StringHasRightParentheses(t *testing.T) { + str := "something)" + assert.Assert(t, !wrappedWithParentheses(str)) +} + +func TestWrappedWithParentheses_StringParenthesesInside(t *testing.T) { + str := "so)m(et(hin)g" + assert.Assert(t, !wrappedWithParentheses(str)) +} + +func TestWrappedWithParentheses_Empty(t *testing.T) { + str := "" + assert.Assert(t, !wrappedWithParentheses(str)) +} + +func TestCheckForWildcard_AsteriskTest(t *testing.T) { + pattern := "*" + value := "anything" + empty := "" + + assert.Assert(t, checkForWildcard(value, pattern)) + assert.Assert(t, checkForWildcard(empty, pattern)) +} + +func TestCheckForWildcard_LeftAsteriskTest(t *testing.T) { + pattern := "*right" + value := "leftright" + right := "right" + + assert.Assert(t, checkForWildcard(value, pattern)) + assert.Assert(t, checkForWildcard(right, pattern)) + + value = "leftmiddle" + middle := "middle" + + assert.Assert(t, !checkForWildcard(value, pattern)) + assert.Assert(t, !checkForWildcard(middle, pattern)) +} + +func TestCheckForWildcard_MiddleAsteriskTest(t *testing.T) { + pattern := "ab*ba" + value := "abbba" + assert.Assert(t, checkForWildcard(value, pattern)) + + value = "abbca" + assert.Assert(t, !checkForWildcard(value, pattern)) +} + +func TestCheckForWildcard_QuestionMark(t *testing.T) { + pattern := "ab?ba" + value := "abbba" + assert.Assert(t, checkForWildcard(value, pattern)) + + value = "abbbba" + assert.Assert(t, !checkForWildcard(value, pattern)) +} + +func TestCheckSingleValue_CheckInt(t *testing.T) { + pattern := 89 + value := 89 + assert.Assert(t, checkSingleValue(value, pattern)) + + value = 202 + assert.Assert(t, !checkSingleValue(value, pattern)) +} + +func TestCheckSingleValue_CheckFloat(t *testing.T) { + pattern := 89.9091 + value := 89.9091 + assert.Assert(t, checkSingleValue(value, pattern)) + + value = 89.9092 + assert.Assert(t, !checkSingleValue(value, pattern)) +} + +func TestCheckSingleValue_CheckOperatorMore(t *testing.T) { + pattern := ">10" + value := 89 + assert.Assert(t, checkSingleValue(value, pattern)) + + pattern = ">10" + floatValue := 89.901 + assert.Assert(t, checkSingleValue(floatValue, pattern)) +} + +func TestCheckSingleValue_CheckWildcard(t *testing.T) { + pattern := "nirmata_*" + value := "nirmata_awesome" + assert.Assert(t, checkSingleValue(value, pattern)) + + pattern = "nirmata_*" + value = "spasex_awesome" + assert.Assert(t, !checkSingleValue(value, pattern)) + + pattern = "g?t" + value = "git" + assert.Assert(t, checkSingleValue(value, pattern)) +} + +func TestSkipArrayObject_OneAnchor(t *testing.T) { + + rawAnchors := []byte(`{"(name)": "nirmata-*"}`) + rawResource := []byte(`{"name": "nirmata-resource", "namespace": "kube-policy", "object": { "label": "app", "array": [ 1, 2, 3 ]}}`) + + var resource, anchor map[string]interface{} + + json.Unmarshal(rawAnchors, &anchor) + json.Unmarshal(rawResource, &resource) + + assert.Assert(t, !skipArrayObject(resource, anchor)) +} + +func TestSkipArrayObject_OneNumberAnchorPass(t *testing.T) { + + rawAnchors := []byte(`{"(count)": 1}`) + rawResource := []byte(`{"name": "nirmata-resource", "count": 1, "namespace": "kube-policy", "object": { "label": "app", "array": [ 1, 2, 3 ]}}`) + + var resource, anchor map[string]interface{} + + json.Unmarshal(rawAnchors, &anchor) + json.Unmarshal(rawResource, &resource) + + assert.Assert(t, !skipArrayObject(resource, anchor)) +} + +func TestSkipArrayObject_TwoAnchorsPass(t *testing.T) { + rawAnchors := []byte(`{"(name)": "nirmata-*", "(namespace)": "kube-?olicy"}`) + rawResource := []byte(`{"name": "nirmata-resource", "namespace": "kube-policy", "object": { "label": "app", "array": [ 1, 2, 3 ]}}`) + + var resource, anchor map[string]interface{} + + json.Unmarshal(rawAnchors, &anchor) + json.Unmarshal(rawResource, &resource) + + assert.Assert(t, !skipArrayObject(resource, anchor)) +} + +func TestSkipArrayObject_TwoAnchorsSkip(t *testing.T) { + rawAnchors := []byte(`{"(name)": "nirmata-*", "(namespace)": "some-?olicy"}`) + rawResource := []byte(`{"name": "nirmata-resource", "namespace": "kube-policy", "object": { "label": "app", "array": [ 1, 2, 3 ]}}`) + + var resource, anchor map[string]interface{} + + json.Unmarshal(rawAnchors, &anchor) + json.Unmarshal(rawResource, &resource) + + assert.Assert(t, skipArrayObject(resource, anchor)) +} + +func TestGetAnchorsFromMap_ThereAreAnchors(t *testing.T) { + rawMap := []byte(`{"(name)": "nirmata-*", "notAnchor1": 123, "(namespace)": "kube-?olicy", "notAnchor2": "sample-text", "object": { "key1": "value1", "(key2)": "value2"}}`) + + var unmarshalled map[string]interface{} + json.Unmarshal(rawMap, &unmarshalled) + + actualMap, err := getAnchorsFromMap(unmarshalled) + assert.NilError(t, err) + assert.Equal(t, len(actualMap), 2) + assert.Equal(t, actualMap["(name)"].(string), "nirmata-*") + assert.Equal(t, actualMap["(namespace)"].(string), "kube-?olicy") +} + +func TestGetAnchorsFromMap_ThereAreNoAnchors(t *testing.T) { + rawMap := []byte(`{"name": "nirmata-*", "notAnchor1": 123, "namespace": "kube-?olicy", "notAnchor2": "sample-text", "object": { "key1": "value1", "(key2)": "value2"}}`) + + var unmarshalled map[string]interface{} + json.Unmarshal(rawMap, &unmarshalled) + + actualMap, err := getAnchorsFromMap(unmarshalled) + assert.NilError(t, err) + assert.Assert(t, len(actualMap) == 0) +} + +func TestValidateMapElement_TwoElementsInArrayOnePass(t *testing.T) { + rawPattern := []byte(`[ { "(name)": "nirmata-*", "object": [ { "(key1)": "value*", "key2": "value*" } ] } ]`) + rawMap := []byte(`[ { "name": "nirmata-1", "object": [ { "key1": "value1", "key2": "value2" } ] }, { "name": "nirmata-1", "object": [ { "key1": "not_value", "key2": "not_value" } ] } ]`) + + var pattern, resource interface{} + json.Unmarshal(rawPattern, &pattern) + json.Unmarshal(rawMap, &resource) + + assert.Assert(t, validateMapElement(resource, pattern)) +} + +func TestValidateMapElement_OneElementInArrayPass(t *testing.T) { + rawPattern := []byte(`[ { "(name)": "nirmata-*", "object": [ { "(key1)": "value*", "key2": "value*" } ] } ]`) + rawMap := []byte(`[ { "name": "nirmata-1", "object": [ { "key1": "value1", "key2": "value2" } ] } ]`) + + var pattern, resource interface{} + json.Unmarshal(rawPattern, &pattern) + json.Unmarshal(rawMap, &resource) + + assert.Assert(t, validateMapElement(resource, pattern)) +} + +func TestValidateMapElement_OneElementInArrayNotPass(t *testing.T) { + rawPattern := []byte(`[{"(name)": "nirmata-*", "object":[{"(key1)": "value*", "key2": "value*"}]}]`) + rawMap := []byte(`[ { "name": "nirmata-1", "object": [ { "key1": "value5", "key2": "1value1" } ] } ]`) + + var pattern, resource interface{} + json.Unmarshal(rawPattern, &pattern) + json.Unmarshal(rawMap, &resource) + + assert.Assert(t, !validateMapElement(resource, pattern)) +} diff --git a/pkg/webhooks/server.go b/pkg/webhooks/server.go index 4bdb60de9e..8af8b951d0 100644 --- a/pkg/webhooks/server.go +++ b/pkg/webhooks/server.go @@ -148,8 +148,8 @@ func (ws *WebhookServer) HandleMutation(request *v1beta1.AdmissionRequest) *v1be allPatches = append(allPatches, policyPatches...) if len(policyPatches) > 0 { - namespace := mutation.ParseNamespaceFromObject(request.Object.Raw) - name := mutation.ParseNameFromObject(request.Object.Raw) + namespace := engine.ParseNamespaceFromObject(request.Object.Raw) + name := engine.ParseNameFromObject(request.Object.Raw) ws.logger.Printf("Policy %s applied to %s %s/%s", policy.Name, request.Kind.Kind, namespace, name) } }