From 8b4f7ad2438a854d1062037cdbe0c5a602570569 Mon Sep 17 00:00:00 2001 From: Siarhei Rasiukevich Date: Tue, 14 Nov 2023 18:08:36 +0200 Subject: [PATCH] feat(manager): Add forbiddenAnnotations,forbiddenLabels to serviceOptions --- .../crd/bases/capsule.clastix.io_tenants.yaml | 44 ++++ e2e/service_forbidden_metadata_test.go | 161 ++++++++++++++ e2e/utils_test.go | 208 ++++++++++-------- pkg/api/forbidden_list.go | 64 +++++- pkg/api/forbidden_list_test.go | 47 ++++ pkg/api/service_options.go | 4 + pkg/api/zz_generated.deepcopy.go | 2 + pkg/webhook/namespace/errors.go | 56 ----- pkg/webhook/namespace/user_metadata.go | 90 ++++---- pkg/webhook/service/validating.go | 22 ++ 10 files changed, 500 insertions(+), 198 deletions(-) create mode 100644 e2e/service_forbidden_metadata_test.go diff --git a/config/crd/bases/capsule.clastix.io_tenants.yaml b/config/crd/bases/capsule.clastix.io_tenants.yaml index 75c547236..715ce1353 100644 --- a/config/crd/bases/capsule.clastix.io_tenants.yaml +++ b/config/crd/bases/capsule.clastix.io_tenants.yaml @@ -1873,6 +1873,28 @@ spec: required: - allowed type: object + forbiddenAnnotations: + description: Define the annotations that a Tenant Owner cannot + set for their Service resources. + properties: + denied: + items: + type: string + type: array + deniedRegex: + type: string + type: object + forbiddenLabels: + description: Define the labels that a Tenant Owner cannot set + for their Service resources. + properties: + denied: + items: + type: string + type: array + deniedRegex: + type: string + type: object type: object storageClasses: description: Specifies the allowed StorageClasses assigned to the @@ -3107,6 +3129,28 @@ spec: required: - allowed type: object + forbiddenAnnotations: + description: Define the annotations that a Tenant Owner cannot + set for their Service resources. + properties: + denied: + items: + type: string + type: array + deniedRegex: + type: string + type: object + forbiddenLabels: + description: Define the labels that a Tenant Owner cannot set + for their Service resources. + properties: + denied: + items: + type: string + type: array + deniedRegex: + type: string + type: object type: object storageClasses: description: Specifies the allowed StorageClasses assigned to the diff --git a/e2e/service_forbidden_metadata_test.go b/e2e/service_forbidden_metadata_test.go new file mode 100644 index 000000000..0a83c1d31 --- /dev/null +++ b/e2e/service_forbidden_metadata_test.go @@ -0,0 +1,161 @@ +//go:build e2e + +// Copyright 2020-2023 Project Capsule Authors. +// SPDX-License-Identifier: Apache-2.0 + +package e2e + +import ( + "context" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + + capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2" + "github.com/projectcapsule/capsule/pkg/api" +) + +var _ = Describe("creating a Service with user-specified labels and annotations", func() { + tnt := &capsulev1beta2.Tenant{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tenant-user-metadata-forbidden", + }, + Spec: capsulev1beta2.TenantSpec{ + ServiceOptions: &api.ServiceOptions{ + ForbiddenLabels: api.ForbiddenListSpec{ + Exact: []string{"foo", "bar"}, + Regex: "^gatsby-.*$", + }, + ForbiddenAnnotations: api.ForbiddenListSpec{ + Exact: []string{"foo", "bar"}, + Regex: "^gatsby-.*$", + }, + }, + Owners: capsulev1beta2.OwnerListSpec{ + { + Name: "gatsby", + Kind: "User", + }, + }, + }, + } + + JustBeforeEach(func() { + EventuallyCreation(func() error { + tnt.ResourceVersion = "" + return k8sClient.Create(context.TODO(), tnt) + }).Should(Succeed()) + }) + JustAfterEach(func() { + Expect(k8sClient.Delete(context.TODO(), tnt)).Should(Succeed()) + }) + + It("should allow", func() { + By("specifying non-forbidden labels", func() { + svc := NewService("") + svc.SetLabels(map[string]string{"bim": "baz"}) + ServiceCreation(svc, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) + }) + By("specifying non-forbidden annotations", func() { + svc := NewService("") + svc.SetAnnotations(map[string]string{"bim": "baz"}) + ServiceCreation(svc, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) + }) + }) + + It("should fail when creating a Service", func() { + By("specifying forbidden labels using exact match", func() { + svc := NewService("") + svc.SetLabels(map[string]string{"foo": "bar"}) + ServiceCreation(svc, tnt.Spec.Owners[0], defaultTimeoutInterval).ShouldNot(Succeed()) + }) + By("specifying forbidden labels using regex match", func() { + svc := NewService("") + svc.SetLabels(map[string]string{"gatsby-foo": "bar"}) + ServiceCreation(svc, tnt.Spec.Owners[0], defaultTimeoutInterval).ShouldNot(Succeed()) + }) + By("specifying forbidden annotations using exact match", func() { + svc := NewService("") + svc.SetAnnotations(map[string]string{"foo": "bar"}) + ServiceCreation(svc, tnt.Spec.Owners[0], defaultTimeoutInterval).ShouldNot(Succeed()) + }) + By("specifying forbidden annotations using regex match", func() { + svc := NewService("") + svc.SetAnnotations(map[string]string{"gatsby-foo": "bar"}) + ServiceCreation(svc, tnt.Spec.Owners[0], defaultTimeoutInterval).ShouldNot(Succeed()) + }) + }) + + It("should fail when updating a Service", func() { + cs := ownerClient(tnt.Spec.Owners[0]) + + By("specifying forbidden labels using exact match", func() { + svc := NewService("forbidden-labels-exact-match") + ServiceCreation(svc, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) + Consistently(func() error { + if err := k8sClient.Get(context.Background(), types.NamespacedName{Name: svc.GetName()}, svc); err != nil { + return nil + } + + svc.SetLabels(map[string]string{"foo": "bar"}) + + _, err := cs.CoreV1().Services().Update(context.Background(), svc, metav1.UpdateOptions{}) + + return err + }, 10*time.Second, time.Second).ShouldNot(Succeed()) + }) + By("specifying forbidden labels using regex match", func() { + svc := NewService("forbidden-labels-regex-match") + + ServiceCreation(svc, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) + Consistently(func() error { + if err := k8sClient.Get(context.Background(), types.NamespacedName{Name: svc.GetName()}, svc); err != nil { + return nil + } + + svc.SetLabels(map[string]string{"gatsby-foo": "bar"}) + + _, err := cs.CoreV1().Services().Update(context.Background(), svc, metav1.UpdateOptions{}) + + return err + }, 3*time.Second, time.Second).ShouldNot(Succeed()) + }) + By("specifying forbidden annotations using exact match", func() { + svc := NewService("forbidden-annotations-exact-match") + + ServiceCreation(svc, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) + rbacPatch(svc.GetName()) + Consistently(func() error { + if err := k8sClient.Get(context.Background(), types.NamespacedName{Name: svc.GetName()}, svc); err != nil { + return nil + } + + svc.SetAnnotations(map[string]string{"foo": "bar"}) + + _, err := cs.CoreV1().Services().Update(context.Background(), svc, metav1.UpdateOptions{}) + + return err + }, 10*time.Second, time.Second).ShouldNot(Succeed()) + }) + By("specifying forbidden annotations using regex match", func() { + svc := NewService("forbidden-annotations-regex-match") + + ServiceCreation(svc, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) + rbacPatch(svc.GetName()) + Consistently(func() error { + if err := k8sClient.Get(context.Background(), types.NamespacedName{Name: svc.GetName()}, svc); err != nil { + return nil + } + + svc.SetAnnotations(map[string]string{"gatsby-foo": "bar"}) + + _, err := cs.CoreV1().Services().Update(context.Background(), svc, metav1.UpdateOptions{}) + + return err + }, 10*time.Second, time.Second).ShouldNot(Succeed()) + }) + }) +}) diff --git a/e2e/utils_test.go b/e2e/utils_test.go index 4bfcae681..85f4f6ad9 100644 --- a/e2e/utils_test.go +++ b/e2e/utils_test.go @@ -6,145 +6,165 @@ package e2e import ( - "context" - "fmt" - "strings" - "time" - - "k8s.io/apimachinery/pkg/util/rand" - "sigs.k8s.io/controller-runtime/pkg/client" - - . "github.com/onsi/gomega" - corev1 "k8s.io/api/core/v1" - rbacv1 "k8s.io/api/rbac/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - versionUtil "k8s.io/apimachinery/pkg/util/version" - "k8s.io/apimachinery/pkg/version" - "k8s.io/client-go/kubernetes" - - capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2" + "context" + "fmt" + "strings" + "time" + + "k8s.io/apimachinery/pkg/util/rand" + "sigs.k8s.io/controller-runtime/pkg/client" + + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + versionUtil "k8s.io/apimachinery/pkg/util/version" + "k8s.io/apimachinery/pkg/version" + "k8s.io/client-go/kubernetes" + + capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2" ) const ( - defaultTimeoutInterval = 20 * time.Second - defaultPollInterval = time.Second + defaultTimeoutInterval = 20 * time.Second + defaultPollInterval = time.Second ) +func NewService(name string) *corev1.Service { + if len(name) == 0 { + name = rand.String(10) + } + + return &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + } +} + +func ServiceCreation(svc *corev1.Service, owner capsulev1beta2.OwnerSpec, timeout time.Duration) AsyncAssertion { + cs := ownerClient(owner) + return Eventually(func() (err error) { + _, err = cs.CoreV1().Service().Create(context.TODO(), svc, metav1.CreateOptions{}) + return + }, timeout, defaultPollInterval) +} + func NewNamespace(name string) *corev1.Namespace { - if len(name) == 0 { - name = rand.String(10) - } - - return &corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - }, - } + if len(name) == 0 { + name = rand.String(10) + } + + return &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + } } func NamespaceCreation(ns *corev1.Namespace, owner capsulev1beta2.OwnerSpec, timeout time.Duration) AsyncAssertion { - cs := ownerClient(owner) - return Eventually(func() (err error) { - _, err = cs.CoreV1().Namespaces().Create(context.TODO(), ns, metav1.CreateOptions{}) - return - }, timeout, defaultPollInterval) + cs := ownerClient(owner) + return Eventually(func() (err error) { + _, err = cs.CoreV1().Namespaces().Create(context.TODO(), ns, metav1.CreateOptions{}) + return + }, timeout, defaultPollInterval) } func TenantNamespaceList(t *capsulev1beta2.Tenant, timeout time.Duration) AsyncAssertion { - return Eventually(func() []string { - Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: t.GetName()}, t)).Should(Succeed()) - return t.Status.Namespaces - }, timeout, defaultPollInterval) + return Eventually(func() []string { + Expect(k8sClient.Get(context.TODO(), types.NamespacedName{Name: t.GetName()}, t)).Should(Succeed()) + return t.Status.Namespaces + }, timeout, defaultPollInterval) } func ModifyNode(fn func(node *corev1.Node) error) error { - nodeList := &corev1.NodeList{} + nodeList := &corev1.NodeList{} - Expect(k8sClient.List(context.Background(), nodeList)).ToNot(HaveOccurred()) + Expect(k8sClient.List(context.Background(), nodeList)).ToNot(HaveOccurred()) - return fn(&nodeList.Items[0]) + return fn(&nodeList.Items[0]) } func EventuallyCreation(f interface{}) AsyncAssertion { - return Eventually(f, defaultTimeoutInterval, defaultPollInterval) + return Eventually(f, defaultTimeoutInterval, defaultPollInterval) } func ModifyCapsuleConfigurationOpts(fn func(configuration *capsulev1beta2.CapsuleConfiguration)) { - config := &capsulev1beta2.CapsuleConfiguration{} - Expect(k8sClient.Get(context.Background(), types.NamespacedName{Name: "default"}, config)).ToNot(HaveOccurred()) + config := &capsulev1beta2.CapsuleConfiguration{} + Expect(k8sClient.Get(context.Background(), types.NamespacedName{Name: "default"}, config)).ToNot(HaveOccurred()) - fn(config) + fn(config) - Expect(k8sClient.Update(context.Background(), config)).ToNot(HaveOccurred()) + Expect(k8sClient.Update(context.Background(), config)).ToNot(HaveOccurred()) - time.Sleep(1 * time.Second) + time.Sleep(1 * time.Second) } func CheckForOwnerRoleBindings(ns *corev1.Namespace, owner capsulev1beta2.OwnerSpec, roles map[string]bool) func() error { - if roles == nil { - roles = map[string]bool{ - "admin": false, - "capsule-namespace-deleter": false, - } - } + if roles == nil { + roles = map[string]bool{ + "admin": false, + "capsule-namespace-deleter": false, + } + } - return func() (err error) { - roleBindings := &rbacv1.RoleBindingList{} + return func() (err error) { + roleBindings := &rbacv1.RoleBindingList{} - if err = k8sClient.List(context.Background(), roleBindings, client.InNamespace(ns.GetName())); err != nil { - return fmt.Errorf("cannot retrieve list of rolebindings: %w", err) - } + if err = k8sClient.List(context.Background(), roleBindings, client.InNamespace(ns.GetName())); err != nil { + return fmt.Errorf("cannot retrieve list of rolebindings: %w", err) + } - var ownerName string + var ownerName string - if owner.Kind == capsulev1beta2.ServiceAccountOwner { - parts := strings.Split(owner.Name, ":") + if owner.Kind == capsulev1beta2.ServiceAccountOwner { + parts := strings.Split(owner.Name, ":") - ownerName = parts[3] - } else { - ownerName = owner.Name - } + ownerName = parts[3] + } else { + ownerName = owner.Name + } - for _, roleBinding := range roleBindings.Items { - _, ok := roles[roleBinding.RoleRef.Name] - if !ok { - continue - } + for _, roleBinding := range roleBindings.Items { + _, ok := roles[roleBinding.RoleRef.Name] + if !ok { + continue + } - subject := roleBinding.Subjects[0] + subject := roleBinding.Subjects[0] - if subject.Name != ownerName { - continue - } + if subject.Name != ownerName { + continue + } - roles[roleBinding.RoleRef.Name] = true - } + roles[roleBinding.RoleRef.Name] = true + } - for role, found := range roles { - if !found { - return fmt.Errorf("role %s for %s.%s has not been reconciled", role, owner.Kind.String(), owner.Name) - } - } + for role, found := range roles { + if !found { + return fmt.Errorf("role %s for %s.%s has not been reconciled", role, owner.Kind.String(), owner.Name) + } + } - return nil - } + return nil + } } func GetKubernetesVersion() *versionUtil.Version { - var serverVersion *version.Info - var err error - var cs kubernetes.Interface - var ver *versionUtil.Version + var serverVersion *version.Info + var err error + var cs kubernetes.Interface + var ver *versionUtil.Version - cs, err = kubernetes.NewForConfig(cfg) - Expect(err).ToNot(HaveOccurred()) + cs, err = kubernetes.NewForConfig(cfg) + Expect(err).ToNot(HaveOccurred()) - serverVersion, err = cs.Discovery().ServerVersion() - Expect(err).ToNot(HaveOccurred()) + serverVersion, err = cs.Discovery().ServerVersion() + Expect(err).ToNot(HaveOccurred()) - ver, err = versionUtil.ParseGeneric(serverVersion.String()) - Expect(err).ToNot(HaveOccurred()) + ver, err = versionUtil.ParseGeneric(serverVersion.String()) + Expect(err).ToNot(HaveOccurred()) - return ver + return ver } diff --git a/pkg/api/forbidden_list.go b/pkg/api/forbidden_list.go index b92a66a40..77de462fd 100644 --- a/pkg/api/forbidden_list.go +++ b/pkg/api/forbidden_list.go @@ -4,13 +4,21 @@ package api import ( + "fmt" + "reflect" "regexp" "sort" "strings" ) -// +kubebuilder:object:generate=true +const ( + // ForbiddenLabelReason used as reason string to deny forbidden labels. + ForbiddenLabelReason = "ForbiddenLabel" + // ForbiddenAnnotationReason used as reason string to deny forbidden annotations. + ForbiddenAnnotationReason = "ForbiddenAnnotation" +) +// +kubebuilder:object:generate=true type ForbiddenListSpec struct { Exact []string `json:"denied,omitempty"` Regex string `json:"deniedRegex,omitempty"` @@ -37,3 +45,57 @@ func (in ForbiddenListSpec) RegexMatch(value string) (ok bool) { return } + +type ForbiddenError struct { + key string + spec ForbiddenListSpec +} + +func NewForbiddenError(key string, forbiddenSpec ForbiddenListSpec) error { + return &ForbiddenError{ + key: key, + spec: forbiddenSpec, + } +} + +//nolint:predeclared +func (f *ForbiddenError) appendForbiddenError() (append string) { + append += "Forbidden are " + if len(f.spec.Exact) > 0 { + append += fmt.Sprintf("one of the following (%s)", strings.Join(f.spec.Exact, ", ")) + if len(f.spec.Regex) > 0 { + append += " or " + } + } + + if len(f.spec.Regex) > 0 { + append += fmt.Sprintf("matching the regex %s", f.spec.Regex) + } + + return +} + +func (f ForbiddenError) Error() string { + return fmt.Sprintf("%s is forbidden for the current Tenant. %s", f.key, f.appendForbiddenError()) +} + +func ValidateForbidden(metadata map[string]string, forbiddenList ForbiddenListSpec) error { + if reflect.DeepEqual(ForbiddenListSpec{}, forbiddenList) { + return nil + } + + for key := range metadata { + var forbidden, matched bool + forbidden = forbiddenList.ExactMatch(key) + matched = forbiddenList.RegexMatch(key) + + if forbidden || matched { + return NewForbiddenError( + key, + forbiddenList, + ) + } + } + + return nil +} diff --git a/pkg/api/forbidden_list_test.go b/pkg/api/forbidden_list_test.go index f721e89a3..daefb7d4d 100644 --- a/pkg/api/forbidden_list_test.go +++ b/pkg/api/forbidden_list_test.go @@ -72,3 +72,50 @@ func TestForbiddenListSpec_RegexMatch(t *testing.T) { } } } + +func TestValidateForbidden(t *testing.T) { + type tc struct { + Keys map[string]string + ForbiddenSpec ForbiddenListSpec + HasError bool + } + + for _, tc := range []tc{ + { + Keys: map[string]string{"foobar": "", "thesecondkey": "", "anotherkey": ""}, + ForbiddenSpec: ForbiddenListSpec{ + Exact: []string{"foobar", "somelabelkey1"}, + }, + HasError: true, + }, + { + Keys: map[string]string{"foobar": ""}, + ForbiddenSpec: ForbiddenListSpec{ + Exact: []string{"foobar.io", "somelabelkey1", "test-exact"}, + }, + HasError: false, + }, + { + Keys: map[string]string{"foobar": "", "barbaz": ""}, + ForbiddenSpec: ForbiddenListSpec{ + Regex: "foo.*", + }, + HasError: true, + }, + { + Keys: map[string]string{"foobar": "", "another-annotation-key": ""}, + ForbiddenSpec: ForbiddenListSpec{ + Regex: "foo1111", + }, + HasError: false, + }, + } { + if tc.HasError { + assert.Error(t, ValidateForbidden(tc.Keys, tc.ForbiddenSpec)) + } + + if !tc.HasError { + assert.NoError(t, ValidateForbidden(tc.Keys, tc.ForbiddenSpec)) + } + } +} diff --git a/pkg/api/service_options.go b/pkg/api/service_options.go index ad77217ac..21127d0fa 100644 --- a/pkg/api/service_options.go +++ b/pkg/api/service_options.go @@ -12,4 +12,8 @@ type ServiceOptions struct { AllowedServices *AllowedServices `json:"allowedServices,omitempty"` // Specifies the external IPs that can be used in Services with type ClusterIP. An empty list means no IPs are allowed. Optional. ExternalServiceIPs *ExternalServiceIPsSpec `json:"externalIPs,omitempty"` + // Define the labels that a Tenant Owner cannot set for their Service resources. + ForbiddenLabels ForbiddenListSpec `json:"forbiddenLabels,omitempty"` + // Define the annotations that a Tenant Owner cannot set for their Service resources. + ForbiddenAnnotations ForbiddenListSpec `json:"forbiddenAnnotations,omitempty"` } diff --git a/pkg/api/zz_generated.deepcopy.go b/pkg/api/zz_generated.deepcopy.go index 70c8265b9..29cf993b5 100644 --- a/pkg/api/zz_generated.deepcopy.go +++ b/pkg/api/zz_generated.deepcopy.go @@ -270,6 +270,8 @@ func (in *ServiceOptions) DeepCopyInto(out *ServiceOptions) { *out = new(ExternalServiceIPsSpec) (*in).DeepCopyInto(*out) } + in.ForbiddenLabels.DeepCopyInto(&out.ForbiddenLabels) + in.ForbiddenAnnotations.DeepCopyInto(&out.ForbiddenAnnotations) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceOptions. diff --git a/pkg/webhook/namespace/errors.go b/pkg/webhook/namespace/errors.go index bdfd0fe06..cfaac497d 100644 --- a/pkg/webhook/namespace/errors.go +++ b/pkg/webhook/namespace/errors.go @@ -3,30 +3,6 @@ package namespace -import ( - "fmt" - "strings" - - capsuleapi "github.com/projectcapsule/capsule/pkg/api" -) - -//nolint:predeclared -func appendForbiddenError(spec *capsuleapi.ForbiddenListSpec) (append string) { - append += "Forbidden are " - if len(spec.Exact) > 0 { - append += fmt.Sprintf("one of the following (%s)", strings.Join(spec.Exact, ", ")) - if len(spec.Regex) > 0 { - append += " or " - } - } - - if len(spec.Regex) > 0 { - append += fmt.Sprintf("matching the regex %s", spec.Regex) - } - - return -} - type namespaceQuotaExceededError struct{} func NewNamespaceQuotaExceededError() error { @@ -36,35 +12,3 @@ func NewNamespaceQuotaExceededError() error { func (namespaceQuotaExceededError) Error() string { return "Cannot exceed Namespace quota: please, reach out to the system administrators" } - -type namespaceLabelForbiddenError struct { - label string - spec *capsuleapi.ForbiddenListSpec -} - -func NewNamespaceLabelForbiddenError(label string, forbiddenSpec *capsuleapi.ForbiddenListSpec) error { - return &namespaceLabelForbiddenError{ - label: label, - spec: forbiddenSpec, - } -} - -func (f namespaceLabelForbiddenError) Error() string { - return fmt.Sprintf("Label %s is forbidden for namespaces in the current Tenant. %s", f.label, appendForbiddenError(f.spec)) -} - -type namespaceAnnotationForbiddenError struct { - annotation string - spec *capsuleapi.ForbiddenListSpec -} - -func NewNamespaceAnnotationForbiddenError(annotation string, forbiddenSpec *capsuleapi.ForbiddenListSpec) error { - return &namespaceAnnotationForbiddenError{ - annotation: annotation, - spec: forbiddenSpec, - } -} - -func (f namespaceAnnotationForbiddenError) Error() string { - return fmt.Sprintf("Annotation %s is forbidden for namespaces in the current Tenant. %s", f.annotation, appendForbiddenError(f.spec)) -} diff --git a/pkg/webhook/namespace/user_metadata.go b/pkg/webhook/namespace/user_metadata.go index e2badedbd..b667567e2 100644 --- a/pkg/webhook/namespace/user_metadata.go +++ b/pkg/webhook/namespace/user_metadata.go @@ -5,8 +5,8 @@ package namespace import ( "context" - "fmt" + "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/tools/record" @@ -14,6 +14,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook/admission" capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2" + "github.com/projectcapsule/capsule/pkg/api" capsulewebhook "github.com/projectcapsule/capsule/pkg/webhook" "github.com/projectcapsule/capsule/pkg/webhook/utils" ) @@ -24,48 +25,6 @@ func UserMetadataHandler() capsulewebhook.Handler { return &userMetadataHandler{} } -func (r *userMetadataHandler) validateUserMetadata(tnt *capsulev1beta2.Tenant, recorder record.EventRecorder, labels map[string]string, annotations map[string]string) *admission.Response { - if tnt.Spec.NamespaceOptions != nil { - forbiddenLabels := tnt.Spec.NamespaceOptions.ForbiddenLabels - - for label := range labels { - var forbidden, matched bool - forbidden = forbiddenLabels.ExactMatch(label) - matched = forbiddenLabels.RegexMatch(label) - - if forbidden || matched { - recorder.Eventf(tnt, corev1.EventTypeWarning, "ForbiddenNamespaceLabel", fmt.Sprintf("Label %s is forbidden for a namespaces of the current Tenant ", label)) - - response := admission.Denied(NewNamespaceLabelForbiddenError(label, &forbiddenLabels).Error()) - - return &response - } - } - } - - if tnt.Spec.NamespaceOptions == nil { - return nil - } - - forbiddenAnnotations := tnt.Spec.NamespaceOptions.ForbiddenLabels - - for annotation := range annotations { - var forbidden, matched bool - forbidden = forbiddenAnnotations.ExactMatch(annotation) - matched = forbiddenAnnotations.RegexMatch(annotation) - - if forbidden || matched { - recorder.Eventf(tnt, corev1.EventTypeWarning, "ForbiddenNamespaceAnnotation", fmt.Sprintf("Annotation %s is forbidden for a namespaces of the current Tenant ", annotation)) - - response := admission.Denied(NewNamespaceAnnotationForbiddenError(annotation, &forbiddenAnnotations).Error()) - - return &response - } - } - - return nil -} - func (r *userMetadataHandler) OnCreate(client client.Client, decoder *admission.Decoder, recorder record.EventRecorder) capsulewebhook.Func { return func(ctx context.Context, req admission.Request) *admission.Response { ns := &corev1.Namespace{} @@ -81,10 +40,27 @@ func (r *userMetadataHandler) OnCreate(client client.Client, decoder *admission. } } - labels := ns.GetLabels() - annotations := ns.GetAnnotations() + if tnt.Spec.NamespaceOptions != nil { + err := api.ValidateForbidden(ns.ObjectMeta.Annotations, tnt.Spec.NamespaceOptions.ForbiddenAnnotations) + if err != nil { + err = errors.Wrap(err, "namespace annotations validation failed") + recorder.Eventf(tnt, corev1.EventTypeWarning, api.ForbiddenAnnotationReason, err.Error()) + response := admission.Denied(err.Error()) + + return &response + } + + err = api.ValidateForbidden(ns.ObjectMeta.Labels, tnt.Spec.NamespaceOptions.ForbiddenLabels) + if err != nil { + err = errors.Wrap(err, "namespace labels validation failed") + recorder.Eventf(tnt, corev1.EventTypeWarning, api.ForbiddenLabelReason, err.Error()) + response := admission.Denied(err.Error()) - return r.validateUserMetadata(tnt, recorder, labels, annotations) + return &response + } + } + + return nil } } @@ -173,6 +149,26 @@ func (r *userMetadataHandler) OnUpdate(client client.Client, decoder *admission. delete(annotations, key) } - return r.validateUserMetadata(tnt, recorder, labels, annotations) + if tnt.Spec.NamespaceOptions != nil { + err := api.ValidateForbidden(annotations, tnt.Spec.NamespaceOptions.ForbiddenAnnotations) + if err != nil { + err = errors.Wrap(err, "namespace annotations validation failed") + recorder.Eventf(tnt, corev1.EventTypeWarning, api.ForbiddenAnnotationReason, err.Error()) + response := admission.Denied(err.Error()) + + return &response + } + + err = api.ValidateForbidden(labels, tnt.Spec.NamespaceOptions.ForbiddenLabels) + if err != nil { + err = errors.Wrap(err, "namespace labels validation failed") + recorder.Eventf(tnt, corev1.EventTypeWarning, api.ForbiddenLabelReason, err.Error()) + response := admission.Denied(err.Error()) + + return &response + } + } + + return nil } } diff --git a/pkg/webhook/service/validating.go b/pkg/webhook/service/validating.go index 38868b76c..904aa1db3 100644 --- a/pkg/webhook/service/validating.go +++ b/pkg/webhook/service/validating.go @@ -8,6 +8,7 @@ import ( "net" "strings" + "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/fields" "k8s.io/client-go/tools/record" @@ -15,6 +16,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook/admission" capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2" + "github.com/projectcapsule/capsule/pkg/api" capsulewebhook "github.com/projectcapsule/capsule/pkg/webhook" "github.com/projectcapsule/capsule/pkg/webhook/utils" ) @@ -68,6 +70,26 @@ func (r *handler) handleService(ctx context.Context, clt client.Client, decoder return &response } + if tnt.Spec.ServiceOptions != nil { + err := api.ValidateForbidden(svc.Annotations, tnt.Spec.ServiceOptions.ForbiddenAnnotations) + if err != nil { + err = errors.Wrap(err, "service annotations validation failed") + recorder.Eventf(&tnt, corev1.EventTypeWarning, api.ForbiddenAnnotationReason, err.Error()) + response := admission.Denied(err.Error()) + + return &response + } + + err = api.ValidateForbidden(svc.Labels, tnt.Spec.ServiceOptions.ForbiddenLabels) + if err != nil { + err = errors.Wrap(err, "service labels validation failed") + recorder.Eventf(&tnt, corev1.EventTypeWarning, api.ForbiddenLabelReason, err.Error()) + response := admission.Denied(err.Error()) + + return &response + } + } + if svc.Spec.ExternalIPs == nil || (tnt.Spec.ServiceOptions == nil || tnt.Spec.ServiceOptions.ExternalServiceIPs == nil) { return nil }