diff --git a/pkg/validatingadmissionpolicy/validate.go b/pkg/validatingadmissionpolicy/validate.go index 81a55f22c6..870dc18105 100644 --- a/pkg/validatingadmissionpolicy/validate.go +++ b/pkg/validatingadmissionpolicy/validate.go @@ -14,6 +14,8 @@ import ( "golang.org/x/text/language" admissionregistrationv1 "k8s.io/api/admissionregistration/v1" "k8s.io/api/admissionregistration/v1alpha1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -63,7 +65,12 @@ func GetKinds(policy v1alpha1.ValidatingAdmissionPolicy) []string { return kindList } -func Validate(policyData PolicyData, resource unstructured.Unstructured, namespaceSelectorMap map[string]map[string]string, client dclient.Interface) (engineapi.EngineResponse, error) { +func Validate( + policyData PolicyData, + resource unstructured.Unstructured, + namespaceSelectorMap map[string]map[string]string, + client dclient.Interface, +) (engineapi.EngineResponse, error) { resPath := fmt.Sprintf("%s/%s/%s", resource.GetNamespace(), resource.GetKind(), resource.GetName()) policy := policyData.definition bindings := policyData.bindings @@ -75,6 +82,24 @@ func Validate(policyData PolicyData, resource unstructured.Unstructured, namespa Version: gvk.Version, Resource: strings.ToLower(gvk.Kind) + "s", } + + var namespace *corev1.Namespace + namespaceName := resource.GetNamespace() + // Special case, the namespace object has the namespace of itself. + // unset it if the incoming object is a namespace + if gvk.Kind == "Namespace" && gvk.Version == "v1" && gvk.Group == "" { + namespaceName = "" + } + + if namespaceName != "" { + namespace = &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: namespaceName, + Labels: namespaceSelectorMap[namespaceName], + }, + } + } + a := admission.NewAttributesRecord(resource.DeepCopyObject(), nil, resource.GroupVersionKind(), resource.GetNamespace(), resource.GetName(), gvr, "", admission.Create, nil, false, nil) if len(bindings) == 0 { @@ -86,7 +111,7 @@ func Validate(policyData PolicyData, resource unstructured.Unstructured, namespa return engineResponse, nil } logger.V(3).Info("validate resource %s against policy %s", resPath, policy.GetName()) - return validateResource(policy, nil, resource, a) + return validateResource(policy, nil, resource, *namespace, a) } if client != nil { @@ -113,6 +138,13 @@ func Validate(policyData PolicyData, resource unstructured.Unstructured, namespa return engineResponse, nil } + if namespaceName != "" { + namespace, err = client.GetKubeClient().CoreV1().Namespaces().Get(context.TODO(), namespaceName, metav1.GetOptions{}) + if err != nil { + return engineResponse, err + } + } + for i, binding := range bindings { // convert policy binding from v1alpha1 to v1beta1 v1beta1binding := ConvertValidatingAdmissionPolicyBinding(binding) @@ -125,7 +157,7 @@ func Validate(policyData PolicyData, resource unstructured.Unstructured, namespa } logger.V(3).Info("validate resource %s against policy %s with binding %s", resPath, policy.GetName(), binding.GetName()) - return validateResource(policy, &bindings[i], resource, a) + return validateResource(policy, &bindings[i], resource, *namespace, a) } } else { for i, binding := range bindings { @@ -137,14 +169,20 @@ func Validate(policyData PolicyData, resource unstructured.Unstructured, namespa continue } logger.V(3).Info("validate resource %s against policy %s with binding %s", resPath, policy.GetName(), binding.GetName()) - return validateResource(policy, &bindings[i], resource, a) + return validateResource(policy, &bindings[i], resource, *namespace, a) } } return engineResponse, nil } -func validateResource(policy v1alpha1.ValidatingAdmissionPolicy, binding *v1alpha1.ValidatingAdmissionPolicyBinding, resource unstructured.Unstructured, a admission.Attributes) (engineapi.EngineResponse, error) { +func validateResource( + policy v1alpha1.ValidatingAdmissionPolicy, + binding *v1alpha1.ValidatingAdmissionPolicyBinding, + resource unstructured.Unstructured, + namespace corev1.Namespace, + a admission.Attributes, +) (engineapi.EngineResponse, error) { startTime := time.Now() engineResponse := engineapi.NewEngineResponse(resource, engineapi.NewValidatingAdmissionPolicy(policy), nil) @@ -184,7 +222,7 @@ func validateResource(policy v1alpha1.ValidatingAdmissionPolicy, binding *v1alph &failPolicy, ) versionedAttr, _ := admission.NewVersionedAttributes(a, a.GetKind(), nil) - validateResult := validator.Validate(context.TODO(), a.GetResource(), versionedAttr, nil, nil, celconfig.RuntimeCELCostBudget, nil) + validateResult := validator.Validate(context.TODO(), a.GetResource(), versionedAttr, nil, &namespace, celconfig.RuntimeCELCostBudget, nil) isPass := true for _, policyDecision := range validateResult.Decisions { diff --git a/test/cli/test-validating-admission-policy/with-namespaceObject-1/kyverno-test.yaml b/test/cli/test-validating-admission-policy/with-namespaceObject-1/kyverno-test.yaml new file mode 100644 index 0000000000..5edfad575c --- /dev/null +++ b/test/cli/test-validating-admission-policy/with-namespaceObject-1/kyverno-test.yaml @@ -0,0 +1,22 @@ +apiVersion: cli.kyverno.io/v1alpha1 +kind: Test +metadata: + name: kyverno-test.yaml +policies: +- policy.yaml +resources: +- resources.yaml +results: +- isValidatingAdmissionPolicy: true + kind: Deployment + policy: check-deployment-namespace + resources: + - bad-deployment + result: fail +- isValidatingAdmissionPolicy: true + kind: Deployment + policy: check-deployment-namespace + resources: + - good-deployment + result: pass +variables: values.yaml diff --git a/test/cli/test-validating-admission-policy/with-namespaceObject-1/policy.yaml b/test/cli/test-validating-admission-policy/with-namespaceObject-1/policy.yaml new file mode 100644 index 0000000000..0086f9b8b8 --- /dev/null +++ b/test/cli/test-validating-admission-policy/with-namespaceObject-1/policy.yaml @@ -0,0 +1,19 @@ +apiVersion: admissionregistration.k8s.io/v1beta1 +kind: ValidatingAdmissionPolicy +metadata: + name: "check-deployment-namespace" +spec: + matchConstraints: + resourceRules: + - apiGroups: + - apps + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - deployments + validations: + - expression: "namespaceObject.metadata.name != 'default'" + message: "Using 'default' namespace is not allowed for pod controllers." diff --git a/test/cli/test-validating-admission-policy/with-namespaceObject-1/resources.yaml b/test/cli/test-validating-admission-policy/with-namespaceObject-1/resources.yaml new file mode 100644 index 0000000000..6b6a326d8d --- /dev/null +++ b/test/cli/test-validating-admission-policy/with-namespaceObject-1/resources.yaml @@ -0,0 +1,37 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: bad-deployment + namespace: default +spec: + replicas: 2 + selector: + matchLabels: + app: busybox + template: + metadata: + labels: + app: busybox + spec: + containers: + - name: busybox + image: busybox:latest +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: good-deployment + namespace: staging +spec: + replicas: 2 + selector: + matchLabels: + app: busybox + template: + metadata: + labels: + app: busybox + spec: + containers: + - name: busybox + image: busybox:latest diff --git a/test/cli/test-validating-admission-policy/with-namespaceObject-1/values.yaml b/test/cli/test-validating-admission-policy/with-namespaceObject-1/values.yaml new file mode 100644 index 0000000000..95f1fa601b --- /dev/null +++ b/test/cli/test-validating-admission-policy/with-namespaceObject-1/values.yaml @@ -0,0 +1,11 @@ +apiVersion: cli.kyverno.io/v1alpha1 +kind: Value +metadata: + name: values +namespaceSelector: +- labels: + environment: staging + name: staging +- labels: + environment: default + name: default diff --git a/test/cli/test-validating-admission-policy/with-namespaceObject-2/kyverno-test.yaml b/test/cli/test-validating-admission-policy/with-namespaceObject-2/kyverno-test.yaml new file mode 100644 index 0000000000..70fd927106 --- /dev/null +++ b/test/cli/test-validating-admission-policy/with-namespaceObject-2/kyverno-test.yaml @@ -0,0 +1,29 @@ +apiVersion: cli.kyverno.io/v1alpha1 +kind: Test +metadata: + name: kyverno-test.yaml +policies: +- policy.yaml +resources: +- resources.yaml +results: +- isValidatingAdmissionPolicy: true + kind: Deployment + policy: check-deployment-namespace + resources: + - bad-deployment + result: fail +- isValidatingAdmissionPolicy: true + kind: Deployment + policy: check-deployment-namespace + resources: + - good-deployment + result: pass +- isValidatingAdmissionPolicy: true + kind: Deployment + policy: check-deployment-namespace + resources: + - skipped-deployment-1 + - skipped-deployment-2 + result: skip +variables: values.yaml diff --git a/test/cli/test-validating-admission-policy/with-namespaceObject-2/policy.yaml b/test/cli/test-validating-admission-policy/with-namespaceObject-2/policy.yaml new file mode 100644 index 0000000000..0dda98cef9 --- /dev/null +++ b/test/cli/test-validating-admission-policy/with-namespaceObject-2/policy.yaml @@ -0,0 +1,31 @@ +apiVersion: admissionregistration.k8s.io/v1beta1 +kind: ValidatingAdmissionPolicy +metadata: + name: "check-deployment-namespace" +spec: + matchConstraints: + resourceRules: + - apiGroups: + - apps + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - deployments + validations: + - expression: "namespaceObject.metadata.name != 'default'" + message: "Using 'default' namespace is not allowed for pod controllers." +--- +apiVersion: admissionregistration.k8s.io/v1beta1 +kind: ValidatingAdmissionPolicyBinding +metadata: + name: "check-deployment-namespace-binding" +spec: + policyName: "check-deployment-namespace" + validationActions: [Deny] + matchResources: + objectSelector: + matchLabels: + app: nginx diff --git a/test/cli/test-validating-admission-policy/with-namespaceObject-2/resources.yaml b/test/cli/test-validating-admission-policy/with-namespaceObject-2/resources.yaml new file mode 100644 index 0000000000..f467fc81f2 --- /dev/null +++ b/test/cli/test-validating-admission-policy/with-namespaceObject-2/resources.yaml @@ -0,0 +1,83 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: skipped-deployment-1 + namespace: default + labels: + app: busybox +spec: + replicas: 2 + selector: + matchLabels: + app: busybox + template: + metadata: + labels: + app: busybox + spec: + containers: + - name: busybox + image: busybox:latest +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: skipped-deployment-2 + namespace: staging + labels: + app: busybox +spec: + replicas: 2 + selector: + matchLabels: + app: busybox + template: + metadata: + labels: + app: busybox + spec: + containers: + - name: busybox + image: busybox:latest +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: bad-deployment + namespace: default + labels: + app: nginx +spec: + replicas: 2 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:latest +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: good-deployment + namespace: staging + labels: + app: nginx +spec: + replicas: 2 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:latest diff --git a/test/cli/test-validating-admission-policy/with-namespaceObject-2/values.yaml b/test/cli/test-validating-admission-policy/with-namespaceObject-2/values.yaml new file mode 100644 index 0000000000..95f1fa601b --- /dev/null +++ b/test/cli/test-validating-admission-policy/with-namespaceObject-2/values.yaml @@ -0,0 +1,11 @@ +apiVersion: cli.kyverno.io/v1alpha1 +kind: Value +metadata: + name: values +namespaceSelector: +- labels: + environment: staging + name: staging +- labels: + environment: default + name: default