diff --git a/config/core/roles/webhook-clusterrole.yaml b/config/core/roles/webhook-clusterrole.yaml index 1e021b1fb06..ae66a35c05a 100644 --- a/config/core/roles/webhook-clusterrole.yaml +++ b/config/core/roles/webhook-clusterrole.yaml @@ -162,6 +162,13 @@ rules: - "patch" - "watch" + # For checking if user has permissions to make a cross namespace resource + - apiGroups: + - "authorization.k8s.io" + resources: + - "subjectaccessreviews" + verbs: + - "create" # Necessary for conversion webhook. These are copied from the serving # TODO: Do we really need all these permissions? diff --git a/pkg/apis/eventing/v1/trigger_types.go b/pkg/apis/eventing/v1/trigger_types.go index 91a1aa75a22..bb6d7c6f907 100644 --- a/pkg/apis/eventing/v1/trigger_types.go +++ b/pkg/apis/eventing/v1/trigger_types.go @@ -226,3 +226,11 @@ type TriggerList struct { func (t *Trigger) GetStatus() *duckv1.Status { return &t.Status.Status } + +// GetCrossNamespaceRef returns the Broker reference for the Trigger. Implements the ResourceInfo interface. +func (t *Trigger) GetCrossNamespaceRef() duckv1.KReference { + if t.Spec.BrokerRef != nil { + return *t.Spec.BrokerRef + } + return duckv1.KReference{} +} diff --git a/pkg/apis/eventing/v1/trigger_validation.go b/pkg/apis/eventing/v1/trigger_validation.go index 64c3fb2db33..3edbf4f3b92 100644 --- a/pkg/apis/eventing/v1/trigger_validation.go +++ b/pkg/apis/eventing/v1/trigger_validation.go @@ -25,6 +25,7 @@ import ( cesqlparser "github.com/cloudevents/sdk-go/sql/v2/parser" "go.uber.org/zap" corev1 "k8s.io/api/core/v1" + cn "knative.dev/eventing/pkg/crossnamespace" "knative.dev/pkg/apis" "knative.dev/pkg/kmp" "knative.dev/pkg/logging" @@ -46,13 +47,29 @@ func (t *Trigger) Validate(ctx context.Context) *apis.FieldError { original := apis.GetBaseline(ctx).(*Trigger) errs = errs.Also(t.CheckImmutableFields(ctx, original)) } + if feature.FromContext(ctx).IsEnabled(feature.CrossNamespaceEventLinks) && t.Spec.BrokerRef != nil { + crossNamespaceError := cn.CheckNamespace(ctx, t) + if crossNamespaceError != nil { + errs = errs.Also(crossNamespaceError) + } + } return errs } // Validate the TriggerSpec. func (ts *TriggerSpec) Validate(ctx context.Context) (errs *apis.FieldError) { - if ts.Broker == "" { + if ts.BrokerRef == nil && ts.Broker == "" { errs = errs.Also(apis.ErrMissingField("broker")) + } else if ts.BrokerRef != nil && ts.Broker != "" { + errs = errs.Also(apis.ErrMultipleOneOf("broker", "brokerRef")) + } + + if !feature.FromContext(ctx).IsEnabled(feature.CrossNamespaceEventLinks) && ts.BrokerRef != nil { + if ts.BrokerRef.Namespace != "" { + fe := apis.ErrDisallowedFields("namespace") + fe.Details = "only name, apiVersion and kind are supported fields when feature.CrossNamespaceEventLinks is disabled" + errs = errs.Also(fe) + } } return errs.Also( diff --git a/pkg/apis/eventing/v1/trigger_validation_test.go b/pkg/apis/eventing/v1/trigger_validation_test.go index 12e53291b9a..33fbe1f871c 100644 --- a/pkg/apis/eventing/v1/trigger_validation_test.go +++ b/pkg/apis/eventing/v1/trigger_validation_test.go @@ -475,6 +475,30 @@ func TestTriggerSpecValidation(t *testing.T) { }, }, want: apis.ErrInvalidValue(invalidString, "delivery.backoffDelay"), + }, { + name: "empty Broker and empty BrokerRef", + ts: &TriggerSpec{ + Broker: "", + BrokerRef: nil, + Filter: validTriggerFilter, + Subscriber: validSubscriber, + }, + want: apis.ErrMissingField("broker"), + }, { + name: "BrokerRef has different namespace", + ts: &TriggerSpec{ + BrokerRef: &duckv1.KReference{ + Name: "test-broker", + Namespace: "test-broker-ns", + }, + Filter: validTriggerFilter, + Subscriber: validSubscriber, + }, + want: func() *apis.FieldError { + fe := apis.ErrDisallowedFields("namespace") + fe.Details = "only name, apiVersion and kind are supported fields when feature.CrossNamespaceEventLinks is disabled" + return fe + }(), }} for _, test := range tests { @@ -487,6 +511,186 @@ func TestTriggerSpecValidation(t *testing.T) { } } +func TestTriggerSpecValidationWithCrossNamespaceEventLinksFeatureEnabled(t *testing.T) { + invalidString := "invalid time" + tests := []struct { + name string + ts *TriggerSpec + want *apis.FieldError + }{{ + name: "invalid trigger spec", + ts: &TriggerSpec{}, + want: func() *apis.FieldError { + var errs *apis.FieldError + fe := apis.ErrMissingField("broker") + errs = errs.Also(fe) + fe = apis.ErrGeneric("expected at least one, got none", "subscriber.ref", "subscriber.uri") + errs = errs.Also(fe) + return errs + }(), + }, { + name: "missing broker", + ts: &TriggerSpec{ + Broker: "", + Filter: validTriggerFilter, + Subscriber: validSubscriber, + }, + want: func() *apis.FieldError { + fe := apis.ErrMissingField("broker") + return fe + }(), + }, { + name: "missing attributes keys, match all", + ts: &TriggerSpec{ + Broker: "test_broker", + Filter: validEmptyTriggerFilter, + Subscriber: validSubscriber, + }, + want: &apis.FieldError{}, + }, { + name: "invalid attribute name, start with number", + ts: &TriggerSpec{ + Broker: "test_broker", + Filter: newTriggerFilter( + map[string]string{ + "0invalid": "my-value", + }), + Subscriber: validSubscriber, + }, + want: apis.ErrInvalidKeyName("0invalid", apis.CurrentField, + "Attribute name must start with a letter and can only contain "+ + "lowercase alphanumeric").ViaFieldKey("attributes", "0invalid").ViaField("filter"), + }, { + name: "invalid attribute name, capital letters", + ts: &TriggerSpec{ + Broker: "test_broker", + Filter: newTriggerFilter( + map[string]string{ + "invALID": "my-value", + }), + Subscriber: validSubscriber, + }, + want: apis.ErrInvalidKeyName("invALID", apis.CurrentField, + "Attribute name must start with a letter and can only contain "+ + "lowercase alphanumeric").ViaFieldKey("attributes", "invALID").ViaField("filter"), + }, { + name: "missing subscriber", + ts: &TriggerSpec{ + Broker: "test_broker", + Filter: validTriggerFilter, + }, + want: apis.ErrGeneric("expected at least one, got none", "subscriber.ref", "subscriber.uri"), + }, { + name: "missing subscriber.ref.name", + ts: &TriggerSpec{ + Broker: "test_broker", + Filter: validTriggerFilter, + Subscriber: invalidSubscriber, + }, + want: apis.ErrMissingField("subscriber.ref.name"), + }, { + name: "missing broker", + ts: &TriggerSpec{ + Broker: "", + Filter: validTriggerFilter, + Subscriber: validSubscriber, + }, + want: apis.ErrMissingField("broker"), + }, { + name: "valid empty filter", + ts: &TriggerSpec{ + Broker: "test_broker", + Filter: validEmptyTriggerFilter, + Subscriber: validSubscriber, + }, + want: &apis.FieldError{}, + }, { + name: "valid SourceAndType filter", + ts: &TriggerSpec{ + Broker: "test_broker", + Filter: validTriggerFilter, + Subscriber: validSubscriber, + }, + want: &apis.FieldError{}, + }, { + name: "valid Attributes filter", + ts: &TriggerSpec{ + Broker: "test_broker", + Filter: validTriggerFilter, + Subscriber: validSubscriber, + }, + want: &apis.FieldError{}, + }, { + name: "invalid delivery, invalid delay string", + ts: &TriggerSpec{ + Broker: "test_broker", + Filter: validEmptyTriggerFilter, + Subscriber: validSubscriber, + Delivery: &eventingduckv1.DeliverySpec{ + BackoffDelay: &invalidString, + }, + }, + want: apis.ErrInvalidValue(invalidString, "delivery.backoffDelay"), + }, { + name: "using BrokerRef", + ts: &TriggerSpec{ + BrokerRef: &duckv1.KReference{ + Name: "test-broker", + Namespace: "test-broker-ns", + }, + Filter: validTriggerFilter, + Subscriber: validSubscriber, + }, + want: nil, + }, { + name: "non-empty Broker but empty BrokerRef", + ts: &TriggerSpec{ + BrokerRef: &duckv1.KReference{ + Name: "test-broker", + Namespace: "test-broker-ns", + }, + Filter: validTriggerFilter, + Subscriber: validSubscriber, + }, + want: nil, + }, { + name: "empty Broker but non-empty BrokerRef", + ts: &TriggerSpec{ + BrokerRef: &duckv1.KReference{ + Name: "test-broker", + Namespace: "test-broker-ns", + }, + Filter: validTriggerFilter, + Subscriber: validSubscriber, + }, + want: nil, + }, { + name: "non-empty Broker and BrokerRef", + ts: &TriggerSpec{ + Broker: "test-broker", + BrokerRef: &duckv1.KReference{ + Name: "test-broker", + Namespace: "test-broker-ns", + }, + Filter: validTriggerFilter, + Subscriber: validSubscriber, + }, + want: apis.ErrMultipleOneOf("broker", "brokerRef"), + }} + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctx := feature.ToContext(context.TODO(), feature.Flags{ + feature.CrossNamespaceEventLinks: feature.Enabled, + }) + got := test.ts.Validate(ctx) + if diff := cmp.Diff(test.want.Error(), got.Error()); diff != "" { + t.Errorf("Validate TriggerSpec (-want, +got) =\n%s", diff) + } + }) + } +} + func TestFilterSpecValidation(t *testing.T) { newTriggerFiltersEnabledCtx := feature.ToContext(context.TODO(), feature.Flags{ feature.NewTriggerFilters: feature.Enabled, diff --git a/pkg/apis/messaging/v1/subscribable_channelable_validation.go b/pkg/apis/messaging/v1/subscribable_channelable_validation.go index 72766a1f69a..767fcca2ac5 100644 --- a/pkg/apis/messaging/v1/subscribable_channelable_validation.go +++ b/pkg/apis/messaging/v1/subscribable_channelable_validation.go @@ -20,6 +20,7 @@ import ( "context" "k8s.io/apimachinery/pkg/api/equality" + "knative.dev/eventing/pkg/apis/feature" "knative.dev/pkg/apis" duckv1 "knative.dev/pkg/apis/duck/v1" ) @@ -32,11 +33,13 @@ func isChannelEmpty(f duckv1.KReference) bool { func isValidChannel(ctx context.Context, f duckv1.KReference) *apis.FieldError { errs := f.Validate(ctx) - // Namespace field is disallowed - if f.Namespace != "" { - fe := apis.ErrDisallowedFields("namespace") - fe.Details = "only name, apiVersion and kind are supported fields" - errs = errs.Also(fe) + if !feature.FromContext(ctx).IsEnabled(feature.CrossNamespaceEventLinks) { + // Only name, apiVersion and kind are supported fields when feature.CrossNamespaceEventLinks is disabled + if f.Namespace != "" { + fe := apis.ErrDisallowedFields("namespace") + fe.Details = "only name, apiVersion and kind are supported fields when feature.CrossNamespaceEventLinks is disabled" + errs = errs.Also(fe) + } } return errs diff --git a/pkg/apis/messaging/v1/subscribable_channelable_validation_test.go b/pkg/apis/messaging/v1/subscribable_channelable_validation_test.go index df1404b49fe..38c69760002 100644 --- a/pkg/apis/messaging/v1/subscribable_channelable_validation_test.go +++ b/pkg/apis/messaging/v1/subscribable_channelable_validation_test.go @@ -58,7 +58,7 @@ var validationTests = []struct { }, want: func() *apis.FieldError { fe := apis.ErrDisallowedFields("namespace") - fe.Details = "only name, apiVersion and kind are supported fields" + fe.Details = "only name, apiVersion and kind are supported fields when feature.CrossNamespaceEventLinks is disabled" return fe }(), }, diff --git a/pkg/apis/messaging/v1/subscription_types.go b/pkg/apis/messaging/v1/subscription_types.go index 7f683d6be22..9901c9a6a2b 100644 --- a/pkg/apis/messaging/v1/subscription_types.go +++ b/pkg/apis/messaging/v1/subscription_types.go @@ -182,3 +182,8 @@ func (s *Subscription) GetUntypedSpec() interface{} { func (s *Subscription) GetStatus() *duckv1.Status { return &s.Status.Status } + +// GetCrossNamespaceRef returns the Channel reference for the Subscription. Implements the ResourceInfo interface. +func (s *Subscription) GetCrossNamespaceRef() duckv1.KReference { + return s.Spec.Channel +} diff --git a/pkg/apis/messaging/v1/subscription_validation.go b/pkg/apis/messaging/v1/subscription_validation.go index d2e2b6627ab..f2cd298971b 100644 --- a/pkg/apis/messaging/v1/subscription_validation.go +++ b/pkg/apis/messaging/v1/subscription_validation.go @@ -21,6 +21,8 @@ import ( "github.com/google/go-cmp/cmp/cmpopts" "k8s.io/apimachinery/pkg/api/equality" + "knative.dev/eventing/pkg/apis/feature" + cn "knative.dev/eventing/pkg/crossnamespace" "knative.dev/pkg/apis" duckv1 "knative.dev/pkg/apis/duck/v1" "knative.dev/pkg/kmp" @@ -32,6 +34,13 @@ func (s *Subscription) Validate(ctx context.Context) *apis.FieldError { original := apis.GetBaseline(ctx).(*Subscription) errs = errs.Also(s.CheckImmutableFields(ctx, original)) } + // s.Validate(ctx) because krshaped is defined on the entire subscription, not just the spec + if feature.FromContext(ctx).IsEnabled(feature.CrossNamespaceEventLinks) { + crossNamespaceError := cn.CheckNamespace(ctx, s) + if crossNamespaceError != nil { + errs = errs.Also(crossNamespaceError) + } + } return errs } diff --git a/pkg/apis/messaging/v1/subscription_validation_test.go b/pkg/apis/messaging/v1/subscription_validation_test.go index bf68a5d9ca1..0327fcfb6a1 100644 --- a/pkg/apis/messaging/v1/subscription_validation_test.go +++ b/pkg/apis/messaging/v1/subscription_validation_test.go @@ -34,6 +34,7 @@ const ( routeKind = "Route" routeAPIVersion = "serving.knative.dev/v1" channelName = "subscribedChannel" + channelNamespace = "subscribedChannelNamespace" replyChannelName = "toChannel" subscriberName = "subscriber" namespace = "namespace" @@ -266,6 +267,22 @@ func TestSubscriptionSpecValidation(t *testing.T) { Delivery: getDelivery(backoffDelayInvalid), }, want: apis.ErrInvalidValue(backoffDelayInvalid, "delivery.backoffDelay"), + }, { + name: "non-empty Channel namespace", + c: &SubscriptionSpec{ + Channel: duckv1.KReference{ + Kind: channelKind, + APIVersion: channelAPIVersion, + Name: channelName, + Namespace: channelNamespace, + }, + Subscriber: getValidDestination(), + }, + want: func() *apis.FieldError { + fe := apis.ErrDisallowedFields("channel.namespace") + fe.Details = "only name, apiVersion and kind are supported fields when feature.CrossNamespaceEventLinks is disabled" + return fe + }(), }} for _, test := range tests { @@ -445,6 +462,207 @@ func TestSubscriptionSpecValidationWithKRefGroupFeatureEnabled(t *testing.T) { } } +func TestSubscriptionValidationSpecWithCrossNamespaceEventLinksFeatureEnabled(t *testing.T) { + tests := []struct { + name string + c *SubscriptionSpec + want *apis.FieldError + }{{ + name: "valid", + c: &SubscriptionSpec{ + Channel: getValidChannelRef(), + Subscriber: getValidDestination(), + }, + want: nil, + }, { + name: "valid with reply", + c: &SubscriptionSpec{ + Channel: getValidChannelRef(), + Subscriber: getValidDestination(), + Reply: getValidReply(), + }, + want: nil, + }, { + name: "valid with delivery", + c: &SubscriptionSpec{ + Channel: getValidChannelRef(), + Subscriber: getValidDestination(), + Delivery: getDelivery(backoffDelayValid), + }, + want: nil, + }, { + name: "empty Channel", + c: &SubscriptionSpec{ + Channel: duckv1.KReference{}, + }, + want: func() *apis.FieldError { + fe := apis.ErrMissingField("channel") + fe.Details = "the Subscription must reference a channel" + return fe + }(), + }, { + name: "missing name in Channel", + c: &SubscriptionSpec{ + Channel: duckv1.KReference{ + Namespace: channelNamespace, + Kind: channelKind, + APIVersion: channelAPIVersion, + }, + Subscriber: getValidDestination(), + }, + want: func() *apis.FieldError { + fe := apis.ErrMissingField("channel.name") + return fe + }(), + }, { + name: "missing Subscriber and Reply", + c: &SubscriptionSpec{ + Channel: getValidChannelRef(), + }, + want: func() *apis.FieldError { + fe := apis.ErrMissingField("subscriber") + fe.Details = "the Subscription must reference a subscriber" + return fe + }(), + }, { + name: "empty Subscriber and Reply", + c: &SubscriptionSpec{ + Channel: getValidChannelRef(), + Subscriber: &duckv1.Destination{}, + Reply: &duckv1.Destination{}, + }, + want: func() *apis.FieldError { + fe := apis.ErrMissingField("subscriber") + fe.Details = "the Subscription must reference a subscriber" + return fe + }(), + }, { + name: "empty Reply", + c: &SubscriptionSpec{ + Channel: getValidChannelRef(), + Subscriber: getValidDestination(), + Reply: &duckv1.Destination{}, + }, + want: nil, + }, { + name: "missing Subscriber", + c: &SubscriptionSpec{ + Channel: getValidChannelRef(), + Reply: getValidReply(), + }, + want: func() *apis.FieldError { + fe := apis.ErrMissingField("subscriber") + fe.Details = "the Subscription must reference a subscriber" + return fe + }(), + }, { + name: "empty Subscriber", + c: &SubscriptionSpec{ + Channel: getValidChannelRef(), + Subscriber: &duckv1.Destination{}, + Reply: getValidReply(), + }, + want: func() *apis.FieldError { + fe := apis.ErrMissingField("subscriber") + fe.Details = "the Subscription must reference a subscriber" + return fe + }(), + }, { + name: "missing name in channel, and missing subscriber", + c: &SubscriptionSpec{ + Channel: duckv1.KReference{ + Kind: channelKind, + APIVersion: channelAPIVersion, + }, + }, + want: func() *apis.FieldError { + fe := apis.ErrMissingField("subscriber") + fe.Details = "the Subscription must reference a subscriber" + return apis.ErrMissingField("channel.name").Also(fe) + }(), + }, { + name: "empty", + c: &SubscriptionSpec{}, + want: func() *apis.FieldError { + fe := apis.ErrMissingField("channel") + fe.Details = "the Subscription must reference a channel" + return fe + }(), + }, { + name: "missing name in Subscriber.Ref", + c: &SubscriptionSpec{ + Channel: getValidChannelRef(), + Subscriber: &duckv1.Destination{ + Ref: &duckv1.KReference{ + Namespace: namespace, + Kind: channelKind, + APIVersion: channelAPIVersion, + }, + }, + }, + want: apis.ErrMissingField("subscriber.ref.name"), + }, { + name: "missing name in Subscriber.Ref", + c: &SubscriptionSpec{ + Channel: getValidChannelRef(), + Subscriber: getValidDestination(), + Reply: &duckv1.Destination{ + Ref: &duckv1.KReference{ + Namespace: namespace, + Name: "", + Kind: channelKind, + APIVersion: channelAPIVersion, + }, + }, + }, + want: apis.ErrMissingField("reply.ref.name"), + }, { + name: "invalid Delivery", + c: &SubscriptionSpec{ + Channel: getValidChannelRef(), + Subscriber: getValidDestination(), + Delivery: getDelivery(backoffDelayInvalid), + }, + want: apis.ErrInvalidValue(backoffDelayInvalid, "delivery.backoffDelay"), + }, { + name: "valid empty channel namespace", + c: &SubscriptionSpec{ + Channel: duckv1.KReference{ + Name: channelName, + Namespace: "", + Kind: channelKind, + APIVersion: channelAPIVersion, + }, + Subscriber: getValidDestination(), + }, + want: nil, + }, { + name: "valid cross namespace referencing", + c: &SubscriptionSpec{ + Channel: duckv1.KReference{ + Name: channelName, + Namespace: channelNamespace, + Kind: channelKind, + APIVersion: channelAPIVersion, + }, + Subscriber: getValidDestination(), + }, + want: nil, + }} + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctx := feature.ToContext(context.TODO(), feature.Flags{ + feature.CrossNamespaceEventLinks: feature.Enabled, + }) + got := test.c.Validate(ctx) + if diff := cmp.Diff(test.want.Error(), got.Error()); diff != "" { + t.Errorf("%s: validateChannel (-want, +got) = %v", test.name, diff) + } + }) + } +} + func TestSubscriptionImmutable(t *testing.T) { newChannel := getValidChannelRef() newChannel.Name = "newChannel" @@ -626,7 +844,7 @@ func TestValidChannel(t *testing.T) { }, want: func() *apis.FieldError { fe := apis.ErrDisallowedFields("namespace") - fe.Details = "only name, apiVersion and kind are supported fields" + fe.Details = "only name, apiVersion and kind are supported fields when feature.CrossNamespaceEventLinks is disabled" return fe }(), }, { diff --git a/pkg/crossnamespace/validation.go b/pkg/crossnamespace/validation.go new file mode 100644 index 00000000000..850d6bd00ce --- /dev/null +++ b/pkg/crossnamespace/validation.go @@ -0,0 +1,117 @@ +/* +Copyright 2024 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package crossnamespace + +import ( + "context" + "fmt" + + "go.uber.org/zap" + authv1 "k8s.io/api/authorization/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "knative.dev/pkg/apis" + duckv1 "knative.dev/pkg/apis/duck/v1" + "knative.dev/pkg/injection" + "knative.dev/pkg/logging" +) + +type ResourceInfo interface { + duckv1.KRShaped + GetCrossNamespaceRef() duckv1.KReference +} + +func CheckNamespace(ctx context.Context, r ResourceInfo) *apis.FieldError { + targetKind := r.GroupVersionKind().Kind + targetGroup := r.GroupVersionKind().Group + targetName := r.GetCrossNamespaceRef().Name + targetNamespace := r.GetCrossNamespaceRef().Namespace + targetFieldName := fmt.Sprintf("spec.%sNamespace", targetKind) + + // If the target namespace is empty or the same as the object namespace, this function is skipped + if targetNamespace == "" || targetNamespace == r.GetNamespace() { + return nil + } + + // GetUserInfo accesses the UserInfo attached to the webhook context. + userInfo := apis.GetUserInfo(ctx) + if userInfo == nil { + return &apis.FieldError{ + Paths: []string{targetFieldName}, + Message: "failed to get userInfo, which is needed to validate access to the target namespace", + } + } + + // GetConfig gets the current config from the context. + config := injection.GetConfig(ctx) + logging.FromContext(ctx).Info("got config", zap.Any("config", config)) + if config == nil { + return &apis.FieldError{ + Paths: []string{targetFieldName}, + Message: "failed to get config, which is needed to validate the resources created with the namespace different than the target's namespace", + } + } + + // NewForConfig creates a new Clientset for the given config. + // If config's RateLimiter is not set and QPS and Burst are acceptable, + // NewForConfig will generate a rate-limiter in configShallowCopy. + // NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), + // where httpClient was generated with rest.HTTPClientFor(c). + client, err := kubernetes.NewForConfig(config) + if err != nil { + return &apis.FieldError{ + Paths: []string{targetFieldName}, + Message: "failed to get k8s client, which is needed to validate the resources created with the namespace different than the target's namespace", + } + } + + // SubjectAccessReview checks if the user is authorized to perform an action. + action := authv1.ResourceAttributes{ + Name: targetName, + Namespace: targetNamespace, + Verb: "get", + Group: targetGroup, + Resource: targetKind, + } + + // Create the SubjectAccessReview + check := authv1.SubjectAccessReview{ + Spec: authv1.SubjectAccessReviewSpec{ + ResourceAttributes: &action, + User: userInfo.Username, + Groups: userInfo.Groups, + }, + } + + resp, err := client.AuthorizationV1().SubjectAccessReviews().Create(ctx, &check, metav1.CreateOptions{}) + + if err != nil { + return &apis.FieldError{ + Paths: []string{targetFieldName}, + Message: fmt.Sprintf("failed to make authorization request to see if user can get brokers in namespace: %s", err.Error()), + } + } + + if !resp.Status.Allowed { + return &apis.FieldError{ + Paths: []string{targetFieldName}, + Message: fmt.Sprintf("user %s is not authorized to get target resource in namespace: %s", userInfo.Username, targetNamespace), + } + } + + return nil +}