diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go new file mode 100644 index 0000000000..06d8ea22a2 --- /dev/null +++ b/pkg/auth/auth.go @@ -0,0 +1,108 @@ +package auth + +import ( + "fmt" + "reflect" + + "github.com/golang/glog" + client "github.com/nirmata/kyverno/pkg/dclient" + authorizationv1 "k8s.io/api/authorization/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +//CanIOptions provides utility ti check if user has authorization for the given operation +type CanIOptions struct { + namespace string + verb string + kind string + client *client.Client +} + +//NewCanI returns a new instance of operation access controler evaluator +func NewCanI(client *client.Client, kind, namespace, verb string) *CanIOptions { + o := CanIOptions{ + client: client, + } + + o.namespace = namespace + o.kind = kind + o.verb = verb + + return &o +} + +//RunAccessCheck checks if the caller can perform the operation +// - operation is a combination of namespace, kind, verb +// - can only evaluate a single verb +// - group version resource is determined from the kind using the discovery client REST mapper +// - If disallowed, the reason and evaluationError is avialable in the logs +// - each can generates a SelfSubjectAccessReview resource and response is evaluated for permissions +func (o *CanIOptions) RunAccessCheck() (bool, error) { + // get GroupVersionResource from RESTMapper + // get GVR from kind + gvr := o.client.DiscoveryClient.GetGVRFromKind(o.kind) + if reflect.DeepEqual(gvr, schema.GroupVersionResource{}) { + // cannot find GVR + return false, fmt.Errorf("failed to get the Group Version Resource for kind %s", o.kind) + } + + var sar *authorizationv1.SelfSubjectAccessReview + + sar = &authorizationv1.SelfSubjectAccessReview{ + Spec: authorizationv1.SelfSubjectAccessReviewSpec{ + ResourceAttributes: &authorizationv1.ResourceAttributes{ + Namespace: o.namespace, + Verb: o.verb, + Group: gvr.Group, + Resource: gvr.Resource, + }, + }, + } + // Set self subject access review + // - namespace + // - verb + // - resource + // - subresource + + // Create the Resource + resp, err := o.client.CreateResource("SelfSubjectAccessReview", "", sar, false) + if err != nil { + glog.Errorf("failed to create resource %s/%s/%s", sar.Kind, sar.Namespace, sar.Name) + return false, err + } + + // status.allowed + allowed, ok, err := unstructured.NestedBool(resp.Object, "status", "allowed") + if !ok { + if err != nil { + glog.Errorf("unexpected error when getting status.allowed for %s/%s/%s", sar.Kind, sar.Namespace, sar.Name) + } + glog.Errorf("status.allowed not found for %s/%s/%s", sar.Kind, sar.Namespace, sar.Name) + } + + if !allowed { + // status.reason + reason, ok, err := unstructured.NestedString(resp.Object, "status", "reason") + if !ok { + if err != nil { + glog.Errorf("unexpected error when getting status.reason for %s/%s/%s", sar.Kind, sar.Namespace, sar.Name) + } + glog.Errorf("status.reason not found for %s/%s/%s", sar.Kind, sar.Namespace, sar.Name) + } + // status.evaluationError + evaluationError, ok, err := unstructured.NestedString(resp.Object, "status", "evaludationError") + if !ok { + if err != nil { + glog.Errorf("unexpected error when getting status.evaluationError for %s/%s/%s", sar.Kind, sar.Namespace, sar.Name) + } + glog.Errorf("status.evaluationError not found for %s/%s/%s", sar.Kind, sar.Namespace, sar.Name) + } + + // Reporting ? (just logs) + glog.Errorf("reason to disallow operation: %s", reason) + glog.Errorf("evaluationError to disallow operation: %s", evaluationError) + } + + return allowed, nil +} diff --git a/pkg/auth/auth_test.go b/pkg/auth/auth_test.go new file mode 100644 index 0000000000..6dbc800285 --- /dev/null +++ b/pkg/auth/auth_test.go @@ -0,0 +1,48 @@ +package auth + +// import ( +// "testing" +// "time" + +// "github.com/golang/glog" +// "github.com/nirmata/kyverno/pkg/config" +// dclient "github.com/nirmata/kyverno/pkg/dclient" +// "github.com/nirmata/kyverno/pkg/signal" +// ) + +// func Test_Auth_pass(t *testing.T) { +// // needs running cluster +// var kubeconfig string +// stopCh := signal.SetupSignalHandler() +// kubeconfig = "/Users/shivd/.kube/config" +// clientConfig, err := config.CreateClientConfig(kubeconfig) +// if err != nil { +// glog.Fatalf("Error building kubeconfig: %v\n", err) +// } + +// // DYNAMIC CLIENT +// // - client for all registered resources +// // - invalidate local cache of registered resource every 10 seconds +// client, err := dclient.NewClient(clientConfig, 10*time.Second, stopCh) +// if err != nil { +// glog.Fatalf("Error creating client: %v\n", err) +// } + +// // Can i authenticate + +// kind := "Deployment" +// namespace := "default" +// verb := "test" +// canI := NewCanI(client, kind, namespace, verb) +// ok, err := canI.RunAccessCheck() +// if err != nil { +// t.Error(err) +// } +// if ok { +// t.Log("allowed") +// } else { +// t.Log("notallowed") +// } +// t.FailNow() + +// } diff --git a/pkg/engine/variables/common.go b/pkg/engine/variables/common.go new file mode 100644 index 0000000000..751040a03d --- /dev/null +++ b/pkg/engine/variables/common.go @@ -0,0 +1,14 @@ +package variables + +import "regexp" + +//IsVariable returns true if the element contains a 'valid' variable {{}} +func IsVariable(element string) bool { + validRegex := regexp.MustCompile(variableRegex) + groups := validRegex.FindAllStringSubmatch(element, -1) + if len(groups) == 0 { + // there was no match + return false + } + return true +} diff --git a/pkg/policy/actions.go b/pkg/policy/actions.go new file mode 100644 index 0000000000..cdf37ed1a8 --- /dev/null +++ b/pkg/policy/actions.go @@ -0,0 +1,50 @@ +package policy + +import ( + "fmt" + + kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1" + dclient "github.com/nirmata/kyverno/pkg/dclient" + "github.com/nirmata/kyverno/pkg/policy/generate" + "github.com/nirmata/kyverno/pkg/policy/mutate" + "github.com/nirmata/kyverno/pkg/policy/validate" +) + +//Validation provides methods to validate a rule +type Validation interface { + Validate() (string, error) +} + +//validateAction performs validation on the rule actions +// - Mutate +// - Validation +// - Generate +func validateActions(idx int, rule kyverno.Rule, client *dclient.Client) error { + var checker Validation + + // Mutate + if rule.HasMutate() { + checker = mutate.NewMutateFactory(rule.Mutation) + if path, err := checker.Validate(); err != nil { + return fmt.Errorf("path: spec.rules[%d].mutate.%s.: %v", idx, path, err) + } + } + + // Validate + if rule.HasValidate() { + 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) + } + } + + // Generate + if rule.HasGenerate() { + checker = generate.NewGenerateFactory(client, rule.Generation) + if path, err := checker.Validate(); err != nil { + return fmt.Errorf("path: spec.rules[%d].generate.%s.: %v", idx, path, err) + } + } + + return nil +} diff --git a/pkg/policy/common/common.go b/pkg/policy/common/common.go new file mode 100644 index 0000000000..35bc132007 --- /dev/null +++ b/pkg/policy/common/common.go @@ -0,0 +1,85 @@ +package common + +import ( + "fmt" + "regexp" + "strconv" + + "github.com/nirmata/kyverno/pkg/engine/anchor" +) + +//ValidatePattern validates the pattern +func ValidatePattern(patternElement interface{}, path string, supportedAnchors []anchor.IsAnchor) (string, error) { + switch typedPatternElement := patternElement.(type) { + case map[string]interface{}: + return validateMap(typedPatternElement, path, supportedAnchors) + case []interface{}: + return validateArray(typedPatternElement, path, supportedAnchors) + case string, float64, int, int64, bool, nil: + //TODO? check operator + return "", nil + default: + return path, fmt.Errorf("Validation rule failed at '%s', pattern contains unknown type", path) + } +} +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 () + re, err := regexp.Compile(`^.?\(.+\)$`) + if err != nil { + return path + "/" + key, fmt.Errorf("Unable to parse the field %s: %v", key, err) + } + + matched := re.MatchString(key) + // 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 existence anchor + // value must be of type list + if anchor.IsExistenceAnchor(key) { + typedValue, ok := value.([]interface{}) + if !ok { + return path + "/" + key, fmt.Errorf("Existence 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("Existence anchor: single value expected, multiple specified") + } + } + } + // lets validate the values now :) + if errPath, err := ValidatePattern(value, path+"/"+key, supportedAnchors); err != nil { + return errPath, err + } + } + return "", nil +} + +func validateArray(patternArray []interface{}, path string, supportedAnchors []anchor.IsAnchor) (string, error) { + for i, patternElement := range patternArray { + currentPath := path + strconv.Itoa(i) + "/" + // 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/policy/generate/auth.go b/pkg/policy/generate/auth.go new file mode 100644 index 0000000000..0bcd0cb0a1 --- /dev/null +++ b/pkg/policy/generate/auth.go @@ -0,0 +1,71 @@ +package generate + +import ( + "github.com/nirmata/kyverno/pkg/auth" + dclient "github.com/nirmata/kyverno/pkg/dclient" +) + +//Operations provides methods to performing operations on resource +type Operations interface { + // CanICreate returns 'true' if self can 'create' resource + CanICreate(kind, namespace string) (bool, error) + // CanIUpdate returns 'true' if self can 'update' resource + CanIUpdate(kind, namespace string) (bool, error) + // CanIDelete returns 'true' if self can 'delete' resource + CanIDelete(kind, namespace string) (bool, error) + // CanIGet returns 'true' if self can 'get' resource + CanIGet(kind, namespace string) (bool, error) +} + +//Auth provides implementation to check if caller/self/kyverno has access to perofrm operations +type Auth struct { + client *dclient.Client +} + +//NewAuth returns a new instance of Auth for operations +func NewAuth(client *dclient.Client) *Auth { + a := Auth{ + client: client, + } + return &a +} + +// CanICreate returns 'true' if self can 'create' resource +func (a *Auth) CanICreate(kind, namespace string) (bool, error) { + canI := auth.NewCanI(a.client, kind, namespace, "create") + ok, err := canI.RunAccessCheck() + if err != nil { + return false, err + } + return ok, nil +} + +// CanIUpdate returns 'true' if self can 'update' resource +func (a *Auth) CanIUpdate(kind, namespace string) (bool, error) { + canI := auth.NewCanI(a.client, kind, namespace, "update") + ok, err := canI.RunAccessCheck() + if err != nil { + return false, err + } + return ok, nil +} + +// CanIDelete returns 'true' if self can 'delete' resource +func (a *Auth) CanIDelete(kind, namespace string) (bool, error) { + canI := auth.NewCanI(a.client, kind, namespace, "delete") + ok, err := canI.RunAccessCheck() + if err != nil { + return false, err + } + return ok, nil +} + +// CanIGet returns 'true' if self can 'get' resource +func (a *Auth) CanIGet(kind, namespace string) (bool, error) { + canI := auth.NewCanI(a.client, kind, namespace, "get") + ok, err := canI.RunAccessCheck() + if err != nil { + return false, err + } + return ok, nil +} diff --git a/pkg/policy/generate/fake.go b/pkg/policy/generate/fake.go new file mode 100644 index 0000000000..0d561c7a94 --- /dev/null +++ b/pkg/policy/generate/fake.go @@ -0,0 +1,21 @@ +package generate + +import ( + kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1" + "github.com/nirmata/kyverno/pkg/policy/generate/fake" +) + +//FakeGenerate provides implementation for generate rule processing +// with mocks/fakes for cluster interactions +type FakeGenerate struct { + Generate +} + +//NewFakeGenerate returns a new instance of generatecheck that uses +// fake/mock implementation for operation access(always returns true) +func NewFakeGenerate(rule kyverno.Generation) *FakeGenerate { + g := FakeGenerate{} + g.rule = rule + g.authCheck = fake.NewFakeAuth() + return &g +} diff --git a/pkg/policy/generate/fake/auth.go b/pkg/policy/generate/fake/auth.go new file mode 100644 index 0000000000..3e7467bf06 --- /dev/null +++ b/pkg/policy/generate/fake/auth.go @@ -0,0 +1,31 @@ +package fake + +//FakeAuth providers implementation for testing, retuning true for all operations +type FakeAuth struct { +} + +//NewFakeAuth returns a new instance of Fake Auth that returns true for each operation +func NewFakeAuth() *FakeAuth { + a := FakeAuth{} + return &a +} + +// CanICreate returns 'true' +func (a *FakeAuth) CanICreate(kind, namespace string) (bool, error) { + return true, nil +} + +// CanIUpdate returns 'true' +func (a *FakeAuth) CanIUpdate(kind, namespace string) (bool, error) { + return true, nil +} + +// CanIDelete returns 'true' +func (a *FakeAuth) CanIDelete(kind, namespace string) (bool, error) { + return true, nil +} + +// CanIGet returns 'true' +func (a *FakeAuth) CanIGet(kind, namespace string) (bool, error) { + return true, nil +} diff --git a/pkg/policy/generate/validate.go b/pkg/policy/generate/validate.go new file mode 100644 index 0000000000..230891516b --- /dev/null +++ b/pkg/policy/generate/validate.go @@ -0,0 +1,148 @@ +package generate + +import ( + "fmt" + "reflect" + + "github.com/golang/glog" + kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1" + dclient "github.com/nirmata/kyverno/pkg/dclient" + "github.com/nirmata/kyverno/pkg/engine/anchor" + "github.com/nirmata/kyverno/pkg/engine/variables" + "github.com/nirmata/kyverno/pkg/policy/common" +) + +// Generate provides implementation to validate 'generate' rule +type Generate struct { + // rule to hold 'generate' rule specifications + rule kyverno.Generation + // authCheck to check access for operations + authCheck Operations +} + +//NewGenerateFactory returns a new instance of Generate validation checker +func NewGenerateFactory(client *dclient.Client, rule kyverno.Generation) *Generate { + g := Generate{ + rule: rule, + authCheck: NewAuth(client), + } + + return &g +} + +//Validate validates the generate rule in validation +func (g *Generate) Validate() (string, error) { + rule := g.rule + if rule.Data == nil && rule.Clone == (kyverno.CloneFrom{}) { + return "", fmt.Errorf("clone or data are required") + } + if rule.Data != nil && rule.Clone != (kyverno.CloneFrom{}) { + return "", fmt.Errorf("only one operation allowed per generate rule(data or clone)") + } + kind, name, namespace := rule.Kind, rule.Name, rule.Namespace + + if name == "" { + return "name", fmt.Errorf("name cannot be empty") + } + if kind == "" { + return "kind", fmt.Errorf("kind cannot be empty") + } + // Can I generate resource + + if !reflect.DeepEqual(rule.Clone, kyverno.CloneFrom{}) { + if path, err := g.validateClone(rule.Clone, kind); err != nil { + return fmt.Sprintf("clone.%s", path), err + } + } + if rule.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 := common.ValidatePattern(rule.Data, "/", []anchor.IsAnchor{}); err != nil { + return fmt.Sprintf("data.%s", path), fmt.Errorf("anchors not supported on generate resources: %v", err) + } + } + + // Kyverno generate-controller create/update/deletes the resources specified in generate rule of policy + // kyverno uses SA 'kyverno-service-account' and has default ClusterRoles and ClusterRoleBindings + // instuctions to modify the RBAC for kyverno are mentioned at https://github.com/nirmata/kyverno/blob/master/documentation/installation.md + // - operations required: create/update/delete/get + // If kind and namespace contain variables, then we cannot resolve then so we skip the processing + if err := g.canIGenerate(kind, namespace); err != nil { + return "", err + } + return "", nil +} + +func (g *Generate) validateClone(c kyverno.CloneFrom, kind string) (string, error) { + if c.Name == "" { + return "name", fmt.Errorf("name cannot be empty") + } + if c.Namespace == "" { + return "namespace", fmt.Errorf("namespace cannot be empty") + } + namespace := c.Namespace + // Skip if there is variable defined + if !variables.IsVariable(kind) && !variables.IsVariable(namespace) { + // GET + ok, err := g.authCheck.CanIGet(kind, namespace) + if err != nil { + return "", err + } + if !ok { + return "", fmt.Errorf("kyverno does not have permissions to 'get' resource %s/%s. Update permissions in ClusterRole 'kyverno:generatecontroller'", kind, namespace) + } + } else { + glog.V(4).Info("name & namespace uses variables, so cannot be resolved. Skipping Auth Checks.") + } + return "", nil +} + +//canIGenerate returns a error if kyverno cannot perform oprations +func (g *Generate) canIGenerate(kind, namespace string) error { + // Skip if there is variable defined + authCheck := g.authCheck + if !variables.IsVariable(kind) && !variables.IsVariable(namespace) { + // CREATE + ok, err := authCheck.CanICreate(kind, namespace) + if err != nil { + // machinery error + return err + } + if !ok { + return fmt.Errorf("kyverno does not have permissions to 'create' resource %s/%s. Update permissions in ClusterRole 'kyverno:generatecontroller'", kind, namespace) + } + // UPDATE + ok, err = authCheck.CanIUpdate(kind, namespace) + if err != nil { + // machinery error + return err + } + if !ok { + return fmt.Errorf("kyverno does not have permissions to 'update' resource %s/%s. Update permissions in ClusterRole 'kyverno:generatecontroller'", kind, namespace) + } + // GET + ok, err = authCheck.CanIGet(kind, namespace) + if err != nil { + // machinery error + return err + } + if !ok { + return fmt.Errorf("kyverno does not have permissions to 'get' resource %s/%s. Update permissions in ClusterRole 'kyverno:generatecontroller'", kind, namespace) + } + + // DELETE + ok, err = authCheck.CanIDelete(kind, namespace) + if err != nil { + // machinery error + return err + } + if !ok { + return fmt.Errorf("kyverno does not have permissions to 'delete' resource %s/%s. Update permissions in ClusterRole 'kyverno:generatecontroller'", kind, namespace) + } + + } else { + glog.V(4).Info("name & namespace uses variables, so cannot be resolved. Skipping Auth Checks.") + } + + return nil +} diff --git a/pkg/policy/generate/validate_test.go b/pkg/policy/generate/validate_test.go new file mode 100644 index 0000000000..631aac6ed3 --- /dev/null +++ b/pkg/policy/generate/validate_test.go @@ -0,0 +1,89 @@ +package generate + +import ( + "encoding/json" + "testing" + + kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1" + "gotest.tools/assert" +) + +func Test_Validate_Generate(t *testing.T) { + rawGenerate := []byte(` + { + "kind": "NetworkPolicy", + "name": "defaultnetworkpolicy", + "data": { + "spec": { + "podSelector": {}, + "policyTypes": [ + "Ingress", + "Egress" + ], + "ingress": [ + {} + ], + "egress": [ + {} + ] + } + } + }`) + + var genRule kyverno.Generation + err := json.Unmarshal(rawGenerate, &genRule) + assert.NilError(t, err) + checker := NewFakeGenerate(genRule) + if _, err := checker.Validate(); err != nil { + assert.Assert(t, err != nil) + } +} + +func Test_Validate_Generate_HasAnchors(t *testing.T) { + var err error + rawGenerate := []byte(` + { + "kind": "NetworkPolicy", + "name": "defaultnetworkpolicy", + "data": { + "spec": { + "(podSelector)": {}, + "policyTypes": [ + "Ingress", + "Egress" + ], + "ingress": [ + {} + ], + "egress": [ + {} + ] + } + } + }`) + + var genRule kyverno.Generation + err = json.Unmarshal(rawGenerate, &genRule) + assert.NilError(t, err) + checker := NewFakeGenerate(genRule) + if _, err := checker.Validate(); err != nil { + assert.Assert(t, err != nil) + } + + rawGenerate = []byte(` + { + "kind": "ConfigMap", + "name": "copied-cm", + "clone": { + "^(namespace)": "default", + "name": "game" + } + }`) + + err = json.Unmarshal(rawGenerate, &genRule) + assert.NilError(t, err) + checker = NewFakeGenerate(genRule) + if _, err := checker.Validate(); err != nil { + assert.Assert(t, err != nil) + } +} diff --git a/pkg/policy/mutate/validate.go b/pkg/policy/mutate/validate.go new file mode 100644 index 0000000000..581645740e --- /dev/null +++ b/pkg/policy/mutate/validate.go @@ -0,0 +1,63 @@ +package mutate + +import ( + "errors" + "fmt" + + kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1" + "github.com/nirmata/kyverno/pkg/engine/anchor" + "github.com/nirmata/kyverno/pkg/policy/common" +) + +// Mutate provides implementation to validate 'mutate' rule +type Mutate struct { + // rule to hold 'mutate' rule specifications + rule kyverno.Mutation +} + +//NewMutateFactory returns a new instance of Mutate validation checker +func NewMutateFactory(rule kyverno.Mutation) *Mutate { + m := Mutate{ + rule: rule, + } + return &m +} + +//Validate validates the 'mutate' rul +func (m *Mutate) Validate() (string, error) { + rule := m.rule + // JSON Patches + if len(rule.Patches) != 0 { + for i, patch := range rule.Patches { + if err := validatePatch(patch); err != nil { + return fmt.Sprintf("patch[%d]", i), err + } + } + } + // Overlay + if rule.Overlay != nil { + path, err := common.ValidatePattern(rule.Overlay, "/", []anchor.IsAnchor{anchor.IsConditionAnchor, anchor.IsAddingAnchor}) + if err != nil { + return path, err + } + } + return "", nil +} + +// Validate if all mandatory PolicyPatch fields are set +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) + } + + return nil + } else if pp.Operation == "remove" { + return nil + } + + return fmt.Errorf("Unsupported JSONPatch operation '%s'", pp.Operation) +} diff --git a/pkg/policy/mutate/validate_test.go b/pkg/policy/mutate/validate_test.go new file mode 100644 index 0000000000..84ff0e59b7 --- /dev/null +++ b/pkg/policy/mutate/validate_test.go @@ -0,0 +1,151 @@ +package mutate + +import ( + "encoding/json" + "testing" + + kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1" + "gotest.tools/assert" +) + +func Test_Validate_Mutate_ConditionAnchor(t *testing.T) { + rawMutate := []byte(` + { + "overlay": { + "spec": { + "(serviceAccountName)": "*", + "automountServiceAccountToken": false + } + } + }`) + + var mutate kyverno.Mutation + err := json.Unmarshal(rawMutate, &mutate) + assert.NilError(t, err) + checker := NewMutateFactory(mutate) + if _, err := checker.Validate(); err != nil { + assert.NilError(t, err) + } +} + +func Test_Validate_Mutate_PlusAnchor(t *testing.T) { + rawMutate := []byte(` + { + "overlay": { + "spec": { + "+(serviceAccountName)": "*", + "automountServiceAccountToken": false + } + } + }`) + + var mutate kyverno.Mutation + err := json.Unmarshal(rawMutate, &mutate) + assert.NilError(t, err) + + checker := NewMutateFactory(mutate) + if _, err := checker.Validate(); err != nil { + assert.NilError(t, err) + } +} + +func Test_Validate_Mutate_Mismatched(t *testing.T) { + rawMutate := []byte(` + { + "overlay": { + "spec": { + "^(serviceAccountName)": "*", + "automountServiceAccountToken": false + } + } + }`) + + var mutateExistence kyverno.Mutation + err := json.Unmarshal(rawMutate, &mutateExistence) + assert.NilError(t, err) + + checker := NewMutateFactory(mutateExistence) + if _, err := checker.Validate(); err != nil { + assert.Assert(t, err != nil) + } + + var mutateEqual kyverno.Mutation + rawMutate = []byte(` + { + "overlay": { + "spec": { + "=(serviceAccountName)": "*", + "automountServiceAccountToken": false + } + } + }`) + + err = json.Unmarshal(rawMutate, &mutateEqual) + assert.NilError(t, err) + + checker = NewMutateFactory(mutateEqual) + if _, err := checker.Validate(); err != nil { + assert.Assert(t, err != nil) + } + + var mutateNegation kyverno.Mutation + rawMutate = []byte(` + { + "overlay": { + "spec": { + "X(serviceAccountName)": "*", + "automountServiceAccountToken": false + } + } + }`) + + err = json.Unmarshal(rawMutate, &mutateNegation) + assert.NilError(t, err) + + checker = NewMutateFactory(mutateEqual) + if _, err := checker.Validate(); err != nil { + assert.Assert(t, err != nil) + } +} + +func Test_Validate_Mutate_Unsupported(t *testing.T) { + var err error + var mutate kyverno.Mutation + // case 1 + rawMutate := []byte(` + { + "overlay": { + "spec": { + "!(serviceAccountName)": "*", + "automountServiceAccountToken": false + } + } + }`) + + err = json.Unmarshal(rawMutate, &mutate) + assert.NilError(t, err) + + checker := NewMutateFactory(mutate) + if _, err := checker.Validate(); err != nil { + assert.Assert(t, err != nil) + } + + // case 2 + rawMutate = []byte(` + { + "overlay": { + "spec": { + "~(serviceAccountName)": "*", + "automountServiceAccountToken": false + } + } + }`) + + err = json.Unmarshal(rawMutate, &mutate) + assert.NilError(t, err) + + checker = NewMutateFactory(mutate) + if _, err := checker.Validate(); err != nil { + assert.Assert(t, err != nil) + } +} diff --git a/pkg/policy/validate.go b/pkg/policy/validate.go index bc44d7524f..f6ac936c90 100644 --- a/pkg/policy/validate.go +++ b/pkg/policy/validate.go @@ -4,12 +4,10 @@ import ( "errors" "fmt" "reflect" - "regexp" - "strconv" "strings" kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1" - "github.com/nirmata/kyverno/pkg/engine/anchor" + dclient "github.com/nirmata/kyverno/pkg/dclient" rbacv1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -17,7 +15,7 @@ import ( // Validate does some initial check to verify some conditions // - One operation per rule // - ResourceDescription mandatory checks -func Validate(p kyverno.ClusterPolicy) error { +func Validate(p kyverno.ClusterPolicy, client *dclient.Client) error { if path, err := validateUniqueRuleName(p); err != nil { return fmt.Errorf("path: spec.%s: %v", path, err) } @@ -50,24 +48,12 @@ func Validate(p kyverno.ClusterPolicy) error { // 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) - } + // validate rule actions + // - Mutate + // - Validate + // - Generate + if err := validateActions(i, rule, client); err != nil { + return err } // If a rules match block does not match any kind, @@ -262,193 +248,3 @@ func validateResourceDescription(rd kyverno.ResourceDescription) error { } return nil } - -func validateMutation(m kyverno.Mutation) (string, error) { - // JSON Patches - if len(m.Patches) != 0 { - for i, patch := range m.Patches { - if err := validatePatch(patch); err != nil { - return fmt.Sprintf("patch[%d]", i), err - } - } - } - // Overlay - if m.Overlay != nil { - path, err := validatePattern(m.Overlay, "/", []anchor.IsAnchor{anchor.IsConditionAnchor, anchor.IsAddingAnchor}) - if err != nil { - return path, err - } - } - return "", nil -} - -// Validate if all mandatory PolicyPatch fields are set -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) - } - - return nil - } else if pp.Operation == "remove" { - return nil - } - - return fmt.Errorf("Unsupported JSONPatch operation '%s'", pp.Operation) -} - -func validateValidation(v kyverno.Validation) (string, error) { - if err := validateOverlayPattern(v); err != nil { - // no need to proceed ahead - return "", err - } - - if v.Pattern != nil { - if path, err := validatePattern(v.Pattern, "/", []anchor.IsAnchor{anchor.IsConditionAnchor, anchor.IsExistenceAnchor, anchor.IsEqualityAnchor, anchor.IsNegationAnchor}); err != nil { - return fmt.Sprintf("pattern.%s", path), err - } - } - - if len(v.AnyPattern) != 0 { - for i, pattern := range v.AnyPattern { - if path, err := validatePattern(pattern, "/", []anchor.IsAnchor{anchor.IsConditionAnchor, anchor.IsExistenceAnchor, anchor.IsEqualityAnchor, anchor.IsNegationAnchor}); err != nil { - return fmt.Sprintf("anyPattern[%d].%s", i, path), err - } - } - } - return "", nil -} - -// validateOverlayPattern checks one of pattern/anyPattern must exist -func validateOverlayPattern(v kyverno.Validation) error { - if v.Pattern == nil && len(v.AnyPattern) == 0 { - return fmt.Errorf("a pattern or anyPattern must be specified") - } - - if v.Pattern != nil && len(v.AnyPattern) != 0 { - 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) (string, error) { - - if gen.Data == nil && gen.Clone == (kyverno.CloneFrom{}) { - return "", fmt.Errorf("clone or data are required") - } - if gen.Data != nil && gen.Clone != (kyverno.CloneFrom{}) { - return "", fmt.Errorf("only one operation allowed per generate rule(data or clone)") - } - // 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 path, err := validateClone(gen.Clone); err != nil { - return fmt.Sprintf("clone.%s", path), err - } - } - 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 resources: %v", err) - } - } - return "", nil -} - -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 validateMap(typedPatternElement, path, supportedAnchors) - case []interface{}: - return validateArray(typedPatternElement, path, supportedAnchors) - case string, float64, int, int64, bool, nil: - //TODO? check operator - return "", nil - default: - return path, fmt.Errorf("Validation rule failed at '%s', pattern contains unknown type", path) - } -} - -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 () - re, err := regexp.Compile(`^.?\(.+\)$`) - if err != nil { - return path + "/" + key, fmt.Errorf("Unable to parse the field %s: %v", key, err) - } - - matched := re.MatchString(key) - // 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 existence anchor - // value must be of type list - if anchor.IsExistenceAnchor(key) { - typedValue, ok := value.([]interface{}) - if !ok { - return path + "/" + key, fmt.Errorf("Existence 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("Existence anchor: single value expected, multiple specified") - } - } - } - // lets validate the values now :) - if errPath, err := validatePattern(value, path+"/"+key, supportedAnchors); err != nil { - return errPath, err - } - } - return "", nil -} - -func validateArray(patternArray []interface{}, path string, supportedAnchors []anchor.IsAnchor) (string, error) { - for i, patternElement := range patternArray { - currentPath := path + strconv.Itoa(i) + "/" - // 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/policy/validate/validate.go b/pkg/policy/validate/validate.go new file mode 100644 index 0000000000..3889f9f163 --- /dev/null +++ b/pkg/policy/validate/validate.go @@ -0,0 +1,61 @@ +package validate + +import ( + "fmt" + + kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1" + "github.com/nirmata/kyverno/pkg/engine/anchor" + "github.com/nirmata/kyverno/pkg/policy/common" +) + +// Validate provides implementation to validate 'validate' rule +type Validate struct { + // rule to hold 'validate' rule specifications + rule kyverno.Validation +} + +//NewValidateFactory returns a new instance of Mutate validation checker +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 { + // no need to proceed ahead + return "", err + } + + if rule.Pattern != nil { + if path, err := common.ValidatePattern(rule.Pattern, "/", []anchor.IsAnchor{anchor.IsConditionAnchor, anchor.IsExistenceAnchor, anchor.IsEqualityAnchor, anchor.IsNegationAnchor}); err != nil { + return fmt.Sprintf("pattern.%s", path), err + } + } + + if len(rule.AnyPattern) != 0 { + for i, pattern := range rule.AnyPattern { + if path, err := common.ValidatePattern(pattern, "/", []anchor.IsAnchor{anchor.IsConditionAnchor, anchor.IsExistenceAnchor, anchor.IsEqualityAnchor, anchor.IsNegationAnchor}); err != nil { + return fmt.Sprintf("anyPattern[%d].%s", i, path), err + } + } + } + return "", nil +} + +// validateOverlayPattern checks one of pattern/anyPattern must exist +func (v *Validate) validateOverlayPattern() error { + rule := v.rule + if rule.Pattern == nil && len(rule.AnyPattern) == 0 { + return fmt.Errorf("a pattern or anyPattern must be specified") + } + + if rule.Pattern != nil && len(rule.AnyPattern) != 0 { + return fmt.Errorf("only one operation allowed per validation rule(pattern or anyPattern)") + } + + return nil +} diff --git a/pkg/policy/validate/validate_test.go b/pkg/policy/validate/validate_test.go new file mode 100644 index 0000000000..ee83204707 --- /dev/null +++ b/pkg/policy/validate/validate_test.go @@ -0,0 +1,381 @@ +package validate + +import ( + "encoding/json" + "testing" + + kyverno "github.com/nirmata/kyverno/pkg/api/kyverno/v1" + "gotest.tools/assert" +) + +func Test_Validate_OverlayPattern_Empty(t *testing.T) { + rawValidation := []byte(` + {}`) + + var validation kyverno.Validation + err := json.Unmarshal(rawValidation, &validation) + assert.NilError(t, err) + + checker := NewValidateFactory(validation) + if _, err := checker.Validate(); err != nil { + assert.Assert(t, err != nil) + } +} +func Test_Validate_OverlayPattern_Nil_PatternAnypattern(t *testing.T) { + rawValidation := []byte(` + { "message": "Privileged mode is not allowed. Set allowPrivilegeEscalation and privileged to false" + } + `) + + var validation kyverno.Validation + err := json.Unmarshal(rawValidation, &validation) + assert.NilError(t, err) + checker := NewValidateFactory(validation) + if _, err := checker.Validate(); err != nil { + assert.Assert(t, err != nil) + } +} + +func Test_Validate_OverlayPattern_Exist_PatternAnypattern(t *testing.T) { + 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 validation kyverno.Validation + err := json.Unmarshal(rawValidation, &validation) + assert.NilError(t, err) + checker := NewValidateFactory(validation) + if _, err := checker.Validate(); err != nil { + assert.Assert(t, err != nil) + } +} +func Test_Validate_OverlayPattern_Valid(t *testing.T) { + 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 validation kyverno.Validation + err := json.Unmarshal(rawValidation, &validation) + assert.NilError(t, err) + checker := NewValidateFactory(validation) + if _, err := checker.Validate(); err != nil { + assert.NilError(t, err) + } +} + +func Test_Validate_ExistingAnchor_AnchorOnMap(t *testing.T) { + rawValidation := []byte(` + { + "message": "validate container security contexts", + "anyPattern": [ + { + "spec": { + "template": { + "spec": { + "containers": [ + { + "^(securityContext)": { + "runAsNonRoot": true + } + } + ] + } + } + } + } + ] + } +`) + + var validation kyverno.Validation + err := json.Unmarshal(rawValidation, &validation) + assert.NilError(t, err) + checker := NewValidateFactory(validation) + if _, err := checker.Validate(); err != nil { + assert.Assert(t, err != nil) + } +} + +func Test_Validate_ExistingAnchor_AnchorOnString(t *testing.T) { + rawValidation := []byte(`{ + "message": "validate container security contexts", + "pattern": { + "spec": { + "template": { + "spec": { + "containers": [ + { + "securityContext": { + "allowPrivilegeEscalation": "^(false)" + } + } + ] + } + } + } + } + } + `) + + var validation kyverno.Validation + err := json.Unmarshal(rawValidation, &validation) + assert.NilError(t, err) + checker := NewValidateFactory(validation) + if _, err := checker.Validate(); err != nil { + assert.Assert(t, err != nil) + } +} + +func Test_Validate_ExistingAnchor_Valid(t *testing.T) { + var err error + var validation kyverno.Validation + rawValidation := []byte(` + { + "message": "validate container security contexts", + "anyPattern": [ + { + "spec": { + "template": { + "spec": { + "^(containers)": [ + { + "securityContext": { + "runAsNonRoot": "true" + } + } + ] + } + } + } + } + ] + }`) + + err = json.Unmarshal(rawValidation, &validation) + assert.NilError(t, err) + checker := NewValidateFactory(validation) + if _, err := checker.Validate(); err != nil { + assert.Assert(t, err != nil) + } + rawValidation = []byte(` + { + "message": "validate container security contexts", + "pattern": { + "spec": { + "template": { + "spec": { + "^(containers)": [ + { + "securityContext": { + "allowPrivilegeEscalation": "false" + } + } + ] + } + } + } + } + } `) + err = json.Unmarshal(rawValidation, &validation) + assert.NilError(t, err) + checker = NewValidateFactory(validation) + if _, err := checker.Validate(); 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) + + checker := NewValidateFactory(validate) + if _, err := checker.Validate(); err != nil { + assert.NilError(t, err) + } + + // case 2 + 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) + + checker = NewValidateFactory(validate) + if _, err := checker.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 validate kyverno.Validation + err := json.Unmarshal(rawValidate, &validate) + assert.NilError(t, err) + checker := NewValidateFactory(validate) + if _, err := checker.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) + checker := NewValidateFactory(validate) + if _, err := checker.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) + + checker = NewValidateFactory(validate) + if _, err := checker.Validate(); err != nil { + assert.Assert(t, err != nil) + } + +} diff --git a/pkg/policy/validate_test.go b/pkg/policy/validate_test.go index 387c87707b..e020001627 100644 --- a/pkg/policy/validate_test.go +++ b/pkg/policy/validate_test.go @@ -287,369 +287,6 @@ func Test_Validate_ResourceDescription_InvalidSelector(t *testing.T) { assert.Assert(t, err != nil) } -func Test_Validate_OverlayPattern_Empty(t *testing.T) { - rawValidation := []byte(` - {}`) - - var validation kyverno.Validation - err := json.Unmarshal(rawValidation, &validation) - assert.NilError(t, err) - - if _, err := validateValidation(validation); err != nil { - assert.Assert(t, err != nil) - } -} - -func Test_Validate_OverlayPattern_Nil_PatternAnypattern(t *testing.T) { - rawValidation := []byte(` - { "message": "Privileged mode is not allowed. Set allowPrivilegeEscalation and privileged to false" - } - `) - - var validation kyverno.Validation - err := json.Unmarshal(rawValidation, &validation) - assert.NilError(t, err) - if _, err := validateValidation(validation); err != nil { - assert.Assert(t, err != nil) - } -} - -func Test_Validate_OverlayPattern_Exist_PatternAnypattern(t *testing.T) { - 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 validation kyverno.Validation - err := json.Unmarshal(rawValidation, &validation) - assert.NilError(t, err) - if _, err := validateValidation(validation); err != nil { - assert.Assert(t, err != nil) - } -} - -func Test_Validate_OverlayPattern_Valid(t *testing.T) { - 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 validation kyverno.Validation - err := json.Unmarshal(rawValidation, &validation) - assert.NilError(t, err) - if _, err := validateValidation(validation); err != nil { - assert.NilError(t, err) - } - -} - -func Test_Validate_ExistingAnchor_AnchorOnMap(t *testing.T) { - rawValidation := []byte(` - { - "message": "validate container security contexts", - "anyPattern": [ - { - "spec": { - "template": { - "spec": { - "containers": [ - { - "^(securityContext)": { - "runAsNonRoot": true - } - } - ] - } - } - } - } - ] - } -`) - - var validation kyverno.Validation - err := json.Unmarshal(rawValidation, &validation) - assert.NilError(t, err) - if _, err := validateValidation(validation); err != nil { - assert.Assert(t, err != nil) - } - -} - -func Test_Validate_ExistingAnchor_AnchorOnString(t *testing.T) { - rawValidation := []byte(`{ - "message": "validate container security contexts", - "pattern": { - "spec": { - "template": { - "spec": { - "containers": [ - { - "securityContext": { - "allowPrivilegeEscalation": "^(false)" - } - } - ] - } - } - } - } - } - `) - - var validation kyverno.Validation - err := json.Unmarshal(rawValidation, &validation) - assert.NilError(t, err) - if _, err := validateValidation(validation); err != nil { - assert.Assert(t, err != nil) - } -} - -func Test_Validate_ExistingAnchor_Valid(t *testing.T) { - var err error - var validation kyverno.Validation - rawValidation := []byte(` - { - "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 = []byte(` - { - "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 - 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 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) - - if _, err := validateValidation(validate); err != nil { - assert.Assert(t, err != nil) - } - -} - func Test_Validate_Policy(t *testing.T) { rawPolicy := []byte(` { @@ -736,225 +373,10 @@ func Test_Validate_Policy(t *testing.T) { err := json.Unmarshal(rawPolicy, &policy) assert.NilError(t, err) - err = Validate(policy) + err = Validate(policy, nil) assert.NilError(t, err) } -func Test_Validate_Mutate_ConditionAnchor(t *testing.T) { - rawMutate := []byte(` - { - "overlay": { - "spec": { - "(serviceAccountName)": "*", - "automountServiceAccountToken": false - } - } - }`) - - var mutate kyverno.Mutation - err := json.Unmarshal(rawMutate, &mutate) - assert.NilError(t, err) - if _, err := validateMutation(mutate); err != nil { - assert.NilError(t, err) - } -} - -func Test_Validate_Mutate_PlusAnchor(t *testing.T) { - rawMutate := []byte(` - { - "overlay": { - "spec": { - "+(serviceAccountName)": "*", - "automountServiceAccountToken": false - } - } - }`) - - var mutate kyverno.Mutation - err := json.Unmarshal(rawMutate, &mutate) - assert.NilError(t, err) - - if _, err := validateMutation(mutate); err != nil { - assert.NilError(t, err) - } -} - -func Test_Validate_Mutate_Mismatched(t *testing.T) { - rawMutate := []byte(` - { - "overlay": { - "spec": { - "^(serviceAccountName)": "*", - "automountServiceAccountToken": false - } - } - }`) - - var mutateExistence kyverno.Mutation - err := json.Unmarshal(rawMutate, &mutateExistence) - assert.NilError(t, err) - - if _, err := validateMutation(mutateExistence); err != nil { - assert.Assert(t, err != nil) - } - - var mutateEqual kyverno.Mutation - rawMutate = []byte(` - { - "overlay": { - "spec": { - "=(serviceAccountName)": "*", - "automountServiceAccountToken": false - } - } - }`) - - err = json.Unmarshal(rawMutate, &mutateEqual) - assert.NilError(t, err) - - if _, err := validateMutation(mutateEqual); err != nil { - assert.Assert(t, err != nil) - } - - var mutateNegation kyverno.Mutation - rawMutate = []byte(` - { - "overlay": { - "spec": { - "X(serviceAccountName)": "*", - "automountServiceAccountToken": false - } - } - }`) - - err = json.Unmarshal(rawMutate, &mutateNegation) - assert.NilError(t, err) - - if _, err := validateMutation(mutateEqual); err != nil { - assert.Assert(t, err != nil) - } -} - -func Test_Validate_Mutate_Unsupported(t *testing.T) { - var err error - var mutate kyverno.Mutation - // case 1 - rawMutate := []byte(` - { - "overlay": { - "spec": { - "!(serviceAccountName)": "*", - "automountServiceAccountToken": false - } - } - }`) - - err = json.Unmarshal(rawMutate, &mutate) - assert.NilError(t, err) - - if _, err := validateMutation(mutate); err != nil { - assert.Assert(t, err != nil) - } - - // case 2 - rawMutate = []byte(` - { - "overlay": { - "spec": { - "~(serviceAccountName)": "*", - "automountServiceAccountToken": false - } - } - }`) - - err = json.Unmarshal(rawMutate, &mutate) - assert.NilError(t, err) - - if _, err := validateMutation(mutate); err != nil { - assert.Assert(t, err != nil) - } -} - -func Test_Validate_Generate(t *testing.T) { - rawGenerate := []byte(` - { - "kind": "NetworkPolicy", - "name": "defaultnetworkpolicy", - "data": { - "spec": { - "podSelector": {}, - "policyTypes": [ - "Ingress", - "Egress" - ], - "ingress": [ - {} - ], - "egress": [ - {} - ] - } - } - }`) - - var generate kyverno.Generation - err := json.Unmarshal(rawGenerate, &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", - "name": "defaultnetworkpolicy", - "data": { - "spec": { - "(podSelector)": {}, - "policyTypes": [ - "Ingress", - "Egress" - ], - "ingress": [ - {} - ], - "egress": [ - {} - ] - } - } - }`) - - err = json.Unmarshal(rawGenerate, &generate) - assert.NilError(t, err) - if _, err := validateGeneration(generate); err != nil { - assert.Assert(t, err != nil) - } - - rawGenerate = []byte(` - { - "kind": "ConfigMap", - "name": "copied-cm", - "clone": { - "^(namespace)": "default", - "name": "game" - } - }`) - - errNew := json.Unmarshal(rawGenerate, &generate) - assert.NilError(t, errNew) - 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) { rawPolicy := []byte(` { @@ -1097,7 +519,7 @@ func Test_Validate_ErrorFormat(t *testing.T) { err := json.Unmarshal(rawPolicy, &policy) assert.NilError(t, err) - err = Validate(policy) + err = Validate(policy, nil) assert.Assert(t, err != nil) } diff --git a/pkg/webhooks/policyvalidation.go b/pkg/webhooks/policyvalidation.go index ebe45e47cc..fcb79c7163 100644 --- a/pkg/webhooks/policyvalidation.go +++ b/pkg/webhooks/policyvalidation.go @@ -27,7 +27,7 @@ func (ws *WebhookServer) handlePolicyValidation(request *v1beta1.AdmissionReques Message: fmt.Sprintf("Failed to unmarshal policy admission request err %v", err), }} } - if err := policyvalidate.Validate(*policy); err != nil { + if err := policyvalidate.Validate(*policy, ws.client); err != nil { admissionResp = &v1beta1.AdmissionResponse{ Allowed: false, Result: &metav1.Status{