diff --git a/api/v1beta2/authpolicy_types.go b/api/v1beta2/authpolicy_types.go index 459c7205a..d2569624e 100644 --- a/api/v1beta2/authpolicy_types.go +++ b/api/v1beta2/authpolicy_types.go @@ -115,8 +115,17 @@ type CallbackSpec struct { CommonAuthRuleSpec `json:""` } +// +kubebuilder:validation:XValidation:rule="self.targetRef.kind != 'Gateway' || !has(self.routeSelectors)",message="route selectors not supported when targeting a Gateway" +// +kubebuilder:validation:XValidation:rule="self.targetRef.kind != 'Gateway' || !has(self.rules.authentication) || !self.rules.authentication.exists(x, has(self.rules.authentication[x].routeSelectors))",message="route selectors not supported when targeting a Gateway" +// +kubebuilder:validation:XValidation:rule="self.targetRef.kind != 'Gateway' || !has(self.rules.metadata) || !self.rules.metadata.exists(x, has(self.rules.metadata[x].routeSelectors))",message="route selectors not supported when targeting a Gateway" +// +kubebuilder:validation:XValidation:rule="self.targetRef.kind != 'Gateway' || !has(self.rules.authorization) || !self.rules.authorization.exists(x, has(self.rules.authorization[x].routeSelectors))",message="route selectors not supported when targeting a Gateway" +// +kubebuilder:validation:XValidation:rule="self.targetRef.kind != 'Gateway' || !has(self.rules.response) || !has(self.rules.response.success) || self.rules.response.success.headers.exists(x, has(self.rules.response.success.headers[x].routeSelectors))",message="route selectors not supported when targeting a Gateway" +// +kubebuilder:validation:XValidation:rule="self.targetRef.kind != 'Gateway' || !has(self.rules.response) || !has(self.rules.response.success) || self.rules.response.success.dynamicMetadata.exists(x, has(self.rules.response.success.dynamicMetadata[x].routeSelectors))",message="route selectors not supported when targeting a Gateway" +// +kubebuilder:validation:XValidation:rule="self.targetRef.kind != 'Gateway' || !has(self.rules.callbacks) || !self.rules.callbacks.exists(x, has(self.rules.callbacks[x].routeSelectors))",message="route selectors not supported when targeting a Gateway" type AuthPolicySpec struct { // TargetRef identifies an API object to apply policy to. + // +kubebuilder:validation:XValidation:rule="self.group == 'gateway.networking.k8s.io'",message="Invalid targetRef.group. The only supported value is 'gateway.networking.k8s.io'" + // +kubebuilder:validation:XValidation:rule="self.kind == 'HTTPRoute' || self.kind == 'Gateway'",message="Invalid targetRef.kind. The only supported values are 'HTTPRoute' and 'Gateway'" TargetRef gatewayapiv1alpha2.PolicyTargetReference `json:"targetRef"` // Top-level route selectors. @@ -203,25 +212,6 @@ func (ap *AuthPolicy) TargetKey() client.ObjectKey { } } -//+kubebuilder:object:root=true - -// AuthPolicyList contains a list of AuthPolicy -type AuthPolicyList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - Items []AuthPolicy `json:"items"` -} - -func (l *AuthPolicyList) GetItems() []common.KuadrantPolicy { - return common.Map(l.Items, func(item AuthPolicy) common.KuadrantPolicy { - return &item - }) -} - -func init() { - SchemeBuilder.Register(&AuthPolicy{}, &AuthPolicyList{}) -} - func (ap *AuthPolicy) Validate() error { if ap.Spec.TargetRef.Group != ("gateway.networking.k8s.io") { return fmt.Errorf("invalid targetRef.Group %s. The only supported group is gateway.networking.k8s.io", ap.Spec.TargetRef.Group) @@ -238,6 +228,37 @@ func (ap *AuthPolicy) Validate() error { if ap.Spec.TargetRef.Namespace != nil && string(*ap.Spec.TargetRef.Namespace) != ap.Namespace { return fmt.Errorf("invalid targetRef.Namespace %s. Currently only supporting references to the same namespace", *ap.Spec.TargetRef.Namespace) } + + // prevents usage of routeSelectors in a gateway AuthPolicy + if ap.Spec.TargetRef.Kind == ("Gateway") { + containRouteSelectors := func(config map[string]RouteSelectorsGetter) bool { + if config == nil { + return false + } + for _, config := range config { + if len(config.GetRouteSelectors()) > 0 { + return true + } + } + return false + } + configs := []map[string]RouteSelectorsGetter{ + {"": ap.Spec}, + toRouteSelectorGetterMap(ap.Spec.AuthScheme.Authentication), + toRouteSelectorGetterMap(ap.Spec.AuthScheme.Metadata), + toRouteSelectorGetterMap(ap.Spec.AuthScheme.Authorization), + toRouteSelectorGetterMap(ap.Spec.AuthScheme.Callbacks), + } + if r := ap.Spec.AuthScheme.Response; r != nil { + configs = append(configs, toRouteSelectorGetterMap(r.Success.Headers), toRouteSelectorGetterMap(r.Success.DynamicMetadata)) + } + for _, config := range configs { + if containRouteSelectors(config) { + return fmt.Errorf("route selectors not supported when targeting a Gateway") + } + } + } + return nil } @@ -283,3 +304,30 @@ func (ap *AuthPolicy) GetRulesHostnames() (ruleHosts []string) { return } + +//+kubebuilder:object:root=true + +// AuthPolicyList contains a list of AuthPolicy +type AuthPolicyList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []AuthPolicy `json:"items"` +} + +func (l *AuthPolicyList) GetItems() []common.KuadrantPolicy { + return common.Map(l.Items, func(item AuthPolicy) common.KuadrantPolicy { + return &item + }) +} + +func init() { + SchemeBuilder.Register(&AuthPolicy{}, &AuthPolicyList{}) +} + +func toRouteSelectorGetterMap[T RouteSelectorsGetter](m map[string]T) map[string]RouteSelectorsGetter { + result := make(map[string]RouteSelectorsGetter) + for k, v := range m { + result[k] = v + } + return result +} diff --git a/api/v1beta2/authpolicy_types_test.go b/api/v1beta2/authpolicy_types_test.go index d349e16f4..9e5bb6b6f 100644 --- a/api/v1beta2/authpolicy_types_test.go +++ b/api/v1beta2/authpolicy_types_test.go @@ -11,6 +11,7 @@ import ( gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" gatewayapiv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" + authorinoapi "github.com/kuadrant/authorino/api/v1beta2" "github.com/kuadrant/kuadrant-operator/pkg/common" ) @@ -239,6 +240,209 @@ func TestAuthPolicyGetRulesHostnames(t *testing.T) { } } +func TestAuthPolicyValidate(t *testing.T) { + testCases := []struct { + name string + policy *AuthPolicy + valid bool + message string + }{ + { + name: "valid policy targeting a httproute", + policy: &AuthPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-policy", + Namespace: "my-namespace", + }, + Spec: AuthPolicySpec{ + TargetRef: gatewayapiv1alpha2.PolicyTargetReference{ + Group: "gateway.networking.k8s.io", + Kind: "HTTPRoute", + Name: "my-route", + }, + }, + }, + valid: true, + }, + { + name: "valid policy targeting a gateway", + policy: &AuthPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-policy", + Namespace: "my-namespace", + }, + Spec: AuthPolicySpec{ + TargetRef: gatewayapiv1alpha2.PolicyTargetReference{ + Group: "gateway.networking.k8s.io", + Kind: "Gateway", + Name: "my-gw", + }, + }, + }, + valid: true, + }, + { + name: "invalid targetRef group", + policy: &AuthPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-policy", + Namespace: "my-namespace", + }, + Spec: AuthPolicySpec{ + TargetRef: gatewayapiv1alpha2.PolicyTargetReference{ + Group: "not-gateway.networking.k8s.io.group", + Kind: "HTTPRoute", + Name: "my-non-gwapi-route", + }, + }, + }, + message: "invalid targetRef.Group not-gateway.networking.k8s.io.group. The only supported group is gateway.networking.k8s.io", + }, + { + name: "invalid targetRef kind", + policy: &AuthPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-policy", + Namespace: "my-namespace", + }, + Spec: AuthPolicySpec{ + TargetRef: gatewayapiv1alpha2.PolicyTargetReference{ + Group: "gateway.networking.k8s.io", + Kind: "TCPRoute", + Name: "my-tcp-route", + }, + }, + }, + message: "invalid targetRef.Kind TCPRoute. The only supported kinds are HTTPRoute and Gateway", + }, + { + name: "invalid usage of top-level route selectors with a gateway targetRef", + policy: &AuthPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-policy", + Namespace: "my-namespace", + }, + Spec: AuthPolicySpec{ + TargetRef: gatewayapiv1alpha2.PolicyTargetReference{ + Group: "gateway.networking.k8s.io", + Kind: "Gateway", + Name: "my-gw", + }, + RouteSelectors: []RouteSelector{ + { + Hostnames: []gatewayapiv1beta1.Hostname{"*.foo.io"}, + Matches: []gatewayapiv1beta1.HTTPRouteMatch{ + { + Path: &gatewayapiv1beta1.HTTPPathMatch{ + Value: ptr.To("/foo"), + }, + }, + }, + }, + }, + }, + }, + message: "route selectors not supported when targeting a Gateway", + }, + { + name: "invalid usage of config-level route selectors with a gateway targetRef", + policy: &AuthPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-policy", + Namespace: "my-namespace", + }, + Spec: AuthPolicySpec{ + TargetRef: gatewayapiv1alpha2.PolicyTargetReference{ + Group: "gateway.networking.k8s.io", + Kind: "Gateway", + Name: "my-gw", + }, + AuthScheme: AuthSchemeSpec{ + Authentication: map[string]AuthenticationSpec{ + "my-rule": { + AuthenticationSpec: authorinoapi.AuthenticationSpec{ + AuthenticationMethodSpec: authorinoapi.AuthenticationMethodSpec{ + AnonymousAccess: &authorinoapi.AnonymousAccessSpec{}, + }, + }, + CommonAuthRuleSpec: CommonAuthRuleSpec{ + RouteSelectors: []RouteSelector{ + { + Hostnames: []gatewayapiv1beta1.Hostname{"*.foo.io"}, + Matches: []gatewayapiv1beta1.HTTPRouteMatch{ + { + Path: &gatewayapiv1beta1.HTTPPathMatch{ + Value: ptr.To("/foo"), + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + message: "route selectors not supported when targeting a Gateway", + }, + { + name: "invalid targetRef namespace", + policy: &AuthPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-policy", + Namespace: "my-namespace", + }, + Spec: AuthPolicySpec{ + TargetRef: gatewayapiv1alpha2.PolicyTargetReference{ + Group: "gateway.networking.k8s.io", + Kind: "HTTPRoute", + Name: "my-route", + Namespace: ptr.To(gatewayapiv1beta1.Namespace("other-namespace")), + }, + AuthScheme: AuthSchemeSpec{ + Authentication: map[string]AuthenticationSpec{ + "my-rule": { + AuthenticationSpec: authorinoapi.AuthenticationSpec{ + AuthenticationMethodSpec: authorinoapi.AuthenticationMethodSpec{ + AnonymousAccess: &authorinoapi.AnonymousAccessSpec{}, + }, + }, + CommonAuthRuleSpec: CommonAuthRuleSpec{ + RouteSelectors: []RouteSelector{ + { + Hostnames: []gatewayapiv1beta1.Hostname{"*.foo.io"}, + Matches: []gatewayapiv1beta1.HTTPRouteMatch{ + { + Path: &gatewayapiv1beta1.HTTPPathMatch{ + Value: ptr.To("/foo"), + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + message: "invalid targetRef.Namespace other-namespace. Currently only supporting references to the same namespace", + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := tc.policy.Validate() + if tc.valid && result != nil { + t.Errorf("Expected policy to be valid, got %t", result) + } + if !tc.valid && result == nil { + t.Error("Expected policy to be invalid, got no validation error") + } + }) + } +} + func testBuildRouteSelector() RouteSelector { return RouteSelector{ Hostnames: []gatewayapiv1beta1.Hostname{"toystore.kuadrant.io"}, diff --git a/bundle/manifests/kuadrant-operator.clusterserviceversion.yaml b/bundle/manifests/kuadrant-operator.clusterserviceversion.yaml index dee7c5b69..9efbcc92b 100644 --- a/bundle/manifests/kuadrant-operator.clusterserviceversion.yaml +++ b/bundle/manifests/kuadrant-operator.clusterserviceversion.yaml @@ -41,7 +41,7 @@ metadata: capabilities: Basic Install categories: Integration & Delivery containerImage: quay.io/kuadrant/kuadrant-operator:latest - createdAt: "2023-10-20T10:36:08Z" + createdAt: "2023-10-20T10:46:36Z" operators.operatorframework.io/builder: operator-sdk-v1.28.1 operators.operatorframework.io/project_layout: go.kubebuilder.io/v3 repository: https://github.com/Kuadrant/kuadrant-operator diff --git a/bundle/manifests/kuadrant.io_authpolicies.yaml b/bundle/manifests/kuadrant.io_authpolicies.yaml index 6e900f5d0..971f8e947 100644 --- a/bundle/manifests/kuadrant.io_authpolicies.yaml +++ b/bundle/manifests/kuadrant.io_authpolicies.yaml @@ -4121,6 +4121,12 @@ spec: - kind - name type: object + x-kubernetes-validations: + - message: Invalid targetRef.group. The only supported value is 'gateway.networking.k8s.io' + rule: self.group == 'gateway.networking.k8s.io' + - message: Invalid targetRef.kind. The only supported values are 'HTTPRoute' + and 'Gateway' + rule: self.kind == 'HTTPRoute' || self.kind == 'Gateway' when: description: Overall conditions for the AuthPolicy to be enforced. If omitted, the AuthPolicy will be enforced at all requests to the @@ -4177,6 +4183,29 @@ spec: required: - targetRef type: object + x-kubernetes-validations: + - message: route selectors not supported when targeting a Gateway + rule: self.targetRef.kind != 'Gateway' || !has(self.routeSelectors) + - message: route selectors not supported when targeting a Gateway + rule: self.targetRef.kind != 'Gateway' || !has(self.rules.authentication) + || !self.rules.authentication.exists(x, has(self.rules.authentication[x].routeSelectors)) + - message: route selectors not supported when targeting a Gateway + rule: self.targetRef.kind != 'Gateway' || !has(self.rules.metadata) + || !self.rules.metadata.exists(x, has(self.rules.metadata[x].routeSelectors)) + - message: route selectors not supported when targeting a Gateway + rule: self.targetRef.kind != 'Gateway' || !has(self.rules.authorization) + || !self.rules.authorization.exists(x, has(self.rules.authorization[x].routeSelectors)) + - message: route selectors not supported when targeting a Gateway + rule: self.targetRef.kind != 'Gateway' || !has(self.rules.response) + || !has(self.rules.response.success) || self.rules.response.success.headers.exists(x, + has(self.rules.response.success.headers[x].routeSelectors)) + - message: route selectors not supported when targeting a Gateway + rule: self.targetRef.kind != 'Gateway' || !has(self.rules.response) + || !has(self.rules.response.success) || self.rules.response.success.dynamicMetadata.exists(x, + has(self.rules.response.success.dynamicMetadata[x].routeSelectors)) + - message: route selectors not supported when targeting a Gateway + rule: self.targetRef.kind != 'Gateway' || !has(self.rules.callbacks) + || !self.rules.callbacks.exists(x, has(self.rules.callbacks[x].routeSelectors)) status: properties: conditions: diff --git a/config/crd/bases/kuadrant.io_authpolicies.yaml b/config/crd/bases/kuadrant.io_authpolicies.yaml index 319163dab..86765f134 100644 --- a/config/crd/bases/kuadrant.io_authpolicies.yaml +++ b/config/crd/bases/kuadrant.io_authpolicies.yaml @@ -4120,6 +4120,12 @@ spec: - kind - name type: object + x-kubernetes-validations: + - message: Invalid targetRef.group. The only supported value is 'gateway.networking.k8s.io' + rule: self.group == 'gateway.networking.k8s.io' + - message: Invalid targetRef.kind. The only supported values are 'HTTPRoute' + and 'Gateway' + rule: self.kind == 'HTTPRoute' || self.kind == 'Gateway' when: description: Overall conditions for the AuthPolicy to be enforced. If omitted, the AuthPolicy will be enforced at all requests to the @@ -4176,6 +4182,29 @@ spec: required: - targetRef type: object + x-kubernetes-validations: + - message: route selectors not supported when targeting a Gateway + rule: self.targetRef.kind != 'Gateway' || !has(self.routeSelectors) + - message: route selectors not supported when targeting a Gateway + rule: self.targetRef.kind != 'Gateway' || !has(self.rules.authentication) + || !self.rules.authentication.exists(x, has(self.rules.authentication[x].routeSelectors)) + - message: route selectors not supported when targeting a Gateway + rule: self.targetRef.kind != 'Gateway' || !has(self.rules.metadata) + || !self.rules.metadata.exists(x, has(self.rules.metadata[x].routeSelectors)) + - message: route selectors not supported when targeting a Gateway + rule: self.targetRef.kind != 'Gateway' || !has(self.rules.authorization) + || !self.rules.authorization.exists(x, has(self.rules.authorization[x].routeSelectors)) + - message: route selectors not supported when targeting a Gateway + rule: self.targetRef.kind != 'Gateway' || !has(self.rules.response) + || !has(self.rules.response.success) || self.rules.response.success.headers.exists(x, + has(self.rules.response.success.headers[x].routeSelectors)) + - message: route selectors not supported when targeting a Gateway + rule: self.targetRef.kind != 'Gateway' || !has(self.rules.response) + || !has(self.rules.response.success) || self.rules.response.success.dynamicMetadata.exists(x, + has(self.rules.response.success.dynamicMetadata[x].routeSelectors)) + - message: route selectors not supported when targeting a Gateway + rule: self.targetRef.kind != 'Gateway' || !has(self.rules.callbacks) + || !self.rules.callbacks.exists(x, has(self.rules.callbacks[x].routeSelectors)) status: properties: conditions: