diff --git a/charts/capsule/crds/tenant-crd.yaml b/charts/capsule/crds/tenant-crd.yaml index 8ae0d6ae..b00ce31e 100644 --- a/charts/capsule/crds/tenant-crd.yaml +++ b/charts/capsule/crds/tenant-crd.yaml @@ -3133,6 +3133,28 @@ spec: type: string type: object 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 allowedServices: description: Block or deny certain type of Services. Optional. properties: diff --git a/config/crd/bases/capsule.clastix.io_tenants.yaml b/config/crd/bases/capsule.clastix.io_tenants.yaml index a1d0f616..d792f369 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 @@ -3125,6 +3147,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/docs/content/general/crds-apis.md b/docs/content/general/crds-apis.md index 29a5d794..93f5187c 100644 --- a/docs/content/general/crds-apis.md +++ b/docs/content/general/crds-apis.md @@ -4822,6 +4822,20 @@ Specifies options for the Service, such as additional metadata or block of certa Specifies the external IPs that can be used in Services with type ClusterIP. An empty list means no IPs are allowed. Optional.
false + + forbiddenAnnotations + object + + Define the annotations that a Tenant Owner cannot set for their Service resources.
+ + false + + forbiddenLabels + object + + Define the labels that a Tenant Owner cannot set for their Service resources.
+ + false @@ -4931,6 +4945,72 @@ Specifies the external IPs that can be used in Services with type ClusterIP. An +### Tenant.spec.serviceOptions.forbiddenAnnotations + + + +Define the annotations that a Tenant Owner cannot set for their Service resources. + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
denied[]string +
+
false
deniedRegexstring +
+
false
+ + +### Tenant.spec.serviceOptions.forbiddenLabels + + + +Define the labels that a Tenant Owner cannot set for their Service resources. + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
denied[]string +
+
false
deniedRegexstring +
+
false
+ + ### Tenant.spec.storageClasses @@ -6681,6 +6761,20 @@ Specifies options for the Service, such as additional metadata or block of certa Specifies the external IPs that can be used in Services with type ClusterIP. An empty list means no IPs are allowed. Optional.
false + + forbiddenAnnotations + object + + Define the annotations that a Tenant Owner cannot set for their Service resources.
+ + false + + forbiddenLabels + object + + Define the labels that a Tenant Owner cannot set for their Service resources.
+ + false @@ -6790,6 +6884,72 @@ Specifies the external IPs that can be used in Services with type ClusterIP. An +### Tenant.spec.serviceOptions.forbiddenAnnotations + + + +Define the annotations that a Tenant Owner cannot set for their Service resources. + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
denied[]string +
+
false
deniedRegexstring +
+
false
+ + +### Tenant.spec.serviceOptions.forbiddenLabels + + + +Define the labels that a Tenant Owner cannot set for their Service resources. + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
denied[]string +
+
false
deniedRegexstring +
+
false
+ + ### Tenant.spec.storageClasses diff --git a/e2e/service_forbidden_metadata_test.go b/e2e/service_forbidden_metadata_test.go new file mode 100644 index 00000000..ba0cde42 --- /dev/null +++ b/e2e/service_forbidden_metadata_test.go @@ -0,0 +1,229 @@ +//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() { + ns := NewNamespace("") + NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) + TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName())) + + svc := NewService(types.NamespacedName{ + Namespace: ns.GetName(), + Name: "non-forbidden-labels", + }) + svc.SetLabels(map[string]string{"bim": "baz"}) + ServiceCreation(svc, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) + }) + By("specifying non-forbidden annotations", func() { + ns := NewNamespace("") + NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) + TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName())) + + svc := NewService(types.NamespacedName{ + Namespace: ns.GetName(), + Name: "non-forbidden-annotations", + }) + 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() { + ns := NewNamespace("") + NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) + TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName())) + + svc := NewService(types.NamespacedName{ + Namespace: ns.GetName(), + Name: "forbidden-labels-exact", + }) + svc.SetLabels(map[string]string{"foo": "bar"}) + ServiceCreation(svc, tnt.Spec.Owners[0], defaultTimeoutInterval).ShouldNot(Succeed()) + }) + By("specifying forbidden labels using regex match", func() { + ns := NewNamespace("") + NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) + TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName())) + + svc := NewService(types.NamespacedName{ + Namespace: ns.GetName(), + Name: "forbidden-labels-regex", + }) + 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() { + ns := NewNamespace("") + NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) + TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName())) + + svc := NewService(types.NamespacedName{ + Namespace: ns.GetName(), + Name: "forbidden-annotations-exact", + }) + svc.SetAnnotations(map[string]string{"foo": "bar"}) + ServiceCreation(svc, tnt.Spec.Owners[0], defaultTimeoutInterval).ShouldNot(Succeed()) + }) + By("specifying forbidden annotations using regex match", func() { + ns := NewNamespace("") + NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) + TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName())) + + svc := NewService(types.NamespacedName{ + Namespace: ns.GetName(), + Name: "forbidden-annotations-regex", + }) + 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() { + ns := NewNamespace("") + NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) + TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName())) + + svc := NewService(types.NamespacedName{ + Namespace: ns.GetName(), + Name: "forbidden-labels-exact-match", + }) + ServiceCreation(svc, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) + Consistently(func() error { + svc, err := cs.CoreV1().Services(svc.Namespace).Get(context.Background(), svc.GetName(), metav1.GetOptions{}) + if err != nil { + return nil + } + svc.SetLabels(map[string]string{"foo": "bar"}) + + _, err = cs.CoreV1().Services(svc.Namespace).Update(context.Background(), svc, metav1.UpdateOptions{}) + + return err + }, 10*time.Second, time.Second).ShouldNot(Succeed()) + }) + By("specifying forbidden labels using regex match", func() { + ns := NewNamespace("") + NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) + TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName())) + + svc := NewService(types.NamespacedName{ + Namespace: ns.GetName(), + Name: "forbidden-labels-regex-match", + }) + ServiceCreation(svc, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) + Consistently(func() error { + svc, err := cs.CoreV1().Services(svc.Namespace).Get(context.Background(), svc.GetName(), metav1.GetOptions{}) + if err != nil { + return nil + } + + svc.SetLabels(map[string]string{"gatsby-foo": "bar"}) + + _, err = cs.CoreV1().Services(svc.Namespace).Update(context.Background(), svc, metav1.UpdateOptions{}) + + return err + }, 3*time.Second, time.Second).ShouldNot(Succeed()) + }) + By("specifying forbidden annotations using exact match", func() { + ns := NewNamespace("") + NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) + TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName())) + + svc := NewService(types.NamespacedName{ + Namespace: ns.GetName(), + Name: "forbidden-annotations-exact-match", + }) + ServiceCreation(svc, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) + Consistently(func() error { + svc, err := cs.CoreV1().Services(svc.Namespace).Get(context.Background(), svc.GetName(), metav1.GetOptions{}) + if err != nil { + return nil + } + + svc.SetAnnotations(map[string]string{"foo": "bar"}) + + _, err = cs.CoreV1().Services(svc.Namespace).Update(context.Background(), svc, metav1.UpdateOptions{}) + + return err + }, 10*time.Second, time.Second).ShouldNot(Succeed()) + }) + By("specifying forbidden annotations using regex match", func() { + ns := NewNamespace("") + NamespaceCreation(ns, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) + TenantNamespaceList(tnt, defaultTimeoutInterval).Should(ContainElement(ns.GetName())) + + svc := NewService(types.NamespacedName{ + Namespace: ns.GetName(), + Name: "forbidden-annotations-regex-match", + }) + ServiceCreation(svc, tnt.Spec.Owners[0], defaultTimeoutInterval).Should(Succeed()) + Consistently(func() error { + svc, err := cs.CoreV1().Services(svc.Namespace).Get(context.Background(), svc.GetName(), metav1.GetOptions{}) + if err != nil { + return nil + } + + svc.SetAnnotations(map[string]string{"gatsby-foo": "bar"}) + + _, err = cs.CoreV1().Services(svc.Namespace).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 5e0cda3c..5ab07446 100644 --- a/e2e/utils_test.go +++ b/e2e/utils_test.go @@ -31,6 +31,28 @@ const ( defaultPollInterval = time.Second ) +func NewService(svc types.NamespacedName) *corev1.Service { + return &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: svc.Name, + Namespace: svc.Namespace, + }, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + {Port: int32(80)}, + }, + }, + } +} + +func ServiceCreation(svc *corev1.Service, owner capsulev1beta2.OwnerSpec, timeout time.Duration) AsyncAssertion { + cs := ownerClient(owner) + return Eventually(func() (err error) { + _, err = cs.CoreV1().Services(svc.Namespace).Create(context.TODO(), svc, metav1.CreateOptions{}) + return + }, timeout, defaultPollInterval) +} + func NewNamespace(name string) *corev1.Namespace { if len(name) == 0 { name = rand.String(10) diff --git a/pkg/api/forbidden_list.go b/pkg/api/forbidden_list.go index b92a66a4..77de462f 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 f721e89a..daefb7d4 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 ad77217a..21127d0f 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 d09c76c3..41d1c96d 100644 --- a/pkg/api/zz_generated.deepcopy.go +++ b/pkg/api/zz_generated.deepcopy.go @@ -290,6 +290,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 bdfd0fe0..cfaac497 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 e2badedb..b667567e 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 38868b76..904aa1db 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 }