diff --git a/Makefile b/Makefile index 732a1facce..506f4fb2a1 100644 --- a/Makefile +++ b/Makefile @@ -12,6 +12,7 @@ REGISTRY=index.docker.io REPO=$(REGISTRY)/nirmata/kyverno IMAGE_TAG=$(GIT_VERSION) GOOS ?= $(shell go env GOOS) +PACKAGE ?=github.com/nirmata/kyverno LD_FLAGS="-s -w -X $(PACKAGE)/pkg/version.BuildVersion=$(GIT_VERSION) -X $(PACKAGE)/pkg/version.BuildHash=$(GIT_HASH) -X $(PACKAGE)/pkg/version.BuildTime=$(TIMESTAMP)" ################################## diff --git a/definitions/install.yaml b/definitions/install.yaml index fd7fc1a58b..8e5b660096 100644 --- a/definitions/install.yaml +++ b/definitions/install.yaml @@ -520,10 +520,10 @@ spec: serviceAccountName: kyverno-service-account initContainers: - name: kyverno-pre - image: nirmata/kyvernopre:v1.1.0 + image: nirmata/kyvernopre:v1.1.1 containers: - name: kyverno - image: nirmata/kyverno:v1.1.0 + image: nirmata/kyverno:v1.1.1 args: - "--filterK8Resources=[Event,*,*][*,kube-system,*][*,kube-public,*][*,kube-node-lease,*][Node,*,*][APIService,*,*][TokenReview,*,*][SubjectAccessReview,*,*][*,kyverno,*]" # customize webhook timout diff --git a/definitions/rolebinding.yaml b/definitions/rolebinding.yaml deleted file mode 100644 index b8240c0014..0000000000 --- a/definitions/rolebinding.yaml +++ /dev/null @@ -1,21 +0,0 @@ -apiVersion: rbac.authorization.k8s.io/v1beta1 -kind: RoleBinding -metadata: - name: policyviolation - # change namespace below to create rolebinding for the namespace admin - namespace: default -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: policyviolation -subjects: -# configure below to access policy violation for the namespace admin -- kind: ServiceAccount - name: default - namespace: default -# - apiGroup: rbac.authorization.k8s.io -# kind: User -# name: -# - apiGroup: rbac.authorization.k8s.io -# kind: Group -# name: \ No newline at end of file diff --git a/documentation/installation.md b/documentation/installation.md index 5c609f283f..702c6bc94d 100644 --- a/documentation/installation.md +++ b/documentation/installation.md @@ -116,12 +116,34 @@ Here is a script that generates a self-signed CA, a TLS certificate-key pair, an # Configure a namespace admin to access policy violations -During Kyverno installation, it creates a ClusterRole `policyviolation` which has the `list,get,watch` operation on resource `policyviolations`. To grant access to a namespace admin, configure [definitions/rolebinding.yaml](../definitions/rolebinding.yaml) then apply to the cluster. +During Kyverno installation, it creates a ClusterRole `policyviolation` which has the `list,get,watch` operation on resource `policyviolations`. To grant access to a namespace admin, configure the following YAML file then apply to the cluster. - Replace `metadata.namespace` with namespace of the admin - Configure `subjects` field to bind admin's role to the ClusterRole `policyviolation` - +````yaml +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: RoleBinding +metadata: + name: policyviolation + # change namespace below to create rolebinding for the namespace admin + namespace: default +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: policyviolation +subjects: +# configure below to access policy violation for the namespace admin +- kind: ServiceAccount + name: default + namespace: default +# - apiGroup: rbac.authorization.k8s.io +# kind: User +# name: +# - apiGroup: rbac.authorization.k8s.io +# kind: Group +# name: +```` # Installing outside of the cluster (debug mode) To build Kyverno in a development environment see: https://github.com/nirmata/kyverno/wiki/Building diff --git a/documentation/writing-policies.md b/documentation/writing-policies.md index 60ccf5beb2..1c0d2bcc4f 100644 --- a/documentation/writing-policies.md +++ b/documentation/writing-policies.md @@ -98,7 +98,6 @@ Example userName=`system:serviceaccount:nirmata:user1` will store variable valu - `serviceAccountNamespace` : extracts the `namespace` of the serviceAccount. Example userName=`system:serviceaccount:nirmata:user1` will store variable value as `nirmata`. - Examples: 1. Refer to resource name(type string) @@ -113,5 +112,24 @@ Examples: `{{request.object.metadata}}` +# PreConditions: +Apart from using `match` & `exclude` conditions on resource to filter which resources to apply the rule on, `preconditions` can be used to define custom filters. +```yaml + - name: generate-owner-role + match: + resources: + kinds: + - Namespace + preconditions: + - key: "{{request.userInfo.username}}" + operator: NotEqual + value: "" +``` +In the above example, if the variable `{{request.userInfo.username}}` is blank then we dont apply the rule on resource. + +Operators supported: +- Equal +- NotEqual + --- <small>*Read Next >> [Validate](/documentation/writing-policies-validate.md)*</small> \ No newline at end of file diff --git a/pkg/engine/mutate/overlay.go b/pkg/engine/mutate/overlay.go index 51b0640b25..59d1f50865 100644 --- a/pkg/engine/mutate/overlay.go +++ b/pkg/engine/mutate/overlay.go @@ -387,11 +387,11 @@ func preparePath(path string) string { } annPath := "/metadata/annotations/" - idx := strings.Index(path, annPath) // escape slash in annotation patch if strings.Contains(path, annPath) { + idx := strings.Index(path, annPath) p := path[idx+len(annPath):] - path = annPath + strings.ReplaceAll(p, "/", "~1") + path = path[:idx+len(annPath)] + strings.ReplaceAll(p, "/", "~1") } return path } diff --git a/pkg/engine/policy/background.go b/pkg/engine/policy/background.go index 636d4f7666..e027d7c6c7 100644 --- a/pkg/engine/policy/background.go +++ b/pkg/engine/policy/background.go @@ -26,8 +26,10 @@ func ContainsUserInfo(policy kyverno.ClusterPolicy) error { // - validate.pattern // - validate.anyPattern[*] // variables to filter - // - request.userInfo - filterVars := []string{"request.userInfo*"} + // - request.userInfo* + // - serviceAccountName + // - serviceAccountNamespace + filterVars := []string{"request.userInfo*", "serviceAccountName", "serviceAccountNamespace"} for condIdx, condition := range rule.Conditions { if err := variables.CheckVariables(condition.Key, filterVars, "/"); err != nil { return fmt.Errorf("path: spec/rules[%d]/condition[%d]/key%s", idx, condIdx, err) diff --git a/pkg/engine/policy/validate_test.go b/pkg/engine/policy/validate_test.go index bd791e84a8..bce81bcab9 100644 --- a/pkg/engine/policy/validate_test.go +++ b/pkg/engine/policy/validate_test.go @@ -1434,3 +1434,81 @@ func Test_BackGroundUserInfo_validate_anyPattern(t *testing.T) { t.Error("Incorrect Path") } } + +func Test_BackGroundUserInfo_validate_anyPattern_multiple_var(t *testing.T) { + var err error + rawPolicy := []byte(` + { + "apiVersion": "kyverno.io/v1", + "kind": "ClusterPolicy", + "metadata": { + "name": "disallow-root-user" + }, + "spec": { + "rules": [ + { + "name": "validate.anyPattern", + "validate": { + "anyPattern": [ + { + "var1": "temp" + }, + { + "var1": "{{request.userInfo}}-{{temp}}" + } + ] + } + } + ] + } + } `) + var policy *kyverno.ClusterPolicy + err = json.Unmarshal(rawPolicy, &policy) + assert.NilError(t, err) + + err = ContainsUserInfo(*policy) + + if err.Error() != "path: spec/rules[0]/validate/anyPattern[1]/var1/{{request.userInfo}}-{{temp}}" { + t.Log(err) + t.Error("Incorrect Path") + } +} + +func Test_BackGroundUserInfo_validate_anyPattern_serviceAccount(t *testing.T) { + var err error + rawPolicy := []byte(` + { + "apiVersion": "kyverno.io/v1", + "kind": "ClusterPolicy", + "metadata": { + "name": "disallow-root-user" + }, + "spec": { + "rules": [ + { + "name": "validate.anyPattern", + "validate": { + "anyPattern": [ + { + "var1": "temp" + }, + { + "var1": "{{serviceAccountName}}" + } + ] + } + } + ] + } + } `) + var policy *kyverno.ClusterPolicy + err = json.Unmarshal(rawPolicy, &policy) + assert.NilError(t, err) + + err = ContainsUserInfo(*policy) + + if err.Error() != "path: spec/rules[0]/validate/anyPattern[1]/var1/{{serviceAccountName}}" { + t.Log(err) + t.Error("Incorrect Path") + } +} diff --git a/pkg/engine/variables/validatevariables.go b/pkg/engine/variables/validatevariables.go index 78e9d687d6..23e3edf2bb 100644 --- a/pkg/engine/variables/validatevariables.go +++ b/pkg/engine/variables/validatevariables.go @@ -13,25 +13,27 @@ import ( func ValidateVariables(ctx context.EvalInterface, pattern interface{}) string { var pathsNotPresent []string variableList := extractVariables(pattern) - for i := 0; i < len(variableList)-1; i = i + 2 { - p := variableList[i+1] - glog.V(3).Infof("validating variables %s", p) - val, err := ctx.Query(p) - // reference path is not present - if err == nil && val == nil { - pathsNotPresent = append(pathsNotPresent, p) + for _, variable := range variableList { + if len(variable) == 2 { + varName := variable[0] + varValue := variable[1] + glog.V(3).Infof("validating variable %s", varName) + val, err := ctx.Query(varValue) + if err == nil && val == nil { + // path is not present, returns nil interface + pathsNotPresent = append(pathsNotPresent, varValue) + } } } - if len(variableList) != 0 && len(pathsNotPresent) != 0 { + if len(pathsNotPresent) != 0 { return strings.Join(pathsNotPresent, ";") } - return "" } // extractVariables extracts variables in the pattern -func extractVariables(pattern interface{}) []string { +func extractVariables(pattern interface{}) [][]string { switch typedPattern := pattern.(type) { case map[string]interface{}: return extractMap(typedPattern) @@ -44,8 +46,8 @@ func extractVariables(pattern interface{}) []string { } } -func extractMap(patternMap map[string]interface{}) []string { - var variableList []string +func extractMap(patternMap map[string]interface{}) [][]string { + var variableList [][]string for _, patternElement := range patternMap { if vars := extractVariables(patternElement); vars != nil { @@ -55,8 +57,8 @@ func extractMap(patternMap map[string]interface{}) []string { return variableList } -func extractArray(patternList []interface{}) []string { - var variableList []string +func extractArray(patternList []interface{}) [][]string { + var variableList [][]string for _, patternElement := range patternList { if vars := extractVariables(patternElement); vars != nil { @@ -66,17 +68,22 @@ func extractArray(patternList []interface{}) []string { return variableList } -func extractValue(valuePattern string) []string { +func extractValue(valuePattern string) [][]string { operatorVariable := getOperator(valuePattern) variable := valuePattern[len(operatorVariable):] return extractValueVariable(variable) } -func extractValueVariable(valuePattern string) []string { +// returns multiple variable match groups +func extractValueVariable(valuePattern string) [][]string { variableRegex := regexp.MustCompile(variableRegex) - groups := variableRegex.FindStringSubmatch(valuePattern) - if len(groups)%2 != 0 { + groups := variableRegex.FindAllStringSubmatch(valuePattern, -1) + if len(groups) == 0 { + // no variables return nil } + // group[*] <- all the matches + // group[*][0] <- group match + // group[*][1] <- group capture value return groups } diff --git a/pkg/engine/variables/validatevariables_test.go b/pkg/engine/variables/validatevariables_test.go index c71a966bdd..a6e5154f1a 100644 --- a/pkg/engine/variables/validatevariables_test.go +++ b/pkg/engine/variables/validatevariables_test.go @@ -40,7 +40,9 @@ func Test_ExtractVariables(t *testing.T) { json.Unmarshal(patternRaw, &pattern) vars := extractVariables(pattern) - result := []string{"{{request.userInfo.username}}", "request.userInfo.username", "{{request.object.metadata.name}}", "request.object.metadata.name"} + + result := [][]string{[]string{"{{request.userInfo.username}}", "request.userInfo.username"}, + []string{"{{request.object.metadata.name}}", "request.object.metadata.name"}} assert.Assert(t, len(vars) == len(result), fmt.Sprintf("result does not match, var: %s", vars)) } diff --git a/pkg/engine/variables/variables.go b/pkg/engine/variables/variables.go index 7d9b352e4e..1f71c16aaf 100644 --- a/pkg/engine/variables/variables.go +++ b/pkg/engine/variables/variables.go @@ -110,17 +110,18 @@ func getValues(ctx context.EvalInterface, groups [][]string) map[string]interfac for _, group := range groups { if len(group) == 2 { // 0th is string - // 1st is the capture group - variable, err := ctx.Query(group[1]) + varName := group[0] + varValue := group[1] + variable, err := ctx.Query(varValue) if err != nil { - glog.V(4).Infof("variable substitution failed for query %s: %v", group[0], err) - subs[group[0]] = emptyInterface + glog.V(4).Infof("variable substitution failed for query %s: %v", varName, err) + subs[varName] = emptyInterface continue } if variable == nil { - subs[group[0]] = emptyInterface + subs[varName] = emptyInterface } else { - subs[group[0]] = variable + subs[varName] = variable } } } diff --git a/pkg/engine/variables/variables_check.go b/pkg/engine/variables/variables_check.go index 46aa56b0fe..19ee50172e 100644 --- a/pkg/engine/variables/variables_check.go +++ b/pkg/engine/variables/variables_check.go @@ -50,11 +50,22 @@ func checkValue(valuePattern string, variables []string, path string) error { func checkValueVariable(valuePattern string, variables []string) bool { variableRegex := regexp.MustCompile(variableRegex) - groups := variableRegex.FindStringSubmatch(valuePattern) - if len(groups) < 2 { + groups := variableRegex.FindAllStringSubmatch(valuePattern, -1) + if len(groups) == 0 { + // no variables return false } - return variablePatternSearch(groups[1], variables) + // if variables are defined, check against the list of variables to be filtered + for _, group := range groups { + if len(group) == 2 { + // group[0] -> {{variable}} + // group[1] -> variable + if variablePatternSearch(group[1], variables) { + return true + } + } + } + return false } func variablePatternSearch(pattern string, regexs []string) bool { diff --git a/pkg/webhooks/common.go b/pkg/webhooks/common.go index 611e81cd2e..16b0843d6c 100644 --- a/pkg/webhooks/common.go +++ b/pkg/webhooks/common.go @@ -92,7 +92,7 @@ const ( func processResourceWithPatches(patch []byte, resource []byte) []byte { if patch == nil { - return nil + return resource } resource, err := engineutils.ApplyPatchNew(resource, patch) diff --git a/pkg/webhooks/generation.go b/pkg/webhooks/generation.go index 3929f6e330..9c9e1fa586 100644 --- a/pkg/webhooks/generation.go +++ b/pkg/webhooks/generation.go @@ -33,7 +33,7 @@ func (ws *WebhookServer) HandleGenerate(request *v1beta1.AdmissionRequest, polic // build context ctx := context.NewContext() // load incoming resource into the context - // ctx.AddResource(request.Object.Raw) + ctx.AddResource(request.Object.Raw) ctx.AddUserInfo(userRequestInfo) // load service account in context ctx.AddSA(userRequestInfo.AdmissionUserInfo.Username) diff --git a/test/policy/generate/variable.yaml b/test/policy/generate/variable.yaml index 03850b09dd..79ca2f42bc 100644 --- a/test/policy/generate/variable.yaml +++ b/test/policy/generate/variable.yaml @@ -6,16 +6,40 @@ metadata: policies.kyverno.io/category: Workload Isolation policies.kyverno.io/description: Create roles and role bindings for a new namespace spec: + background: false rules: + - name: add-sa-annotation + match: + resources: + kinds: + - Namespace + mutate: + overlay: + metadata: + annotations: + nirmata.io/ns-creator: "{{serviceAccountName}}" - name: generate-owner-role match: resources: kinds: - Namespace + preconditions: + - key: "{{request.userInfo.username}}" + operator: NotEqual + value: "" + - key: "{{serviceAccountName}}" + operator: NotEqual + value: "" + - key: "{{serviceAccountNamespace}}" + operator: NotEqual + value: "" generate: kind: ClusterRole name: "ns-owner-{{request.object.metadata.name}}-{{request.userInfo.username}}" data: + metadata: + annotations: + nirmata.io/ns-creator: "{{serviceAccountName}}" rules: - apiGroups: [""] resources: ["namespaces"] @@ -27,10 +51,23 @@ spec: resources: kinds: - Namespace + preconditions: + - key: "{{request.userInfo.username}}" + operator: NotEqual + value: "" + - key: "{{serviceAccountName}}" + operator: NotEqual + value: "" + - key: "{{serviceAccountNamespace}}" + operator: NotEqual + value: "" generate: kind: ClusterRoleBinding name: "ns-owner-{{request.object.metadata.name}}-{{request.userInfo.username}}-binding" data: + metadata: + annotations: + nirmata.io/ns-creator: "{{serviceAccountName}}" roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole @@ -45,11 +82,24 @@ spec: resources: kinds: - Namespace + preconditions: + - key: "{{request.userInfo.username}}" + operator: NotEqual + value: "" + - key: "{{serviceAccountName}}" + operator: NotEqual + value: "" + - key: "{{serviceAccountNamespace}}" + operator: NotEqual + value: "" generate: kind: RoleBinding name: "ns-admin-{{request.object.metadata.name}}-{{request.userInfo.username}}-binding" namespace: "{{request.object.metadata.name}}" data: + metadata: + annotations: + nirmata.io/ns-creator: "{{serviceAccountName}}" roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole @@ -57,4 +107,4 @@ spec: subjects: - kind: ServiceAccount name: "{{serviceAccountName}}" - namespace: "{{serviceAccountNamespace}}" + namespace: "{{serviceAccountNamespace}}" \ No newline at end of file