diff --git a/api/v1beta2/authpolicy_types.go b/api/v1beta2/authpolicy_types.go index 141f7c3ea..79d2701a0 100644 --- a/api/v1beta2/authpolicy_types.go +++ b/api/v1beta2/authpolicy_types.go @@ -6,6 +6,7 @@ import ( "github.com/go-logr/logr" "github.com/google/go-cmp/cmp" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" gatewayapiv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" @@ -24,51 +25,126 @@ type AuthSchemeSpec struct { // +optional Conditions []authorinoapi.PatternExpressionOrRef `json:"when,omitempty"` - // TODO(@guicassolato): define top-level `routeSelectors` - // Authentication configs. // At least one config MUST evaluate to a valid identity object for the auth request to be successful. // +optional - Authentication map[string]authorinoapi.AuthenticationSpec `json:"authentication,omitempty"` + Authentication map[string]AuthenticationSpec `json:"authentication,omitempty"` // Metadata sources. // Authorino fetches auth metadata as JSON from sources specified in this config. // +optional - Metadata map[string]authorinoapi.MetadataSpec `json:"metadata,omitempty"` + Metadata map[string]MetadataSpec `json:"metadata,omitempty"` // Authorization policies. // All policies MUST evaluate to "allowed = true" for the auth request be successful. // +optional - Authorization map[string]authorinoapi.AuthorizationSpec `json:"authorization,omitempty"` + Authorization map[string]AuthorizationSpec `json:"authorization,omitempty"` // Response items. // Authorino builds custom responses to the client of the auth request. // +optional - Response *authorinoapi.ResponseSpec `json:"response,omitempty"` + Response *ResponseSpec `json:"response,omitempty"` // Callback functions. // Authorino sends callbacks at the end of the auth pipeline to the endpoints specified in this config. // +optional - Callbacks map[string]authorinoapi.CallbackSpec `json:"callbacks,omitempty"` + Callbacks map[string]CallbackSpec `json:"callbacks,omitempty"` +} + +type CommonAuthRuleSpec struct { + // Top-level route selectors. + // If present, the elements will be used to select HTTPRoute rules that, when activated, trigger the auth rule. + // At least one selected HTTPRoute rule must match to trigger the auth rule. + // If no route selectors are specified, the auth rule will be evaluated at all requests to the protected routes. + // +optional + RouteSelectors []RouteSelector `json:"routeSelectors,omitempty"` +} + +// GetRouteSelectors returns the route selectors of the auth rule spec. +// impl: RouteSelectorsGetter +func (s CommonAuthRuleSpec) GetRouteSelectors() []RouteSelector { + return s.RouteSelectors +} + +type AuthenticationSpec struct { + authorinoapi.AuthenticationSpec `json:""` + CommonAuthRuleSpec `json:""` +} + +type MetadataSpec struct { + authorinoapi.MetadataSpec `json:""` + CommonAuthRuleSpec `json:""` +} + +type AuthorizationSpec struct { + authorinoapi.AuthorizationSpec `json:""` + CommonAuthRuleSpec `json:""` +} + +type ResponseSpec struct { + // Customizations on the denial status attributes when the request is unauthenticated. + // For integration of Authorino via proxy, the proxy must honour the response status attributes specified in this config. + // Default: 401 Unauthorized + // +optional + Unauthenticated *authorinoapi.DenyWithSpec `json:"unauthenticated,omitempty"` + + // Customizations on the denial status attributes when the request is unauthorized. + // For integration of Authorino via proxy, the proxy must honour the response status attributes specified in this config. + // Default: 403 Forbidden + // +optional + Unauthorized *authorinoapi.DenyWithSpec `json:"unauthorized,omitempty"` + + // Response items to be included in the auth response when the request is authenticated and authorized. + // For integration of Authorino via proxy, the proxy must use these settings to propagate dynamic metadata and/or inject data in the request. + // +optional + Success WrappedSuccessResponseSpec `json:"success,omitempty"` +} + +type WrappedSuccessResponseSpec struct { + // Custom success response items wrapped as HTTP headers. + // For integration of Authorino via proxy, the proxy must use these settings to inject data in the request. + Headers map[string]HeaderSuccessResponseSpec `json:"headers,omitempty"` + + // Custom success response items wrapped as HTTP headers. + // For integration of Authorino via proxy, the proxy must use these settings to propagate dynamic metadata. + // See https://www.envoyproxy.io/docs/envoy/latest/configuration/advanced/well_known_dynamic_metadata + DynamicMetadata map[string]SuccessResponseSpec `json:"dynamicMetadata,omitempty"` +} + +type HeaderSuccessResponseSpec struct { + SuccessResponseSpec `json:""` +} + +type SuccessResponseSpec struct { + authorinoapi.SuccessResponseSpec `json:""` + CommonAuthRuleSpec `json:""` +} + +type CallbackSpec struct { + authorinoapi.CallbackSpec `json:""` + CommonAuthRuleSpec `json:""` } type AuthPolicySpec struct { // TargetRef identifies an API object to apply policy to. TargetRef gatewayapiv1alpha2.PolicyTargetReference `json:"targetRef"` - // Route rules specify the HTTP route attributes that trigger the external authorization service - // TODO(@guicassolato): remove – conditions to trigger the ext-authz service will be computed from `routeSelectors` - RouteRules []RouteRule `json:"routes,omitempty"` + // Top-level route selectors. + // If present, the elements will be used to select HTTPRoute rules that, when activated, trigger the external authorization service. + // At least one selected HTTPRoute rule must match to trigger the AuthPolicy. + // If no route selectors are specified, the AuthPolicy will be enforced at all requests to the protected routes. + // +optional + RouteSelectors []RouteSelector `json:"routeSelectors,omitempty"` // The auth rules of the policy. // See Authorino's AuthConfig CRD for more details. AuthScheme AuthSchemeSpec `json:"rules,omitempty"` } -type RouteRule struct { - Hosts []string `json:"hosts,omitempty"` - Methods []string `json:"methods,omitempty"` - Paths []string `json:"paths,omitempty"` +// GetRouteSelectors returns the top-level route selectors of the auth scheme. +// impl: RouteSelectorsGetter +func (s AuthPolicySpec) GetRouteSelectors() []RouteSelector { + return s.RouteSelectors } type AuthPolicyStatus struct { @@ -114,6 +190,18 @@ type AuthPolicy struct { Status AuthPolicyStatus `json:"status,omitempty"` } +func (ap *AuthPolicy) TargetKey() client.ObjectKey { + ns := ap.Namespace + if ap.Spec.TargetRef.Namespace != nil { + ns = string(*ap.Spec.TargetRef.Namespace) + } + + return client.ObjectKey{ + Name: string(ap.Spec.TargetRef.Name), + Namespace: ns, + } +} + //+kubebuilder:object:root=true // AuthPolicyList contains a list of AuthPolicy @@ -123,6 +211,12 @@ type AuthPolicyList struct { 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{}) } @@ -154,10 +248,37 @@ func (ap *AuthPolicy) GetWrappedNamespace() gatewayapiv1beta1.Namespace { return gatewayapiv1beta1.Namespace(ap.Namespace) } +// GetRulesHostnames returns all hostnames referenced in the route selectors of the policy. func (ap *AuthPolicy) GetRulesHostnames() (ruleHosts []string) { ruleHosts = make([]string, 0) - for _, rule := range ap.Spec.RouteRules { - ruleHosts = append(ruleHosts, rule.Hosts...) + + appendRuleHosts := func(obj RouteSelectorsGetter) { + for _, routeSelector := range obj.GetRouteSelectors() { + ruleHosts = append(ruleHosts, common.HostnamesToStrings(routeSelector.Hostnames)...) + } + } + + appendRuleHosts(ap.Spec) + for _, config := range ap.Spec.AuthScheme.Authentication { + appendRuleHosts(config) } + for _, config := range ap.Spec.AuthScheme.Metadata { + appendRuleHosts(config) + } + for _, config := range ap.Spec.AuthScheme.Authorization { + appendRuleHosts(config) + } + if response := ap.Spec.AuthScheme.Response; response != nil { + for _, config := range response.Success.Headers { + appendRuleHosts(config) + } + for _, config := range response.Success.DynamicMetadata { + appendRuleHosts(config) + } + } + for _, config := range ap.Spec.AuthScheme.Callbacks { + appendRuleHosts(config) + } + return } diff --git a/api/v1beta2/authpolicy_types_test.go b/api/v1beta2/authpolicy_types_test.go new file mode 100644 index 000000000..d349e16f4 --- /dev/null +++ b/api/v1beta2/authpolicy_types_test.go @@ -0,0 +1,253 @@ +//go:build unit + +package v1beta2 + +import ( + "reflect" + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + gatewayapiv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" + + "github.com/kuadrant/kuadrant-operator/pkg/common" +) + +func TestCommonAuthRuleSpecGetRouteSelectors(t *testing.T) { + spec := &CommonAuthRuleSpec{} + if spec.GetRouteSelectors() != nil { + t.Errorf("Expected nil route selectors") + } + routeSelector := testBuildRouteSelector() + spec.RouteSelectors = []RouteSelector{routeSelector} + result := spec.GetRouteSelectors() + if len(result) != 1 { + t.Errorf("Expected 1 route selector, got %d", len(result)) + } + if !reflect.DeepEqual(result[0], routeSelector) { + t.Errorf("Expected route selector %v, got %v", routeSelector, result[0]) + } +} + +func TestAuthPolicySpecGetRouteSelectors(t *testing.T) { + spec := &AuthPolicySpec{} + if spec.GetRouteSelectors() != nil { + t.Errorf("Expected nil route selectors") + } + routeSelector := testBuildRouteSelector() + spec.RouteSelectors = []RouteSelector{routeSelector} + result := spec.GetRouteSelectors() + if len(result) != 1 { + t.Errorf("Expected 1 route selector, got %d", len(result)) + } + if !reflect.DeepEqual(result[0], routeSelector) { + t.Errorf("Expected route selector %v, got %v", routeSelector, result[0]) + } +} + +func TestAuthPolicyTargetKey(t *testing.T) { + 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", + }, + }, + } + // targetRef missing namespace + expected := "my-namespace/my-route" + if result := policy.TargetKey().String(); result != expected { + t.Errorf("Expected target key %s, got %s", expected, result) + } + + // targetRef with namespace + policy.Spec.TargetRef.Namespace = ptr.To(gatewayapiv1beta1.Namespace("route-namespace")) + expected = "route-namespace/my-route" + if result := policy.TargetKey().String(); result != expected { + t.Errorf("Expected target key %s, got %s", expected, result) + } +} + +func TestAuthPolicyListGetItems(t *testing.T) { + list := &AuthPolicyList{} + if len(list.GetItems()) != 0 { + t.Errorf("Expected empty list of items") + } + policy := AuthPolicy{} + list.Items = []AuthPolicy{policy} + result := list.GetItems() + if len(result) != 1 { + t.Errorf("Expected 1 item, got %d", len(result)) + } + _, ok := result[0].(common.KuadrantPolicy) + if !ok { + t.Errorf("Expected item to be a KuadrantPolicy") + } +} + +func TestAuthPolicyGetRulesHostnames(t *testing.T) { + 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", + }, + }, + } + // no route selectors + result := policy.GetRulesHostnames() + if expected := 0; len(result) != expected { + t.Errorf("Expected %d hostnames, got %d", expected, len(result)) + } + policy.Spec.RouteSelectors = []RouteSelector{ + { + Hostnames: []gatewayapiv1beta1.Hostname{"*.kuadrant.io", "toystore.kuadrant.io"}, + }, + } + // 1 top-level route selectors with 2 hostnames + result = policy.GetRulesHostnames() + if expected := 2; len(result) != expected { + t.Errorf("Expected %d hostnames, got %d", expected, len(result)) + } + if expected := "*.kuadrant.io"; result[0] != expected { + t.Errorf("Expected hostname to be %s, got %s", expected, result[0]) + } + if expected := "toystore.kuadrant.io"; result[1] != expected { + t.Errorf("Expected hostname to be %s, got %s", expected, result[1]) + } + // + 1 authentication route selector with 1 hostname + policy.Spec.AuthScheme.Authentication = map[string]AuthenticationSpec{ + "my-authn": { + CommonAuthRuleSpec: CommonAuthRuleSpec{ + RouteSelectors: []RouteSelector{testBuildRouteSelector()}, + }, + }, + } + result = policy.GetRulesHostnames() + if expected := 3; len(result) != expected { + t.Errorf("Expected %d hostnames, got %d", expected, len(result)) + } + if expected := "*.kuadrant.io"; result[0] != expected { + t.Errorf("Expected hostname to be %s, got %s", expected, result[0]) + } + if expected := "toystore.kuadrant.io"; result[1] != expected { + t.Errorf("Expected hostname to be %s, got %s", expected, result[1]) + } + if expected := "toystore.kuadrant.io"; result[2] != expected { + t.Errorf("Expected hostname to be %s, got %s", expected, result[2]) + } + // + 1 metadata route selector with 1 hostname + policy.Spec.AuthScheme.Metadata = map[string]MetadataSpec{ + "my-metadata": { + CommonAuthRuleSpec: CommonAuthRuleSpec{ + RouteSelectors: []RouteSelector{testBuildRouteSelector()}, + }, + }, + } + result = policy.GetRulesHostnames() + if expected := 4; len(result) != expected { + t.Errorf("Expected %d hostnames, got %d", expected, len(result)) + } + if expected := "toystore.kuadrant.io"; result[3] != expected { + t.Errorf("Expected hostname to be %s, got %s", expected, result[3]) + } + // + 2 authorization route selector with 1 hostname each + policy.Spec.AuthScheme.Authorization = map[string]AuthorizationSpec{ + "my-authz": { + CommonAuthRuleSpec: CommonAuthRuleSpec{ + RouteSelectors: []RouteSelector{testBuildRouteSelector(), testBuildRouteSelector()}, + }, + }, + } + result = policy.GetRulesHostnames() + if expected := 6; len(result) != expected { + t.Errorf("Expected %d hostnames, got %d", expected, len(result)) + } + if expected := "toystore.kuadrant.io"; result[4] != expected { + t.Errorf("Expected hostname to be %s, got %s", expected, result[4]) + } + if expected := "toystore.kuadrant.io"; result[5] != expected { + t.Errorf("Expected hostname to be %s, got %s", expected, result[5]) + } + // + 2 response route selectors with 2+1 hostnames + policy.Spec.AuthScheme.Response = &ResponseSpec{ + Success: WrappedSuccessResponseSpec{ + Headers: map[string]HeaderSuccessResponseSpec{ + "my-header": { + SuccessResponseSpec: SuccessResponseSpec{ + CommonAuthRuleSpec: CommonAuthRuleSpec{ + RouteSelectors: []RouteSelector{ + { + Hostnames: []gatewayapiv1beta1.Hostname{"*.kuadrant.io", "toystore.kuadrant.io"}, + }, + }, + }, + }, + }, + }, + DynamicMetadata: map[string]SuccessResponseSpec{ + "my-dynmetadata": { + CommonAuthRuleSpec: CommonAuthRuleSpec{ + RouteSelectors: []RouteSelector{ + { + Hostnames: []gatewayapiv1beta1.Hostname{"*.kuadrant.io"}, + }, + }, + }, + }, + }, + }, + } + result = policy.GetRulesHostnames() + if expected := 9; len(result) != expected { + t.Errorf("Expected %d hostnames, got %d", expected, len(result)) + } + if expected := "*.kuadrant.io"; result[6] != expected { + t.Errorf("Expected hostname to be %s, got %s", expected, result[6]) + } + if expected := "toystore.kuadrant.io"; result[7] != expected { + t.Errorf("Expected hostname to be %s, got %s", expected, result[7]) + } + if expected := "*.kuadrant.io"; result[8] != expected { + t.Errorf("Expected hostname to be %s, got %s", expected, result[8]) + } + // + 1 callbacks route selector with 1 hostname + policy.Spec.AuthScheme.Callbacks = map[string]CallbackSpec{ + "my-callback": { + CommonAuthRuleSpec: CommonAuthRuleSpec{ + RouteSelectors: []RouteSelector{testBuildRouteSelector()}, + }, + }, + } + result = policy.GetRulesHostnames() + if expected := 10; len(result) != expected { + t.Errorf("Expected %d hostnames, got %d", expected, len(result)) + } + if expected := "toystore.kuadrant.io"; result[9] != expected { + t.Errorf("Expected hostname to be %s, got %s", expected, result[9]) + } +} + +func testBuildRouteSelector() RouteSelector { + return RouteSelector{ + Hostnames: []gatewayapiv1beta1.Hostname{"toystore.kuadrant.io"}, + Matches: []gatewayapiv1beta1.HTTPRouteMatch{ + { + Path: &gatewayapiv1beta1.HTTPPathMatch{ + Value: ptr.To("/toy"), + }, + }, + }, + } +} diff --git a/api/v1beta2/ratelimitpolicy_types.go b/api/v1beta2/ratelimitpolicy_types.go index 753236206..1ba25a4a2 100644 --- a/api/v1beta2/ratelimitpolicy_types.go +++ b/api/v1beta2/ratelimitpolicy_types.go @@ -216,6 +216,12 @@ type RateLimitPolicyList struct { Items []RateLimitPolicy `json:"items"` } +func (l *RateLimitPolicyList) GetItems() []common.KuadrantPolicy { + return common.Map(l.Items, func(item RateLimitPolicy) common.KuadrantPolicy { + return &item + }) +} + func (r *RateLimitPolicy) GetTargetRef() gatewayapiv1alpha2.PolicyTargetReference { return r.Spec.TargetRef } diff --git a/api/v1beta2/ratelimitpolicy_types_test.go b/api/v1beta2/ratelimitpolicy_types_test.go index 351789e3e..140e82e13 100644 --- a/api/v1beta2/ratelimitpolicy_types_test.go +++ b/api/v1beta2/ratelimitpolicy_types_test.go @@ -9,6 +9,8 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" gatewayapiv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" + + "github.com/kuadrant/kuadrant-operator/pkg/common" ) func testBuildBasicRLP(name string, kind gatewayapiv1beta1.Kind) *RateLimitPolicy { @@ -92,3 +94,20 @@ func TestRateLimitPolicyValidation(t *testing.T) { t.Fatalf(`rlp.Validate() did not return expected error. Instead: %v`, err) } } + +func TestRateLimitPolicyListGetItems(t *testing.T) { + list := &RateLimitPolicyList{} + if len(list.GetItems()) != 0 { + t.Errorf("Expected empty list of items") + } + policy := RateLimitPolicy{} + list.Items = []RateLimitPolicy{policy} + result := list.GetItems() + if len(result) != 1 { + t.Errorf("Expected 1 item, got %d", len(result)) + } + _, ok := result[0].(common.KuadrantPolicy) + if !ok { + t.Errorf("Expected item to be a KuadrantPolicy") + } +} diff --git a/api/v1beta2/route_selectors.go b/api/v1beta2/route_selectors.go index cec7936d8..a40af94f2 100644 --- a/api/v1beta2/route_selectors.go +++ b/api/v1beta2/route_selectors.go @@ -51,3 +51,24 @@ func (s *RouteSelector) SelectRules(route *gatewayapiv1beta1.HTTPRoute) (rules [ } return } + +// HostnamesForConditions allows avoiding building conditions for hostnames that are excluded by the selector +// or when the hostname is irrelevant (i.e. matches all hostnames) +func (s *RouteSelector) HostnamesForConditions(route *gatewayapiv1beta1.HTTPRoute) []gatewayapiv1beta1.Hostname { + hostnames := route.Spec.Hostnames + + if len(s.Hostnames) > 0 { + hostnames = common.Intersection(s.Hostnames, hostnames) + } + + if common.SameElements(hostnames, route.Spec.Hostnames) { + return []gatewayapiv1beta1.Hostname{"*"} + } + + return hostnames +} + +// +kubebuilder:object:generate=false +type RouteSelectorsGetter interface { + GetRouteSelectors() []RouteSelector +} diff --git a/api/v1beta2/route_selectors_test.go b/api/v1beta2/route_selectors_test.go index 347dbe20e..ee2cf9095 100644 --- a/api/v1beta2/route_selectors_test.go +++ b/api/v1beta2/route_selectors_test.go @@ -8,71 +8,14 @@ import ( "testing" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" gatewayapiv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" "github.com/kuadrant/kuadrant-operator/pkg/common" ) func TestRouteSelectors(t *testing.T) { - gatewayHostnames := []gatewayapiv1beta1.Hostname{ - "*.toystore.com", - } - - gateway := &gatewayapiv1beta1.Gateway{ - ObjectMeta: metav1.ObjectMeta{ - Name: "my-gateway", - }, - } - - for _, hostname := range gatewayHostnames { - gateway.Spec.Listeners = append(gateway.Spec.Listeners, gatewayapiv1beta1.Listener{Hostname: &hostname}) - } - - route := &gatewayapiv1beta1.HTTPRoute{ - Spec: gatewayapiv1beta1.HTTPRouteSpec{ - CommonRouteSpec: gatewayapiv1beta1.CommonRouteSpec{ - ParentRefs: []gatewayapiv1beta1.ParentReference{ - { - Name: gatewayapiv1beta1.ObjectName(gateway.Name), - }, - }, - }, - Hostnames: []gatewayapiv1beta1.Hostname{"api.toystore.com"}, - Rules: []gatewayapiv1beta1.HTTPRouteRule{ - { - Matches: []gatewayapiv1beta1.HTTPRouteMatch{ - // get /toys* - { - Path: &gatewayapiv1beta1.HTTPPathMatch{ - Type: &[]gatewayapiv1beta1.PathMatchType{gatewayapiv1beta1.PathMatchPathPrefix}[0], - Value: &[]string{"/toy"}[0], - }, - Method: &[]gatewayapiv1beta1.HTTPMethod{gatewayapiv1beta1.HTTPMethod("GET")}[0], - }, - // post /toys* - { - Path: &gatewayapiv1beta1.HTTPPathMatch{ - Type: &[]gatewayapiv1beta1.PathMatchType{gatewayapiv1beta1.PathMatchPathPrefix}[0], - Value: &[]string{"/toy"}[0], - }, - Method: &[]gatewayapiv1beta1.HTTPMethod{gatewayapiv1beta1.HTTPMethod("POST")}[0], - }, - }, - }, - { - Matches: []gatewayapiv1beta1.HTTPRouteMatch{ - // /assets* - { - Path: &gatewayapiv1beta1.HTTPPathMatch{ - Type: &[]gatewayapiv1beta1.PathMatchType{gatewayapiv1beta1.PathMatchPathPrefix}[0], - Value: &[]string{"/assets"}[0], - }, - }, - }, - }, - }, - }, - } + route := testBuildHttpRoute(testBuildGateway()) testCases := []struct { name string @@ -209,3 +152,134 @@ func TestRouteSelectors(t *testing.T) { }) } } + +func TestRouteSelectorsHostnamesForConditions(t *testing.T) { + route := testBuildHttpRoute(testBuildGateway()) + route.Spec.Hostnames = append(route.Spec.Hostnames, gatewayapiv1beta1.Hostname("www.toystore.com")) + + // route and selector with exact same hostnames + selector := RouteSelector{ + Hostnames: []gatewayapiv1beta1.Hostname{"api.toystore.com", "www.toystore.com"}, + } + result := selector.HostnamesForConditions(route) + if expected := 1; len(result) != expected { + t.Errorf("Expected %d hostnames, got %d", expected, len(result)) + } + if expected := "*"; string(result[0]) != expected { + t.Errorf("Expected hostname to be %s, got %s", expected, result[0]) + } + + // route and selector with some overlapping hostnames + selector = RouteSelector{ + Hostnames: []gatewayapiv1beta1.Hostname{"api.toystore.com", "other.io"}, + } + result = selector.HostnamesForConditions(route) + if expected := 1; len(result) != expected { + t.Errorf("Expected %d hostnames, got %d", expected, len(result)) + } + if expected := "api.toystore.com"; string(result[0]) != expected { + t.Errorf("Expected hostname to be %s, got %s", expected, result[0]) + } + + // route and selector with no overlapping hostnames + selector = RouteSelector{ + Hostnames: []gatewayapiv1beta1.Hostname{"other.io"}, + } + result = selector.HostnamesForConditions(route) + if expected := 0; len(result) != expected { + t.Errorf("Expected %d hostnames, got %d", expected, len(result)) + } + + // route with hostnames and selector without hostnames + selector = RouteSelector{} + result = selector.HostnamesForConditions(route) + if expected := 1; len(result) != expected { + t.Errorf("Expected %d hostnames, got %d", expected, len(result)) + } + if expected := "*"; string(result[0]) != expected { + t.Errorf("Expected hostname to be %s, got %s", expected, result[0]) + } + + // route without hostnames and selector with hostnames + route.Spec.Hostnames = []gatewayapiv1beta1.Hostname{} + selector = RouteSelector{ + Hostnames: []gatewayapiv1beta1.Hostname{"api.toystore.com"}, + } + result = selector.HostnamesForConditions(route) + if expected := 1; len(result) != expected { + t.Errorf("Expected %d hostnames, got %d", expected, len(result)) + } + + // route and selector without hostnames + selector = RouteSelector{} + result = selector.HostnamesForConditions(route) + if expected := 1; len(result) != expected { + t.Errorf("Expected %d hostnames, got %d", expected, len(result)) + } + if expected := "*"; string(result[0]) != expected { + t.Errorf("Expected hostname to be %s, got %s", expected, result[0]) + } +} + +func testBuildGateway() *gatewayapiv1beta1.Gateway { + return &gatewayapiv1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-gateway", + }, + Spec: gatewayapiv1beta1.GatewaySpec{ + Listeners: []gatewayapiv1beta1.Listener{ + { + Hostname: ptr.To(gatewayapiv1beta1.Hostname("*.toystore.com")), + }, + }, + }, + } +} + +func testBuildHttpRoute(parentGateway *gatewayapiv1beta1.Gateway) *gatewayapiv1beta1.HTTPRoute { + return &gatewayapiv1beta1.HTTPRoute{ + Spec: gatewayapiv1beta1.HTTPRouteSpec{ + CommonRouteSpec: gatewayapiv1beta1.CommonRouteSpec{ + ParentRefs: []gatewayapiv1beta1.ParentReference{ + { + Name: gatewayapiv1beta1.ObjectName(parentGateway.Name), + }, + }, + }, + Hostnames: []gatewayapiv1beta1.Hostname{"api.toystore.com"}, + Rules: []gatewayapiv1beta1.HTTPRouteRule{ + { + Matches: []gatewayapiv1beta1.HTTPRouteMatch{ + // get /toys* + { + Path: &gatewayapiv1beta1.HTTPPathMatch{ + Type: &[]gatewayapiv1beta1.PathMatchType{gatewayapiv1beta1.PathMatchPathPrefix}[0], + Value: &[]string{"/toy"}[0], + }, + Method: &[]gatewayapiv1beta1.HTTPMethod{gatewayapiv1beta1.HTTPMethod("GET")}[0], + }, + // post /toys* + { + Path: &gatewayapiv1beta1.HTTPPathMatch{ + Type: &[]gatewayapiv1beta1.PathMatchType{gatewayapiv1beta1.PathMatchPathPrefix}[0], + Value: &[]string{"/toy"}[0], + }, + Method: &[]gatewayapiv1beta1.HTTPMethod{gatewayapiv1beta1.HTTPMethod("POST")}[0], + }, + }, + }, + { + Matches: []gatewayapiv1beta1.HTTPRouteMatch{ + // /assets* + { + Path: &gatewayapiv1beta1.HTTPPathMatch{ + Type: &[]gatewayapiv1beta1.PathMatchType{gatewayapiv1beta1.PathMatchPathPrefix}[0], + Value: &[]string{"/assets"}[0], + }, + }, + }, + }, + }, + }, + } +} diff --git a/api/v1beta2/zz_generated.deepcopy.go b/api/v1beta2/zz_generated.deepcopy.go index 0270fd4a8..8261e7556 100644 --- a/api/v1beta2/zz_generated.deepcopy.go +++ b/api/v1beta2/zz_generated.deepcopy.go @@ -90,9 +90,9 @@ func (in *AuthPolicyList) DeepCopyObject() runtime.Object { func (in *AuthPolicySpec) DeepCopyInto(out *AuthPolicySpec) { *out = *in in.TargetRef.DeepCopyInto(&out.TargetRef) - if in.RouteRules != nil { - in, out := &in.RouteRules, &out.RouteRules - *out = make([]RouteRule, len(*in)) + if in.RouteSelectors != nil { + in, out := &in.RouteSelectors, &out.RouteSelectors + *out = make([]RouteSelector, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -160,33 +160,33 @@ func (in *AuthSchemeSpec) DeepCopyInto(out *AuthSchemeSpec) { } if in.Authentication != nil { in, out := &in.Authentication, &out.Authentication - *out = make(map[string]apiv1beta2.AuthenticationSpec, len(*in)) + *out = make(map[string]AuthenticationSpec, len(*in)) for key, val := range *in { (*out)[key] = *val.DeepCopy() } } if in.Metadata != nil { in, out := &in.Metadata, &out.Metadata - *out = make(map[string]apiv1beta2.MetadataSpec, len(*in)) + *out = make(map[string]MetadataSpec, len(*in)) for key, val := range *in { (*out)[key] = *val.DeepCopy() } } if in.Authorization != nil { in, out := &in.Authorization, &out.Authorization - *out = make(map[string]apiv1beta2.AuthorizationSpec, len(*in)) + *out = make(map[string]AuthorizationSpec, len(*in)) for key, val := range *in { (*out)[key] = *val.DeepCopy() } } if in.Response != nil { in, out := &in.Response, &out.Response - *out = new(apiv1beta2.ResponseSpec) + *out = new(ResponseSpec) (*in).DeepCopyInto(*out) } if in.Callbacks != nil { in, out := &in.Callbacks, &out.Callbacks - *out = make(map[string]apiv1beta2.CallbackSpec, len(*in)) + *out = make(map[string]CallbackSpec, len(*in)) for key, val := range *in { (*out)[key] = *val.DeepCopy() } @@ -203,6 +203,95 @@ func (in *AuthSchemeSpec) DeepCopy() *AuthSchemeSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AuthenticationSpec) DeepCopyInto(out *AuthenticationSpec) { + *out = *in + in.AuthenticationSpec.DeepCopyInto(&out.AuthenticationSpec) + in.CommonAuthRuleSpec.DeepCopyInto(&out.CommonAuthRuleSpec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthenticationSpec. +func (in *AuthenticationSpec) DeepCopy() *AuthenticationSpec { + if in == nil { + return nil + } + out := new(AuthenticationSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AuthorizationSpec) DeepCopyInto(out *AuthorizationSpec) { + *out = *in + in.AuthorizationSpec.DeepCopyInto(&out.AuthorizationSpec) + in.CommonAuthRuleSpec.DeepCopyInto(&out.CommonAuthRuleSpec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthorizationSpec. +func (in *AuthorizationSpec) DeepCopy() *AuthorizationSpec { + if in == nil { + return nil + } + out := new(AuthorizationSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CallbackSpec) DeepCopyInto(out *CallbackSpec) { + *out = *in + in.CallbackSpec.DeepCopyInto(&out.CallbackSpec) + in.CommonAuthRuleSpec.DeepCopyInto(&out.CommonAuthRuleSpec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CallbackSpec. +func (in *CallbackSpec) DeepCopy() *CallbackSpec { + if in == nil { + return nil + } + out := new(CallbackSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CommonAuthRuleSpec) DeepCopyInto(out *CommonAuthRuleSpec) { + *out = *in + if in.RouteSelectors != nil { + in, out := &in.RouteSelectors, &out.RouteSelectors + *out = make([]RouteSelector, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CommonAuthRuleSpec. +func (in *CommonAuthRuleSpec) DeepCopy() *CommonAuthRuleSpec { + if in == nil { + return nil + } + out := new(CommonAuthRuleSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HeaderSuccessResponseSpec) DeepCopyInto(out *HeaderSuccessResponseSpec) { + *out = *in + in.SuccessResponseSpec.DeepCopyInto(&out.SuccessResponseSpec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HeaderSuccessResponseSpec. +func (in *HeaderSuccessResponseSpec) DeepCopy() *HeaderSuccessResponseSpec { + if in == nil { + return nil + } + out := new(HeaderSuccessResponseSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Limit) DeepCopyInto(out *Limit) { *out = *in @@ -240,6 +329,23 @@ func (in *Limit) DeepCopy() *Limit { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MetadataSpec) DeepCopyInto(out *MetadataSpec) { + *out = *in + in.MetadataSpec.DeepCopyInto(&out.MetadataSpec) + in.CommonAuthRuleSpec.DeepCopyInto(&out.CommonAuthRuleSpec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MetadataSpec. +func (in *MetadataSpec) DeepCopy() *MetadataSpec { + if in == nil { + return nil + } + out := new(MetadataSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Rate) DeepCopyInto(out *Rate) { *out = *in @@ -360,31 +466,27 @@ func (in *RateLimitPolicyStatus) DeepCopy() *RateLimitPolicyStatus { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *RouteRule) DeepCopyInto(out *RouteRule) { +func (in *ResponseSpec) DeepCopyInto(out *ResponseSpec) { *out = *in - if in.Hosts != nil { - in, out := &in.Hosts, &out.Hosts - *out = make([]string, len(*in)) - copy(*out, *in) - } - if in.Methods != nil { - in, out := &in.Methods, &out.Methods - *out = make([]string, len(*in)) - copy(*out, *in) + if in.Unauthenticated != nil { + in, out := &in.Unauthenticated, &out.Unauthenticated + *out = new(apiv1beta2.DenyWithSpec) + (*in).DeepCopyInto(*out) } - if in.Paths != nil { - in, out := &in.Paths, &out.Paths - *out = make([]string, len(*in)) - copy(*out, *in) + if in.Unauthorized != nil { + in, out := &in.Unauthorized, &out.Unauthorized + *out = new(apiv1beta2.DenyWithSpec) + (*in).DeepCopyInto(*out) } + in.Success.DeepCopyInto(&out.Success) } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RouteRule. -func (in *RouteRule) DeepCopy() *RouteRule { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResponseSpec. +func (in *ResponseSpec) DeepCopy() *ResponseSpec { if in == nil { return nil } - out := new(RouteRule) + out := new(ResponseSpec) in.DeepCopyInto(out) return out } @@ -416,6 +518,23 @@ func (in *RouteSelector) DeepCopy() *RouteSelector { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SuccessResponseSpec) DeepCopyInto(out *SuccessResponseSpec) { + *out = *in + in.SuccessResponseSpec.DeepCopyInto(&out.SuccessResponseSpec) + in.CommonAuthRuleSpec.DeepCopyInto(&out.CommonAuthRuleSpec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SuccessResponseSpec. +func (in *SuccessResponseSpec) DeepCopy() *SuccessResponseSpec { + if in == nil { + return nil + } + out := new(SuccessResponseSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *WhenCondition) DeepCopyInto(out *WhenCondition) { *out = *in @@ -430,3 +549,32 @@ func (in *WhenCondition) DeepCopy() *WhenCondition { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WrappedSuccessResponseSpec) DeepCopyInto(out *WrappedSuccessResponseSpec) { + *out = *in + if in.Headers != nil { + in, out := &in.Headers, &out.Headers + *out = make(map[string]HeaderSuccessResponseSpec, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } + if in.DynamicMetadata != nil { + in, out := &in.DynamicMetadata, &out.DynamicMetadata + *out = make(map[string]SuccessResponseSpec, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WrappedSuccessResponseSpec. +func (in *WrappedSuccessResponseSpec) DeepCopy() *WrappedSuccessResponseSpec { + if in == nil { + return nil + } + out := new(WrappedSuccessResponseSpec) + in.DeepCopyInto(out) + return out +} diff --git a/bundle/manifests/kuadrant-operator.clusterserviceversion.yaml b/bundle/manifests/kuadrant-operator.clusterserviceversion.yaml index 2c9e38570..b53b79a31 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-18T09:46:22Z" + createdAt: "2023-10-18T09:46:48Z" 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 67575631c..5a4e88fda 100644 --- a/bundle/manifests/kuadrant.io_authpolicies.yaml +++ b/bundle/manifests/kuadrant.io_authpolicies.yaml @@ -34,24 +34,212 @@ spec: type: object spec: properties: - routes: - description: 'Route rules specify the HTTP route attributes that trigger - the external authorization service TODO(@guicassolato): remove – - conditions to trigger the ext-authz service will be computed from - `routeSelectors`' + routeSelectors: + description: Top-level route selectors. If present, the elements will + be used to select HTTPRoute rules that, when activated, trigger + the external authorization service. At least one selected HTTPRoute + rule must match to trigger the AuthPolicy. If no route selectors + are specified, the AuthPolicy will be enforced at all requests to + the protected routes. items: + description: RouteSelector defines semantics for matching an HTTP + request based on conditions https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRouteSpec properties: - hosts: + hostnames: + description: Hostnames defines a set of hostname that should + match against the HTTP Host header to select a HTTPRoute to + process the request https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRouteSpec items: + description: "Hostname is the fully qualified domain name + of a network host. This matches the RFC 1123 definition + of a hostname with 2 notable exceptions: \n 1. IPs are not + allowed. 2. A hostname may be prefixed with a wildcard label + (`*.`). The wildcard label must appear by itself as the + first label. \n Hostname can be \"precise\" which is a domain + name without the terminating dot of a network host (e.g. + \"foo.example.com\") or \"wildcard\", which is a domain + name prefixed with a single wildcard label (e.g. `*.example.com`). + \n Note that as per RFC1035 and RFC1123, a *label* must + consist of lower case alphanumeric characters or '-', and + must start and end with an alphanumeric character. No other + punctuation is allowed." + maxLength: 253 + minLength: 1 + pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string type: array - methods: + matches: + description: Matches define conditions used for matching the + rule against incoming HTTP requests. https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRouteSpec items: - type: string - type: array - paths: - items: - type: string + description: "HTTPRouteMatch defines the predicate used to + match requests to a given action. Multiple match types are + ANDed together, i.e. the match will evaluate to true only + if all conditions are satisfied. \n For example, the match + below will match a HTTP request only if its path starts + with `/foo` AND it contains the `version: v1` header: \n + ``` match: \n path: value: \"/foo\" headers: - name: \"version\" + value \"v1\" \n ```" + properties: + headers: + description: Headers specifies HTTP request header matchers. + Multiple match values are ANDed together, meaning, a + request must match all the specified headers to select + the route. + items: + description: HTTPHeaderMatch describes how to select + a HTTP route by matching HTTP request headers. + properties: + name: + description: "Name is the name of the HTTP Header + to be matched. Name matching MUST be case insensitive. + (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent header + names, only the first entry with an equivalent + name MUST be considered for a match. Subsequent + entries with an equivalent header name MUST be + ignored. Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered equivalent. + \n When a header is repeated in an HTTP request, + it is implementation-specific behavior as to how + this is represented. Generally, proxies should + follow the guidance from the RFC: https://www.rfc-editor.org/rfc/rfc7230.html#section-3.2.2 + regarding processing a repeated header, with special + handling for \"Set-Cookie\"." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + type: + default: Exact + description: "Type specifies how to match against + the value of the header. \n Support: Core (Exact) + \n Support: Implementation-specific (RegularExpression) + \n Since RegularExpression HeaderMatchType has + implementation-specific conformance, implementations + can support POSIX, PCRE or any other dialects + of regular expressions. Please read the implementation's + documentation to determine the supported dialect." + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value of HTTP Header to + be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + method: + description: "Method specifies HTTP method matcher. When + specified, this route will be matched only if the request + has the specified method. \n Support: Extended" + enum: + - GET + - HEAD + - POST + - PUT + - DELETE + - CONNECT + - OPTIONS + - TRACE + - PATCH + type: string + path: + default: + type: PathPrefix + value: / + description: Path specifies a HTTP request path matcher. + If this field is not specified, a default prefix match + on the "/" path is provided. + properties: + type: + default: PathPrefix + description: "Type specifies how to match against + the path Value. \n Support: Core (Exact, PathPrefix) + \n Support: Implementation-specific (RegularExpression)" + enum: + - Exact + - PathPrefix + - RegularExpression + type: string + value: + default: / + description: Value of the HTTP path to match against. + maxLength: 1024 + type: string + type: object + queryParams: + description: "QueryParams specifies HTTP query parameter + matchers. Multiple match values are ANDed together, + meaning, a request must match all the specified query + parameters to select the route. \n Support: Extended" + items: + description: HTTPQueryParamMatch describes how to select + a HTTP route by matching HTTP query parameters. + properties: + name: + description: "Name is the name of the HTTP query + param to be matched. This must be an exact string + match. (See https://tools.ietf.org/html/rfc7230#section-2.7.3). + \n If multiple entries specify equivalent query + param names, only the first entry with an equivalent + name MUST be considered for a match. Subsequent + entries with an equivalent query param name MUST + be ignored. \n If a query param is repeated in + an HTTP request, the behavior is purposely left + undefined, since different data planes have different + capabilities. However, it is *recommended* that + implementations should match against the first + value of the param if the data plane supports + it, as this behavior is expected in other load + balancing contexts outside of the Gateway API. + \n Users SHOULD NOT route traffic based on repeated + query params to guard themselves against potential + differences in the implementations." + maxLength: 256 + minLength: 1 + type: string + type: + default: Exact + description: "Type specifies how to match against + the value of the query parameter. \n Support: + Extended (Exact) \n Support: Implementation-specific + (RegularExpression) \n Since RegularExpression + QueryParamMatchType has Implementation-specific + conformance, implementations can support POSIX, + PCRE or any other dialects of regular expressions. + Please read the implementation's documentation + to determine the supported dialect." + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value of HTTP query param + to be matched. + maxLength: 1024 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object type: array type: object type: array @@ -333,6 +521,233 @@ spec: the same priority group are evaluated concurrently; consecutive priority groups are evaluated sequentially. type: integer + routeSelectors: + description: Top-level route selectors. If present, the + elements will be used to select HTTPRoute rules that, + when activated, trigger the auth rule. At least one selected + HTTPRoute rule must match to trigger the auth rule. If + no route selectors are specified, the auth rule will be + evaluated at all requests to the protected routes. + items: + description: RouteSelector defines semantics for matching + an HTTP request based on conditions https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRouteSpec + properties: + hostnames: + description: Hostnames defines a set of hostname that + should match against the HTTP Host header to select + a HTTPRoute to process the request https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRouteSpec + items: + description: "Hostname is the fully qualified domain + name of a network host. This matches the RFC 1123 + definition of a hostname with 2 notable exceptions: + \n 1. IPs are not allowed. 2. A hostname may be + prefixed with a wildcard label (`*.`). The wildcard + label must appear by itself as the first label. + \n Hostname can be \"precise\" which is a domain + name without the terminating dot of a network + host (e.g. \"foo.example.com\") or \"wildcard\", + which is a domain name prefixed with a single + wildcard label (e.g. `*.example.com`). \n Note + that as per RFC1035 and RFC1123, a *label* must + consist of lower case alphanumeric characters + or '-', and must start and end with an alphanumeric + character. No other punctuation is allowed." + maxLength: 253 + minLength: 1 + pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + type: array + matches: + description: Matches define conditions used for matching + the rule against incoming HTTP requests. https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRouteSpec + items: + description: "HTTPRouteMatch defines the predicate + used to match requests to a given action. Multiple + match types are ANDed together, i.e. the match + will evaluate to true only if all conditions are + satisfied. \n For example, the match below will + match a HTTP request only if its path starts with + `/foo` AND it contains the `version: v1` header: + \n ``` match: \n path: value: \"/foo\" headers: + - name: \"version\" value \"v1\" \n ```" + properties: + headers: + description: Headers specifies HTTP request + header matchers. Multiple match values are + ANDed together, meaning, a request must match + all the specified headers to select the route. + items: + description: HTTPHeaderMatch describes how + to select a HTTP route by matching HTTP + request headers. + properties: + name: + description: "Name is the name of the + HTTP Header to be matched. Name matching + MUST be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, only the first entry with + an equivalent name MUST be considered + for a match. Subsequent entries with + an equivalent header name MUST be ignored. + Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered + equivalent. \n When a header is repeated + in an HTTP request, it is implementation-specific + behavior as to how this is represented. + Generally, proxies should follow the + guidance from the RFC: https://www.rfc-editor.org/rfc/rfc7230.html#section-3.2.2 + regarding processing a repeated header, + with special handling for \"Set-Cookie\"." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + type: + default: Exact + description: "Type specifies how to match + against the value of the header. \n + Support: Core (Exact) \n Support: Implementation-specific + (RegularExpression) \n Since RegularExpression + HeaderMatchType has implementation-specific + conformance, implementations can support + POSIX, PCRE or any other dialects of + regular expressions. Please read the + implementation's documentation to determine + the supported dialect." + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + method: + description: "Method specifies HTTP method matcher. + When specified, this route will be matched + only if the request has the specified method. + \n Support: Extended" + enum: + - GET + - HEAD + - POST + - PUT + - DELETE + - CONNECT + - OPTIONS + - TRACE + - PATCH + type: string + path: + default: + type: PathPrefix + value: / + description: Path specifies a HTTP request path + matcher. If this field is not specified, a + default prefix match on the "/" path is provided. + properties: + type: + default: PathPrefix + description: "Type specifies how to match + against the path Value. \n Support: Core + (Exact, PathPrefix) \n Support: Implementation-specific + (RegularExpression)" + enum: + - Exact + - PathPrefix + - RegularExpression + type: string + value: + default: / + description: Value of the HTTP path to match + against. + maxLength: 1024 + type: string + type: object + queryParams: + description: "QueryParams specifies HTTP query + parameter matchers. Multiple match values + are ANDed together, meaning, a request must + match all the specified query parameters to + select the route. \n Support: Extended" + items: + description: HTTPQueryParamMatch describes + how to select a HTTP route by matching HTTP + query parameters. + properties: + name: + description: "Name is the name of the + HTTP query param to be matched. This + must be an exact string match. (See + https://tools.ietf.org/html/rfc7230#section-2.7.3). + \n If multiple entries specify equivalent + query param names, only the first entry + with an equivalent name MUST be considered + for a match. Subsequent entries with + an equivalent query param name MUST + be ignored. \n If a query param is repeated + in an HTTP request, the behavior is + purposely left undefined, since different + data planes have different capabilities. + However, it is *recommended* that implementations + should match against the first value + of the param if the data plane supports + it, as this behavior is expected in + other load balancing contexts outside + of the Gateway API. \n Users SHOULD + NOT route traffic based on repeated + query params to guard themselves against + potential differences in the implementations." + maxLength: 256 + minLength: 1 + type: string + type: + default: Exact + description: "Type specifies how to match + against the value of the query parameter. + \n Support: Extended (Exact) \n Support: + Implementation-specific (RegularExpression) + \n Since RegularExpression QueryParamMatchType + has Implementation-specific conformance, + implementations can support POSIX, PCRE + or any other dialects of regular expressions. + Please read the implementation's documentation + to determine the supported dialect." + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value of HTTP + query param to be matched. + maxLength: 1024 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + type: array + type: object + type: array when: description: Conditions for Authorino to enforce this config. If omitted, the config will be enforced for all requests. @@ -963,6 +1378,233 @@ spec: the same priority group are evaluated concurrently; consecutive priority groups are evaluated sequentially. type: integer + routeSelectors: + description: Top-level route selectors. If present, the + elements will be used to select HTTPRoute rules that, + when activated, trigger the auth rule. At least one selected + HTTPRoute rule must match to trigger the auth rule. If + no route selectors are specified, the auth rule will be + evaluated at all requests to the protected routes. + items: + description: RouteSelector defines semantics for matching + an HTTP request based on conditions https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRouteSpec + properties: + hostnames: + description: Hostnames defines a set of hostname that + should match against the HTTP Host header to select + a HTTPRoute to process the request https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRouteSpec + items: + description: "Hostname is the fully qualified domain + name of a network host. This matches the RFC 1123 + definition of a hostname with 2 notable exceptions: + \n 1. IPs are not allowed. 2. A hostname may be + prefixed with a wildcard label (`*.`). The wildcard + label must appear by itself as the first label. + \n Hostname can be \"precise\" which is a domain + name without the terminating dot of a network + host (e.g. \"foo.example.com\") or \"wildcard\", + which is a domain name prefixed with a single + wildcard label (e.g. `*.example.com`). \n Note + that as per RFC1035 and RFC1123, a *label* must + consist of lower case alphanumeric characters + or '-', and must start and end with an alphanumeric + character. No other punctuation is allowed." + maxLength: 253 + minLength: 1 + pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + type: array + matches: + description: Matches define conditions used for matching + the rule against incoming HTTP requests. https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRouteSpec + items: + description: "HTTPRouteMatch defines the predicate + used to match requests to a given action. Multiple + match types are ANDed together, i.e. the match + will evaluate to true only if all conditions are + satisfied. \n For example, the match below will + match a HTTP request only if its path starts with + `/foo` AND it contains the `version: v1` header: + \n ``` match: \n path: value: \"/foo\" headers: + - name: \"version\" value \"v1\" \n ```" + properties: + headers: + description: Headers specifies HTTP request + header matchers. Multiple match values are + ANDed together, meaning, a request must match + all the specified headers to select the route. + items: + description: HTTPHeaderMatch describes how + to select a HTTP route by matching HTTP + request headers. + properties: + name: + description: "Name is the name of the + HTTP Header to be matched. Name matching + MUST be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, only the first entry with + an equivalent name MUST be considered + for a match. Subsequent entries with + an equivalent header name MUST be ignored. + Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered + equivalent. \n When a header is repeated + in an HTTP request, it is implementation-specific + behavior as to how this is represented. + Generally, proxies should follow the + guidance from the RFC: https://www.rfc-editor.org/rfc/rfc7230.html#section-3.2.2 + regarding processing a repeated header, + with special handling for \"Set-Cookie\"." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + type: + default: Exact + description: "Type specifies how to match + against the value of the header. \n + Support: Core (Exact) \n Support: Implementation-specific + (RegularExpression) \n Since RegularExpression + HeaderMatchType has implementation-specific + conformance, implementations can support + POSIX, PCRE or any other dialects of + regular expressions. Please read the + implementation's documentation to determine + the supported dialect." + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + method: + description: "Method specifies HTTP method matcher. + When specified, this route will be matched + only if the request has the specified method. + \n Support: Extended" + enum: + - GET + - HEAD + - POST + - PUT + - DELETE + - CONNECT + - OPTIONS + - TRACE + - PATCH + type: string + path: + default: + type: PathPrefix + value: / + description: Path specifies a HTTP request path + matcher. If this field is not specified, a + default prefix match on the "/" path is provided. + properties: + type: + default: PathPrefix + description: "Type specifies how to match + against the path Value. \n Support: Core + (Exact, PathPrefix) \n Support: Implementation-specific + (RegularExpression)" + enum: + - Exact + - PathPrefix + - RegularExpression + type: string + value: + default: / + description: Value of the HTTP path to match + against. + maxLength: 1024 + type: string + type: object + queryParams: + description: "QueryParams specifies HTTP query + parameter matchers. Multiple match values + are ANDed together, meaning, a request must + match all the specified query parameters to + select the route. \n Support: Extended" + items: + description: HTTPQueryParamMatch describes + how to select a HTTP route by matching HTTP + query parameters. + properties: + name: + description: "Name is the name of the + HTTP query param to be matched. This + must be an exact string match. (See + https://tools.ietf.org/html/rfc7230#section-2.7.3). + \n If multiple entries specify equivalent + query param names, only the first entry + with an equivalent name MUST be considered + for a match. Subsequent entries with + an equivalent query param name MUST + be ignored. \n If a query param is repeated + in an HTTP request, the behavior is + purposely left undefined, since different + data planes have different capabilities. + However, it is *recommended* that implementations + should match against the first value + of the param if the data plane supports + it, as this behavior is expected in + other load balancing contexts outside + of the Gateway API. \n Users SHOULD + NOT route traffic based on repeated + query params to guard themselves against + potential differences in the implementations." + maxLength: 256 + minLength: 1 + type: string + type: + default: Exact + description: "Type specifies how to match + against the value of the query parameter. + \n Support: Extended (Exact) \n Support: + Implementation-specific (RegularExpression) + \n Since RegularExpression QueryParamMatchType + has Implementation-specific conformance, + implementations can support POSIX, PCRE + or any other dialects of regular expressions. + Please read the implementation's documentation + to determine the supported dialect." + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value of HTTP + query param to be matched. + maxLength: 1024 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + type: array + type: object + type: array spicedb: description: Authorization decision delegated to external Authzed/SpiceDB server. @@ -1405,6 +2047,233 @@ spec: the same priority group are evaluated concurrently; consecutive priority groups are evaluated sequentially. type: integer + routeSelectors: + description: Top-level route selectors. If present, the + elements will be used to select HTTPRoute rules that, + when activated, trigger the auth rule. At least one selected + HTTPRoute rule must match to trigger the auth rule. If + no route selectors are specified, the auth rule will be + evaluated at all requests to the protected routes. + items: + description: RouteSelector defines semantics for matching + an HTTP request based on conditions https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRouteSpec + properties: + hostnames: + description: Hostnames defines a set of hostname that + should match against the HTTP Host header to select + a HTTPRoute to process the request https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRouteSpec + items: + description: "Hostname is the fully qualified domain + name of a network host. This matches the RFC 1123 + definition of a hostname with 2 notable exceptions: + \n 1. IPs are not allowed. 2. A hostname may be + prefixed with a wildcard label (`*.`). The wildcard + label must appear by itself as the first label. + \n Hostname can be \"precise\" which is a domain + name without the terminating dot of a network + host (e.g. \"foo.example.com\") or \"wildcard\", + which is a domain name prefixed with a single + wildcard label (e.g. `*.example.com`). \n Note + that as per RFC1035 and RFC1123, a *label* must + consist of lower case alphanumeric characters + or '-', and must start and end with an alphanumeric + character. No other punctuation is allowed." + maxLength: 253 + minLength: 1 + pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + type: array + matches: + description: Matches define conditions used for matching + the rule against incoming HTTP requests. https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRouteSpec + items: + description: "HTTPRouteMatch defines the predicate + used to match requests to a given action. Multiple + match types are ANDed together, i.e. the match + will evaluate to true only if all conditions are + satisfied. \n For example, the match below will + match a HTTP request only if its path starts with + `/foo` AND it contains the `version: v1` header: + \n ``` match: \n path: value: \"/foo\" headers: + - name: \"version\" value \"v1\" \n ```" + properties: + headers: + description: Headers specifies HTTP request + header matchers. Multiple match values are + ANDed together, meaning, a request must match + all the specified headers to select the route. + items: + description: HTTPHeaderMatch describes how + to select a HTTP route by matching HTTP + request headers. + properties: + name: + description: "Name is the name of the + HTTP Header to be matched. Name matching + MUST be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, only the first entry with + an equivalent name MUST be considered + for a match. Subsequent entries with + an equivalent header name MUST be ignored. + Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered + equivalent. \n When a header is repeated + in an HTTP request, it is implementation-specific + behavior as to how this is represented. + Generally, proxies should follow the + guidance from the RFC: https://www.rfc-editor.org/rfc/rfc7230.html#section-3.2.2 + regarding processing a repeated header, + with special handling for \"Set-Cookie\"." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + type: + default: Exact + description: "Type specifies how to match + against the value of the header. \n + Support: Core (Exact) \n Support: Implementation-specific + (RegularExpression) \n Since RegularExpression + HeaderMatchType has implementation-specific + conformance, implementations can support + POSIX, PCRE or any other dialects of + regular expressions. Please read the + implementation's documentation to determine + the supported dialect." + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + method: + description: "Method specifies HTTP method matcher. + When specified, this route will be matched + only if the request has the specified method. + \n Support: Extended" + enum: + - GET + - HEAD + - POST + - PUT + - DELETE + - CONNECT + - OPTIONS + - TRACE + - PATCH + type: string + path: + default: + type: PathPrefix + value: / + description: Path specifies a HTTP request path + matcher. If this field is not specified, a + default prefix match on the "/" path is provided. + properties: + type: + default: PathPrefix + description: "Type specifies how to match + against the path Value. \n Support: Core + (Exact, PathPrefix) \n Support: Implementation-specific + (RegularExpression)" + enum: + - Exact + - PathPrefix + - RegularExpression + type: string + value: + default: / + description: Value of the HTTP path to match + against. + maxLength: 1024 + type: string + type: object + queryParams: + description: "QueryParams specifies HTTP query + parameter matchers. Multiple match values + are ANDed together, meaning, a request must + match all the specified query parameters to + select the route. \n Support: Extended" + items: + description: HTTPQueryParamMatch describes + how to select a HTTP route by matching HTTP + query parameters. + properties: + name: + description: "Name is the name of the + HTTP query param to be matched. This + must be an exact string match. (See + https://tools.ietf.org/html/rfc7230#section-2.7.3). + \n If multiple entries specify equivalent + query param names, only the first entry + with an equivalent name MUST be considered + for a match. Subsequent entries with + an equivalent query param name MUST + be ignored. \n If a query param is repeated + in an HTTP request, the behavior is + purposely left undefined, since different + data planes have different capabilities. + However, it is *recommended* that implementations + should match against the first value + of the param if the data plane supports + it, as this behavior is expected in + other load balancing contexts outside + of the Gateway API. \n Users SHOULD + NOT route traffic based on repeated + query params to guard themselves against + potential differences in the implementations." + maxLength: 256 + minLength: 1 + type: string + type: + default: Exact + description: "Type specifies how to match + against the value of the query parameter. + \n Support: Extended (Exact) \n Support: + Implementation-specific (RegularExpression) + \n Since RegularExpression QueryParamMatchType + has Implementation-specific conformance, + implementations can support POSIX, PCRE + or any other dialects of regular expressions. + Please read the implementation's documentation + to determine the supported dialect." + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value of HTTP + query param to be matched. + maxLength: 1024 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + type: array + type: object + type: array when: description: Conditions for Authorino to enforce this config. If omitted, the config will be enforced for all requests. @@ -1718,6 +2587,233 @@ spec: the same priority group are evaluated concurrently; consecutive priority groups are evaluated sequentially. type: integer + routeSelectors: + description: Top-level route selectors. If present, the + elements will be used to select HTTPRoute rules that, + when activated, trigger the auth rule. At least one selected + HTTPRoute rule must match to trigger the auth rule. If + no route selectors are specified, the auth rule will be + evaluated at all requests to the protected routes. + items: + description: RouteSelector defines semantics for matching + an HTTP request based on conditions https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRouteSpec + properties: + hostnames: + description: Hostnames defines a set of hostname that + should match against the HTTP Host header to select + a HTTPRoute to process the request https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRouteSpec + items: + description: "Hostname is the fully qualified domain + name of a network host. This matches the RFC 1123 + definition of a hostname with 2 notable exceptions: + \n 1. IPs are not allowed. 2. A hostname may be + prefixed with a wildcard label (`*.`). The wildcard + label must appear by itself as the first label. + \n Hostname can be \"precise\" which is a domain + name without the terminating dot of a network + host (e.g. \"foo.example.com\") or \"wildcard\", + which is a domain name prefixed with a single + wildcard label (e.g. `*.example.com`). \n Note + that as per RFC1035 and RFC1123, a *label* must + consist of lower case alphanumeric characters + or '-', and must start and end with an alphanumeric + character. No other punctuation is allowed." + maxLength: 253 + minLength: 1 + pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + type: array + matches: + description: Matches define conditions used for matching + the rule against incoming HTTP requests. https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRouteSpec + items: + description: "HTTPRouteMatch defines the predicate + used to match requests to a given action. Multiple + match types are ANDed together, i.e. the match + will evaluate to true only if all conditions are + satisfied. \n For example, the match below will + match a HTTP request only if its path starts with + `/foo` AND it contains the `version: v1` header: + \n ``` match: \n path: value: \"/foo\" headers: + - name: \"version\" value \"v1\" \n ```" + properties: + headers: + description: Headers specifies HTTP request + header matchers. Multiple match values are + ANDed together, meaning, a request must match + all the specified headers to select the route. + items: + description: HTTPHeaderMatch describes how + to select a HTTP route by matching HTTP + request headers. + properties: + name: + description: "Name is the name of the + HTTP Header to be matched. Name matching + MUST be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, only the first entry with + an equivalent name MUST be considered + for a match. Subsequent entries with + an equivalent header name MUST be ignored. + Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered + equivalent. \n When a header is repeated + in an HTTP request, it is implementation-specific + behavior as to how this is represented. + Generally, proxies should follow the + guidance from the RFC: https://www.rfc-editor.org/rfc/rfc7230.html#section-3.2.2 + regarding processing a repeated header, + with special handling for \"Set-Cookie\"." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + type: + default: Exact + description: "Type specifies how to match + against the value of the header. \n + Support: Core (Exact) \n Support: Implementation-specific + (RegularExpression) \n Since RegularExpression + HeaderMatchType has implementation-specific + conformance, implementations can support + POSIX, PCRE or any other dialects of + regular expressions. Please read the + implementation's documentation to determine + the supported dialect." + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + method: + description: "Method specifies HTTP method matcher. + When specified, this route will be matched + only if the request has the specified method. + \n Support: Extended" + enum: + - GET + - HEAD + - POST + - PUT + - DELETE + - CONNECT + - OPTIONS + - TRACE + - PATCH + type: string + path: + default: + type: PathPrefix + value: / + description: Path specifies a HTTP request path + matcher. If this field is not specified, a + default prefix match on the "/" path is provided. + properties: + type: + default: PathPrefix + description: "Type specifies how to match + against the path Value. \n Support: Core + (Exact, PathPrefix) \n Support: Implementation-specific + (RegularExpression)" + enum: + - Exact + - PathPrefix + - RegularExpression + type: string + value: + default: / + description: Value of the HTTP path to match + against. + maxLength: 1024 + type: string + type: object + queryParams: + description: "QueryParams specifies HTTP query + parameter matchers. Multiple match values + are ANDed together, meaning, a request must + match all the specified query parameters to + select the route. \n Support: Extended" + items: + description: HTTPQueryParamMatch describes + how to select a HTTP route by matching HTTP + query parameters. + properties: + name: + description: "Name is the name of the + HTTP query param to be matched. This + must be an exact string match. (See + https://tools.ietf.org/html/rfc7230#section-2.7.3). + \n If multiple entries specify equivalent + query param names, only the first entry + with an equivalent name MUST be considered + for a match. Subsequent entries with + an equivalent query param name MUST + be ignored. \n If a query param is repeated + in an HTTP request, the behavior is + purposely left undefined, since different + data planes have different capabilities. + However, it is *recommended* that implementations + should match against the first value + of the param if the data plane supports + it, as this behavior is expected in + other load balancing contexts outside + of the Gateway API. \n Users SHOULD + NOT route traffic based on repeated + query params to guard themselves against + potential differences in the implementations." + maxLength: 256 + minLength: 1 + type: string + type: + default: Exact + description: "Type specifies how to match + against the value of the query parameter. + \n Support: Extended (Exact) \n Support: + Implementation-specific (RegularExpression) + \n Since RegularExpression QueryParamMatchType + has Implementation-specific conformance, + implementations can support POSIX, PCRE + or any other dialects of regular expressions. + Please read the implementation's documentation + to determine the supported dialect." + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value of HTTP + query param to be matched. + maxLength: 1024 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + type: array + type: object + type: array uma: description: User-Managed Access (UMA) source of resource data. @@ -1860,8 +2956,6 @@ spec: properties: dynamicMetadata: additionalProperties: - description: Settings of the success custom response - item. properties: cache: description: Caching options for the resolved object @@ -1963,6 +3057,264 @@ spec: in the same priority group are evaluated concurrently; consecutive priority groups are evaluated sequentially. type: integer + routeSelectors: + description: Top-level route selectors. If present, + the elements will be used to select HTTPRoute + rules that, when activated, trigger the auth rule. + At least one selected HTTPRoute rule must match + to trigger the auth rule. If no route selectors + are specified, the auth rule will be evaluated + at all requests to the protected routes. + items: + description: RouteSelector defines semantics for + matching an HTTP request based on conditions + https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRouteSpec + properties: + hostnames: + description: Hostnames defines a set of hostname + that should match against the HTTP Host + header to select a HTTPRoute to process + the request https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRouteSpec + items: + description: "Hostname is the fully qualified + domain name of a network host. This matches + the RFC 1123 definition of a hostname + with 2 notable exceptions: \n 1. IPs are + not allowed. 2. A hostname may be prefixed + with a wildcard label (`*.`). The wildcard + label must appear by itself as the first + label. \n Hostname can be \"precise\" + which is a domain name without the terminating + dot of a network host (e.g. \"foo.example.com\") + or \"wildcard\", which is a domain name + prefixed with a single wildcard label + (e.g. `*.example.com`). \n Note that as + per RFC1035 and RFC1123, a *label* must + consist of lower case alphanumeric characters + or '-', and must start and end with an + alphanumeric character. No other punctuation + is allowed." + maxLength: 253 + minLength: 1 + pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + type: array + matches: + description: Matches define conditions used + for matching the rule against incoming HTTP + requests. https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRouteSpec + items: + description: "HTTPRouteMatch defines the + predicate used to match requests to a + given action. Multiple match types are + ANDed together, i.e. the match will evaluate + to true only if all conditions are satisfied. + \n For example, the match below will match + a HTTP request only if its path starts + with `/foo` AND it contains the `version: + v1` header: \n ``` match: \n path: value: + \"/foo\" headers: - name: \"version\" + value \"v1\" \n ```" + properties: + headers: + description: Headers specifies HTTP + request header matchers. Multiple + match values are ANDed together, meaning, + a request must match all the specified + headers to select the route. + items: + description: HTTPHeaderMatch describes + how to select a HTTP route by matching + HTTP request headers. + properties: + name: + description: "Name is the name + of the HTTP Header to be matched. + Name matching MUST be case insensitive. + (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify + equivalent header names, only + the first entry with an equivalent + name MUST be considered for + a match. Subsequent entries + with an equivalent header name + MUST be ignored. Due to the + case-insensitivity of header + names, \"foo\" and \"Foo\" are + considered equivalent. \n When + a header is repeated in an HTTP + request, it is implementation-specific + behavior as to how this is represented. + Generally, proxies should follow + the guidance from the RFC: https://www.rfc-editor.org/rfc/rfc7230.html#section-3.2.2 + regarding processing a repeated + header, with special handling + for \"Set-Cookie\"." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + type: + default: Exact + description: "Type specifies how + to match against the value of + the header. \n Support: Core + (Exact) \n Support: Implementation-specific + (RegularExpression) \n Since + RegularExpression HeaderMatchType + has implementation-specific + conformance, implementations + can support POSIX, PCRE or any + other dialects of regular expressions. + Please read the implementation's + documentation to determine the + supported dialect." + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value + of HTTP Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + method: + description: "Method specifies HTTP + method matcher. When specified, this + route will be matched only if the + request has the specified method. + \n Support: Extended" + enum: + - GET + - HEAD + - POST + - PUT + - DELETE + - CONNECT + - OPTIONS + - TRACE + - PATCH + type: string + path: + default: + type: PathPrefix + value: / + description: Path specifies a HTTP request + path matcher. If this field is not + specified, a default prefix match + on the "/" path is provided. + properties: + type: + default: PathPrefix + description: "Type specifies how + to match against the path Value. + \n Support: Core (Exact, PathPrefix) + \n Support: Implementation-specific + (RegularExpression)" + enum: + - Exact + - PathPrefix + - RegularExpression + type: string + value: + default: / + description: Value of the HTTP path + to match against. + maxLength: 1024 + type: string + type: object + queryParams: + description: "QueryParams specifies + HTTP query parameter matchers. Multiple + match values are ANDed together, meaning, + a request must match all the specified + query parameters to select the route. + \n Support: Extended" + items: + description: HTTPQueryParamMatch describes + how to select a HTTP route by matching + HTTP query parameters. + properties: + name: + description: "Name is the name + of the HTTP query param to be + matched. This must be an exact + string match. (See https://tools.ietf.org/html/rfc7230#section-2.7.3). + \n If multiple entries specify + equivalent query param names, + only the first entry with an + equivalent name MUST be considered + for a match. Subsequent entries + with an equivalent query param + name MUST be ignored. \n If + a query param is repeated in + an HTTP request, the behavior + is purposely left undefined, + since different data planes + have different capabilities. + However, it is *recommended* + that implementations should + match against the first value + of the param if the data plane + supports it, as this behavior + is expected in other load balancing + contexts outside of the Gateway + API. \n Users SHOULD NOT route + traffic based on repeated query + params to guard themselves against + potential differences in the + implementations." + maxLength: 256 + minLength: 1 + type: string + type: + default: Exact + description: "Type specifies how + to match against the value of + the query parameter. \n Support: + Extended (Exact) \n Support: + Implementation-specific (RegularExpression) + \n Since RegularExpression QueryParamMatchType + has Implementation-specific + conformance, implementations + can support POSIX, PCRE or any + other dialects of regular expressions. + Please read the implementation's + documentation to determine the + supported dialect." + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value + of HTTP query param to be matched. + maxLength: 1024 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + type: array + type: object + type: array when: description: Conditions for Authorino to enforce this config. If omitted, the config will be enforced @@ -2200,6 +3552,264 @@ spec: in the same priority group are evaluated concurrently; consecutive priority groups are evaluated sequentially. type: integer + routeSelectors: + description: Top-level route selectors. If present, + the elements will be used to select HTTPRoute + rules that, when activated, trigger the auth rule. + At least one selected HTTPRoute rule must match + to trigger the auth rule. If no route selectors + are specified, the auth rule will be evaluated + at all requests to the protected routes. + items: + description: RouteSelector defines semantics for + matching an HTTP request based on conditions + https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRouteSpec + properties: + hostnames: + description: Hostnames defines a set of hostname + that should match against the HTTP Host + header to select a HTTPRoute to process + the request https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRouteSpec + items: + description: "Hostname is the fully qualified + domain name of a network host. This matches + the RFC 1123 definition of a hostname + with 2 notable exceptions: \n 1. IPs are + not allowed. 2. A hostname may be prefixed + with a wildcard label (`*.`). The wildcard + label must appear by itself as the first + label. \n Hostname can be \"precise\" + which is a domain name without the terminating + dot of a network host (e.g. \"foo.example.com\") + or \"wildcard\", which is a domain name + prefixed with a single wildcard label + (e.g. `*.example.com`). \n Note that as + per RFC1035 and RFC1123, a *label* must + consist of lower case alphanumeric characters + or '-', and must start and end with an + alphanumeric character. No other punctuation + is allowed." + maxLength: 253 + minLength: 1 + pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + type: array + matches: + description: Matches define conditions used + for matching the rule against incoming HTTP + requests. https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRouteSpec + items: + description: "HTTPRouteMatch defines the + predicate used to match requests to a + given action. Multiple match types are + ANDed together, i.e. the match will evaluate + to true only if all conditions are satisfied. + \n For example, the match below will match + a HTTP request only if its path starts + with `/foo` AND it contains the `version: + v1` header: \n ``` match: \n path: value: + \"/foo\" headers: - name: \"version\" + value \"v1\" \n ```" + properties: + headers: + description: Headers specifies HTTP + request header matchers. Multiple + match values are ANDed together, meaning, + a request must match all the specified + headers to select the route. + items: + description: HTTPHeaderMatch describes + how to select a HTTP route by matching + HTTP request headers. + properties: + name: + description: "Name is the name + of the HTTP Header to be matched. + Name matching MUST be case insensitive. + (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify + equivalent header names, only + the first entry with an equivalent + name MUST be considered for + a match. Subsequent entries + with an equivalent header name + MUST be ignored. Due to the + case-insensitivity of header + names, \"foo\" and \"Foo\" are + considered equivalent. \n When + a header is repeated in an HTTP + request, it is implementation-specific + behavior as to how this is represented. + Generally, proxies should follow + the guidance from the RFC: https://www.rfc-editor.org/rfc/rfc7230.html#section-3.2.2 + regarding processing a repeated + header, with special handling + for \"Set-Cookie\"." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + type: + default: Exact + description: "Type specifies how + to match against the value of + the header. \n Support: Core + (Exact) \n Support: Implementation-specific + (RegularExpression) \n Since + RegularExpression HeaderMatchType + has implementation-specific + conformance, implementations + can support POSIX, PCRE or any + other dialects of regular expressions. + Please read the implementation's + documentation to determine the + supported dialect." + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value + of HTTP Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + method: + description: "Method specifies HTTP + method matcher. When specified, this + route will be matched only if the + request has the specified method. + \n Support: Extended" + enum: + - GET + - HEAD + - POST + - PUT + - DELETE + - CONNECT + - OPTIONS + - TRACE + - PATCH + type: string + path: + default: + type: PathPrefix + value: / + description: Path specifies a HTTP request + path matcher. If this field is not + specified, a default prefix match + on the "/" path is provided. + properties: + type: + default: PathPrefix + description: "Type specifies how + to match against the path Value. + \n Support: Core (Exact, PathPrefix) + \n Support: Implementation-specific + (RegularExpression)" + enum: + - Exact + - PathPrefix + - RegularExpression + type: string + value: + default: / + description: Value of the HTTP path + to match against. + maxLength: 1024 + type: string + type: object + queryParams: + description: "QueryParams specifies + HTTP query parameter matchers. Multiple + match values are ANDed together, meaning, + a request must match all the specified + query parameters to select the route. + \n Support: Extended" + items: + description: HTTPQueryParamMatch describes + how to select a HTTP route by matching + HTTP query parameters. + properties: + name: + description: "Name is the name + of the HTTP query param to be + matched. This must be an exact + string match. (See https://tools.ietf.org/html/rfc7230#section-2.7.3). + \n If multiple entries specify + equivalent query param names, + only the first entry with an + equivalent name MUST be considered + for a match. Subsequent entries + with an equivalent query param + name MUST be ignored. \n If + a query param is repeated in + an HTTP request, the behavior + is purposely left undefined, + since different data planes + have different capabilities. + However, it is *recommended* + that implementations should + match against the first value + of the param if the data plane + supports it, as this behavior + is expected in other load balancing + contexts outside of the Gateway + API. \n Users SHOULD NOT route + traffic based on repeated query + params to guard themselves against + potential differences in the + implementations." + maxLength: 256 + minLength: 1 + type: string + type: + default: Exact + description: "Type specifies how + to match against the value of + the query parameter. \n Support: + Extended (Exact) \n Support: + Implementation-specific (RegularExpression) + \n Since RegularExpression QueryParamMatchType + has Implementation-specific + conformance, implementations + can support POSIX, PCRE or any + other dialects of regular expressions. + Please read the implementation's + documentation to determine the + supported dialect." + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value + of HTTP query param to be matched. + maxLength: 1024 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + type: array + type: object + type: array when: description: Conditions for Authorino to enforce this config. If omitted, the config will be enforced diff --git a/config/crd/bases/kuadrant.io_authpolicies.yaml b/config/crd/bases/kuadrant.io_authpolicies.yaml index 80a623c5e..1ca616a82 100644 --- a/config/crd/bases/kuadrant.io_authpolicies.yaml +++ b/config/crd/bases/kuadrant.io_authpolicies.yaml @@ -32,24 +32,212 @@ spec: type: object spec: properties: - routes: - description: 'Route rules specify the HTTP route attributes that trigger - the external authorization service TODO(@guicassolato): remove – - conditions to trigger the ext-authz service will be computed from - `routeSelectors`' + routeSelectors: + description: Top-level route selectors. If present, the elements will + be used to select HTTPRoute rules that, when activated, trigger + the external authorization service. At least one selected HTTPRoute + rule must match to trigger the AuthPolicy. If no route selectors + are specified, the AuthPolicy will be enforced at all requests to + the protected routes. items: + description: RouteSelector defines semantics for matching an HTTP + request based on conditions https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRouteSpec properties: - hosts: + hostnames: + description: Hostnames defines a set of hostname that should + match against the HTTP Host header to select a HTTPRoute to + process the request https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRouteSpec items: + description: "Hostname is the fully qualified domain name + of a network host. This matches the RFC 1123 definition + of a hostname with 2 notable exceptions: \n 1. IPs are not + allowed. 2. A hostname may be prefixed with a wildcard label + (`*.`). The wildcard label must appear by itself as the + first label. \n Hostname can be \"precise\" which is a domain + name without the terminating dot of a network host (e.g. + \"foo.example.com\") or \"wildcard\", which is a domain + name prefixed with a single wildcard label (e.g. `*.example.com`). + \n Note that as per RFC1035 and RFC1123, a *label* must + consist of lower case alphanumeric characters or '-', and + must start and end with an alphanumeric character. No other + punctuation is allowed." + maxLength: 253 + minLength: 1 + pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ type: string type: array - methods: + matches: + description: Matches define conditions used for matching the + rule against incoming HTTP requests. https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRouteSpec items: - type: string - type: array - paths: - items: - type: string + description: "HTTPRouteMatch defines the predicate used to + match requests to a given action. Multiple match types are + ANDed together, i.e. the match will evaluate to true only + if all conditions are satisfied. \n For example, the match + below will match a HTTP request only if its path starts + with `/foo` AND it contains the `version: v1` header: \n + ``` match: \n path: value: \"/foo\" headers: - name: \"version\" + value \"v1\" \n ```" + properties: + headers: + description: Headers specifies HTTP request header matchers. + Multiple match values are ANDed together, meaning, a + request must match all the specified headers to select + the route. + items: + description: HTTPHeaderMatch describes how to select + a HTTP route by matching HTTP request headers. + properties: + name: + description: "Name is the name of the HTTP Header + to be matched. Name matching MUST be case insensitive. + (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent header + names, only the first entry with an equivalent + name MUST be considered for a match. Subsequent + entries with an equivalent header name MUST be + ignored. Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered equivalent. + \n When a header is repeated in an HTTP request, + it is implementation-specific behavior as to how + this is represented. Generally, proxies should + follow the guidance from the RFC: https://www.rfc-editor.org/rfc/rfc7230.html#section-3.2.2 + regarding processing a repeated header, with special + handling for \"Set-Cookie\"." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + type: + default: Exact + description: "Type specifies how to match against + the value of the header. \n Support: Core (Exact) + \n Support: Implementation-specific (RegularExpression) + \n Since RegularExpression HeaderMatchType has + implementation-specific conformance, implementations + can support POSIX, PCRE or any other dialects + of regular expressions. Please read the implementation's + documentation to determine the supported dialect." + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value of HTTP Header to + be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + method: + description: "Method specifies HTTP method matcher. When + specified, this route will be matched only if the request + has the specified method. \n Support: Extended" + enum: + - GET + - HEAD + - POST + - PUT + - DELETE + - CONNECT + - OPTIONS + - TRACE + - PATCH + type: string + path: + default: + type: PathPrefix + value: / + description: Path specifies a HTTP request path matcher. + If this field is not specified, a default prefix match + on the "/" path is provided. + properties: + type: + default: PathPrefix + description: "Type specifies how to match against + the path Value. \n Support: Core (Exact, PathPrefix) + \n Support: Implementation-specific (RegularExpression)" + enum: + - Exact + - PathPrefix + - RegularExpression + type: string + value: + default: / + description: Value of the HTTP path to match against. + maxLength: 1024 + type: string + type: object + queryParams: + description: "QueryParams specifies HTTP query parameter + matchers. Multiple match values are ANDed together, + meaning, a request must match all the specified query + parameters to select the route. \n Support: Extended" + items: + description: HTTPQueryParamMatch describes how to select + a HTTP route by matching HTTP query parameters. + properties: + name: + description: "Name is the name of the HTTP query + param to be matched. This must be an exact string + match. (See https://tools.ietf.org/html/rfc7230#section-2.7.3). + \n If multiple entries specify equivalent query + param names, only the first entry with an equivalent + name MUST be considered for a match. Subsequent + entries with an equivalent query param name MUST + be ignored. \n If a query param is repeated in + an HTTP request, the behavior is purposely left + undefined, since different data planes have different + capabilities. However, it is *recommended* that + implementations should match against the first + value of the param if the data plane supports + it, as this behavior is expected in other load + balancing contexts outside of the Gateway API. + \n Users SHOULD NOT route traffic based on repeated + query params to guard themselves against potential + differences in the implementations." + maxLength: 256 + minLength: 1 + type: string + type: + default: Exact + description: "Type specifies how to match against + the value of the query parameter. \n Support: + Extended (Exact) \n Support: Implementation-specific + (RegularExpression) \n Since RegularExpression + QueryParamMatchType has Implementation-specific + conformance, implementations can support POSIX, + PCRE or any other dialects of regular expressions. + Please read the implementation's documentation + to determine the supported dialect." + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value of HTTP query param + to be matched. + maxLength: 1024 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object type: array type: object type: array @@ -331,6 +519,233 @@ spec: the same priority group are evaluated concurrently; consecutive priority groups are evaluated sequentially. type: integer + routeSelectors: + description: Top-level route selectors. If present, the + elements will be used to select HTTPRoute rules that, + when activated, trigger the auth rule. At least one selected + HTTPRoute rule must match to trigger the auth rule. If + no route selectors are specified, the auth rule will be + evaluated at all requests to the protected routes. + items: + description: RouteSelector defines semantics for matching + an HTTP request based on conditions https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRouteSpec + properties: + hostnames: + description: Hostnames defines a set of hostname that + should match against the HTTP Host header to select + a HTTPRoute to process the request https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRouteSpec + items: + description: "Hostname is the fully qualified domain + name of a network host. This matches the RFC 1123 + definition of a hostname with 2 notable exceptions: + \n 1. IPs are not allowed. 2. A hostname may be + prefixed with a wildcard label (`*.`). The wildcard + label must appear by itself as the first label. + \n Hostname can be \"precise\" which is a domain + name without the terminating dot of a network + host (e.g. \"foo.example.com\") or \"wildcard\", + which is a domain name prefixed with a single + wildcard label (e.g. `*.example.com`). \n Note + that as per RFC1035 and RFC1123, a *label* must + consist of lower case alphanumeric characters + or '-', and must start and end with an alphanumeric + character. No other punctuation is allowed." + maxLength: 253 + minLength: 1 + pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + type: array + matches: + description: Matches define conditions used for matching + the rule against incoming HTTP requests. https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRouteSpec + items: + description: "HTTPRouteMatch defines the predicate + used to match requests to a given action. Multiple + match types are ANDed together, i.e. the match + will evaluate to true only if all conditions are + satisfied. \n For example, the match below will + match a HTTP request only if its path starts with + `/foo` AND it contains the `version: v1` header: + \n ``` match: \n path: value: \"/foo\" headers: + - name: \"version\" value \"v1\" \n ```" + properties: + headers: + description: Headers specifies HTTP request + header matchers. Multiple match values are + ANDed together, meaning, a request must match + all the specified headers to select the route. + items: + description: HTTPHeaderMatch describes how + to select a HTTP route by matching HTTP + request headers. + properties: + name: + description: "Name is the name of the + HTTP Header to be matched. Name matching + MUST be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, only the first entry with + an equivalent name MUST be considered + for a match. Subsequent entries with + an equivalent header name MUST be ignored. + Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered + equivalent. \n When a header is repeated + in an HTTP request, it is implementation-specific + behavior as to how this is represented. + Generally, proxies should follow the + guidance from the RFC: https://www.rfc-editor.org/rfc/rfc7230.html#section-3.2.2 + regarding processing a repeated header, + with special handling for \"Set-Cookie\"." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + type: + default: Exact + description: "Type specifies how to match + against the value of the header. \n + Support: Core (Exact) \n Support: Implementation-specific + (RegularExpression) \n Since RegularExpression + HeaderMatchType has implementation-specific + conformance, implementations can support + POSIX, PCRE or any other dialects of + regular expressions. Please read the + implementation's documentation to determine + the supported dialect." + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + method: + description: "Method specifies HTTP method matcher. + When specified, this route will be matched + only if the request has the specified method. + \n Support: Extended" + enum: + - GET + - HEAD + - POST + - PUT + - DELETE + - CONNECT + - OPTIONS + - TRACE + - PATCH + type: string + path: + default: + type: PathPrefix + value: / + description: Path specifies a HTTP request path + matcher. If this field is not specified, a + default prefix match on the "/" path is provided. + properties: + type: + default: PathPrefix + description: "Type specifies how to match + against the path Value. \n Support: Core + (Exact, PathPrefix) \n Support: Implementation-specific + (RegularExpression)" + enum: + - Exact + - PathPrefix + - RegularExpression + type: string + value: + default: / + description: Value of the HTTP path to match + against. + maxLength: 1024 + type: string + type: object + queryParams: + description: "QueryParams specifies HTTP query + parameter matchers. Multiple match values + are ANDed together, meaning, a request must + match all the specified query parameters to + select the route. \n Support: Extended" + items: + description: HTTPQueryParamMatch describes + how to select a HTTP route by matching HTTP + query parameters. + properties: + name: + description: "Name is the name of the + HTTP query param to be matched. This + must be an exact string match. (See + https://tools.ietf.org/html/rfc7230#section-2.7.3). + \n If multiple entries specify equivalent + query param names, only the first entry + with an equivalent name MUST be considered + for a match. Subsequent entries with + an equivalent query param name MUST + be ignored. \n If a query param is repeated + in an HTTP request, the behavior is + purposely left undefined, since different + data planes have different capabilities. + However, it is *recommended* that implementations + should match against the first value + of the param if the data plane supports + it, as this behavior is expected in + other load balancing contexts outside + of the Gateway API. \n Users SHOULD + NOT route traffic based on repeated + query params to guard themselves against + potential differences in the implementations." + maxLength: 256 + minLength: 1 + type: string + type: + default: Exact + description: "Type specifies how to match + against the value of the query parameter. + \n Support: Extended (Exact) \n Support: + Implementation-specific (RegularExpression) + \n Since RegularExpression QueryParamMatchType + has Implementation-specific conformance, + implementations can support POSIX, PCRE + or any other dialects of regular expressions. + Please read the implementation's documentation + to determine the supported dialect." + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value of HTTP + query param to be matched. + maxLength: 1024 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + type: array + type: object + type: array when: description: Conditions for Authorino to enforce this config. If omitted, the config will be enforced for all requests. @@ -961,6 +1376,233 @@ spec: the same priority group are evaluated concurrently; consecutive priority groups are evaluated sequentially. type: integer + routeSelectors: + description: Top-level route selectors. If present, the + elements will be used to select HTTPRoute rules that, + when activated, trigger the auth rule. At least one selected + HTTPRoute rule must match to trigger the auth rule. If + no route selectors are specified, the auth rule will be + evaluated at all requests to the protected routes. + items: + description: RouteSelector defines semantics for matching + an HTTP request based on conditions https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRouteSpec + properties: + hostnames: + description: Hostnames defines a set of hostname that + should match against the HTTP Host header to select + a HTTPRoute to process the request https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRouteSpec + items: + description: "Hostname is the fully qualified domain + name of a network host. This matches the RFC 1123 + definition of a hostname with 2 notable exceptions: + \n 1. IPs are not allowed. 2. A hostname may be + prefixed with a wildcard label (`*.`). The wildcard + label must appear by itself as the first label. + \n Hostname can be \"precise\" which is a domain + name without the terminating dot of a network + host (e.g. \"foo.example.com\") or \"wildcard\", + which is a domain name prefixed with a single + wildcard label (e.g. `*.example.com`). \n Note + that as per RFC1035 and RFC1123, a *label* must + consist of lower case alphanumeric characters + or '-', and must start and end with an alphanumeric + character. No other punctuation is allowed." + maxLength: 253 + minLength: 1 + pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + type: array + matches: + description: Matches define conditions used for matching + the rule against incoming HTTP requests. https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRouteSpec + items: + description: "HTTPRouteMatch defines the predicate + used to match requests to a given action. Multiple + match types are ANDed together, i.e. the match + will evaluate to true only if all conditions are + satisfied. \n For example, the match below will + match a HTTP request only if its path starts with + `/foo` AND it contains the `version: v1` header: + \n ``` match: \n path: value: \"/foo\" headers: + - name: \"version\" value \"v1\" \n ```" + properties: + headers: + description: Headers specifies HTTP request + header matchers. Multiple match values are + ANDed together, meaning, a request must match + all the specified headers to select the route. + items: + description: HTTPHeaderMatch describes how + to select a HTTP route by matching HTTP + request headers. + properties: + name: + description: "Name is the name of the + HTTP Header to be matched. Name matching + MUST be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, only the first entry with + an equivalent name MUST be considered + for a match. Subsequent entries with + an equivalent header name MUST be ignored. + Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered + equivalent. \n When a header is repeated + in an HTTP request, it is implementation-specific + behavior as to how this is represented. + Generally, proxies should follow the + guidance from the RFC: https://www.rfc-editor.org/rfc/rfc7230.html#section-3.2.2 + regarding processing a repeated header, + with special handling for \"Set-Cookie\"." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + type: + default: Exact + description: "Type specifies how to match + against the value of the header. \n + Support: Core (Exact) \n Support: Implementation-specific + (RegularExpression) \n Since RegularExpression + HeaderMatchType has implementation-specific + conformance, implementations can support + POSIX, PCRE or any other dialects of + regular expressions. Please read the + implementation's documentation to determine + the supported dialect." + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + method: + description: "Method specifies HTTP method matcher. + When specified, this route will be matched + only if the request has the specified method. + \n Support: Extended" + enum: + - GET + - HEAD + - POST + - PUT + - DELETE + - CONNECT + - OPTIONS + - TRACE + - PATCH + type: string + path: + default: + type: PathPrefix + value: / + description: Path specifies a HTTP request path + matcher. If this field is not specified, a + default prefix match on the "/" path is provided. + properties: + type: + default: PathPrefix + description: "Type specifies how to match + against the path Value. \n Support: Core + (Exact, PathPrefix) \n Support: Implementation-specific + (RegularExpression)" + enum: + - Exact + - PathPrefix + - RegularExpression + type: string + value: + default: / + description: Value of the HTTP path to match + against. + maxLength: 1024 + type: string + type: object + queryParams: + description: "QueryParams specifies HTTP query + parameter matchers. Multiple match values + are ANDed together, meaning, a request must + match all the specified query parameters to + select the route. \n Support: Extended" + items: + description: HTTPQueryParamMatch describes + how to select a HTTP route by matching HTTP + query parameters. + properties: + name: + description: "Name is the name of the + HTTP query param to be matched. This + must be an exact string match. (See + https://tools.ietf.org/html/rfc7230#section-2.7.3). + \n If multiple entries specify equivalent + query param names, only the first entry + with an equivalent name MUST be considered + for a match. Subsequent entries with + an equivalent query param name MUST + be ignored. \n If a query param is repeated + in an HTTP request, the behavior is + purposely left undefined, since different + data planes have different capabilities. + However, it is *recommended* that implementations + should match against the first value + of the param if the data plane supports + it, as this behavior is expected in + other load balancing contexts outside + of the Gateway API. \n Users SHOULD + NOT route traffic based on repeated + query params to guard themselves against + potential differences in the implementations." + maxLength: 256 + minLength: 1 + type: string + type: + default: Exact + description: "Type specifies how to match + against the value of the query parameter. + \n Support: Extended (Exact) \n Support: + Implementation-specific (RegularExpression) + \n Since RegularExpression QueryParamMatchType + has Implementation-specific conformance, + implementations can support POSIX, PCRE + or any other dialects of regular expressions. + Please read the implementation's documentation + to determine the supported dialect." + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value of HTTP + query param to be matched. + maxLength: 1024 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + type: array + type: object + type: array spicedb: description: Authorization decision delegated to external Authzed/SpiceDB server. @@ -1403,6 +2045,233 @@ spec: the same priority group are evaluated concurrently; consecutive priority groups are evaluated sequentially. type: integer + routeSelectors: + description: Top-level route selectors. If present, the + elements will be used to select HTTPRoute rules that, + when activated, trigger the auth rule. At least one selected + HTTPRoute rule must match to trigger the auth rule. If + no route selectors are specified, the auth rule will be + evaluated at all requests to the protected routes. + items: + description: RouteSelector defines semantics for matching + an HTTP request based on conditions https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRouteSpec + properties: + hostnames: + description: Hostnames defines a set of hostname that + should match against the HTTP Host header to select + a HTTPRoute to process the request https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRouteSpec + items: + description: "Hostname is the fully qualified domain + name of a network host. This matches the RFC 1123 + definition of a hostname with 2 notable exceptions: + \n 1. IPs are not allowed. 2. A hostname may be + prefixed with a wildcard label (`*.`). The wildcard + label must appear by itself as the first label. + \n Hostname can be \"precise\" which is a domain + name without the terminating dot of a network + host (e.g. \"foo.example.com\") or \"wildcard\", + which is a domain name prefixed with a single + wildcard label (e.g. `*.example.com`). \n Note + that as per RFC1035 and RFC1123, a *label* must + consist of lower case alphanumeric characters + or '-', and must start and end with an alphanumeric + character. No other punctuation is allowed." + maxLength: 253 + minLength: 1 + pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + type: array + matches: + description: Matches define conditions used for matching + the rule against incoming HTTP requests. https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRouteSpec + items: + description: "HTTPRouteMatch defines the predicate + used to match requests to a given action. Multiple + match types are ANDed together, i.e. the match + will evaluate to true only if all conditions are + satisfied. \n For example, the match below will + match a HTTP request only if its path starts with + `/foo` AND it contains the `version: v1` header: + \n ``` match: \n path: value: \"/foo\" headers: + - name: \"version\" value \"v1\" \n ```" + properties: + headers: + description: Headers specifies HTTP request + header matchers. Multiple match values are + ANDed together, meaning, a request must match + all the specified headers to select the route. + items: + description: HTTPHeaderMatch describes how + to select a HTTP route by matching HTTP + request headers. + properties: + name: + description: "Name is the name of the + HTTP Header to be matched. Name matching + MUST be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, only the first entry with + an equivalent name MUST be considered + for a match. Subsequent entries with + an equivalent header name MUST be ignored. + Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered + equivalent. \n When a header is repeated + in an HTTP request, it is implementation-specific + behavior as to how this is represented. + Generally, proxies should follow the + guidance from the RFC: https://www.rfc-editor.org/rfc/rfc7230.html#section-3.2.2 + regarding processing a repeated header, + with special handling for \"Set-Cookie\"." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + type: + default: Exact + description: "Type specifies how to match + against the value of the header. \n + Support: Core (Exact) \n Support: Implementation-specific + (RegularExpression) \n Since RegularExpression + HeaderMatchType has implementation-specific + conformance, implementations can support + POSIX, PCRE or any other dialects of + regular expressions. Please read the + implementation's documentation to determine + the supported dialect." + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + method: + description: "Method specifies HTTP method matcher. + When specified, this route will be matched + only if the request has the specified method. + \n Support: Extended" + enum: + - GET + - HEAD + - POST + - PUT + - DELETE + - CONNECT + - OPTIONS + - TRACE + - PATCH + type: string + path: + default: + type: PathPrefix + value: / + description: Path specifies a HTTP request path + matcher. If this field is not specified, a + default prefix match on the "/" path is provided. + properties: + type: + default: PathPrefix + description: "Type specifies how to match + against the path Value. \n Support: Core + (Exact, PathPrefix) \n Support: Implementation-specific + (RegularExpression)" + enum: + - Exact + - PathPrefix + - RegularExpression + type: string + value: + default: / + description: Value of the HTTP path to match + against. + maxLength: 1024 + type: string + type: object + queryParams: + description: "QueryParams specifies HTTP query + parameter matchers. Multiple match values + are ANDed together, meaning, a request must + match all the specified query parameters to + select the route. \n Support: Extended" + items: + description: HTTPQueryParamMatch describes + how to select a HTTP route by matching HTTP + query parameters. + properties: + name: + description: "Name is the name of the + HTTP query param to be matched. This + must be an exact string match. (See + https://tools.ietf.org/html/rfc7230#section-2.7.3). + \n If multiple entries specify equivalent + query param names, only the first entry + with an equivalent name MUST be considered + for a match. Subsequent entries with + an equivalent query param name MUST + be ignored. \n If a query param is repeated + in an HTTP request, the behavior is + purposely left undefined, since different + data planes have different capabilities. + However, it is *recommended* that implementations + should match against the first value + of the param if the data plane supports + it, as this behavior is expected in + other load balancing contexts outside + of the Gateway API. \n Users SHOULD + NOT route traffic based on repeated + query params to guard themselves against + potential differences in the implementations." + maxLength: 256 + minLength: 1 + type: string + type: + default: Exact + description: "Type specifies how to match + against the value of the query parameter. + \n Support: Extended (Exact) \n Support: + Implementation-specific (RegularExpression) + \n Since RegularExpression QueryParamMatchType + has Implementation-specific conformance, + implementations can support POSIX, PCRE + or any other dialects of regular expressions. + Please read the implementation's documentation + to determine the supported dialect." + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value of HTTP + query param to be matched. + maxLength: 1024 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + type: array + type: object + type: array when: description: Conditions for Authorino to enforce this config. If omitted, the config will be enforced for all requests. @@ -1716,6 +2585,233 @@ spec: the same priority group are evaluated concurrently; consecutive priority groups are evaluated sequentially. type: integer + routeSelectors: + description: Top-level route selectors. If present, the + elements will be used to select HTTPRoute rules that, + when activated, trigger the auth rule. At least one selected + HTTPRoute rule must match to trigger the auth rule. If + no route selectors are specified, the auth rule will be + evaluated at all requests to the protected routes. + items: + description: RouteSelector defines semantics for matching + an HTTP request based on conditions https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRouteSpec + properties: + hostnames: + description: Hostnames defines a set of hostname that + should match against the HTTP Host header to select + a HTTPRoute to process the request https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRouteSpec + items: + description: "Hostname is the fully qualified domain + name of a network host. This matches the RFC 1123 + definition of a hostname with 2 notable exceptions: + \n 1. IPs are not allowed. 2. A hostname may be + prefixed with a wildcard label (`*.`). The wildcard + label must appear by itself as the first label. + \n Hostname can be \"precise\" which is a domain + name without the terminating dot of a network + host (e.g. \"foo.example.com\") or \"wildcard\", + which is a domain name prefixed with a single + wildcard label (e.g. `*.example.com`). \n Note + that as per RFC1035 and RFC1123, a *label* must + consist of lower case alphanumeric characters + or '-', and must start and end with an alphanumeric + character. No other punctuation is allowed." + maxLength: 253 + minLength: 1 + pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + type: array + matches: + description: Matches define conditions used for matching + the rule against incoming HTTP requests. https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRouteSpec + items: + description: "HTTPRouteMatch defines the predicate + used to match requests to a given action. Multiple + match types are ANDed together, i.e. the match + will evaluate to true only if all conditions are + satisfied. \n For example, the match below will + match a HTTP request only if its path starts with + `/foo` AND it contains the `version: v1` header: + \n ``` match: \n path: value: \"/foo\" headers: + - name: \"version\" value \"v1\" \n ```" + properties: + headers: + description: Headers specifies HTTP request + header matchers. Multiple match values are + ANDed together, meaning, a request must match + all the specified headers to select the route. + items: + description: HTTPHeaderMatch describes how + to select a HTTP route by matching HTTP + request headers. + properties: + name: + description: "Name is the name of the + HTTP Header to be matched. Name matching + MUST be case insensitive. (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify equivalent + header names, only the first entry with + an equivalent name MUST be considered + for a match. Subsequent entries with + an equivalent header name MUST be ignored. + Due to the case-insensitivity of header + names, \"foo\" and \"Foo\" are considered + equivalent. \n When a header is repeated + in an HTTP request, it is implementation-specific + behavior as to how this is represented. + Generally, proxies should follow the + guidance from the RFC: https://www.rfc-editor.org/rfc/rfc7230.html#section-3.2.2 + regarding processing a repeated header, + with special handling for \"Set-Cookie\"." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + type: + default: Exact + description: "Type specifies how to match + against the value of the header. \n + Support: Core (Exact) \n Support: Implementation-specific + (RegularExpression) \n Since RegularExpression + HeaderMatchType has implementation-specific + conformance, implementations can support + POSIX, PCRE or any other dialects of + regular expressions. Please read the + implementation's documentation to determine + the supported dialect." + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value of HTTP + Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + method: + description: "Method specifies HTTP method matcher. + When specified, this route will be matched + only if the request has the specified method. + \n Support: Extended" + enum: + - GET + - HEAD + - POST + - PUT + - DELETE + - CONNECT + - OPTIONS + - TRACE + - PATCH + type: string + path: + default: + type: PathPrefix + value: / + description: Path specifies a HTTP request path + matcher. If this field is not specified, a + default prefix match on the "/" path is provided. + properties: + type: + default: PathPrefix + description: "Type specifies how to match + against the path Value. \n Support: Core + (Exact, PathPrefix) \n Support: Implementation-specific + (RegularExpression)" + enum: + - Exact + - PathPrefix + - RegularExpression + type: string + value: + default: / + description: Value of the HTTP path to match + against. + maxLength: 1024 + type: string + type: object + queryParams: + description: "QueryParams specifies HTTP query + parameter matchers. Multiple match values + are ANDed together, meaning, a request must + match all the specified query parameters to + select the route. \n Support: Extended" + items: + description: HTTPQueryParamMatch describes + how to select a HTTP route by matching HTTP + query parameters. + properties: + name: + description: "Name is the name of the + HTTP query param to be matched. This + must be an exact string match. (See + https://tools.ietf.org/html/rfc7230#section-2.7.3). + \n If multiple entries specify equivalent + query param names, only the first entry + with an equivalent name MUST be considered + for a match. Subsequent entries with + an equivalent query param name MUST + be ignored. \n If a query param is repeated + in an HTTP request, the behavior is + purposely left undefined, since different + data planes have different capabilities. + However, it is *recommended* that implementations + should match against the first value + of the param if the data plane supports + it, as this behavior is expected in + other load balancing contexts outside + of the Gateway API. \n Users SHOULD + NOT route traffic based on repeated + query params to guard themselves against + potential differences in the implementations." + maxLength: 256 + minLength: 1 + type: string + type: + default: Exact + description: "Type specifies how to match + against the value of the query parameter. + \n Support: Extended (Exact) \n Support: + Implementation-specific (RegularExpression) + \n Since RegularExpression QueryParamMatchType + has Implementation-specific conformance, + implementations can support POSIX, PCRE + or any other dialects of regular expressions. + Please read the implementation's documentation + to determine the supported dialect." + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value of HTTP + query param to be matched. + maxLength: 1024 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + type: array + type: object + type: array uma: description: User-Managed Access (UMA) source of resource data. @@ -1858,8 +2954,6 @@ spec: properties: dynamicMetadata: additionalProperties: - description: Settings of the success custom response - item. properties: cache: description: Caching options for the resolved object @@ -1961,6 +3055,264 @@ spec: in the same priority group are evaluated concurrently; consecutive priority groups are evaluated sequentially. type: integer + routeSelectors: + description: Top-level route selectors. If present, + the elements will be used to select HTTPRoute + rules that, when activated, trigger the auth rule. + At least one selected HTTPRoute rule must match + to trigger the auth rule. If no route selectors + are specified, the auth rule will be evaluated + at all requests to the protected routes. + items: + description: RouteSelector defines semantics for + matching an HTTP request based on conditions + https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRouteSpec + properties: + hostnames: + description: Hostnames defines a set of hostname + that should match against the HTTP Host + header to select a HTTPRoute to process + the request https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRouteSpec + items: + description: "Hostname is the fully qualified + domain name of a network host. This matches + the RFC 1123 definition of a hostname + with 2 notable exceptions: \n 1. IPs are + not allowed. 2. A hostname may be prefixed + with a wildcard label (`*.`). The wildcard + label must appear by itself as the first + label. \n Hostname can be \"precise\" + which is a domain name without the terminating + dot of a network host (e.g. \"foo.example.com\") + or \"wildcard\", which is a domain name + prefixed with a single wildcard label + (e.g. `*.example.com`). \n Note that as + per RFC1035 and RFC1123, a *label* must + consist of lower case alphanumeric characters + or '-', and must start and end with an + alphanumeric character. No other punctuation + is allowed." + maxLength: 253 + minLength: 1 + pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + type: array + matches: + description: Matches define conditions used + for matching the rule against incoming HTTP + requests. https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRouteSpec + items: + description: "HTTPRouteMatch defines the + predicate used to match requests to a + given action. Multiple match types are + ANDed together, i.e. the match will evaluate + to true only if all conditions are satisfied. + \n For example, the match below will match + a HTTP request only if its path starts + with `/foo` AND it contains the `version: + v1` header: \n ``` match: \n path: value: + \"/foo\" headers: - name: \"version\" + value \"v1\" \n ```" + properties: + headers: + description: Headers specifies HTTP + request header matchers. Multiple + match values are ANDed together, meaning, + a request must match all the specified + headers to select the route. + items: + description: HTTPHeaderMatch describes + how to select a HTTP route by matching + HTTP request headers. + properties: + name: + description: "Name is the name + of the HTTP Header to be matched. + Name matching MUST be case insensitive. + (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify + equivalent header names, only + the first entry with an equivalent + name MUST be considered for + a match. Subsequent entries + with an equivalent header name + MUST be ignored. Due to the + case-insensitivity of header + names, \"foo\" and \"Foo\" are + considered equivalent. \n When + a header is repeated in an HTTP + request, it is implementation-specific + behavior as to how this is represented. + Generally, proxies should follow + the guidance from the RFC: https://www.rfc-editor.org/rfc/rfc7230.html#section-3.2.2 + regarding processing a repeated + header, with special handling + for \"Set-Cookie\"." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + type: + default: Exact + description: "Type specifies how + to match against the value of + the header. \n Support: Core + (Exact) \n Support: Implementation-specific + (RegularExpression) \n Since + RegularExpression HeaderMatchType + has implementation-specific + conformance, implementations + can support POSIX, PCRE or any + other dialects of regular expressions. + Please read the implementation's + documentation to determine the + supported dialect." + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value + of HTTP Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + method: + description: "Method specifies HTTP + method matcher. When specified, this + route will be matched only if the + request has the specified method. + \n Support: Extended" + enum: + - GET + - HEAD + - POST + - PUT + - DELETE + - CONNECT + - OPTIONS + - TRACE + - PATCH + type: string + path: + default: + type: PathPrefix + value: / + description: Path specifies a HTTP request + path matcher. If this field is not + specified, a default prefix match + on the "/" path is provided. + properties: + type: + default: PathPrefix + description: "Type specifies how + to match against the path Value. + \n Support: Core (Exact, PathPrefix) + \n Support: Implementation-specific + (RegularExpression)" + enum: + - Exact + - PathPrefix + - RegularExpression + type: string + value: + default: / + description: Value of the HTTP path + to match against. + maxLength: 1024 + type: string + type: object + queryParams: + description: "QueryParams specifies + HTTP query parameter matchers. Multiple + match values are ANDed together, meaning, + a request must match all the specified + query parameters to select the route. + \n Support: Extended" + items: + description: HTTPQueryParamMatch describes + how to select a HTTP route by matching + HTTP query parameters. + properties: + name: + description: "Name is the name + of the HTTP query param to be + matched. This must be an exact + string match. (See https://tools.ietf.org/html/rfc7230#section-2.7.3). + \n If multiple entries specify + equivalent query param names, + only the first entry with an + equivalent name MUST be considered + for a match. Subsequent entries + with an equivalent query param + name MUST be ignored. \n If + a query param is repeated in + an HTTP request, the behavior + is purposely left undefined, + since different data planes + have different capabilities. + However, it is *recommended* + that implementations should + match against the first value + of the param if the data plane + supports it, as this behavior + is expected in other load balancing + contexts outside of the Gateway + API. \n Users SHOULD NOT route + traffic based on repeated query + params to guard themselves against + potential differences in the + implementations." + maxLength: 256 + minLength: 1 + type: string + type: + default: Exact + description: "Type specifies how + to match against the value of + the query parameter. \n Support: + Extended (Exact) \n Support: + Implementation-specific (RegularExpression) + \n Since RegularExpression QueryParamMatchType + has Implementation-specific + conformance, implementations + can support POSIX, PCRE or any + other dialects of regular expressions. + Please read the implementation's + documentation to determine the + supported dialect." + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value + of HTTP query param to be matched. + maxLength: 1024 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + type: array + type: object + type: array when: description: Conditions for Authorino to enforce this config. If omitted, the config will be enforced @@ -2198,6 +3550,264 @@ spec: in the same priority group are evaluated concurrently; consecutive priority groups are evaluated sequentially. type: integer + routeSelectors: + description: Top-level route selectors. If present, + the elements will be used to select HTTPRoute + rules that, when activated, trigger the auth rule. + At least one selected HTTPRoute rule must match + to trigger the auth rule. If no route selectors + are specified, the auth rule will be evaluated + at all requests to the protected routes. + items: + description: RouteSelector defines semantics for + matching an HTTP request based on conditions + https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRouteSpec + properties: + hostnames: + description: Hostnames defines a set of hostname + that should match against the HTTP Host + header to select a HTTPRoute to process + the request https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRouteSpec + items: + description: "Hostname is the fully qualified + domain name of a network host. This matches + the RFC 1123 definition of a hostname + with 2 notable exceptions: \n 1. IPs are + not allowed. 2. A hostname may be prefixed + with a wildcard label (`*.`). The wildcard + label must appear by itself as the first + label. \n Hostname can be \"precise\" + which is a domain name without the terminating + dot of a network host (e.g. \"foo.example.com\") + or \"wildcard\", which is a domain name + prefixed with a single wildcard label + (e.g. `*.example.com`). \n Note that as + per RFC1035 and RFC1123, a *label* must + consist of lower case alphanumeric characters + or '-', and must start and end with an + alphanumeric character. No other punctuation + is allowed." + maxLength: 253 + minLength: 1 + pattern: ^(\*\.)?[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + type: array + matches: + description: Matches define conditions used + for matching the rule against incoming HTTP + requests. https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRouteSpec + items: + description: "HTTPRouteMatch defines the + predicate used to match requests to a + given action. Multiple match types are + ANDed together, i.e. the match will evaluate + to true only if all conditions are satisfied. + \n For example, the match below will match + a HTTP request only if its path starts + with `/foo` AND it contains the `version: + v1` header: \n ``` match: \n path: value: + \"/foo\" headers: - name: \"version\" + value \"v1\" \n ```" + properties: + headers: + description: Headers specifies HTTP + request header matchers. Multiple + match values are ANDed together, meaning, + a request must match all the specified + headers to select the route. + items: + description: HTTPHeaderMatch describes + how to select a HTTP route by matching + HTTP request headers. + properties: + name: + description: "Name is the name + of the HTTP Header to be matched. + Name matching MUST be case insensitive. + (See https://tools.ietf.org/html/rfc7230#section-3.2). + \n If multiple entries specify + equivalent header names, only + the first entry with an equivalent + name MUST be considered for + a match. Subsequent entries + with an equivalent header name + MUST be ignored. Due to the + case-insensitivity of header + names, \"foo\" and \"Foo\" are + considered equivalent. \n When + a header is repeated in an HTTP + request, it is implementation-specific + behavior as to how this is represented. + Generally, proxies should follow + the guidance from the RFC: https://www.rfc-editor.org/rfc/rfc7230.html#section-3.2.2 + regarding processing a repeated + header, with special handling + for \"Set-Cookie\"." + maxLength: 256 + minLength: 1 + pattern: ^[A-Za-z0-9!#$%&'*+\-.^_\x60|~]+$ + type: string + type: + default: Exact + description: "Type specifies how + to match against the value of + the header. \n Support: Core + (Exact) \n Support: Implementation-specific + (RegularExpression) \n Since + RegularExpression HeaderMatchType + has implementation-specific + conformance, implementations + can support POSIX, PCRE or any + other dialects of regular expressions. + Please read the implementation's + documentation to determine the + supported dialect." + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value + of HTTP Header to be matched. + maxLength: 4096 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + method: + description: "Method specifies HTTP + method matcher. When specified, this + route will be matched only if the + request has the specified method. + \n Support: Extended" + enum: + - GET + - HEAD + - POST + - PUT + - DELETE + - CONNECT + - OPTIONS + - TRACE + - PATCH + type: string + path: + default: + type: PathPrefix + value: / + description: Path specifies a HTTP request + path matcher. If this field is not + specified, a default prefix match + on the "/" path is provided. + properties: + type: + default: PathPrefix + description: "Type specifies how + to match against the path Value. + \n Support: Core (Exact, PathPrefix) + \n Support: Implementation-specific + (RegularExpression)" + enum: + - Exact + - PathPrefix + - RegularExpression + type: string + value: + default: / + description: Value of the HTTP path + to match against. + maxLength: 1024 + type: string + type: object + queryParams: + description: "QueryParams specifies + HTTP query parameter matchers. Multiple + match values are ANDed together, meaning, + a request must match all the specified + query parameters to select the route. + \n Support: Extended" + items: + description: HTTPQueryParamMatch describes + how to select a HTTP route by matching + HTTP query parameters. + properties: + name: + description: "Name is the name + of the HTTP query param to be + matched. This must be an exact + string match. (See https://tools.ietf.org/html/rfc7230#section-2.7.3). + \n If multiple entries specify + equivalent query param names, + only the first entry with an + equivalent name MUST be considered + for a match. Subsequent entries + with an equivalent query param + name MUST be ignored. \n If + a query param is repeated in + an HTTP request, the behavior + is purposely left undefined, + since different data planes + have different capabilities. + However, it is *recommended* + that implementations should + match against the first value + of the param if the data plane + supports it, as this behavior + is expected in other load balancing + contexts outside of the Gateway + API. \n Users SHOULD NOT route + traffic based on repeated query + params to guard themselves against + potential differences in the + implementations." + maxLength: 256 + minLength: 1 + type: string + type: + default: Exact + description: "Type specifies how + to match against the value of + the query parameter. \n Support: + Extended (Exact) \n Support: + Implementation-specific (RegularExpression) + \n Since RegularExpression QueryParamMatchType + has Implementation-specific + conformance, implementations + can support POSIX, PCRE or any + other dialects of regular expressions. + Please read the implementation's + documentation to determine the + supported dialect." + enum: + - Exact + - RegularExpression + type: string + value: + description: Value is the value + of HTTP query param to be matched. + maxLength: 1024 + minLength: 1 + type: string + required: + - name + - value + type: object + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + type: array + type: object + type: array when: description: Conditions for Authorino to enforce this config. If omitted, the config will be enforced diff --git a/controllers/authpolicy_auth_config.go b/controllers/authpolicy_auth_config.go index 873dc9664..17554e77f 100644 --- a/controllers/authpolicy_auth_config.go +++ b/controllers/authpolicy_auth_config.go @@ -2,13 +2,16 @@ package controllers import ( "context" + "errors" "fmt" "reflect" + "strings" "github.com/go-logr/logr" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" + gatewayapiv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" authorinoapi "github.com/kuadrant/authorino/api/v1beta2" api "github.com/kuadrant/kuadrant-operator/api/v1beta2" @@ -21,7 +24,7 @@ func (r *AuthPolicyReconciler) reconcileAuthConfigs(ctx context.Context, ap *api return err } - authConfig, err := r.desiredAuthConfig(ap, targetNetworkObject) + authConfig, err := r.desiredAuthConfig(ctx, ap, targetNetworkObject) if err != nil { return err } @@ -60,13 +63,11 @@ func (r *AuthPolicyReconciler) deleteAuthConfigs(ctx context.Context, ap *api.Au return nil } -func (r *AuthPolicyReconciler) desiredAuthConfig(ap *api.AuthPolicy, targetNetworkObject client.Object) (*authorinoapi.AuthConfig, error) { - hosts, err := r.policyHosts(ap, targetNetworkObject) - if err != nil { - return nil, err - } +func (r *AuthPolicyReconciler) desiredAuthConfig(ctx context.Context, ap *api.AuthPolicy, targetNetworkObject client.Object) (*authorinoapi.AuthConfig, error) { + logger, _ := logr.FromContext(ctx) + logger = logger.WithName("desiredAuthConfig") - return &authorinoapi.AuthConfig{ + authConfig := &authorinoapi.AuthConfig{ TypeMeta: metav1.TypeMeta{ Kind: "AuthConfig", APIVersion: authorinoapi.GroupVersion.String(), @@ -75,41 +76,110 @@ func (r *AuthPolicyReconciler) desiredAuthConfig(ap *api.AuthPolicy, targetNetwo Name: authConfigName(client.ObjectKeyFromObject(ap)), Namespace: ap.Namespace, }, - Spec: authorinoapi.AuthConfigSpec{ - Hosts: hosts, - NamedPatterns: ap.Spec.AuthScheme.NamedPatterns, - Conditions: ap.Spec.AuthScheme.Conditions, - Authentication: ap.Spec.AuthScheme.Authentication, - Metadata: ap.Spec.AuthScheme.Metadata, - Authorization: ap.Spec.AuthScheme.Authorization, - Response: ap.Spec.AuthScheme.Response, - }, - }, nil -} - -func (r *AuthPolicyReconciler) policyHosts(ap *api.AuthPolicy, targetNetworkObject client.Object) ([]string, error) { - if len(ap.Spec.RouteRules) == 0 { - return common.TargetHostnames(targetNetworkObject) + Spec: authorinoapi.AuthConfigSpec{}, } - uniqueHostnamesMap := make(map[string]any) - for idx := range ap.Spec.RouteRules { - if len(ap.Spec.RouteRules[idx].Hosts) == 0 { - // When one of the rules does not have hosts, just return target hostnames - return common.TargetHostnames(targetNetworkObject) + var route *gatewayapiv1beta1.HTTPRoute + var hosts []string + + switch obj := targetNetworkObject.(type) { + case *gatewayapiv1beta1.HTTPRoute: + route = obj + var err error + hosts, err = common.HostnamesFromHTTPRoute(ctx, obj, r.Client()) + if err != nil { + return nil, err + } + case *gatewayapiv1beta1.Gateway: + // fake a single httproute with all rules from all httproutes accepted by the gateway, + // that do not have an authpolicy of its own, so we can generate wasm rules for those cases + gw := common.GatewayWrapper{Gateway: obj} + gwHostnames := gw.Hostnames() + if len(hosts) == 0 { + gwHostnames = []gatewayapiv1beta1.Hostname{"*"} } + hosts = common.HostnamesToStrings(gwHostnames) - for _, hostname := range ap.Spec.RouteRules[idx].Hosts { - uniqueHostnamesMap[hostname] = nil + rules := make([]gatewayapiv1beta1.HTTPRouteRule, 0) + routes := r.FetchAcceptedGatewayHTTPRoutes(ctx, ap.TargetKey()) + for idx := range routes { + route := routes[idx] + // skip routes that have an authpolicy of its own + if route.GetAnnotations()[common.AuthPolicyBackRefAnnotation] != "" { + continue + } + rules = append(rules, route.Spec.Rules...) + } + if len(rules) == 0 { + logger.V(1).Info("no httproutes attached to the targeted gateway, skipping authorino authconfig for the gateway authpolicy") + common.TagObjectToDelete(authConfig) + return authConfig, nil + } + route = &gatewayapiv1beta1.HTTPRoute{ + Spec: gatewayapiv1beta1.HTTPRouteSpec{ + Hostnames: gwHostnames, + Rules: rules, + }, } } - hostnames := make([]string, 0, len(uniqueHostnamesMap)) - for k := range uniqueHostnamesMap { - hostnames = append(hostnames, k) + // hosts + authConfig.Spec.Hosts = hosts + + // named patterns + if namedPatterns := ap.Spec.AuthScheme.NamedPatterns; len(namedPatterns) > 0 { + authConfig.Spec.NamedPatterns = namedPatterns } - return hostnames, nil + // top-level conditions + topLevelConditionsFromRouteSelectors, err := authorinoConditionsFromRouteSelectors(route, ap.Spec) + if err != nil { + return nil, err + } + if len(topLevelConditionsFromRouteSelectors) == 0 { + topLevelConditionsFromRouteSelectors = authorinoConditionsFromHTTPRoute(route) + } + if len(topLevelConditionsFromRouteSelectors) > 0 || len(ap.Spec.AuthScheme.Conditions) > 0 { + authConfig.Spec.Conditions = append(ap.Spec.AuthScheme.Conditions, topLevelConditionsFromRouteSelectors...) + } + + // authentication + if authentication := ap.Spec.AuthScheme.Authentication; len(authentication) > 0 { + authConfig.Spec.Authentication = authorinoSpecsFromConfigs(authentication, func(config api.AuthenticationSpec) authorinoapi.AuthenticationSpec { return config.AuthenticationSpec }) + } + + // metadata + if metadata := ap.Spec.AuthScheme.Metadata; len(metadata) > 0 { + authConfig.Spec.Metadata = authorinoSpecsFromConfigs(metadata, func(config api.MetadataSpec) authorinoapi.MetadataSpec { return config.MetadataSpec }) + } + + // authorization + if authorization := ap.Spec.AuthScheme.Authorization; len(authorization) > 0 { + authConfig.Spec.Authorization = authorinoSpecsFromConfigs(authorization, func(config api.AuthorizationSpec) authorinoapi.AuthorizationSpec { return config.AuthorizationSpec }) + } + + // response + if response := ap.Spec.AuthScheme.Response; response != nil { + authConfig.Spec.Response = &authorinoapi.ResponseSpec{ + Unauthenticated: response.Unauthenticated, + Unauthorized: response.Unauthorized, + Success: authorinoapi.WrappedSuccessResponseSpec{ + Headers: authorinoSpecsFromConfigs(response.Success.Headers, func(config api.HeaderSuccessResponseSpec) authorinoapi.HeaderSuccessResponseSpec { + return authorinoapi.HeaderSuccessResponseSpec{SuccessResponseSpec: config.SuccessResponseSpec.SuccessResponseSpec} + }), + DynamicMetadata: authorinoSpecsFromConfigs(response.Success.DynamicMetadata, func(config api.SuccessResponseSpec) authorinoapi.SuccessResponseSpec { + return config.SuccessResponseSpec + }), + }, + } + } + + // callbacks + if callbacks := ap.Spec.AuthScheme.Callbacks; len(callbacks) > 0 { + authConfig.Spec.Callbacks = authorinoSpecsFromConfigs(callbacks, func(config api.CallbackSpec) authorinoapi.CallbackSpec { return config.CallbackSpec }) + } + + return mergeConditionsFromRouteSelectorsIntoConfigs(ap, route, authConfig) } // authConfigName returns the name of Authorino AuthConfig CR. @@ -117,6 +187,331 @@ func authConfigName(apKey client.ObjectKey) string { return fmt.Sprintf("ap-%s-%s", apKey.Namespace, apKey.Name) } +func authorinoSpecsFromConfigs[T, U any](configs map[string]U, extractAuthorinoSpec func(U) T) map[string]T { + specs := make(map[string]T, len(configs)) + for name, config := range configs { + authorinoConfig := extractAuthorinoSpec(config) + specs[name] = authorinoConfig + } + return specs +} + +func mergeConditionsFromRouteSelectorsIntoConfigs(ap *api.AuthPolicy, route *gatewayapiv1beta1.HTTPRoute, authConfig *authorinoapi.AuthConfig) (*authorinoapi.AuthConfig, error) { + // authentication + for name, config := range ap.Spec.AuthScheme.Authentication { + conditions, err := authorinoConditionsFromRouteSelectors(route, config) + if err != nil { + return nil, err + } + if len(conditions) == 0 { + continue + } + c := authConfig.Spec.Authentication[name] + c.Conditions = append(c.Conditions, conditions...) + authConfig.Spec.Authentication[name] = c + } + + // metadata + for name, config := range ap.Spec.AuthScheme.Metadata { + conditions, err := authorinoConditionsFromRouteSelectors(route, config) + if err != nil { + return nil, err + } + if len(conditions) == 0 { + continue + } + c := authConfig.Spec.Metadata[name] + c.Conditions = append(c.Conditions, conditions...) + authConfig.Spec.Metadata[name] = c + } + + // authorization + for name, config := range ap.Spec.AuthScheme.Authorization { + conditions, err := authorinoConditionsFromRouteSelectors(route, config) + if err != nil { + return nil, err + } + if len(conditions) == 0 { + continue + } + c := authConfig.Spec.Authorization[name] + c.Conditions = append(c.Conditions, conditions...) + authConfig.Spec.Authorization[name] = c + } + + // response + if response := ap.Spec.AuthScheme.Response; response != nil { + // response success headers + for name, config := range response.Success.Headers { + conditions, err := authorinoConditionsFromRouteSelectors(route, config) + if err != nil { + return nil, err + } + if len(conditions) == 0 { + continue + } + c := authConfig.Spec.Response.Success.Headers[name] + c.Conditions = append(c.Conditions, conditions...) + authConfig.Spec.Response.Success.Headers[name] = c + } + + // response success dynamic metadata + for name, config := range response.Success.DynamicMetadata { + conditions, err := authorinoConditionsFromRouteSelectors(route, config) + if err != nil { + return nil, err + } + if len(conditions) == 0 { + continue + } + c := authConfig.Spec.Response.Success.DynamicMetadata[name] + c.Conditions = append(c.Conditions, conditions...) + authConfig.Spec.Response.Success.DynamicMetadata[name] = c + } + } + + // callbacks + for name, config := range ap.Spec.AuthScheme.Callbacks { + conditions, err := authorinoConditionsFromRouteSelectors(route, config) + if err != nil { + return nil, err + } + if len(conditions) == 0 { + continue + } + c := authConfig.Spec.Callbacks[name] + c.Conditions = append(c.Conditions, conditions...) + authConfig.Spec.Callbacks[name] = c + } + + return authConfig, nil +} + +// authorinoConditionFromRouteSelectors builds a list of Authorino conditions from a config that may specify route selectors +func authorinoConditionsFromRouteSelectors(route *gatewayapiv1beta1.HTTPRoute, config api.RouteSelectorsGetter) ([]authorinoapi.PatternExpressionOrRef, error) { + routeSelectors := config.GetRouteSelectors() + + if len(routeSelectors) == 0 { + return nil, nil + } + + // build conditions from the rules selected by the route selectors + conditions := []authorinoapi.PatternExpressionOrRef{} + for idx := range routeSelectors { + routeSelector := routeSelectors[idx] + hostnamesForConditions := routeSelector.HostnamesForConditions(route) + for _, rule := range routeSelector.SelectRules(route) { + conditions = append(conditions, authorinoConditionsFromHTTPRouteRule(rule, hostnamesForConditions)...) + } + } + if len(conditions) == 0 { + return nil, errors.New("cannot match any route rules, check for invalid route selectors in the policy") + } + return toAuthorinoOneOfPatternExpressionsOrRefs(conditions), nil +} + +// authorinoConditionsFromHTTPRoute builds a list of Authorino conditions from an HTTPRoute, without using route selectors. +func authorinoConditionsFromHTTPRoute(route *gatewayapiv1beta1.HTTPRoute) []authorinoapi.PatternExpressionOrRef { + conditions := []authorinoapi.PatternExpressionOrRef{} + hostnamesForConditions := (&api.RouteSelector{}).HostnamesForConditions(route) + for _, rule := range route.Spec.Rules { + conditions = append(conditions, authorinoConditionsFromHTTPRouteRule(rule, hostnamesForConditions)...) + } + return toAuthorinoOneOfPatternExpressionsOrRefs(conditions) +} + +// authorinoConditionsFromHTTPRouteRule builds a list of Authorino conditions from a HTTPRouteRule and a list of hostnames +// * Each combination of HTTPRouteMatch and hostname yields one condition. +// * Rules that specify no explicit HTTPRouteMatch are assumed to match all requests (i.e. implicit catch-all rule.) +// * Empty list of hostnames yields a condition without a hostname pattern expression. +func authorinoConditionsFromHTTPRouteRule(rule gatewayapiv1beta1.HTTPRouteRule, hostnames []gatewayapiv1beta1.Hostname) []authorinoapi.PatternExpressionOrRef { + hosts := []string{} + for _, hostname := range hostnames { + if hostname == "*" { + continue + } + hosts = append(hosts, string(hostname)) + } + + // no http route matches → we only need one simple authorino condition or even no condition at all + if len(rule.Matches) == 0 { + if len(hosts) == 0 { + return nil + } + return []authorinoapi.PatternExpressionOrRef{hostnameRuleToAuthorinoCondition(hosts)} + } + + var oneOf []authorinoapi.PatternExpressionOrRef + + // http route matches and possibly hostnames → we need one authorino rule per http route match + for _, match := range rule.Matches { + var allOf []authorinoapi.PatternExpressionOrRef + + // hosts + if len(hosts) > 0 { + allOf = append(allOf, hostnameRuleToAuthorinoCondition(hosts)) + } + + // method + if method := match.Method; method != nil { + allOf = append(allOf, httpMethodRuleToAuthorinoCondition(*method)) + } + + // path + if path := match.Path; path != nil { + allOf = append(allOf, httpPathRuleToAuthorinoCondition(*path)) + } + + // headers + if headers := match.Headers; len(headers) > 0 { + allOf = append(allOf, httpHeadersRuleToAuthorinoConditions(headers)...) + } + + // query params + if queryParams := match.QueryParams; len(queryParams) > 0 { + allOf = append(allOf, httpQueryParamsRuleToAuthorinoConditions(queryParams)...) + } + + if len(allOf) > 0 { + oneOf = append(oneOf, authorinoapi.PatternExpressionOrRef{ + All: common.Map(allOf, toAuthorinoUnstructuredPatternExpressionOrRef), + }) + } + } + return toAuthorinoOneOfPatternExpressionsOrRefs(oneOf) +} + +func hostnameRuleToAuthorinoCondition(hostnames []string) authorinoapi.PatternExpressionOrRef { + return authorinoapi.PatternExpressionOrRef{ + PatternExpression: authorinoapi.PatternExpression{ + Selector: "request.host", + Operator: "matches", + Value: hostnamesToRegex(hostnames), + }, + } +} + +func hostnamesToRegex(hostnames []string) string { + return strings.Join(common.Map(hostnames, func(hostname string) string { + return strings.ReplaceAll(strings.ReplaceAll(hostname, ".", `\.`), "*", ".*") + }), "|") +} + +func httpMethodRuleToAuthorinoCondition(method gatewayapiv1beta1.HTTPMethod) authorinoapi.PatternExpressionOrRef { + return authorinoapi.PatternExpressionOrRef{ + PatternExpression: authorinoapi.PatternExpression{ + Selector: "request.method", + Operator: "eq", + Value: string(method), + }, + } +} + +func httpPathRuleToAuthorinoCondition(path gatewayapiv1beta1.HTTPPathMatch) authorinoapi.PatternExpressionOrRef { + value := "/" + if path.Value != nil { + value = *path.Value + } + var operator string + + matchType := path.Type + if matchType == nil { + p := gatewayapiv1beta1.PathMatchPathPrefix + matchType = &p // gateway api defaults to PathMatchPathPrefix + } + + switch *matchType { + case gatewayapiv1beta1.PathMatchExact: + operator = "eq" + case gatewayapiv1beta1.PathMatchPathPrefix: + operator = "matches" + value += ".*" + case gatewayapiv1beta1.PathMatchRegularExpression: + operator = "matches" + } + + return authorinoapi.PatternExpressionOrRef{ + PatternExpression: authorinoapi.PatternExpression{ + Selector: `request.url_path`, + Operator: authorinoapi.PatternExpressionOperator(operator), + Value: value, + }, + } +} + +func httpHeadersRuleToAuthorinoConditions(headers []gatewayapiv1beta1.HTTPHeaderMatch) []authorinoapi.PatternExpressionOrRef { + conditions := make([]authorinoapi.PatternExpressionOrRef, 0, len(headers)) + for _, header := range headers { + condition := httpHeaderRuleToAuthorinoCondition(header) + conditions = append(conditions, condition) + } + return conditions +} + +func httpHeaderRuleToAuthorinoCondition(header gatewayapiv1beta1.HTTPHeaderMatch) authorinoapi.PatternExpressionOrRef { + operator := "eq" // gateway api defaults to HeaderMatchExact + if header.Type != nil && *header.Type == gatewayapiv1beta1.HeaderMatchRegularExpression { + operator = "matches" + } + return authorinoapi.PatternExpressionOrRef{ + PatternExpression: authorinoapi.PatternExpression{ + Selector: fmt.Sprintf("request.headers.%s", strings.ToLower(string(header.Name))), + Operator: authorinoapi.PatternExpressionOperator(operator), + Value: header.Value, + }, + } +} + +func httpQueryParamsRuleToAuthorinoConditions(queryParams []gatewayapiv1beta1.HTTPQueryParamMatch) []authorinoapi.PatternExpressionOrRef { + conditions := make([]authorinoapi.PatternExpressionOrRef, 0, len(queryParams)) + for _, queryParam := range queryParams { + condition := httpQueryParamRuleToAuthorinoCondition(queryParam) + conditions = append(conditions, condition) + } + return conditions +} + +func httpQueryParamRuleToAuthorinoCondition(queryParam gatewayapiv1beta1.HTTPQueryParamMatch) authorinoapi.PatternExpressionOrRef { + operator := "eq" // gateway api defaults to QueryParamMatchExact + if queryParam.Type != nil && *queryParam.Type == gatewayapiv1beta1.QueryParamMatchRegularExpression { + operator = "matches" + } + return authorinoapi.PatternExpressionOrRef{ + Any: []authorinoapi.UnstructuredPatternExpressionOrRef{ + { + PatternExpressionOrRef: authorinoapi.PatternExpressionOrRef{ + PatternExpression: authorinoapi.PatternExpression{ + Selector: fmt.Sprintf(`request.path.@extract:{"sep":"?%s=","pos":1}|@extract:{"sep":"&"}`, queryParam.Name), + Operator: authorinoapi.PatternExpressionOperator(operator), + Value: queryParam.Value, + }, + }, + }, + { + PatternExpressionOrRef: authorinoapi.PatternExpressionOrRef{ + PatternExpression: authorinoapi.PatternExpression{ + Selector: fmt.Sprintf(`request.path.@extract:{"sep":"&%s=","pos":1}|@extract:{"sep":"&"}`, queryParam.Name), + Operator: authorinoapi.PatternExpressionOperator(operator), + Value: queryParam.Value, + }, + }, + }, + }, + } +} + +func toAuthorinoUnstructuredPatternExpressionOrRef(patternExpressionOrRef authorinoapi.PatternExpressionOrRef) authorinoapi.UnstructuredPatternExpressionOrRef { + return authorinoapi.UnstructuredPatternExpressionOrRef{PatternExpressionOrRef: patternExpressionOrRef} +} + +func toAuthorinoOneOfPatternExpressionsOrRefs(oneOf []authorinoapi.PatternExpressionOrRef) []authorinoapi.PatternExpressionOrRef { + return []authorinoapi.PatternExpressionOrRef{ + { + Any: common.Map(oneOf, toAuthorinoUnstructuredPatternExpressionOrRef), + }, + } +} + func alwaysUpdateAuthConfig(existingObj, desiredObj client.Object) (bool, error) { existing, ok := existingObj.(*authorinoapi.AuthConfig) if !ok { diff --git a/controllers/authpolicy_controller.go b/controllers/authpolicy_controller.go index 7a420534e..1c872749d 100644 --- a/controllers/authpolicy_controller.go +++ b/controllers/authpolicy_controller.go @@ -118,6 +118,14 @@ func (r *AuthPolicyReconciler) Reconcile(eventCtx context.Context, req ctrl.Requ return statusResult, nil } + // trigger concurrent reconciliations of possibly affected gateway policies + switch route := targetNetworkObject.(type) { + case *gatewayapiv1beta1.HTTPRoute: + if err := r.reconcileRouteParentGatewayPolicies(ctx, route); err != nil { + return ctrl.Result{}, err + } + } + logger.Info("AuthPolicy reconciled successfully") return ctrl.Result{}, nil } @@ -190,6 +198,24 @@ func (r *AuthPolicyReconciler) deleteNetworkResourceDirectBackReference(ctx cont return r.DeleteTargetBackReference(ctx, targetNetworkObject, common.AuthPolicyBackRefAnnotation) } +// reconcileRouteParentGatewayPolicies triggers the concurrent reconciliation of all policies that target gateways that are parents of a route +func (r *AuthPolicyReconciler) reconcileRouteParentGatewayPolicies(ctx context.Context, route *gatewayapiv1beta1.HTTPRoute) error { + logger, err := logr.FromContext(ctx) + if err != nil { + return err + } + mapper := HTTPRouteParentRefsEventMapper{ + Logger: logger, + Client: r.Client(), + } + requests := mapper.MapToAuthPolicy(route) + for i := range requests { + request := requests[i] + go r.Reconcile(context.Background(), request) + } + return nil +} + // SetupWithManager sets up the controller with the Manager. func (r *AuthPolicyReconciler) SetupWithManager(mgr ctrl.Manager) error { httpRouteEventMapper := &HTTPRouteEventMapper{ diff --git a/controllers/authpolicy_controller_test.go b/controllers/authpolicy_controller_test.go index e26fea6aa..67087f0c2 100644 --- a/controllers/authpolicy_controller_test.go +++ b/controllers/authpolicy_controller_test.go @@ -4,7 +4,9 @@ package controllers import ( "context" + "encoding/json" "path/filepath" + "strings" "time" . "github.com/onsi/ginkgo/v2" @@ -13,7 +15,9 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + k8sruntime "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" logf "sigs.k8s.io/controller-runtime/pkg/log" gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" @@ -25,173 +29,341 @@ import ( ) const ( - CustomGatewayName = "toystore-gw" - CustomHTTPRouteName = "toystore-route" + testGatewayName = "toystore-gw" + testHTTPRouteName = "toystore-route" ) var _ = Describe("AuthPolicy controller", func() { - var ( - testNamespace string - ) + var testNamespace string - beforeEachCallback := func() { + BeforeEach(func() { CreateNamespace(&testNamespace) - gateway := testBuildBasicGateway(CustomGatewayName, testNamespace) + + gateway := testBuildBasicGateway(testGatewayName, testNamespace) err := k8sClient.Create(context.Background(), gateway) Expect(err).ToNot(HaveOccurred()) Eventually(func() bool { existingGateway := &gatewayapiv1beta1.Gateway{} err := k8sClient.Get(context.Background(), client.ObjectKeyFromObject(gateway), existingGateway) - if err != nil { - logf.Log.V(1).Info("[WARN] Creating gateway failed", "error", err) - return false - } - - if meta.IsStatusConditionFalse(existingGateway.Status.Conditions, common.GatewayProgrammedConditionType) { - logf.Log.V(1).Info("[WARN] Gateway not ready") - return false - } - - return true + return err == nil && meta.IsStatusConditionTrue(existingGateway.Status.Conditions, common.GatewayProgrammedConditionType) }, 15*time.Second, 5*time.Second).Should(BeTrue()) ApplyKuadrantCR(testNamespace) - } - - BeforeEach(beforeEachCallback) + }) AfterEach(DeleteNamespaceCallback(&testNamespace)) - Context("Attach to HTTPRoute and Gateway", func() { - It("Should create and delete everything successfully", func() { + Context("Basic HTTPRoute", func() { + BeforeEach(func() { err := ApplyResources(filepath.Join("..", "examples", "toystore", "toystore.yaml"), k8sClient, testNamespace) Expect(err).ToNot(HaveOccurred()) - httpRoute := testBuildBasicHttpRoute(CustomHTTPRouteName, CustomGatewayName, testNamespace, []string{"*.toystore.com"}) - err = k8sClient.Create(context.Background(), httpRoute) + route := testBuildBasicHttpRoute(testHTTPRouteName, testGatewayName, testNamespace, []string{"*.toystore.com"}) + err = k8sClient.Create(context.Background(), route) Expect(err).ToNot(HaveOccurred()) Eventually(func() bool { existingRoute := &gatewayapiv1beta1.HTTPRoute{} - err := k8sClient.Get(context.Background(), client.ObjectKeyFromObject(httpRoute), existingRoute) - if err != nil { - logf.Log.V(1).Info("[WARN] Creating route failed", "error", err) - return false - } + err := k8sClient.Get(context.Background(), client.ObjectKeyFromObject(route), existingRoute) + return err == nil && common.IsHTTPRouteAccepted(existingRoute) + }, 15*time.Second, 5*time.Second).Should(BeTrue()) + }) - if !common.IsHTTPRouteAccepted(existingRoute) { - logf.Log.V(1).Info("[WARN] route not accepted") - return false - } + It("Attaches policy to the Gateway", func() { + policy := &api.AuthPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gw-auth", + Namespace: testNamespace, + }, + Spec: api.AuthPolicySpec{ + TargetRef: gatewayapiv1alpha2.PolicyTargetReference{ + Group: "gateway.networking.k8s.io", + Kind: "Gateway", + Name: testGatewayName, + Namespace: ptr.To(gatewayapiv1beta1.Namespace(testNamespace)), + }, + AuthScheme: testBasicAuthScheme(), + }, + } + policy.Spec.AuthScheme.Authentication["apiKey"].ApiKey.Selector.MatchLabels["admin"] = "yes" - return true - }, 15*time.Second, 5*time.Second).Should(BeTrue()) + err := k8sClient.Create(context.Background(), policy) + logf.Log.V(1).Info("Creating AuthPolicy", "key", client.ObjectKeyFromObject(policy).String(), "error", err) + Expect(err).ToNot(HaveOccurred()) + + // check policy status + Eventually(testPolicyIsReady(policy), 30*time.Second, 5*time.Second).Should(BeTrue()) + + // check istio authorizationpolicy + iapKey := types.NamespacedName{Name: istioAuthorizationPolicyName(testGatewayName, policy.Spec.TargetRef), Namespace: testNamespace} + iap := &secv1beta1resources.AuthorizationPolicy{} + Eventually(func() bool { + err := k8sClient.Get(context.Background(), iapKey, iap) + logf.Log.V(1).Info("Fetching Istio's AuthorizationPolicy", "key", iapKey.String(), "error", err) + return err == nil + }, 2*time.Minute, 5*time.Second).Should(BeTrue()) + Expect(iap.Spec.Rules).To(HaveLen(1)) + Expect(iap.Spec.Rules[0].To).To(HaveLen(1)) + Expect(iap.Spec.Rules[0].To[0].Operation).ShouldNot(BeNil()) + Expect(iap.Spec.Rules[0].To[0].Operation.Hosts).To(Equal([]string{"*"})) + Expect(iap.Spec.Rules[0].To[0].Operation.Methods).To(Equal([]string{"GET"})) + Expect(iap.Spec.Rules[0].To[0].Operation.Paths).To(Equal([]string{"/toy*"})) - authpolicies := authPolicies(testNamespace) - - // creating authpolicies - for idx := range authpolicies { - err = k8sClient.Create(context.Background(), authpolicies[idx]) - logf.Log.V(1).Info("Creating AuthPolicy", "key", client.ObjectKeyFromObject(authpolicies[idx]).String(), "error", err) - Expect(err).ToNot(HaveOccurred()) - - // Check AuthPolicy is ready - Eventually(func() bool { - existingKAP := &api.AuthPolicy{} - err := k8sClient.Get(context.Background(), client.ObjectKeyFromObject(authpolicies[idx]), existingKAP) - if err != nil { - return false - } - if !meta.IsStatusConditionTrue(existingKAP.Status.Conditions, "Available") { - return false - } - - return true - }, 30*time.Second, 5*time.Second).Should(BeTrue()) - - // check Istio's AuthorizationPolicy existence - iapKey := types.NamespacedName{ - Name: istioAuthorizationPolicyName(CustomGatewayName, authpolicies[idx].Spec.TargetRef), + // check authorino authconfig + authConfigKey := types.NamespacedName{Name: authConfigName(client.ObjectKeyFromObject(policy)), Namespace: testNamespace} + authConfig := &authorinoapi.AuthConfig{} + Eventually(func() bool { + err := k8sClient.Get(context.Background(), authConfigKey, authConfig) + logf.Log.V(1).Info("Fetching Authorino's AuthConfig", "key", authConfigKey.String(), "error", err) + return err == nil || authConfig.Status.Ready() + }, 2*time.Minute, 5*time.Second).Should(BeTrue()) + logf.Log.V(1).Info("authConfig.Spec", "hosts", authConfig.Spec.Hosts, "conditions", authConfig.Spec.Conditions) + Expect(authConfig.Spec.Hosts).To(Equal([]string{"*"})) + Expect(authConfig.Spec.Conditions).To(HaveLen(1)) + Expect(authConfig.Spec.Conditions[0].Any).To(HaveLen(1)) // 1 HTTPRouteRule in the HTTPRoute + Expect(authConfig.Spec.Conditions[0].Any[0].Any).To(HaveLen(1)) // 1 HTTPRouteMatch in the HTTPRouteRule + Expect(authConfig.Spec.Conditions[0].Any[0].Any[0].All).To(HaveLen(2)) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[0].All[0].Selector).To(Equal("request.method")) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[0].All[0].Operator).To(Equal(authorinoapi.PatternExpressionOperator("eq"))) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[0].All[0].Value).To(Equal("GET")) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[0].All[1].Selector).To(Equal(`request.url_path`)) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[0].All[1].Operator).To(Equal(authorinoapi.PatternExpressionOperator("matches"))) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[0].All[1].Value).To(Equal("/toy.*")) + }) + + It("Attaches policy to the HTTPRoute", func() { + policy := &api.AuthPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "toystore", Namespace: testNamespace, - } - Eventually(func() bool { - iap := &secv1beta1resources.AuthorizationPolicy{} - err := k8sClient.Get(context.Background(), iapKey, iap) - logf.Log.V(1).Info("Fetching Istio's AuthorizationPolicy", "key", iapKey.String(), "error", err) - if err != nil && !apierrors.IsAlreadyExists(err) { - return false - } - - return true - }, 2*time.Minute, 5*time.Second).Should(BeTrue()) - - // check Authorino's AuthConfig existence - Eventually(func() bool { - acKey := types.NamespacedName{ - Name: authConfigName(client.ObjectKeyFromObject(authpolicies[idx])), - Namespace: testNamespace, - } - ac := &authorinoapi.AuthConfig{} - err := k8sClient.Get(context.Background(), acKey, ac) - logf.Log.V(1).Info("Fetching Authorino's AuthConfig", "key", acKey.String(), "error", err) - if err != nil && !apierrors.IsAlreadyExists(err) { - return false - } - if !ac.Status.Ready() { - logf.Log.V(1).Info("authConfig not ready", "key", acKey.String()) - return false - } - - return true - }, 2*time.Minute, 5*time.Second).Should(BeTrue()) + }, + Spec: api.AuthPolicySpec{ + TargetRef: gatewayapiv1alpha2.PolicyTargetReference{ + Group: "gateway.networking.k8s.io", + Kind: "HTTPRoute", + Name: testHTTPRouteName, + Namespace: ptr.To(gatewayapiv1beta1.Namespace(testNamespace)), + }, + AuthScheme: testBasicAuthScheme(), + }, } - // deleting authpolicies - for idx := range authpolicies { - err = k8sClient.Delete(context.Background(), authpolicies[idx]) - logf.Log.V(1).Info("Deleting AuthPolicy", "key", client.ObjectKeyFromObject(authpolicies[idx]).String(), "error", err) - Expect(err).ToNot(HaveOccurred()) + err := k8sClient.Create(context.Background(), policy) + logf.Log.V(1).Info("Creating AuthPolicy", "key", client.ObjectKeyFromObject(policy).String(), "error", err) + Expect(err).ToNot(HaveOccurred()) + + // check policy status + Eventually(testPolicyIsReady(policy), 30*time.Second, 5*time.Second).Should(BeTrue()) - // check Istio's AuthorizationPolicy existence - iapKey := types.NamespacedName{ - Name: istioAuthorizationPolicyName(CustomGatewayName, authpolicies[idx].Spec.TargetRef), + // check istio authorizationpolicy + iapKey := types.NamespacedName{Name: istioAuthorizationPolicyName(testGatewayName, policy.Spec.TargetRef), Namespace: testNamespace} + iap := &secv1beta1resources.AuthorizationPolicy{} + Eventually(func() bool { + err := k8sClient.Get(context.Background(), iapKey, iap) + logf.Log.V(1).Info("Fetching Istio's AuthorizationPolicy", "key", iapKey.String(), "error", err) + return err == nil + }, 2*time.Minute, 5*time.Second).Should(BeTrue()) + Expect(iap.Spec.Rules).To(HaveLen(1)) + Expect(iap.Spec.Rules[0].To).To(HaveLen(1)) + Expect(iap.Spec.Rules[0].To[0].Operation).ShouldNot(BeNil()) + Expect(iap.Spec.Rules[0].To[0].Operation.Hosts).To(Equal([]string{"*.toystore.com"})) + Expect(iap.Spec.Rules[0].To[0].Operation.Methods).To(Equal([]string{"GET"})) + Expect(iap.Spec.Rules[0].To[0].Operation.Paths).To(Equal([]string{"/toy*"})) + + // check authorino authconfig + authConfigKey := types.NamespacedName{Name: authConfigName(client.ObjectKeyFromObject(policy)), Namespace: testNamespace} + authConfig := &authorinoapi.AuthConfig{} + Eventually(func() bool { + err := k8sClient.Get(context.Background(), authConfigKey, authConfig) + logf.Log.V(1).Info("Fetching Authorino's AuthConfig", "key", authConfigKey.String(), "error", err) + return err == nil && authConfig.Status.Ready() + }, 2*time.Minute, 5*time.Second).Should(BeTrue()) + logf.Log.V(1).Info("authConfig.Spec", "hosts", authConfig.Spec.Hosts, "conditions", authConfig.Spec.Conditions) + Expect(authConfig.Spec.Hosts).To(Equal([]string{"*.toystore.com"})) + Expect(authConfig.Spec.Conditions[0].Any).To(HaveLen(1)) // 1 HTTPRouteRule in the HTTPRoute + Expect(authConfig.Spec.Conditions[0].Any[0].Any).To(HaveLen(1)) // 1 HTTPRouteMatch in the HTTPRouteRule + Expect(authConfig.Spec.Conditions[0].Any[0].Any[0].All).To(HaveLen(2)) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[0].All[0].Selector).To(Equal("request.method")) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[0].All[0].Operator).To(Equal(authorinoapi.PatternExpressionOperator("eq"))) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[0].All[0].Value).To(Equal("GET")) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[0].All[1].Selector).To(Equal(`request.url_path`)) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[0].All[1].Operator).To(Equal(authorinoapi.PatternExpressionOperator("matches"))) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[0].All[1].Value).To(Equal("/toy.*")) + }) + + It("Attaches policy to the Gateway while having other policies attached to some HTTPRoutes", func() { + routePolicy := &api.AuthPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "toystore", Namespace: testNamespace, - } - Eventually(func() bool { - err := k8sClient.Get(context.Background(), iapKey, &secv1beta1resources.AuthorizationPolicy{}) - logf.Log.V(1).Info("Fetching Istio's AuthorizationPolicy", "key", iapKey.String(), "error", err) - if err != nil && apierrors.IsNotFound(err) { - return true - } - return false - }, 2*time.Minute, 5*time.Second).Should(BeTrue()) + }, + Spec: api.AuthPolicySpec{ + TargetRef: gatewayapiv1alpha2.PolicyTargetReference{ + Group: "gateway.networking.k8s.io", + Kind: "HTTPRoute", + Name: testHTTPRouteName, + Namespace: ptr.To(gatewayapiv1beta1.Namespace(testNamespace)), + }, + AuthScheme: testBasicAuthScheme(), + }, + } + + err := k8sClient.Create(context.Background(), routePolicy) + logf.Log.V(1).Info("Creating AuthPolicy", "key", client.ObjectKeyFromObject(routePolicy).String(), "error", err) + Expect(err).ToNot(HaveOccurred()) + + // check policy status + Eventually(testPolicyIsReady(routePolicy), 30*time.Second, 5*time.Second).Should(BeTrue()) + + // create second (policyless) httproute + otherRoute := testBuildBasicHttpRoute("policyless-route", testGatewayName, testNamespace, []string{"*.other"}) + otherRoute.Spec.Rules = []gatewayapiv1beta1.HTTPRouteRule{ + { + Matches: []gatewayapiv1beta1.HTTPRouteMatch{ + { + Method: ptr.To(gatewayapiv1beta1.HTTPMethod("POST")), + }, + }, + }, + } + err = k8sClient.Create(context.Background(), otherRoute) + Expect(err).ToNot(HaveOccurred()) - // check Authorino's AuthConfig existence - acKey := types.NamespacedName{ - Name: authConfigName(client.ObjectKeyFromObject(authpolicies[idx])), + // attach policy to the gatewaay + gwPolicy := &api.AuthPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gw-auth", Namespace: testNamespace, - } - Eventually(func() bool { - err := k8sClient.Get(context.Background(), acKey, &authorinoapi.AuthConfig{}) - logf.Log.V(1).Info("Fetching Authorino's AuthConfig", "key", acKey.String(), "error", err) - if err != nil && apierrors.IsNotFound(err) { - return true - } - return false - }, 2*time.Minute, 5*time.Second).Should(BeTrue()) + }, + Spec: api.AuthPolicySpec{ + TargetRef: gatewayapiv1alpha2.PolicyTargetReference{ + Group: "gateway.networking.k8s.io", + Kind: "Gateway", + Name: testGatewayName, + Namespace: ptr.To(gatewayapiv1beta1.Namespace(testNamespace)), + }, + AuthScheme: testBasicAuthScheme(), + }, } + + err = k8sClient.Create(context.Background(), gwPolicy) + logf.Log.V(1).Info("Creating AuthPolicy", "key", client.ObjectKeyFromObject(gwPolicy).String(), "error", err) + Expect(err).ToNot(HaveOccurred()) + + // check policy status + Eventually(testPolicyIsReady(gwPolicy), 30*time.Second, 5*time.Second).Should(BeTrue()) + + // check istio authorizationpolicy + iapKey := types.NamespacedName{Name: istioAuthorizationPolicyName(testGatewayName, gwPolicy.Spec.TargetRef), Namespace: testNamespace} + iap := &secv1beta1resources.AuthorizationPolicy{} + Eventually(func() bool { + err := k8sClient.Get(context.Background(), iapKey, iap) + logf.Log.V(1).Info("Fetching Istio's AuthorizationPolicy", "key", iapKey.String(), "error", err) + return err == nil + }, 2*time.Minute, 5*time.Second).Should(BeTrue()) + Expect(iap.Spec.Rules).To(HaveLen(1)) + Expect(iap.Spec.Rules[0].To).To(HaveLen(1)) + Expect(iap.Spec.Rules[0].To[0].Operation).ShouldNot(BeNil()) + Expect(iap.Spec.Rules[0].To[0].Operation.Hosts).To(Equal([]string{"*"})) + Expect(iap.Spec.Rules[0].To[0].Operation.Methods).To(Equal([]string{"POST"})) + Expect(iap.Spec.Rules[0].To[0].Operation.Paths).To(Equal([]string{"/*"})) + + // check authorino authconfig + authConfigKey := types.NamespacedName{Name: authConfigName(client.ObjectKeyFromObject(gwPolicy)), Namespace: testNamespace} + authConfig := &authorinoapi.AuthConfig{} + Eventually(func() bool { + err := k8sClient.Get(context.Background(), authConfigKey, authConfig) + logf.Log.V(1).Info("Fetching Authorino's AuthConfig", "key", authConfigKey.String(), "error", err) + return err == nil || authConfig.Status.Ready() + }, 2*time.Minute, 5*time.Second).Should(BeTrue()) + logf.Log.V(1).Info("authConfig.Spec", "hosts", authConfig.Spec.Hosts, "conditions", authConfig.Spec.Conditions) + Expect(authConfig.Spec.Hosts).To(Equal([]string{"*"})) + Expect(authConfig.Spec.Conditions).To(HaveLen(1)) + Expect(authConfig.Spec.Conditions[0].Any).To(HaveLen(1)) // 1 HTTPRouteRule in the policyless HTTPRoute + Expect(authConfig.Spec.Conditions[0].Any[0].Any).To(HaveLen(1)) // 1 HTTPRouteMatch in the HTTPRouteRule + Expect(authConfig.Spec.Conditions[0].Any[0].Any[0].All).To(HaveLen(2)) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[0].All[0].Selector).To(Equal("request.method")) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[0].All[0].Operator).To(Equal(authorinoapi.PatternExpressionOperator("eq"))) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[0].All[0].Value).To(Equal("POST")) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[0].All[1].Selector).To(Equal(`request.url_path`)) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[0].All[1].Operator).To(Equal(authorinoapi.PatternExpressionOperator("matches"))) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[0].All[1].Value).To(Equal("/.*")) }) - }) + It("Attaches policy to the Gateway while having other policies attached to all HTTPRoutes", func() { + routePolicy := &api.AuthPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "toystore", + Namespace: testNamespace, + }, + Spec: api.AuthPolicySpec{ + TargetRef: gatewayapiv1alpha2.PolicyTargetReference{ + Group: "gateway.networking.k8s.io", + Kind: "HTTPRoute", + Name: testHTTPRouteName, + Namespace: ptr.To(gatewayapiv1beta1.Namespace(testNamespace)), + }, + AuthScheme: testBasicAuthScheme(), + }, + } - Context("Some rules without hosts", func() { - BeforeEach(func() { - httpRoute := testBuildBasicHttpRoute(CustomHTTPRouteName, CustomGatewayName, testNamespace, []string{"*.toystore.com"}) - err := k8sClient.Create(context.Background(), httpRoute) + err := k8sClient.Create(context.Background(), routePolicy) + logf.Log.V(1).Info("Creating AuthPolicy", "key", client.ObjectKeyFromObject(routePolicy).String(), "error", err) + Expect(err).ToNot(HaveOccurred()) + + // check policy status + Eventually(testPolicyIsReady(routePolicy), 30*time.Second, 5*time.Second).Should(BeTrue()) + + // attach policy to the gatewaay + gwPolicy := &api.AuthPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gw-auth", + Namespace: testNamespace, + }, + Spec: api.AuthPolicySpec{ + TargetRef: gatewayapiv1alpha2.PolicyTargetReference{ + Group: "gateway.networking.k8s.io", + Kind: "Gateway", + Name: testGatewayName, + Namespace: ptr.To(gatewayapiv1beta1.Namespace(testNamespace)), + }, + AuthScheme: testBasicAuthScheme(), + }, + } + + err = k8sClient.Create(context.Background(), gwPolicy) + logf.Log.V(1).Info("Creating AuthPolicy", "key", client.ObjectKeyFromObject(gwPolicy).String(), "error", err) Expect(err).ToNot(HaveOccurred()) - typedNamespace := gatewayapiv1beta1.Namespace(testNamespace) + // check policy status + Eventually(func() bool { + existingPolicy := &api.AuthPolicy{} + err := k8sClient.Get(context.Background(), client.ObjectKeyFromObject(gwPolicy), existingPolicy) + if err != nil { + return false + } + condition := meta.FindStatusCondition(existingPolicy.Status.Conditions, APAvailableConditionType) + return condition != nil && condition.Reason == "AuthSchemeNotReady" + }, 30*time.Second, 5*time.Second).Should(BeTrue()) + + // check istio authorizationpolicy + iapKey := types.NamespacedName{Name: istioAuthorizationPolicyName(testGatewayName, gwPolicy.Spec.TargetRef), Namespace: testNamespace} + Eventually(func() bool { + err := k8sClient.Get(context.Background(), iapKey, &secv1beta1resources.AuthorizationPolicy{}) + logf.Log.V(1).Info("Fetching Istio's AuthorizationPolicy", "key", iapKey.String(), "error", err) + return apierrors.IsNotFound(err) + }, 2*time.Minute, 5*time.Second).Should(BeTrue()) + + // check authorino authconfig + authConfigKey := types.NamespacedName{Name: authConfigName(client.ObjectKeyFromObject(gwPolicy)), Namespace: testNamespace} + Eventually(func() bool { + err := k8sClient.Get(context.Background(), authConfigKey, &authorinoapi.AuthConfig{}) + return apierrors.IsNotFound(err) + }, 30*time.Second, 5*time.Second).Should(BeTrue()) + }) + + It("Rejects policy with only unmatching top-level route selectors while trying to configure the gateway", func() { policy := &api.AuthPolicy{ ObjectMeta: metav1.ObjectMeta{ Name: "toystore", @@ -199,100 +371,426 @@ var _ = Describe("AuthPolicy controller", func() { }, Spec: api.AuthPolicySpec{ TargetRef: gatewayapiv1alpha2.PolicyTargetReference{ - Group: gatewayapiv1beta1.Group(gatewayapiv1beta1.GroupVersion.Group), + Group: "gateway.networking.k8s.io", Kind: "HTTPRoute", - Name: gatewayapiv1beta1.ObjectName(CustomHTTPRouteName), - Namespace: &typedNamespace, + Name: testHTTPRouteName, + Namespace: ptr.To(gatewayapiv1beta1.Namespace(testNamespace)), }, - RouteRules: []api.RouteRule{ - { - Hosts: []string{"*.admin.toystore.com"}, - Methods: []string{"DELETE", "POST"}, - Paths: []string{"/admin*"}, - }, - { - Methods: []string{"GET"}, - Paths: []string{"/private*"}, + RouteSelectors: []api.RouteSelector{ + { // does not select any HTTPRouteRule + Matches: []gatewayapiv1alpha2.HTTPRouteMatch{ + { + Method: ptr.To(gatewayapiv1alpha2.HTTPMethod("DELETE")), + }, + }, }, }, AuthScheme: testBasicAuthScheme(), }, } - err = k8sClient.Create(context.Background(), policy) + err := k8sClient.Create(context.Background(), policy) + logf.Log.V(1).Info("Creating AuthPolicy", "key", client.ObjectKeyFromObject(policy).String(), "error", err) Expect(err).ToNot(HaveOccurred()) - kapKey := client.ObjectKey{Name: "toystore", Namespace: testNamespace} - // Check KAP status is available + // check policy status Eventually(func() bool { - existingKAP := &api.AuthPolicy{} - err := k8sClient.Get(context.Background(), kapKey, existingKAP) + existingPolicy := &api.AuthPolicy{} + err := k8sClient.Get(context.Background(), client.ObjectKeyFromObject(policy), existingPolicy) if err != nil { return false } - if !meta.IsStatusConditionTrue(existingKAP.Status.Conditions, "Available") { - return false - } + condition := meta.FindStatusCondition(existingPolicy.Status.Conditions, APAvailableConditionType) + return condition != nil && condition.Reason == "ReconciliationError" && strings.Contains(condition.Message, "cannot match any route rules, check for invalid route selectors in the policy") + }, 30*time.Second, 5*time.Second).Should(BeTrue()) + + // check istio authorizationpolicy + iapKey := types.NamespacedName{Name: istioAuthorizationPolicyName(testGatewayName, policy.Spec.TargetRef), Namespace: testNamespace} + Eventually(func() bool { + err := k8sClient.Get(context.Background(), iapKey, &secv1beta1resources.AuthorizationPolicy{}) + logf.Log.V(1).Info("Fetching Istio's AuthorizationPolicy", "key", iapKey.String(), "error", err) + return apierrors.IsNotFound(err) + }, 2*time.Minute, 5*time.Second).Should(BeTrue()) - return true + // check authorino authconfig + authConfigKey := types.NamespacedName{Name: authConfigName(client.ObjectKeyFromObject(policy)), Namespace: testNamespace} + Eventually(func() bool { + err := k8sClient.Get(context.Background(), authConfigKey, &authorinoapi.AuthConfig{}) + return apierrors.IsNotFound(err) }, 30*time.Second, 5*time.Second).Should(BeTrue()) }) - It("authconfig's hosts should be route's hostnames", func() { - // Check authconfig's hosts - kapKey := client.ObjectKey{Name: "toystore", Namespace: testNamespace} - existingAuthC := &authorinoapi.AuthConfig{} - authCKey := types.NamespacedName{Name: authConfigName(kapKey), Namespace: testNamespace} + It("Rejects policy with only unmatching config-level route selectors post-configuring the gateway", func() { + policy := &api.AuthPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "toystore", + Namespace: testNamespace, + }, + Spec: api.AuthPolicySpec{ + TargetRef: gatewayapiv1alpha2.PolicyTargetReference{ + Group: "gateway.networking.k8s.io", + Kind: "HTTPRoute", + Name: testHTTPRouteName, + Namespace: ptr.To(gatewayapiv1beta1.Namespace(testNamespace)), + }, + AuthScheme: testBasicAuthScheme(), + }, + } + config := policy.Spec.AuthScheme.Authentication["apiKey"] + config.RouteSelectors = []api.RouteSelector{ + { // does not select any HTTPRouteRule + Matches: []gatewayapiv1alpha2.HTTPRouteMatch{ + { + Method: ptr.To(gatewayapiv1alpha2.HTTPMethod("DELETE")), + }, + }, + }, + } + policy.Spec.AuthScheme.Authentication["apiKey"] = config + + err := k8sClient.Create(context.Background(), policy) + logf.Log.V(1).Info("Creating AuthPolicy", "key", client.ObjectKeyFromObject(policy).String(), "error", err) + Expect(err).ToNot(HaveOccurred()) + + // check policy status Eventually(func() bool { - err := k8sClient.Get(context.Background(), authCKey, existingAuthC) + existingPolicy := &api.AuthPolicy{} + err := k8sClient.Get(context.Background(), client.ObjectKeyFromObject(policy), existingPolicy) + if err != nil { + return false + } + condition := meta.FindStatusCondition(existingPolicy.Status.Conditions, APAvailableConditionType) + return condition != nil && condition.Reason == "ReconciliationError" && strings.Contains(condition.Message, "cannot match any route rules, check for invalid route selectors in the policy") + }, 30*time.Second, 5*time.Second).Should(BeTrue()) + + iapKey := types.NamespacedName{Name: istioAuthorizationPolicyName(testGatewayName, policy.Spec.TargetRef), Namespace: testNamespace} + iap := &secv1beta1resources.AuthorizationPolicy{} + Eventually(func() bool { + err := k8sClient.Get(context.Background(), iapKey, iap) + logf.Log.V(1).Info("Fetching Istio's AuthorizationPolicy", "key", iapKey.String(), "error", err) return err == nil + }, 2*time.Minute, 5*time.Second).Should(BeTrue()) + Expect(iap.Spec.Rules).To(HaveLen(1)) + Expect(iap.Spec.Rules[0].To).To(HaveLen(1)) + Expect(iap.Spec.Rules[0].To[0].Operation).ShouldNot(BeNil()) + Expect(iap.Spec.Rules[0].To[0].Operation.Hosts).To(Equal([]string{"*.toystore.com"})) + Expect(iap.Spec.Rules[0].To[0].Operation.Methods).To(Equal([]string{"GET"})) + Expect(iap.Spec.Rules[0].To[0].Operation.Paths).To(Equal([]string{"/toy*"})) + + // check authorino authconfig + authConfigKey := types.NamespacedName{Name: authConfigName(client.ObjectKeyFromObject(policy)), Namespace: testNamespace} + Eventually(func() bool { + err := k8sClient.Get(context.Background(), authConfigKey, &authorinoapi.AuthConfig{}) + return apierrors.IsNotFound(err) }, 30*time.Second, 5*time.Second).Should(BeTrue()) - Expect(existingAuthC.Spec.Hosts).To(Equal([]string{"*.toystore.com"})) }) - It("Istio's authorizationpolicy should include network resource hostnames on kuadrant rules without hosts", func() { - typedNamespace := gatewayapiv1beta1.Namespace(testNamespace) - targetRef := gatewayapiv1alpha2.PolicyTargetReference{ - Group: gatewayapiv1beta1.Group(gatewayapiv1beta1.GroupVersion.Group), - Kind: "HTTPRoute", - Name: gatewayapiv1beta1.ObjectName(CustomHTTPRouteName), - Namespace: &typedNamespace, + It("Deletes resources when the policy is deleted", func() { + policy := &api.AuthPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "toystore", + Namespace: testNamespace, + }, + Spec: api.AuthPolicySpec{ + TargetRef: gatewayapiv1alpha2.PolicyTargetReference{ + Group: "gateway.networking.k8s.io", + Kind: "HTTPRoute", + Name: testHTTPRouteName, + Namespace: ptr.To(gatewayapiv1beta1.Namespace(testNamespace)), + }, + AuthScheme: testBasicAuthScheme(), + }, } - // Check Istio's authorization policy rules - existingIAP := &secv1beta1resources.AuthorizationPolicy{} - key := types.NamespacedName{ - Name: istioAuthorizationPolicyName(CustomGatewayName, targetRef), - Namespace: testNamespace, - } + err := k8sClient.Create(context.Background(), policy) + Expect(err).ToNot(HaveOccurred()) + + // check policy status + Eventually(testPolicyIsReady(policy), 30*time.Second, 5*time.Second).Should(BeTrue()) + + // delete policy + err = k8sClient.Delete(context.Background(), policy) + logf.Log.V(1).Info("Deleting AuthPolicy", "key", client.ObjectKeyFromObject(policy).String(), "error", err) + Expect(err).ToNot(HaveOccurred()) + // check istio authorizationpolicy + iapKey := types.NamespacedName{Name: istioAuthorizationPolicyName(testGatewayName, policy.Spec.TargetRef), Namespace: testNamespace} Eventually(func() bool { - err := k8sClient.Get(context.Background(), key, existingIAP) - return err == nil + err := k8sClient.Get(context.Background(), iapKey, &secv1beta1resources.AuthorizationPolicy{}) + logf.Log.V(1).Info("Fetching Istio's AuthorizationPolicy", "key", iapKey.String(), "error", err) + return apierrors.IsNotFound(err) + }, 2*time.Minute, 5*time.Second).Should(BeTrue()) + + // check authorino authconfig + authConfigKey := types.NamespacedName{Name: authConfigName(client.ObjectKey{Name: "toystore", Namespace: testNamespace}), Namespace: testNamespace} + Eventually(func() bool { + err := k8sClient.Get(context.Background(), authConfigKey, &authorinoapi.AuthConfig{}) + return apierrors.IsNotFound(err) }, 30*time.Second, 5*time.Second).Should(BeTrue()) + }) + + It("Maps to all fields of the AuthConfig", func() { + policy := &api.AuthPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "toystore", + Namespace: testNamespace, + }, + Spec: api.AuthPolicySpec{ + TargetRef: gatewayapiv1alpha2.PolicyTargetReference{ + Group: "gateway.networking.k8s.io", + Kind: "HTTPRoute", + Name: testHTTPRouteName, + Namespace: ptr.To(gatewayapiv1beta1.Namespace(testNamespace)), + }, + AuthScheme: api.AuthSchemeSpec{ + NamedPatterns: map[string]authorinoapi.PatternExpressions{ + "internal-source": []authorinoapi.PatternExpression{ + { + Selector: "source.ip", + Operator: authorinoapi.PatternExpressionOperator("matches"), + Value: `192\.168\..*`, + }, + }, + "authz-and-rl-required": []authorinoapi.PatternExpression{ + { + Selector: "source.ip", + Operator: authorinoapi.PatternExpressionOperator("neq"), + Value: "192.168.0.10", + }, + }, + }, + Conditions: []authorinoapi.PatternExpressionOrRef{ + { + PatternRef: authorinoapi.PatternRef{ + Name: "internal-source", + }, + }, + }, + Authentication: map[string]api.AuthenticationSpec{ + "jwt": { + AuthenticationSpec: authorinoapi.AuthenticationSpec{ + CommonEvaluatorSpec: authorinoapi.CommonEvaluatorSpec{ + Conditions: []authorinoapi.PatternExpressionOrRef{ + { + PatternExpression: authorinoapi.PatternExpression{ + Selector: `filter_metadata.envoy\.filters\.http\.jwt_authn|verified_jwt`, + Operator: "neq", + Value: "", + }, + }, + }, + }, + AuthenticationMethodSpec: authorinoapi.AuthenticationMethodSpec{ + Plain: &authorinoapi.PlainIdentitySpec{ + Selector: `filter_metadata.envoy\.filters\.http\.jwt_authn|verified_jwt`, + }, + }, + }, + }, + }, + Metadata: map[string]api.MetadataSpec{ + "user-groups": { + MetadataSpec: authorinoapi.MetadataSpec{ + CommonEvaluatorSpec: authorinoapi.CommonEvaluatorSpec{ + Conditions: []authorinoapi.PatternExpressionOrRef{ + { + PatternExpression: authorinoapi.PatternExpression{ + Selector: "auth.identity.admin", + Operator: authorinoapi.PatternExpressionOperator("neq"), + Value: "true", + }, + }, + }, + }, + MetadataMethodSpec: authorinoapi.MetadataMethodSpec{ + Http: &authorinoapi.HttpEndpointSpec{ + Url: "http://user-groups/username={auth.identity.username}", + }, + }, + }, + }, + }, + Authorization: map[string]api.AuthorizationSpec{ + "admin-or-privileged": { + AuthorizationSpec: authorinoapi.AuthorizationSpec{ + CommonEvaluatorSpec: authorinoapi.CommonEvaluatorSpec{ + Conditions: []authorinoapi.PatternExpressionOrRef{ + { + PatternRef: authorinoapi.PatternRef{ + Name: "authz-and-rl-required", + }, + }, + }, + }, + AuthorizationMethodSpec: authorinoapi.AuthorizationMethodSpec{ + PatternMatching: &authorinoapi.PatternMatchingAuthorizationSpec{ + Patterns: []authorinoapi.PatternExpressionOrRef{ + { + Any: []authorinoapi.UnstructuredPatternExpressionOrRef{ + { + PatternExpressionOrRef: authorinoapi.PatternExpressionOrRef{ + PatternExpression: authorinoapi.PatternExpression{ + Selector: "auth.identity.admin", + Operator: authorinoapi.PatternExpressionOperator("eq"), + Value: "true", + }, + }, + }, + { + PatternExpressionOrRef: authorinoapi.PatternExpressionOrRef{ + PatternExpression: authorinoapi.PatternExpression{ + Selector: "auth.metadata.user-groups", + Operator: authorinoapi.PatternExpressionOperator("incl"), + Value: "privileged", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + Response: &api.ResponseSpec{ + Unauthenticated: &authorinoapi.DenyWithSpec{ + Message: &authorinoapi.ValueOrSelector{ + Value: k8sruntime.RawExtension{Raw: []byte(`"Missing verified JWT injected by the gateway"`)}, + }, + }, + Unauthorized: &authorinoapi.DenyWithSpec{ + Message: &authorinoapi.ValueOrSelector{ + Value: k8sruntime.RawExtension{Raw: []byte(`"User must be admin or member of privileged group"`)}, + }, + }, + Success: api.WrappedSuccessResponseSpec{ + Headers: map[string]api.HeaderSuccessResponseSpec{ + "x-username": { + SuccessResponseSpec: api.SuccessResponseSpec{ + SuccessResponseSpec: authorinoapi.SuccessResponseSpec{ + CommonEvaluatorSpec: authorinoapi.CommonEvaluatorSpec{ + Conditions: []authorinoapi.PatternExpressionOrRef{ + { + PatternExpression: authorinoapi.PatternExpression{ + Selector: "request.headers.x-propagate-username.@case:lower", + Operator: authorinoapi.PatternExpressionOperator("matches"), + Value: "1|yes|true", + }, + }, + }, + }, + AuthResponseMethodSpec: authorinoapi.AuthResponseMethodSpec{ + Plain: &authorinoapi.PlainAuthResponseSpec{ + Selector: "auth.identity.username", + }, + }, + }, + }, + }, + }, + DynamicMetadata: map[string]api.SuccessResponseSpec{ + "x-auth-data": { + SuccessResponseSpec: authorinoapi.SuccessResponseSpec{ + CommonEvaluatorSpec: authorinoapi.CommonEvaluatorSpec{ + Conditions: []authorinoapi.PatternExpressionOrRef{ + { + PatternRef: authorinoapi.PatternRef{ + Name: "authz-and-rl-required", + }, + }, + }, + }, + AuthResponseMethodSpec: authorinoapi.AuthResponseMethodSpec{ + Json: &authorinoapi.JsonAuthResponseSpec{ + Properties: authorinoapi.NamedValuesOrSelectors{ + "username": { + Selector: "auth.identity.username", + }, + "groups": { + Selector: "auth.metadata.user-groups", + }, + }, + }, + }, + }, + }, + }, + }, + }, + Callbacks: map[string]api.CallbackSpec{ + "unauthorized-attempt": { + CallbackSpec: authorinoapi.CallbackSpec{ + CommonEvaluatorSpec: authorinoapi.CommonEvaluatorSpec{ + Conditions: []authorinoapi.PatternExpressionOrRef{ + { + PatternRef: authorinoapi.PatternRef{ + Name: "authz-and-rl-required", + }, + }, + { + PatternExpression: authorinoapi.PatternExpression{ + Selector: "auth.authorization.admin-or-privileged", + Operator: authorinoapi.PatternExpressionOperator("neq"), + Value: "true", + }, + }, + }, + }, + CallbackMethodSpec: authorinoapi.CallbackMethodSpec{ + Http: &authorinoapi.HttpEndpointSpec{ + Url: "http://events/unauthorized", + Method: ptr.To(authorinoapi.HttpMethod("POST")), + ContentType: authorinoapi.HttpContentType("application/json"), + Body: &authorinoapi.ValueOrSelector{ + Selector: `\{"identity":{auth.identity},"request-id":{request.id}\}`, + }, + }, + }, + }, + }, + }, + }, + }, + } + + err := k8sClient.Create(context.Background(), policy) + logf.Log.V(1).Info("Creating AuthPolicy", "key", client.ObjectKeyFromObject(policy).String(), "error", err) + Expect(err).ToNot(HaveOccurred()) - Expect(existingIAP.Spec.Rules).To(HaveLen(1)) - Expect(existingIAP.Spec.Rules[0].To).To(HaveLen(2)) - // operation 1 - Expect(existingIAP.Spec.Rules[0].To[0].Operation).ShouldNot(BeNil()) - Expect(existingIAP.Spec.Rules[0].To[0].Operation.Hosts).To(Equal([]string{"*.admin.toystore.com"})) - Expect(existingIAP.Spec.Rules[0].To[0].Operation.Methods).To(Equal([]string{"DELETE", "POST"})) - Expect(existingIAP.Spec.Rules[0].To[0].Operation.Paths).To(Equal([]string{"/admin*"})) - // operation 2 - Expect(existingIAP.Spec.Rules[0].To[1].Operation).ShouldNot(BeNil()) - Expect(existingIAP.Spec.Rules[0].To[1].Operation.Hosts).To(Equal([]string{"*.toystore.com"})) - Expect(existingIAP.Spec.Rules[0].To[1].Operation.Methods).To(Equal([]string{"GET"})) - Expect(existingIAP.Spec.Rules[0].To[1].Operation.Paths).To(Equal([]string{"/private*"})) + // check policy status + Eventually(testPolicyIsReady(policy), 30*time.Second, 5*time.Second).Should(BeTrue()) + + // check authorino authconfig + authConfigKey := types.NamespacedName{Name: authConfigName(client.ObjectKeyFromObject(policy)), Namespace: testNamespace} + authConfig := &authorinoapi.AuthConfig{} + Eventually(func() bool { + err := k8sClient.Get(context.Background(), authConfigKey, authConfig) + logf.Log.V(1).Info("Fetching Authorino's AuthConfig", "key", authConfigKey.String(), "error", err) + return err == nil && authConfig.Status.Ready() + }, 2*time.Minute, 5*time.Second).Should(BeTrue()) + authConfigSpecAsJSON, _ := json.Marshal(authConfig.Spec) + Expect(string(authConfigSpecAsJSON)).To(Equal(`{"hosts":["*.toystore.com"],"patterns":{"authz-and-rl-required":[{"selector":"source.ip","operator":"neq","value":"192.168.0.10"}],"internal-source":[{"selector":"source.ip","operator":"matches","value":"192\\.168\\..*"}]},"when":[{"patternRef":"internal-source"},{"any":[{"any":[{"all":[{"selector":"request.method","operator":"eq","value":"GET"},{"selector":"request.url_path","operator":"matches","value":"/toy.*"}]}]}]}],"authentication":{"jwt":{"when":[{"selector":"filter_metadata.envoy\\.filters\\.http\\.jwt_authn|verified_jwt","operator":"neq"}],"credentials":{"authorizationHeader":{}},"plain":{"selector":"filter_metadata.envoy\\.filters\\.http\\.jwt_authn|verified_jwt"}}},"metadata":{"user-groups":{"when":[{"selector":"auth.identity.admin","operator":"neq","value":"true"}],"http":{"url":"http://user-groups/username={auth.identity.username}","method":"GET","contentType":"application/x-www-form-urlencoded","credentials":{"authorizationHeader":{}}}}},"authorization":{"admin-or-privileged":{"when":[{"patternRef":"authz-and-rl-required"}],"patternMatching":{"patterns":[{"any":[{"selector":"auth.identity.admin","operator":"eq","value":"true"},{"selector":"auth.metadata.user-groups","operator":"incl","value":"privileged"}]}]}}},"response":{"unauthenticated":{"message":{"value":"Missing verified JWT injected by the gateway"}},"unauthorized":{"message":{"value":"User must be admin or member of privileged group"}},"success":{"headers":{"x-username":{"when":[{"selector":"request.headers.x-propagate-username.@case:lower","operator":"matches","value":"1|yes|true"}],"plain":{"value":null,"selector":"auth.identity.username"}}},"dynamicMetadata":{"x-auth-data":{"when":[{"patternRef":"authz-and-rl-required"}],"json":{"properties":{"groups":{"value":null,"selector":"auth.metadata.user-groups"},"username":{"value":null,"selector":"auth.identity.username"}}}}}}},"callbacks":{"unauthorized-attempt":{"when":[{"patternRef":"authz-and-rl-required"},{"selector":"auth.authorization.admin-or-privileged","operator":"neq","value":"true"}],"http":{"url":"http://events/unauthorized","method":"POST","body":{"value":null,"selector":"\\{\"identity\":{auth.identity},\"request-id\":{request.id}\\}"},"contentType":"application/json","credentials":{"authorizationHeader":{}}}}}}`)) }) }) - Context("All rules with subdomains", func() { + Context("Complex HTTPRoute with multiple rules and hostnames", func() { BeforeEach(func() { - httpRoute := testBuildBasicHttpRoute(CustomHTTPRouteName, CustomGatewayName, testNamespace, []string{"*.toystore.com"}) - err := k8sClient.Create(context.Background(), httpRoute) + err := ApplyResources(filepath.Join("..", "examples", "toystore", "toystore.yaml"), k8sClient, testNamespace) + Expect(err).ToNot(HaveOccurred()) + + route := testBuildMultipleRulesHttpRoute(testHTTPRouteName, testGatewayName, testNamespace, []string{"*.toystore.com", "*.admin.toystore.com"}) + err = k8sClient.Create(context.Background(), route) Expect(err).ToNot(HaveOccurred()) - typedNamespace := gatewayapiv1beta1.Namespace(testNamespace) + Eventually(func() bool { + existingRoute := &gatewayapiv1beta1.HTTPRoute{} + err := k8sClient.Get(context.Background(), client.ObjectKeyFromObject(route), existingRoute) + return err == nil && common.IsHTTPRouteAccepted(existingRoute) + }, 15*time.Second, 5*time.Second).Should(BeTrue()) + }) + + It("Attaches simple policy to the HTTPRoute", func() { policy := &api.AuthPolicy{ ObjectMeta: metav1.ObjectMeta{ Name: "toystore", @@ -300,72 +798,328 @@ var _ = Describe("AuthPolicy controller", func() { }, Spec: api.AuthPolicySpec{ TargetRef: gatewayapiv1alpha2.PolicyTargetReference{ - Group: gatewayapiv1beta1.Group(gatewayapiv1beta1.GroupVersion.Group), + Group: "gateway.networking.k8s.io", Kind: "HTTPRoute", - Name: gatewayapiv1beta1.ObjectName(CustomHTTPRouteName), - Namespace: &typedNamespace, + Name: testHTTPRouteName, + Namespace: ptr.To(gatewayapiv1beta1.Namespace(testNamespace)), }, - RouteRules: []api.RouteRule{ - { - Hosts: []string{"*.a.toystore.com"}, - Methods: []string{"DELETE", "POST"}, - Paths: []string{"/admin*"}, - }, - { - Hosts: []string{"*.b.toystore.com"}, - Methods: []string{"POST"}, - Paths: []string{"/other*"}, + AuthScheme: testBasicAuthScheme(), + }, + } + + err := k8sClient.Create(context.Background(), policy) + Expect(err).ToNot(HaveOccurred()) + + // check policy status + Eventually(testPolicyIsReady(policy), 30*time.Second, 5*time.Second).Should(BeTrue()) + + // check istio authorizationpolicy + iapKey := types.NamespacedName{Name: istioAuthorizationPolicyName(testGatewayName, policy.Spec.TargetRef), Namespace: testNamespace} + iap := &secv1beta1resources.AuthorizationPolicy{} + Eventually(func() bool { + err := k8sClient.Get(context.Background(), iapKey, iap) + logf.Log.V(1).Info("Fetching Istio's AuthorizationPolicy", "key", iapKey.String(), "error", err) + return err == nil + }, 2*time.Minute, 5*time.Second).Should(BeTrue()) + Expect(iap.Spec.Rules).To(HaveLen(3)) + Expect(iap.Spec.Rules[0].To).To(HaveLen(1)) + Expect(iap.Spec.Rules[0].To[0].Operation).ShouldNot(BeNil()) + Expect(iap.Spec.Rules[0].To[0].Operation.Hosts).To(Equal([]string{"*.toystore.com", "*.admin.toystore.com"})) + Expect(iap.Spec.Rules[0].To[0].Operation.Methods).To(Equal([]string{"POST"})) + Expect(iap.Spec.Rules[0].To[0].Operation.Paths).To(Equal([]string{"/admin*"})) + Expect(iap.Spec.Rules[1].To).To(HaveLen(1)) + Expect(iap.Spec.Rules[1].To[0].Operation).ShouldNot(BeNil()) + Expect(iap.Spec.Rules[1].To[0].Operation.Hosts).To(Equal([]string{"*.toystore.com", "*.admin.toystore.com"})) + Expect(iap.Spec.Rules[1].To[0].Operation.Methods).To(Equal([]string{"DELETE"})) + Expect(iap.Spec.Rules[1].To[0].Operation.Paths).To(Equal([]string{"/admin*"})) + Expect(iap.Spec.Rules[2].To).To(HaveLen(1)) + Expect(iap.Spec.Rules[2].To[0].Operation).ShouldNot(BeNil()) + Expect(iap.Spec.Rules[2].To[0].Operation.Hosts).To(Equal([]string{"*.toystore.com", "*.admin.toystore.com"})) + Expect(iap.Spec.Rules[2].To[0].Operation.Methods).To(Equal([]string{"GET"})) + Expect(iap.Spec.Rules[2].To[0].Operation.Paths).To(Equal([]string{"/private*"})) + + // check authorino authconfig + authConfigKey := types.NamespacedName{Name: authConfigName(client.ObjectKeyFromObject(policy)), Namespace: testNamespace} + authConfig := &authorinoapi.AuthConfig{} + Eventually(func() bool { + err := k8sClient.Get(context.Background(), authConfigKey, authConfig) + logf.Log.V(1).Info("Fetching Authorino's AuthConfig", "key", authConfigKey.String(), "error", err) + return err == nil || authConfig.Status.Ready() + }, 2*time.Minute, 5*time.Second).Should(BeTrue()) + logf.Log.V(1).Info("authConfig.Spec", "hosts", authConfig.Spec.Hosts, "conditions", authConfig.Spec.Conditions) + Expect(authConfig.Spec.Hosts).To(Equal([]string{"*.toystore.com", "*.admin.toystore.com"})) + Expect(authConfig.Spec.Conditions).To(HaveLen(1)) + Expect(authConfig.Spec.Conditions[0].Any).To(HaveLen(2)) // 2 HTTPRouteRules in the HTTPRoute + Expect(authConfig.Spec.Conditions[0].Any[0].Any).To(HaveLen(2)) // 2 HTTPRouteMatches in the 1st HTTPRouteRule + Expect(authConfig.Spec.Conditions[0].Any[0].Any[0].All).To(HaveLen(2)) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[0].All[0].Selector).To(Equal("request.method")) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[0].All[0].Operator).To(Equal(authorinoapi.PatternExpressionOperator("eq"))) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[0].All[0].Value).To(Equal("POST")) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[0].All[1].Selector).To(Equal(`request.url_path`)) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[0].All[1].Operator).To(Equal(authorinoapi.PatternExpressionOperator("matches"))) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[0].All[1].Value).To(Equal("/admin.*")) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[1].All).To(HaveLen(2)) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[1].All[0].Selector).To(Equal("request.method")) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[1].All[0].Operator).To(Equal(authorinoapi.PatternExpressionOperator("eq"))) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[1].All[0].Value).To(Equal("DELETE")) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[1].All[1].Selector).To(Equal(`request.url_path`)) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[1].All[1].Operator).To(Equal(authorinoapi.PatternExpressionOperator("matches"))) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[1].All[1].Value).To(Equal("/admin.*")) + Expect(authConfig.Spec.Conditions[0].Any[1].Any).To(HaveLen(1)) // 1 HTTPRouteMatch in the 2nd HTTPRouteRule + Expect(authConfig.Spec.Conditions[0].Any[1].Any[0].All).To(HaveLen(2)) + Expect(authConfig.Spec.Conditions[0].Any[1].Any[0].All[0].Selector).To(Equal("request.method")) + Expect(authConfig.Spec.Conditions[0].Any[1].Any[0].All[0].Operator).To(Equal(authorinoapi.PatternExpressionOperator("eq"))) + Expect(authConfig.Spec.Conditions[0].Any[1].Any[0].All[0].Value).To(Equal("GET")) + Expect(authConfig.Spec.Conditions[0].Any[1].Any[0].All[1].Selector).To(Equal(`request.url_path`)) + Expect(authConfig.Spec.Conditions[0].Any[1].Any[0].All[1].Operator).To(Equal(authorinoapi.PatternExpressionOperator("matches"))) + Expect(authConfig.Spec.Conditions[0].Any[1].Any[0].All[1].Value).To(Equal("/private.*")) + }) + + It("Attaches policy with top-level route selectors to the HTTPRoute", func() { + policy := &api.AuthPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "toystore", + Namespace: testNamespace, + }, + Spec: api.AuthPolicySpec{ + TargetRef: gatewayapiv1alpha2.PolicyTargetReference{ + Group: "gateway.networking.k8s.io", + Kind: "HTTPRoute", + Name: testHTTPRouteName, + Namespace: ptr.To(gatewayapiv1beta1.Namespace(testNamespace)), + }, + RouteSelectors: []api.RouteSelector{ + { // Selects: POST|DELETE *.admin.toystore.com/admin* + Matches: []gatewayapiv1alpha2.HTTPRouteMatch{ + { + Path: &gatewayapiv1alpha2.HTTPPathMatch{ + Type: ptr.To(gatewayapiv1alpha2.PathMatchType("PathPrefix")), + Value: ptr.To("/admin"), + }, + }, + }, + Hostnames: []gatewayapiv1beta1.Hostname{"*.admin.toystore.com"}, }, - { - Hosts: []string{"*.a.toystore.com", "*.b.toystore.com"}, - Methods: []string{"GET"}, - Paths: []string{"/private*"}, + { // Selects: GET /private* + Matches: []gatewayapiv1alpha2.HTTPRouteMatch{ + { + Path: &gatewayapiv1alpha2.HTTPPathMatch{ + Type: ptr.To(gatewayapiv1alpha2.PathMatchType("PathPrefix")), + Value: ptr.To("/private"), + }, + }, + }, }, }, AuthScheme: testBasicAuthScheme(), }, } - err = k8sClient.Create(context.Background(), policy) + err := k8sClient.Create(context.Background(), policy) Expect(err).ToNot(HaveOccurred()) - kapKey := client.ObjectKey{Name: "toystore", Namespace: testNamespace} - // Check KAP status is available + // check policy status + Eventually(testPolicyIsReady(policy), 30*time.Second, 5*time.Second).Should(BeTrue()) + + // check istio authorizationpolicy + iapKey := types.NamespacedName{Name: istioAuthorizationPolicyName(testGatewayName, policy.Spec.TargetRef), Namespace: testNamespace} + iap := &secv1beta1resources.AuthorizationPolicy{} Eventually(func() bool { - existingKAP := &api.AuthPolicy{} - err := k8sClient.Get(context.Background(), kapKey, existingKAP) - if err != nil { - return false - } - if !meta.IsStatusConditionTrue(existingKAP.Status.Conditions, "Available") { - return false - } + err := k8sClient.Get(context.Background(), iapKey, iap) + logf.Log.V(1).Info("Fetching Istio's AuthorizationPolicy", "key", iapKey.String(), "error", err) + return err == nil + }, 2*time.Minute, 5*time.Second).Should(BeTrue()) + Expect(iap.Spec.Rules).To(HaveLen(3)) + // POST *.admin.toystore.com/admin* + Expect(iap.Spec.Rules[0].To).To(HaveLen(1)) + Expect(iap.Spec.Rules[0].To[0].Operation).ShouldNot(BeNil()) + Expect(iap.Spec.Rules[0].To[0].Operation.Hosts).To(Equal([]string{"*.admin.toystore.com"})) + Expect(iap.Spec.Rules[0].To[0].Operation.Methods).To(Equal([]string{"POST"})) + Expect(iap.Spec.Rules[0].To[0].Operation.Paths).To(Equal([]string{"/admin*"})) + // DELETE *.admin.toystore.com/admin* + Expect(iap.Spec.Rules[1].To).To(HaveLen(1)) + Expect(iap.Spec.Rules[1].To[0].Operation).ShouldNot(BeNil()) + Expect(iap.Spec.Rules[1].To[0].Operation.Hosts).To(Equal([]string{"*.admin.toystore.com"})) + Expect(iap.Spec.Rules[1].To[0].Operation.Methods).To(Equal([]string{"DELETE"})) + Expect(iap.Spec.Rules[1].To[0].Operation.Paths).To(Equal([]string{"/admin*"})) + // GET (*.toystore.com|*.admin.toystore.com)/private* + Expect(iap.Spec.Rules[2].To).To(HaveLen(1)) + Expect(iap.Spec.Rules[2].To[0].Operation).ShouldNot(BeNil()) + Expect(iap.Spec.Rules[2].To[0].Operation.Hosts).To(Equal([]string{"*.toystore.com", "*.admin.toystore.com"})) + Expect(iap.Spec.Rules[2].To[0].Operation.Methods).To(Equal([]string{"GET"})) + Expect(iap.Spec.Rules[2].To[0].Operation.Paths).To(Equal([]string{"/private*"})) - return true - }, 30*time.Second, 5*time.Second).Should(BeTrue()) + // check authorino authconfig + authConfigKey := types.NamespacedName{Name: authConfigName(client.ObjectKeyFromObject(policy)), Namespace: testNamespace} + authConfig := &authorinoapi.AuthConfig{} + Eventually(func() bool { + err := k8sClient.Get(context.Background(), authConfigKey, authConfig) + logf.Log.V(1).Info("Fetching Authorino's AuthConfig", "key", authConfigKey.String(), "error", err) + return err == nil && authConfig.Status.Ready() + }, 2*time.Minute, 5*time.Second).Should(BeTrue()) + logf.Log.V(1).Info("authConfig.Spec", "hosts", authConfig.Spec.Hosts, "conditions", authConfig.Spec.Conditions) + Expect(authConfig.Spec.Hosts).To(Equal([]string{"*.toystore.com", "*.admin.toystore.com"})) + Expect(authConfig.Spec.Conditions).To(HaveLen(1)) + Expect(authConfig.Spec.Conditions[0].Any).To(HaveLen(2)) // 2 HTTPRouteRules in the HTTPRoute + Expect(authConfig.Spec.Conditions[0].Any[0].Any).To(HaveLen(2)) // 2 HTTPRouteMatches in the 1st HTTPRouteRule + Expect(authConfig.Spec.Conditions[0].Any[0].Any[0].All).To(HaveLen(3)) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[0].All[0].Selector).To(Equal("request.host")) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[0].All[0].Operator).To(Equal(authorinoapi.PatternExpressionOperator("matches"))) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[0].All[0].Value).To(Equal(`.*\.admin\.toystore\.com`)) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[0].All[1].Selector).To(Equal("request.method")) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[0].All[1].Operator).To(Equal(authorinoapi.PatternExpressionOperator("eq"))) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[0].All[1].Value).To(Equal("POST")) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[0].All[2].Selector).To(Equal(`request.url_path`)) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[0].All[2].Operator).To(Equal(authorinoapi.PatternExpressionOperator("matches"))) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[0].All[2].Value).To(Equal("/admin.*")) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[1].All).To(HaveLen(3)) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[1].All[0].Selector).To(Equal("request.host")) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[1].All[0].Operator).To(Equal(authorinoapi.PatternExpressionOperator("matches"))) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[1].All[0].Value).To(Equal(`.*\.admin\.toystore\.com`)) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[1].All[1].Selector).To(Equal("request.method")) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[1].All[1].Operator).To(Equal(authorinoapi.PatternExpressionOperator("eq"))) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[1].All[1].Value).To(Equal("DELETE")) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[1].All[2].Selector).To(Equal(`request.url_path`)) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[1].All[2].Operator).To(Equal(authorinoapi.PatternExpressionOperator("matches"))) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[1].All[2].Value).To(Equal("/admin.*")) + Expect(authConfig.Spec.Conditions[0].Any[1].Any).To(HaveLen(1)) // 1 HTTPRouteMatch in the 2nd HTTPRouteRule + Expect(authConfig.Spec.Conditions[0].Any[1].Any[0].All).To(HaveLen(2)) + Expect(authConfig.Spec.Conditions[0].Any[1].Any[0].All[0].Selector).To(Equal("request.method")) + Expect(authConfig.Spec.Conditions[0].Any[1].Any[0].All[0].Operator).To(Equal(authorinoapi.PatternExpressionOperator("eq"))) + Expect(authConfig.Spec.Conditions[0].Any[1].Any[0].All[0].Value).To(Equal("GET")) + Expect(authConfig.Spec.Conditions[0].Any[1].Any[0].All[1].Selector).To(Equal(`request.url_path`)) + Expect(authConfig.Spec.Conditions[0].Any[1].Any[0].All[1].Operator).To(Equal(authorinoapi.PatternExpressionOperator("matches"))) + Expect(authConfig.Spec.Conditions[0].Any[1].Any[0].All[1].Value).To(Equal("/private.*")) }) - It("authconfig's hosts should be the list of subdomains with unique elements", func() { - // Check authconfig's hosts - kapKey := client.ObjectKey{Name: "toystore", Namespace: testNamespace} - existingAuthC := &authorinoapi.AuthConfig{} - authCKey := types.NamespacedName{Name: authConfigName(kapKey), Namespace: testNamespace} + It("Attaches policy with config-level route selectors to the HTTPRoute", func() { + policy := &api.AuthPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "toystore", + Namespace: testNamespace, + }, + Spec: api.AuthPolicySpec{ + TargetRef: gatewayapiv1alpha2.PolicyTargetReference{ + Group: "gateway.networking.k8s.io", + Kind: "HTTPRoute", + Name: testHTTPRouteName, + Namespace: ptr.To(gatewayapiv1beta1.Namespace(testNamespace)), + }, + AuthScheme: testBasicAuthScheme(), + }, + } + config := policy.Spec.AuthScheme.Authentication["apiKey"] + config.RouteSelectors = []api.RouteSelector{ + { // Selects: POST|DELETE *.admin.toystore.com/admin* + Matches: []gatewayapiv1alpha2.HTTPRouteMatch{ + { + Path: &gatewayapiv1alpha2.HTTPPathMatch{ + Type: ptr.To(gatewayapiv1alpha2.PathMatchType("PathPrefix")), + Value: ptr.To("/admin"), + }, + }, + }, + Hostnames: []gatewayapiv1beta1.Hostname{"*.admin.toystore.com"}, + }, + } + policy.Spec.AuthScheme.Authentication["apiKey"] = config + + err := k8sClient.Create(context.Background(), policy) + Expect(err).ToNot(HaveOccurred()) + + // check policy status + Eventually(testPolicyIsReady(policy), 30*time.Second, 5*time.Second).Should(BeTrue()) + + // check istio authorizationpolicy + iapKey := types.NamespacedName{Name: istioAuthorizationPolicyName(testGatewayName, policy.Spec.TargetRef), Namespace: testNamespace} + iap := &secv1beta1resources.AuthorizationPolicy{} Eventually(func() bool { - err := k8sClient.Get(context.Background(), authCKey, existingAuthC) + err := k8sClient.Get(context.Background(), iapKey, iap) + logf.Log.V(1).Info("Fetching Istio's AuthorizationPolicy", "key", iapKey.String(), "error", err) return err == nil - }, 30*time.Second, 5*time.Second).Should(BeTrue()) - Expect(existingAuthC.Spec.Hosts).To(HaveLen(2)) - Expect(existingAuthC.Spec.Hosts).To(ContainElements("*.a.toystore.com", "*.b.toystore.com")) - }) - }) + }, 2*time.Minute, 5*time.Second).Should(BeTrue()) + Expect(iap.Spec.Rules).To(HaveLen(3)) + // POST *.admin.toystore.com/admin* + Expect(iap.Spec.Rules[0].To).To(HaveLen(1)) + Expect(iap.Spec.Rules[0].To[0].Operation).ShouldNot(BeNil()) + Expect(iap.Spec.Rules[2].To[0].Operation.Hosts).To(Equal([]string{"*.toystore.com", "*.admin.toystore.com"})) + Expect(iap.Spec.Rules[0].To[0].Operation.Methods).To(Equal([]string{"POST"})) + Expect(iap.Spec.Rules[0].To[0].Operation.Paths).To(Equal([]string{"/admin*"})) + // DELETE *.admin.toystore.com/admin* + Expect(iap.Spec.Rules[1].To).To(HaveLen(1)) + Expect(iap.Spec.Rules[1].To[0].Operation).ShouldNot(BeNil()) + Expect(iap.Spec.Rules[2].To[0].Operation.Hosts).To(Equal([]string{"*.toystore.com", "*.admin.toystore.com"})) + Expect(iap.Spec.Rules[1].To[0].Operation.Methods).To(Equal([]string{"DELETE"})) + Expect(iap.Spec.Rules[1].To[0].Operation.Paths).To(Equal([]string{"/admin*"})) + // GET (*.toystore.com|*.admin.toystore.com)/private* + Expect(iap.Spec.Rules[2].To).To(HaveLen(1)) + Expect(iap.Spec.Rules[2].To[0].Operation).ShouldNot(BeNil()) + Expect(iap.Spec.Rules[2].To[0].Operation.Hosts).To(Equal([]string{"*.toystore.com", "*.admin.toystore.com"})) + Expect(iap.Spec.Rules[2].To[0].Operation.Methods).To(Equal([]string{"GET"})) + Expect(iap.Spec.Rules[2].To[0].Operation.Paths).To(Equal([]string{"/private*"})) - Context("No rules", func() { - BeforeEach(func() { - httpRoute := testBuildBasicHttpRoute(CustomHTTPRouteName, CustomGatewayName, testNamespace, []string{"*.toystore.com"}) - err := k8sClient.Create(context.Background(), httpRoute) - Expect(err).ToNot(HaveOccurred()) + // check authorino authconfig + authConfigKey := types.NamespacedName{Name: authConfigName(client.ObjectKeyFromObject(policy)), Namespace: testNamespace} + authConfig := &authorinoapi.AuthConfig{} + Eventually(func() bool { + err := k8sClient.Get(context.Background(), authConfigKey, authConfig) + logf.Log.V(1).Info("Fetching Authorino's AuthConfig", "key", authConfigKey.String(), "error", err) + return err == nil && authConfig.Status.Ready() + }, 2*time.Minute, 5*time.Second).Should(BeTrue()) + apiKeyConditions := authConfig.Spec.Authentication["apiKey"].Conditions + logf.Log.V(1).Info("authConfig.Spec", "hosts", authConfig.Spec.Hosts, "conditions", authConfig.Spec.Conditions, "apiKey conditions", apiKeyConditions) + Expect(authConfig.Spec.Hosts).To(Equal([]string{"*.toystore.com", "*.admin.toystore.com"})) + Expect(authConfig.Spec.Conditions).To(HaveLen(1)) + Expect(authConfig.Spec.Conditions[0].Any).To(HaveLen(2)) // 2 HTTPRouteRules in the HTTPRoute + Expect(authConfig.Spec.Conditions[0].Any[0].Any).To(HaveLen(2)) // 2 HTTPRouteMatches in the 1st HTTPRouteRule + Expect(authConfig.Spec.Conditions[0].Any[0].Any[0].All).To(HaveLen(2)) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[0].All[0].Selector).To(Equal("request.method")) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[0].All[0].Operator).To(Equal(authorinoapi.PatternExpressionOperator("eq"))) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[0].All[0].Value).To(Equal("POST")) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[0].All[1].Selector).To(Equal(`request.url_path`)) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[0].All[1].Operator).To(Equal(authorinoapi.PatternExpressionOperator("matches"))) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[0].All[1].Value).To(Equal("/admin.*")) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[1].All).To(HaveLen(2)) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[1].All[0].Selector).To(Equal("request.method")) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[1].All[0].Operator).To(Equal(authorinoapi.PatternExpressionOperator("eq"))) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[1].All[0].Value).To(Equal("DELETE")) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[1].All[1].Selector).To(Equal(`request.url_path`)) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[1].All[1].Operator).To(Equal(authorinoapi.PatternExpressionOperator("matches"))) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[1].All[1].Value).To(Equal("/admin.*")) + Expect(authConfig.Spec.Conditions[0].Any[1].Any).To(HaveLen(1)) // 1 HTTPRouteMatch in the 2nd HTTPRouteRule + Expect(authConfig.Spec.Conditions[0].Any[1].Any[0].All).To(HaveLen(2)) + Expect(authConfig.Spec.Conditions[0].Any[1].Any[0].All[0].Selector).To(Equal("request.method")) + Expect(authConfig.Spec.Conditions[0].Any[1].Any[0].All[0].Operator).To(Equal(authorinoapi.PatternExpressionOperator("eq"))) + Expect(authConfig.Spec.Conditions[0].Any[1].Any[0].All[0].Value).To(Equal("GET")) + Expect(authConfig.Spec.Conditions[0].Any[1].Any[0].All[1].Selector).To(Equal(`request.url_path`)) + Expect(authConfig.Spec.Conditions[0].Any[1].Any[0].All[1].Operator).To(Equal(authorinoapi.PatternExpressionOperator("matches"))) + Expect(authConfig.Spec.Conditions[0].Any[1].Any[0].All[1].Value).To(Equal("/private.*")) + Expect(apiKeyConditions).To(HaveLen(1)) + Expect(apiKeyConditions[0].Any).To(HaveLen(1)) // 1 HTTPRouteRule selected from the HTTPRoute + Expect(apiKeyConditions[0].Any[0].Any).To(HaveLen(2)) // 2 HTTPRouteMatches in the HTTPRouteRule + Expect(apiKeyConditions[0].Any[0].Any[0].All).To(HaveLen(3)) + Expect(apiKeyConditions[0].Any[0].Any[0].All[0].Selector).To(Equal("request.host")) + Expect(apiKeyConditions[0].Any[0].Any[0].All[0].Operator).To(Equal(authorinoapi.PatternExpressionOperator("matches"))) + Expect(apiKeyConditions[0].Any[0].Any[0].All[0].Value).To(Equal(`.*\.admin\.toystore\.com`)) + Expect(apiKeyConditions[0].Any[0].Any[0].All[1].Selector).To(Equal("request.method")) + Expect(apiKeyConditions[0].Any[0].Any[0].All[1].Operator).To(Equal(authorinoapi.PatternExpressionOperator("eq"))) + Expect(apiKeyConditions[0].Any[0].Any[0].All[1].Value).To(Equal("POST")) + Expect(apiKeyConditions[0].Any[0].Any[0].All[2].Selector).To(Equal(`request.url_path`)) + Expect(apiKeyConditions[0].Any[0].Any[0].All[2].Operator).To(Equal(authorinoapi.PatternExpressionOperator("matches"))) + Expect(apiKeyConditions[0].Any[0].Any[0].All[2].Value).To(Equal("/admin.*")) + Expect(apiKeyConditions[0].Any[0].Any[1].All).To(HaveLen(3)) + Expect(apiKeyConditions[0].Any[0].Any[1].All[0].Selector).To(Equal("request.host")) + Expect(apiKeyConditions[0].Any[0].Any[1].All[0].Operator).To(Equal(authorinoapi.PatternExpressionOperator("matches"))) + Expect(apiKeyConditions[0].Any[0].Any[1].All[0].Value).To(Equal(`.*\.admin\.toystore\.com`)) + Expect(apiKeyConditions[0].Any[0].Any[1].All[1].Selector).To(Equal("request.method")) + Expect(apiKeyConditions[0].Any[0].Any[1].All[1].Operator).To(Equal(authorinoapi.PatternExpressionOperator("eq"))) + Expect(apiKeyConditions[0].Any[0].Any[1].All[1].Value).To(Equal("DELETE")) + Expect(apiKeyConditions[0].Any[0].Any[1].All[2].Selector).To(Equal(`request.url_path`)) + Expect(apiKeyConditions[0].Any[0].Any[1].All[2].Operator).To(Equal(authorinoapi.PatternExpressionOperator("matches"))) + Expect(apiKeyConditions[0].Any[0].Any[1].All[2].Value).To(Equal("/admin.*")) + }) - typedNamespace := gatewayapiv1beta1.Namespace(testNamespace) + It("Mixes route selectors into other conditions", func() { policy := &api.AuthPolicy{ ObjectMeta: metav1.ObjectMeta{ Name: "toystore", @@ -373,64 +1127,118 @@ var _ = Describe("AuthPolicy controller", func() { }, Spec: api.AuthPolicySpec{ TargetRef: gatewayapiv1alpha2.PolicyTargetReference{ - Group: gatewayapiv1beta1.Group(gatewayapiv1beta1.GroupVersion.Group), + Group: "gateway.networking.k8s.io", Kind: "HTTPRoute", - Name: gatewayapiv1beta1.ObjectName(CustomHTTPRouteName), - Namespace: &typedNamespace, + Name: testHTTPRouteName, + Namespace: ptr.To(gatewayapiv1beta1.Namespace(testNamespace)), }, - RouteRules: nil, AuthScheme: testBasicAuthScheme(), }, } + config := policy.Spec.AuthScheme.Authentication["apiKey"] + config.RouteSelectors = []api.RouteSelector{ + { // Selects: GET /private* + Matches: []gatewayapiv1beta1.HTTPRouteMatch{ + { + Path: &gatewayapiv1beta1.HTTPPathMatch{ + Type: ptr.To(gatewayapiv1beta1.PathMatchType("PathPrefix")), + Value: ptr.To("/private"), + }, + Method: ptr.To(gatewayapiv1beta1.HTTPMethod("GET")), + }, + }, + }, + } + config.Conditions = []authorinoapi.PatternExpressionOrRef{ + { + PatternExpression: authorinoapi.PatternExpression{ + Selector: "context.source.address.Address.SocketAddress.address", + Operator: authorinoapi.PatternExpressionOperator("matches"), + Value: `192\.168\.0\..*`, + }, + }, + } + policy.Spec.AuthScheme.Authentication["apiKey"] = config - err = k8sClient.Create(context.Background(), policy) + err := k8sClient.Create(context.Background(), policy) Expect(err).ToNot(HaveOccurred()) - kapKey := client.ObjectKey{Name: "toystore", Namespace: testNamespace} - // Check KAP status is available - Eventually(func() bool { - existingKAP := &api.AuthPolicy{} - err := k8sClient.Get(context.Background(), kapKey, existingKAP) - if err != nil { - return false - } - if !meta.IsStatusConditionTrue(existingKAP.Status.Conditions, "Available") { - return false - } - return true - }, 30*time.Second, 5*time.Second).Should(BeTrue()) - }) + // check policy status + Eventually(testPolicyIsReady(policy), 30*time.Second, 5*time.Second).Should(BeTrue()) - It("authconfig's hosts should be route's hostnames", func() { - // Check authconfig's hosts - kapKey := client.ObjectKey{Name: "toystore", Namespace: testNamespace} - existingAuthC := &authorinoapi.AuthConfig{} - authCKey := types.NamespacedName{Name: authConfigName(kapKey), Namespace: testNamespace} + // check authorino authconfig + authConfigKey := types.NamespacedName{Name: authConfigName(client.ObjectKeyFromObject(policy)), Namespace: testNamespace} + authConfig := &authorinoapi.AuthConfig{} Eventually(func() bool { - err := k8sClient.Get(context.Background(), authCKey, existingAuthC) - return err == nil - }, 30*time.Second, 5*time.Second).Should(BeTrue()) - Expect(existingAuthC.Spec.Hosts).To(Equal([]string{"*.toystore.com"})) + err := k8sClient.Get(context.Background(), authConfigKey, authConfig) + logf.Log.V(1).Info("Fetching Authorino's AuthConfig", "key", authConfigKey.String(), "error", err) + return err == nil && authConfig.Status.Ready() + }, 2*time.Minute, 5*time.Second).Should(BeTrue()) + apiKeyConditions := authConfig.Spec.Authentication["apiKey"].Conditions + logf.Log.V(1).Info("authConfig.Spec", "hosts", authConfig.Spec.Hosts, "conditions", authConfig.Spec.Conditions, "apiKey conditions", apiKeyConditions) + Expect(authConfig.Spec.Hosts).To(Equal([]string{"*.toystore.com", "*.admin.toystore.com"})) + Expect(authConfig.Spec.Conditions).To(HaveLen(1)) + Expect(authConfig.Spec.Conditions[0].Any).To(HaveLen(2)) // 2 HTTPRouteRules in the HTTPRoute + Expect(authConfig.Spec.Conditions[0].Any[0].Any).To(HaveLen(2)) // 2 HTTPRouteMatches in the 1st HTTPRouteRule + Expect(authConfig.Spec.Conditions[0].Any[0].Any[0].All).To(HaveLen(2)) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[0].All[0].Selector).To(Equal("request.method")) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[0].All[0].Operator).To(Equal(authorinoapi.PatternExpressionOperator("eq"))) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[0].All[0].Value).To(Equal("POST")) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[0].All[1].Selector).To(Equal(`request.url_path`)) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[0].All[1].Operator).To(Equal(authorinoapi.PatternExpressionOperator("matches"))) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[0].All[1].Value).To(Equal("/admin.*")) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[1].All).To(HaveLen(2)) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[1].All[0].Selector).To(Equal("request.method")) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[1].All[0].Operator).To(Equal(authorinoapi.PatternExpressionOperator("eq"))) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[1].All[0].Value).To(Equal("DELETE")) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[1].All[1].Selector).To(Equal(`request.url_path`)) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[1].All[1].Operator).To(Equal(authorinoapi.PatternExpressionOperator("matches"))) + Expect(authConfig.Spec.Conditions[0].Any[0].Any[1].All[1].Value).To(Equal("/admin.*")) + Expect(authConfig.Spec.Conditions[0].Any[1].Any).To(HaveLen(1)) // 1 HTTPRouteMatch in the 2nd HTTPRouteRule + Expect(authConfig.Spec.Conditions[0].Any[1].Any[0].All).To(HaveLen(2)) + Expect(authConfig.Spec.Conditions[0].Any[1].Any[0].All[0].Selector).To(Equal("request.method")) + Expect(authConfig.Spec.Conditions[0].Any[1].Any[0].All[0].Operator).To(Equal(authorinoapi.PatternExpressionOperator("eq"))) + Expect(authConfig.Spec.Conditions[0].Any[1].Any[0].All[0].Value).To(Equal("GET")) + Expect(authConfig.Spec.Conditions[0].Any[1].Any[0].All[1].Selector).To(Equal(`request.url_path`)) + Expect(authConfig.Spec.Conditions[0].Any[1].Any[0].All[1].Operator).To(Equal(authorinoapi.PatternExpressionOperator("matches"))) + Expect(authConfig.Spec.Conditions[0].Any[1].Any[0].All[1].Value).To(Equal("/private.*")) + Expect(apiKeyConditions).To(HaveLen(2)) // 1 existed condition + 1 HTTPRouteRule selected from the HTTPRoute + Expect(apiKeyConditions[0].Selector).To(Equal("context.source.address.Address.SocketAddress.address")) + Expect(apiKeyConditions[0].Operator).To(Equal(authorinoapi.PatternExpressionOperator("matches"))) + Expect(apiKeyConditions[0].Value).To(Equal(`192\.168\.0\..*`)) + Expect(apiKeyConditions[1].Any).To(HaveLen(1)) // 1 HTTPRouteRule selected from the HTTPRoute + Expect(apiKeyConditions[1].Any[0].Any).To(HaveLen(1)) // 1 HTTPRouteMatch in the HTTPRouteRule + Expect(apiKeyConditions[1].Any[0].Any[0].All).To(HaveLen(2)) + Expect(apiKeyConditions[1].Any[0].Any[0].All[0].Selector).To(Equal("request.method")) + Expect(apiKeyConditions[1].Any[0].Any[0].All[0].Operator).To(Equal(authorinoapi.PatternExpressionOperator("eq"))) + Expect(apiKeyConditions[1].Any[0].Any[0].All[0].Value).To(Equal("GET")) + Expect(apiKeyConditions[1].Any[0].Any[0].All[1].Selector).To(Equal(`request.url_path`)) + Expect(apiKeyConditions[1].Any[0].Any[0].All[1].Operator).To(Equal(authorinoapi.PatternExpressionOperator("matches"))) + Expect(apiKeyConditions[1].Any[0].Any[0].All[1].Value).To(Equal("/private.*")) }) }) + + Context("TODO: Targeted resource does not exist", func() {}) }) func testBasicAuthScheme() api.AuthSchemeSpec { return api.AuthSchemeSpec{ - Authentication: map[string]authorinoapi.AuthenticationSpec{ + Authentication: map[string]api.AuthenticationSpec{ "apiKey": { - AuthenticationMethodSpec: authorinoapi.AuthenticationMethodSpec{ - ApiKey: &authorinoapi.ApiKeyAuthenticationSpec{ - Selector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - "app": "toystore", + AuthenticationSpec: authorinoapi.AuthenticationSpec{ + AuthenticationMethodSpec: authorinoapi.AuthenticationMethodSpec{ + ApiKey: &authorinoapi.ApiKeyAuthenticationSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "toystore", + }, }, }, }, - }, - Credentials: authorinoapi.Credentials{ - AuthorizationHeader: &authorinoapi.Prefixed{ - Prefix: "APIKEY", + Credentials: authorinoapi.Credentials{ + AuthorizationHeader: &authorinoapi.Prefixed{ + Prefix: "APIKEY", + }, }, }, }, @@ -438,41 +1246,10 @@ func testBasicAuthScheme() api.AuthSchemeSpec { } } -func authPolicies(namespace string) []*api.AuthPolicy { - typedNamespace := gatewayapiv1beta1.Namespace(namespace) - routePolicy := &api.AuthPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: "target-route", - Namespace: namespace, - }, - Spec: api.AuthPolicySpec{ - TargetRef: gatewayapiv1alpha2.PolicyTargetReference{ - Group: "gateway.networking.k8s.io", - Kind: "HTTPRoute", - Name: CustomHTTPRouteName, - Namespace: &typedNamespace, - }, - RouteRules: []api.RouteRule{ - { - Hosts: []string{"*.toystore.com"}, - Methods: []string{"DELETE", "POST"}, - Paths: []string{"/admin*"}, - }, - }, - AuthScheme: testBasicAuthScheme(), - }, +func testPolicyIsReady(policy *api.AuthPolicy) func() bool { + return func() bool { + existingPolicy := &api.AuthPolicy{} + err := k8sClient.Get(context.Background(), client.ObjectKeyFromObject(policy), existingPolicy) + return err == nil && meta.IsStatusConditionTrue(existingPolicy.Status.Conditions, "Available") } - gatewayPolicy := routePolicy.DeepCopy() - gatewayPolicy.SetName("target-gateway") - gatewayPolicy.SetNamespace(namespace) - gatewayPolicy.Spec.TargetRef.Kind = "Gateway" - gatewayPolicy.Spec.TargetRef.Name = CustomGatewayName - gatewayPolicy.Spec.TargetRef.Namespace = &typedNamespace - gatewayPolicy.Spec.RouteRules = []api.RouteRule{ - // Must be different from the other KAP targeting the route, otherwise authconfigs will not be ready - {Hosts: []string{"*.com"}}, - } - gatewayPolicy.Spec.AuthScheme.Authentication["apiKey"].ApiKey.Selector.MatchLabels["admin"] = "yes" - - return []*api.AuthPolicy{routePolicy, gatewayPolicy} } diff --git a/controllers/authpolicy_istio_auth_config_test.go b/controllers/authpolicy_istio_auth_config_test.go new file mode 100644 index 000000000..bef86f20b --- /dev/null +++ b/controllers/authpolicy_istio_auth_config_test.go @@ -0,0 +1,743 @@ +//go:build unit + +package controllers + +import ( + "reflect" + "testing" + + authorinoapi "github.com/kuadrant/authorino/api/v1beta2" + "k8s.io/utils/ptr" + gatewayapiv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" +) + +func TestAuthorinoConditionsFromHTTPRouteRule(t *testing.T) { + testCases := []struct { + name string + hostnames []gatewayapiv1beta1.Hostname + rule gatewayapiv1beta1.HTTPRouteRule + expected []authorinoapi.PatternExpressionOrRef + }{ + { + name: "No HTTPRouteMatch", + hostnames: []gatewayapiv1beta1.Hostname{"toystore.kuadrant.io"}, + rule: gatewayapiv1beta1.HTTPRouteRule{}, + expected: []authorinoapi.PatternExpressionOrRef{ + { + PatternExpression: authorinoapi.PatternExpression{ + Selector: "request.host", + Operator: "matches", + Value: `toystore\.kuadrant\.io`, + }, + }, + }, + }, + { + name: "Single HTTPRouteMatch", + hostnames: []gatewayapiv1beta1.Hostname{"toystore.kuadrant.io"}, + rule: gatewayapiv1beta1.HTTPRouteRule{ + Matches: []gatewayapiv1beta1.HTTPRouteMatch{ + { + Path: &gatewayapiv1beta1.HTTPPathMatch{ + Type: ptr.To(gatewayapiv1beta1.PathMatchType("PathPrefix")), + Value: ptr.To("/toy"), + }, + }, + }, + }, + expected: []authorinoapi.PatternExpressionOrRef{ + { + Any: []authorinoapi.UnstructuredPatternExpressionOrRef{ + { + PatternExpressionOrRef: authorinoapi.PatternExpressionOrRef{ + All: []authorinoapi.UnstructuredPatternExpressionOrRef{ + { + PatternExpressionOrRef: authorinoapi.PatternExpressionOrRef{ + PatternExpression: authorinoapi.PatternExpression{ + Selector: "request.host", + Operator: "matches", + Value: `toystore\.kuadrant\.io`, + }, + }, + }, + { + PatternExpressionOrRef: authorinoapi.PatternExpressionOrRef{ + PatternExpression: authorinoapi.PatternExpression{ + Selector: `request.url_path`, + Operator: "matches", + Value: `/toy.*`, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "Multiple HTTPRouteMatches", + hostnames: []gatewayapiv1beta1.Hostname{"toystore.kuadrant.io"}, + rule: gatewayapiv1beta1.HTTPRouteRule{ + Matches: []gatewayapiv1beta1.HTTPRouteMatch{ + { + Path: &gatewayapiv1beta1.HTTPPathMatch{ + Type: ptr.To(gatewayapiv1beta1.PathMatchType("PathPrefix")), + Value: ptr.To("/toy"), + }, + }, + { + Path: &gatewayapiv1beta1.HTTPPathMatch{ + Type: ptr.To(gatewayapiv1beta1.PathMatchType("Exact")), + Value: ptr.To("/foo"), + }, + }, + }, + }, + expected: []authorinoapi.PatternExpressionOrRef{ + { + Any: []authorinoapi.UnstructuredPatternExpressionOrRef{ + { + PatternExpressionOrRef: authorinoapi.PatternExpressionOrRef{ + All: []authorinoapi.UnstructuredPatternExpressionOrRef{ + { + PatternExpressionOrRef: authorinoapi.PatternExpressionOrRef{ + PatternExpression: authorinoapi.PatternExpression{ + Selector: "request.host", + Operator: "matches", + Value: `toystore\.kuadrant\.io`, + }, + }, + }, + { + PatternExpressionOrRef: authorinoapi.PatternExpressionOrRef{ + PatternExpression: authorinoapi.PatternExpression{ + Selector: `request.url_path`, + Operator: "matches", + Value: `/toy.*`, + }, + }, + }, + }, + }, + }, + { + PatternExpressionOrRef: authorinoapi.PatternExpressionOrRef{ + All: []authorinoapi.UnstructuredPatternExpressionOrRef{ + { + PatternExpressionOrRef: authorinoapi.PatternExpressionOrRef{ + PatternExpression: authorinoapi.PatternExpression{ + Selector: "request.host", + Operator: "matches", + Value: `toystore\.kuadrant\.io`, + }, + }, + }, + { + PatternExpressionOrRef: authorinoapi.PatternExpressionOrRef{ + PatternExpression: authorinoapi.PatternExpression{ + Selector: `request.url_path`, + Operator: "eq", + Value: `/foo`, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "Multiple hosts", + hostnames: []gatewayapiv1beta1.Hostname{"toystore.kuadrant.io", "gamestore.kuadrant.io"}, + rule: gatewayapiv1beta1.HTTPRouteRule{ + Matches: []gatewayapiv1beta1.HTTPRouteMatch{ + { + Path: &gatewayapiv1beta1.HTTPPathMatch{ + Type: ptr.To(gatewayapiv1beta1.PathMatchType("PathPrefix")), + Value: ptr.To("/toy"), + }, + }, + }, + }, + expected: []authorinoapi.PatternExpressionOrRef{ + { + Any: []authorinoapi.UnstructuredPatternExpressionOrRef{ + { + PatternExpressionOrRef: authorinoapi.PatternExpressionOrRef{ + All: []authorinoapi.UnstructuredPatternExpressionOrRef{ + { + PatternExpressionOrRef: authorinoapi.PatternExpressionOrRef{ + PatternExpression: authorinoapi.PatternExpression{ + Selector: "request.host", + Operator: "matches", + Value: `toystore\.kuadrant\.io|gamestore\.kuadrant\.io`, + }, + }, + }, + { + PatternExpressionOrRef: authorinoapi.PatternExpressionOrRef{ + PatternExpression: authorinoapi.PatternExpression{ + Selector: `request.url_path`, + Operator: "matches", + Value: `/toy.*`, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "Host wildcard", + hostnames: []gatewayapiv1beta1.Hostname{"*.kuadrant.io"}, + rule: gatewayapiv1beta1.HTTPRouteRule{ + Matches: []gatewayapiv1beta1.HTTPRouteMatch{ + { + Path: &gatewayapiv1beta1.HTTPPathMatch{ + Type: ptr.To(gatewayapiv1beta1.PathMatchType("PathPrefix")), + Value: ptr.To("/toy"), + }, + }, + }, + }, + expected: []authorinoapi.PatternExpressionOrRef{ + { + Any: []authorinoapi.UnstructuredPatternExpressionOrRef{ + { + PatternExpressionOrRef: authorinoapi.PatternExpressionOrRef{ + All: []authorinoapi.UnstructuredPatternExpressionOrRef{ + { + PatternExpressionOrRef: authorinoapi.PatternExpressionOrRef{ + PatternExpression: authorinoapi.PatternExpression{ + Selector: "request.host", + Operator: "matches", + Value: `.*\.kuadrant\.io`, + }, + }, + }, + { + PatternExpressionOrRef: authorinoapi.PatternExpressionOrRef{ + PatternExpression: authorinoapi.PatternExpression{ + Selector: `request.url_path`, + Operator: "matches", + Value: `/toy.*`, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "Catch-all host is ignored", + hostnames: []gatewayapiv1beta1.Hostname{"toystore.kuadrant.io", "*"}, + rule: gatewayapiv1beta1.HTTPRouteRule{ + Matches: []gatewayapiv1beta1.HTTPRouteMatch{ + { + Path: &gatewayapiv1beta1.HTTPPathMatch{ + Type: ptr.To(gatewayapiv1beta1.PathMatchType("PathPrefix")), + Value: ptr.To("/toy"), + }, + }, + }, + }, + expected: []authorinoapi.PatternExpressionOrRef{ + { + Any: []authorinoapi.UnstructuredPatternExpressionOrRef{ + { + PatternExpressionOrRef: authorinoapi.PatternExpressionOrRef{ + All: []authorinoapi.UnstructuredPatternExpressionOrRef{ + { + PatternExpressionOrRef: authorinoapi.PatternExpressionOrRef{ + PatternExpression: authorinoapi.PatternExpression{ + Selector: "request.host", + Operator: "matches", + Value: `toystore\.kuadrant\.io`, + }, + }, + }, + { + PatternExpressionOrRef: authorinoapi.PatternExpressionOrRef{ + PatternExpression: authorinoapi.PatternExpression{ + Selector: `request.url_path`, + Operator: "matches", + Value: `/toy.*`, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "Method", + rule: gatewayapiv1beta1.HTTPRouteRule{ + Matches: []gatewayapiv1beta1.HTTPRouteMatch{ + { + Method: ptr.To(gatewayapiv1beta1.HTTPMethod("GET")), + }, + }, + }, + expected: []authorinoapi.PatternExpressionOrRef{ + { + Any: []authorinoapi.UnstructuredPatternExpressionOrRef{ + { + PatternExpressionOrRef: authorinoapi.PatternExpressionOrRef{ + All: []authorinoapi.UnstructuredPatternExpressionOrRef{ + { + PatternExpressionOrRef: authorinoapi.PatternExpressionOrRef{ + PatternExpression: authorinoapi.PatternExpression{ + Selector: `request.method`, + Operator: "eq", + Value: `GET`, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "PathMatchExact", + rule: gatewayapiv1beta1.HTTPRouteRule{ + Matches: []gatewayapiv1beta1.HTTPRouteMatch{ + { + Path: &gatewayapiv1beta1.HTTPPathMatch{ + Type: ptr.To(gatewayapiv1beta1.PathMatchType("Exact")), + Value: ptr.To("/toy"), + }, + }, + }, + }, + expected: []authorinoapi.PatternExpressionOrRef{ + { + Any: []authorinoapi.UnstructuredPatternExpressionOrRef{ + { + PatternExpressionOrRef: authorinoapi.PatternExpressionOrRef{ + All: []authorinoapi.UnstructuredPatternExpressionOrRef{ + { + PatternExpressionOrRef: authorinoapi.PatternExpressionOrRef{ + PatternExpression: authorinoapi.PatternExpression{ + Selector: `request.url_path`, + Operator: "eq", + Value: `/toy`, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "PathMatchPrefix", + rule: gatewayapiv1beta1.HTTPRouteRule{ + Matches: []gatewayapiv1beta1.HTTPRouteMatch{ + { + Path: &gatewayapiv1beta1.HTTPPathMatch{ + Type: ptr.To(gatewayapiv1beta1.PathMatchType("PathPrefix")), + Value: ptr.To("/toy"), + }, + }, + }, + }, + expected: []authorinoapi.PatternExpressionOrRef{ + { + Any: []authorinoapi.UnstructuredPatternExpressionOrRef{ + { + PatternExpressionOrRef: authorinoapi.PatternExpressionOrRef{ + All: []authorinoapi.UnstructuredPatternExpressionOrRef{ + { + PatternExpressionOrRef: authorinoapi.PatternExpressionOrRef{ + PatternExpression: authorinoapi.PatternExpression{ + Selector: `request.url_path`, + Operator: "matches", + Value: `/toy.*`, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "PathMatchRegularExpression", + rule: gatewayapiv1beta1.HTTPRouteRule{ + Matches: []gatewayapiv1beta1.HTTPRouteMatch{ + { + Path: &gatewayapiv1beta1.HTTPPathMatch{ + Type: ptr.To(gatewayapiv1beta1.PathMatchType("RegularExpression")), + Value: ptr.To("^/(dolls|cars)"), + }, + }, + }, + }, + expected: []authorinoapi.PatternExpressionOrRef{ + { + Any: []authorinoapi.UnstructuredPatternExpressionOrRef{ + { + PatternExpressionOrRef: authorinoapi.PatternExpressionOrRef{ + All: []authorinoapi.UnstructuredPatternExpressionOrRef{ + { + PatternExpressionOrRef: authorinoapi.PatternExpressionOrRef{ + PatternExpression: authorinoapi.PatternExpression{ + Selector: `request.url_path`, + Operator: "matches", + Value: "^/(dolls|cars)", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "Single header match", + rule: gatewayapiv1beta1.HTTPRouteRule{ + Matches: []gatewayapiv1beta1.HTTPRouteMatch{ + { + Headers: []gatewayapiv1beta1.HTTPHeaderMatch{ + { + Type: ptr.To(gatewayapiv1beta1.HeaderMatchType("Exact")), + Name: "X-Foo", + Value: "a-value", + }, + }, + }, + }, + }, + expected: []authorinoapi.PatternExpressionOrRef{ + { + Any: []authorinoapi.UnstructuredPatternExpressionOrRef{ + { + PatternExpressionOrRef: authorinoapi.PatternExpressionOrRef{ + All: []authorinoapi.UnstructuredPatternExpressionOrRef{ + { + PatternExpressionOrRef: authorinoapi.PatternExpressionOrRef{ + PatternExpression: authorinoapi.PatternExpression{ + Selector: `request.headers.x-foo`, + Operator: "eq", + Value: "a-value", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "Multiple header matches", + rule: gatewayapiv1beta1.HTTPRouteRule{ + Matches: []gatewayapiv1beta1.HTTPRouteMatch{ + { + Headers: []gatewayapiv1beta1.HTTPHeaderMatch{ + { + Type: ptr.To(gatewayapiv1beta1.HeaderMatchType("Exact")), + Name: "x-foo", + Value: "a-value", + }, + { + Type: ptr.To(gatewayapiv1beta1.HeaderMatchType("Exact")), + Name: "x-bar", + Value: "other-value", + }, + }, + }, + }, + }, + expected: []authorinoapi.PatternExpressionOrRef{ + { + Any: []authorinoapi.UnstructuredPatternExpressionOrRef{ + { + PatternExpressionOrRef: authorinoapi.PatternExpressionOrRef{ + All: []authorinoapi.UnstructuredPatternExpressionOrRef{ + { + PatternExpressionOrRef: authorinoapi.PatternExpressionOrRef{ + PatternExpression: authorinoapi.PatternExpression{ + Selector: `request.headers.x-foo`, + Operator: "eq", + Value: "a-value", + }, + }, + }, + { + PatternExpressionOrRef: authorinoapi.PatternExpressionOrRef{ + PatternExpression: authorinoapi.PatternExpression{ + Selector: `request.headers.x-bar`, + Operator: "eq", + Value: "other-value", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "HeaderMatchRegularExpression", + rule: gatewayapiv1beta1.HTTPRouteRule{ + Matches: []gatewayapiv1beta1.HTTPRouteMatch{ + { + Headers: []gatewayapiv1beta1.HTTPHeaderMatch{ + { + Type: ptr.To(gatewayapiv1beta1.HeaderMatchType("RegularExpression")), + Name: "x-foo", + Value: "^a+.*$", + }, + }, + }, + }, + }, + expected: []authorinoapi.PatternExpressionOrRef{ + { + Any: []authorinoapi.UnstructuredPatternExpressionOrRef{ + { + PatternExpressionOrRef: authorinoapi.PatternExpressionOrRef{ + All: []authorinoapi.UnstructuredPatternExpressionOrRef{ + { + PatternExpressionOrRef: authorinoapi.PatternExpressionOrRef{ + PatternExpression: authorinoapi.PatternExpression{ + Selector: `request.headers.x-foo`, + Operator: "matches", + Value: "^a+.*$", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "Single query param match", + rule: gatewayapiv1beta1.HTTPRouteRule{ + Matches: []gatewayapiv1beta1.HTTPRouteMatch{ + { + QueryParams: []gatewayapiv1beta1.HTTPQueryParamMatch{ + { + Type: ptr.To(gatewayapiv1beta1.QueryParamMatchType("Exact")), + Name: "x-foo", + Value: "a-value", + }, + }, + }, + }, + }, + expected: []authorinoapi.PatternExpressionOrRef{ + { + Any: []authorinoapi.UnstructuredPatternExpressionOrRef{ + { + PatternExpressionOrRef: authorinoapi.PatternExpressionOrRef{ + All: []authorinoapi.UnstructuredPatternExpressionOrRef{ + { + PatternExpressionOrRef: authorinoapi.PatternExpressionOrRef{ + Any: []authorinoapi.UnstructuredPatternExpressionOrRef{ + { + PatternExpressionOrRef: authorinoapi.PatternExpressionOrRef{ + PatternExpression: authorinoapi.PatternExpression{ + Selector: `request.path.@extract:{"sep":"?x-foo=","pos":1}.@extract:{"sep":"&"}`, + Operator: "eq", + Value: "a-value", + }, + }, + }, + { + PatternExpressionOrRef: authorinoapi.PatternExpressionOrRef{ + PatternExpression: authorinoapi.PatternExpression{ + Selector: `request.path.@extract:{"sep":"&x-foo=","pos":1}.@extract:{"sep":"&"}`, + Operator: "eq", + Value: "a-value", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "Multiple query param matches", + rule: gatewayapiv1beta1.HTTPRouteRule{ + Matches: []gatewayapiv1beta1.HTTPRouteMatch{ + { + QueryParams: []gatewayapiv1beta1.HTTPQueryParamMatch{ + { + Type: ptr.To(gatewayapiv1beta1.QueryParamMatchType("Exact")), + Name: "x-foo", + Value: "a-value", + }, + { + Type: ptr.To(gatewayapiv1beta1.QueryParamMatchType("Exact")), + Name: "x-bar", + Value: "other-value", + }, + }, + }, + }, + }, + expected: []authorinoapi.PatternExpressionOrRef{ + { + Any: []authorinoapi.UnstructuredPatternExpressionOrRef{ + { + PatternExpressionOrRef: authorinoapi.PatternExpressionOrRef{ + All: []authorinoapi.UnstructuredPatternExpressionOrRef{ + { + PatternExpressionOrRef: authorinoapi.PatternExpressionOrRef{ + Any: []authorinoapi.UnstructuredPatternExpressionOrRef{ + { + PatternExpressionOrRef: authorinoapi.PatternExpressionOrRef{ + PatternExpression: authorinoapi.PatternExpression{ + Selector: `request.path.@extract:{"sep":"?x-foo=","pos":1}.@extract:{"sep":"&"}`, + Operator: "eq", + Value: "a-value", + }, + }, + }, + { + PatternExpressionOrRef: authorinoapi.PatternExpressionOrRef{ + PatternExpression: authorinoapi.PatternExpression{ + Selector: `request.path.@extract:{"sep":"&x-foo=","pos":1}.@extract:{"sep":"&"}`, + Operator: "eq", + Value: "a-value", + }, + }, + }, + }, + }, + }, + { + PatternExpressionOrRef: authorinoapi.PatternExpressionOrRef{ + Any: []authorinoapi.UnstructuredPatternExpressionOrRef{ + { + PatternExpressionOrRef: authorinoapi.PatternExpressionOrRef{ + PatternExpression: authorinoapi.PatternExpression{ + Selector: `request.path.@extract:{"sep":"?x-bar=","pos":1}.@extract:{"sep":"&"}`, + Operator: "eq", + Value: "other-value", + }, + }, + }, + { + PatternExpressionOrRef: authorinoapi.PatternExpressionOrRef{ + PatternExpression: authorinoapi.PatternExpression{ + Selector: `request.path.@extract:{"sep":"&x-bar=","pos":1}.@extract:{"sep":"&"}`, + Operator: "eq", + Value: "other-value", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "QueryParamMatchRegularExpression", + rule: gatewayapiv1beta1.HTTPRouteRule{ + Matches: []gatewayapiv1beta1.HTTPRouteMatch{ + { + QueryParams: []gatewayapiv1beta1.HTTPQueryParamMatch{ + { + Type: ptr.To(gatewayapiv1beta1.QueryParamMatchType("RegularExpression")), + Name: "x-foo", + Value: "^a+.*$", + }, + }, + }, + }, + }, + expected: []authorinoapi.PatternExpressionOrRef{ + { + Any: []authorinoapi.UnstructuredPatternExpressionOrRef{ + { + PatternExpressionOrRef: authorinoapi.PatternExpressionOrRef{ + All: []authorinoapi.UnstructuredPatternExpressionOrRef{ + { + PatternExpressionOrRef: authorinoapi.PatternExpressionOrRef{ + Any: []authorinoapi.UnstructuredPatternExpressionOrRef{ + { + PatternExpressionOrRef: authorinoapi.PatternExpressionOrRef{ + PatternExpression: authorinoapi.PatternExpression{ + Selector: `request.path.@extract:{"sep":"?x-foo=","pos":1}.@extract:{"sep":"&"}`, + Operator: "matches", + Value: "^a+.*$", + }, + }, + }, + { + PatternExpressionOrRef: authorinoapi.PatternExpressionOrRef{ + PatternExpression: authorinoapi.PatternExpression{ + Selector: `request.path.@extract:{"sep":"&x-foo=","pos":1}.@extract:{"sep":"&"}`, + Operator: "matches", + Value: "^a+.*$", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := authorinoConditionsFromHTTPRouteRule(tc.rule, tc.hostnames) + if len(result) != len(tc.expected) { + t.Errorf("Expected %d rule, got %d", len(tc.expected), len(result)) + } + for i := range result { + if !reflect.DeepEqual(result[i], tc.expected[i]) { + t.Errorf("Expected rule %d to be %v, got %v", i, tc.expected[i], result[i]) + } + } + }) + } +} diff --git a/controllers/authpolicy_istio_authorization_policy.go b/controllers/authpolicy_istio_authorization_policy.go index 18066d2a2..4a498aa14 100644 --- a/controllers/authpolicy_istio_authorization_policy.go +++ b/controllers/authpolicy_istio_authorization_policy.go @@ -2,11 +2,11 @@ package controllers import ( "context" + "errors" "fmt" "reflect" "github.com/go-logr/logr" - "golang.org/x/exp/slices" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" @@ -36,19 +36,13 @@ func (r *AuthPolicyReconciler) reconcileIstioAuthorizationPolicies(ctx context.C return err } - targetHostnames, err := common.TargetHostnames(targetNetworkObject) - if err != nil { - return err - } - - // TODO(guicassolato): should the rules filter only the hostnames valid for each gateway? - toRules := istioAuthorizationPolicyRules(ap.Spec.RouteRules, targetHostnames, targetNetworkObject) - // Create IstioAuthorizationPolicy for each gateway directly or indirectly referred by the policy (existing and new) for _, gw := range append(gwDiffObj.GatewaysWithValidPolicyRef, gwDiffObj.GatewaysMissingPolicyRef...) { - iap := r.istioAuthorizationPolicy(ctx, gw.Gateway, ap, toRules) - err := r.ReconcileResource(ctx, &istio.AuthorizationPolicy{}, iap, alwaysUpdateAuthPolicy) - if err != nil && !apierrors.IsAlreadyExists(err) { + iap, err := r.istioAuthorizationPolicy(ctx, ap, targetNetworkObject, gw) + if err != nil { + return err + } + if err := r.ReconcileResource(ctx, &istio.AuthorizationPolicy{}, iap, alwaysUpdateAuthPolicy); err != nil && !apierrors.IsAlreadyExists(err) { logger.Error(err, "failed to reconcile IstioAuthorizationPolicy resource") return err } @@ -84,20 +78,20 @@ func (r *AuthPolicyReconciler) deleteIstioAuthorizationPolicies(ctx context.Cont return nil } -func (r *AuthPolicyReconciler) istioAuthorizationPolicy(ctx context.Context, gateway *gatewayapiv1beta1.Gateway, ap *api.AuthPolicy, toRules []*istiosecurity.Rule_To) *istio.AuthorizationPolicy { - return &istio.AuthorizationPolicy{ +func (r *AuthPolicyReconciler) istioAuthorizationPolicy(ctx context.Context, ap *api.AuthPolicy, targetNetworkObject client.Object, gw common.GatewayWrapper) (*istio.AuthorizationPolicy, error) { + logger, _ := logr.FromContext(ctx) + logger = logger.WithName("istioAuthorizationPolicy") + + gateway := gw.Gateway + + iap := &istio.AuthorizationPolicy{ ObjectMeta: metav1.ObjectMeta{ Name: istioAuthorizationPolicyName(gateway.Name, ap.GetTargetRef()), Namespace: gateway.Namespace, Labels: istioAuthorizationPolicyLabels(client.ObjectKeyFromObject(gateway), client.ObjectKeyFromObject(ap)), }, Spec: istiosecurity.AuthorizationPolicy{ - Action: istiosecurity.AuthorizationPolicy_CUSTOM, - Rules: []*istiosecurity.Rule{ - { - To: toRules, - }, - }, + Action: istiosecurity.AuthorizationPolicy_CUSTOM, Selector: common.IstioWorkloadSelectorFromGateway(ctx, r.Client(), gateway), ActionDetail: &istiosecurity.AuthorizationPolicy_Provider{ Provider: &istiosecurity.AuthorizationPolicy_ExtensionProvider{ @@ -106,6 +100,70 @@ func (r *AuthPolicyReconciler) istioAuthorizationPolicy(ctx context.Context, gat }, }, } + + var route *gatewayapiv1beta1.HTTPRoute + + gwHostnames := gw.Hostnames() + if len(gwHostnames) == 0 { + gwHostnames = []gatewayapiv1beta1.Hostname{"*"} + } + var routeHostnames []gatewayapiv1beta1.Hostname + + switch obj := targetNetworkObject.(type) { + case *gatewayapiv1beta1.HTTPRoute: + route = obj + if len(route.Spec.Hostnames) > 0 { + routeHostnames = common.FilterValidSubdomains(gwHostnames, route.Spec.Hostnames) + } else { + routeHostnames = gwHostnames + } + case *gatewayapiv1beta1.Gateway: + // fake a single httproute with all rules from all httproutes accepted by the gateway, + // that do not have an authpolicy of its own, so we can generate wasm rules for those cases + rules := make([]gatewayapiv1beta1.HTTPRouteRule, 0) + routes := r.FetchAcceptedGatewayHTTPRoutes(ctx, ap.TargetKey()) + for idx := range routes { + route := routes[idx] + // skip routes that have an authpolicy of its own + if route.GetAnnotations()[common.AuthPolicyBackRefAnnotation] != "" { + continue + } + rules = append(rules, route.Spec.Rules...) + } + if len(rules) == 0 { + logger.V(1).Info("no httproutes attached to the targeted gateway, skipping istio authorizationpolicy for the gateway authpolicy") + common.TagObjectToDelete(iap) + return iap, nil + } + route = &gatewayapiv1beta1.HTTPRoute{ + Spec: gatewayapiv1beta1.HTTPRouteSpec{ + Hostnames: gwHostnames, + Rules: rules, + }, + } + routeHostnames = gwHostnames + } + + rules, err := istioAuthorizationPolicyRules(ap, route) + if err != nil { + return nil, err + } + + if len(rules) > 0 { + // make sure all istio authorizationpolicy rules include the hosts so we don't send a request to authorino for hosts that are not in the scope of the policy + hosts := common.HostnamesToStrings(routeHostnames) + for i := range rules { + for j := range rules[i].To { + if len(rules[i].To[j].Operation.Hosts) > 0 { + continue + } + rules[i].To[j].Operation.Hosts = hosts + } + } + iap.Spec.Rules = rules + } + + return iap, nil } // istioAuthorizationPolicyName generates the name of an AuthorizationPolicy. @@ -128,52 +186,159 @@ func istioAuthorizationPolicyLabels(gwKey, apKey client.ObjectKey) map[string]st } } -func istioAuthorizationPolicyRules(authRules []api.RouteRule, targetHostnames []string, targetNetworkObject client.Object) []*istiosecurity.Rule_To { - toRules := []*istiosecurity.Rule_To{} +// istioAuthorizationPolicyRules builds the list of Istio AuthorizationPolicy rules from an AuthPolicy and a HTTPRoute. +// These rules are the conditions that, when matched, will make the gateway to call external authorization. +// If no rules are specified, the gateway will call external authorization for all requests. +// If the route selectors specified in the policy do not match any route rules, an error is returned. +func istioAuthorizationPolicyRules(ap *api.AuthPolicy, route *gatewayapiv1beta1.HTTPRoute) ([]*istiosecurity.Rule, error) { + // use only the top level route selectors if defined + if topLevelRouteSelectors := ap.Spec.RouteSelectors; len(topLevelRouteSelectors) > 0 { + return istioAuthorizationPolicyRulesFromRouteSelectors(route, topLevelRouteSelectors) + } + return istioAuthorizationPolicyRulesFromHTTPRoute(route), nil +} + +// istioAuthorizationPolicyRulesFromRouteSelectors builds a list of Istio AuthorizationPolicy rules from an HTTPRoute, +// filtered to the HTTPRouteRules and hostnames selected by the route selectors. +func istioAuthorizationPolicyRulesFromRouteSelectors(route *gatewayapiv1beta1.HTTPRoute, routeSelectors []api.RouteSelector) ([]*istiosecurity.Rule, error) { + istioRules := []*istiosecurity.Rule{} - // Rules set in the AuthPolicy - for _, rule := range authRules { - hosts := rule.Hosts - if len(rule.Hosts) == 0 { - hosts = targetHostnames + if len(routeSelectors) > 0 { + // build conditions from the rules selected by the route selectors + for idx := range routeSelectors { + routeSelector := routeSelectors[idx] + hostnamesForConditions := routeSelector.HostnamesForConditions(route) + // TODO(@guicassolato): report about route selectors that match no HTTPRouteRule + for _, rule := range routeSelector.SelectRules(route) { + istioRules = append(istioRules, istioAuthorizationPolicyRulesFromHTTPRouteRule(rule, hostnamesForConditions)...) + } + } + if len(istioRules) == 0 { + return nil, errors.New("cannot match any route rules, check for invalid route selectors in the policy") } - toRules = append(toRules, &istiosecurity.Rule_To{ - Operation: &istiosecurity.Operation{ - Hosts: hosts, - Methods: rule.Methods, - Paths: rule.Paths, - }, - }) } - // TODO(guicassolato): always inherit the rules from the target network object and remove AuthRules from the AuthPolicy API + return istioRules, nil +} + +// istioAuthorizationPolicyRulesFromHTTPRoute builds a list of Istio AuthorizationPolicy rules from an HTTPRoute, +// without using route selectors. +func istioAuthorizationPolicyRulesFromHTTPRoute(route *gatewayapiv1beta1.HTTPRoute) []*istiosecurity.Rule { + istioRules := []*istiosecurity.Rule{} + + hostnamesForConditions := (&api.RouteSelector{}).HostnamesForConditions(route) + for _, rule := range route.Spec.Rules { + istioRules = append(istioRules, istioAuthorizationPolicyRulesFromHTTPRouteRule(rule, hostnamesForConditions)...) + } + + return istioRules +} + +// istioAuthorizationPolicyRulesFromHTTPRouteRule builds a list of Istio AuthorizationPolicy rules from a HTTPRouteRule +// and a list of hostnames. +// * Each combination of HTTPRouteMatch and hostname yields one condition. +// * Rules that specify no explicit HTTPRouteMatch are assumed to match all requests (i.e. implicit catch-all rule.) +// * Empty list of hostnames yields a condition without a hostname pattern expression. +func istioAuthorizationPolicyRulesFromHTTPRouteRule(rule gatewayapiv1beta1.HTTPRouteRule, hostnames []gatewayapiv1beta1.Hostname) (istioRules []*istiosecurity.Rule) { + hosts := []string{} + for _, hostname := range hostnames { + if hostname == "*" { + continue + } + hosts = append(hosts, string(hostname)) + } - if len(toRules) == 0 { - // Rules not set in the AuthPolicy - inherit the rules from the target network object - switch obj := targetNetworkObject.(type) { - case *gatewayapiv1beta1.HTTPRoute: - // Rules not set and targeting a HTTPRoute - inherit the rules (hostnames, methods and paths) from the HTTPRoute - httpRouterules := common.RulesFromHTTPRoute(obj) - for idx := range httpRouterules { - toRules = append(toRules, &istiosecurity.Rule_To{ + // no http route matches → we only need one simple istio rule or even no rule at all + if len(rule.Matches) == 0 { + if len(hosts) == 0 { + return + } + istioRule := &istiosecurity.Rule{ + To: []*istiosecurity.Rule_To{ + { Operation: &istiosecurity.Operation{ - Hosts: slices.Clone(httpRouterules[idx].Hosts), - Methods: slices.Clone(httpRouterules[idx].Methods), - Paths: slices.Clone(httpRouterules[idx].Paths), + Hosts: hosts, }, - }) - } - case *gatewayapiv1beta1.Gateway: - // Rules not set and targeting a Gateway - inherit the rules (hostnames) from the Gateway - toRules = append(toRules, &istiosecurity.Rule_To{ - Operation: &istiosecurity.Operation{ - Hosts: targetHostnames, }, - }) + }, } + istioRules = append(istioRules, istioRule) + return } - return toRules + // http route matches and possibly hostnames → we need one istio rule per http route match + for _, match := range rule.Matches { + istioRule := &istiosecurity.Rule{} + + var operation *istiosecurity.Operation + method := match.Method + path := match.Path + + if len(hosts) > 0 || method != nil || path != nil { + operation = &istiosecurity.Operation{} + } + + // hosts + if len(hosts) > 0 { + operation.Hosts = hosts + } + + // method + if method != nil { + operation.Methods = []string{string(*method)} + } + + // path + if path != nil { + operator := "*" // gateway api defaults to PathMatchPathPrefix + skip := false + if path.Type != nil { + switch *path.Type { + case gatewayapiv1beta1.PathMatchExact: + operator = "" + case gatewayapiv1beta1.PathMatchRegularExpression: + // ignore this rule as it is not supported by Istio - Authorino will check it anyway + skip = true + } + } + if !skip { + value := "/" + if path.Value != nil { + value = *path.Value + } + operation.Paths = []string{fmt.Sprintf("%s%s", value, operator)} + } + } + + if operation != nil { + istioRule.To = []*istiosecurity.Rule_To{ + {Operation: operation}, + } + } + + // headers + if len(match.Headers) > 0 { + istioRule.When = []*istiosecurity.Condition{} + + for idx := range match.Headers { + header := match.Headers[idx] + if header.Type != nil && *header.Type == gatewayapiv1beta1.HeaderMatchRegularExpression { + // skip this rule as it is not supported by Istio - Authorino will check it anyway + continue + } + headerCondition := &istiosecurity.Condition{ + Key: fmt.Sprintf("request.headers[%s]", header.Name), + Values: []string{header.Value}, + } + istioRule.When = append(istioRule.When, headerCondition) + } + } + + // query params: istio does not support query params in authorization policies, so we build them in the authconfig instead + + istioRules = append(istioRules, istioRule) + } + return } func alwaysUpdateAuthPolicy(existingObj, desiredObj client.Object) (bool, error) { @@ -186,30 +351,32 @@ func alwaysUpdateAuthPolicy(existingObj, desiredObj client.Object) (bool, error) return false, fmt.Errorf("%T is not an *istio.AuthorizationPolicy", desiredObj) } - if reflect.DeepEqual(existing.Spec.Action, desired.Spec.Action) { - return false, nil + var update bool + + if !reflect.DeepEqual(existing.Spec.Action, desired.Spec.Action) { + update = true + existing.Spec.Action = desired.Spec.Action } - existing.Spec.Action = desired.Spec.Action - if reflect.DeepEqual(existing.Spec.ActionDetail, desired.Spec.ActionDetail) { - return false, nil + if !reflect.DeepEqual(existing.Spec.ActionDetail, desired.Spec.ActionDetail) { + update = true + existing.Spec.ActionDetail = desired.Spec.ActionDetail } - existing.Spec.ActionDetail = desired.Spec.ActionDetail - if reflect.DeepEqual(existing.Spec.Rules, desired.Spec.Rules) { - return false, nil + if !reflect.DeepEqual(existing.Spec.Rules, desired.Spec.Rules) { + update = true + existing.Spec.Rules = desired.Spec.Rules } - existing.Spec.Rules = desired.Spec.Rules - if reflect.DeepEqual(existing.Spec.Selector, desired.Spec.Selector) { - return false, nil + if !reflect.DeepEqual(existing.Spec.Selector, desired.Spec.Selector) { + update = true + existing.Spec.Selector = desired.Spec.Selector } - existing.Spec.Selector = desired.Spec.Selector if reflect.DeepEqual(existing.Annotations, desired.Annotations) { - return false, nil + update = true + existing.Annotations = desired.Annotations } - existing.Annotations = desired.Annotations - return true, nil + return update, nil } diff --git a/controllers/authpolicy_istio_authorization_policy_test.go b/controllers/authpolicy_istio_authorization_policy_test.go new file mode 100644 index 000000000..9c300569f --- /dev/null +++ b/controllers/authpolicy_istio_authorization_policy_test.go @@ -0,0 +1,345 @@ +//go:build unit + +package controllers + +import ( + "reflect" + "testing" + + istiosecurity "istio.io/api/security/v1beta1" + "k8s.io/utils/ptr" + gatewayapiv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" +) + +func TestIstioAuthorizationPolicyRulesFromHTTPRouteRule(t *testing.T) { + testCases := []struct { + name string + hostnames []gatewayapiv1beta1.Hostname + rule gatewayapiv1beta1.HTTPRouteRule + expected []*istiosecurity.Rule + }{ + { + name: "No HTTPRouteMatch", + hostnames: []gatewayapiv1beta1.Hostname{"toystore.kuadrant.io"}, + rule: gatewayapiv1beta1.HTTPRouteRule{}, + expected: []*istiosecurity.Rule{ + { + To: []*istiosecurity.Rule_To{ + { + Operation: &istiosecurity.Operation{ + Hosts: []string{"toystore.kuadrant.io"}, + }, + }, + }, + }, + }, + }, + { + name: "Single HTTPRouteMatch", + hostnames: []gatewayapiv1beta1.Hostname{"toystore.kuadrant.io"}, + rule: gatewayapiv1beta1.HTTPRouteRule{ + Matches: []gatewayapiv1beta1.HTTPRouteMatch{ + { + Path: &gatewayapiv1beta1.HTTPPathMatch{ + Type: ptr.To(gatewayapiv1beta1.PathMatchType("PathPrefix")), + Value: ptr.To("/toy"), + }, + }, + }, + }, + expected: []*istiosecurity.Rule{ + { + To: []*istiosecurity.Rule_To{ + { + Operation: &istiosecurity.Operation{ + Hosts: []string{"toystore.kuadrant.io"}, + Paths: []string{"/toy*"}, + }, + }, + }, + }, + }, + }, + { + name: "Multiple HTTPRouteMatches", + hostnames: []gatewayapiv1beta1.Hostname{"toystore.kuadrant.io"}, + rule: gatewayapiv1beta1.HTTPRouteRule{ + Matches: []gatewayapiv1beta1.HTTPRouteMatch{ + { + Path: &gatewayapiv1beta1.HTTPPathMatch{ + Type: ptr.To(gatewayapiv1beta1.PathMatchType("PathPrefix")), + Value: ptr.To("/toy"), + }, + }, + { + Path: &gatewayapiv1beta1.HTTPPathMatch{ + Type: ptr.To(gatewayapiv1beta1.PathMatchType("Exact")), + Value: ptr.To("/foo"), + }, + }, + }, + }, + expected: []*istiosecurity.Rule{ + { + To: []*istiosecurity.Rule_To{ + { + Operation: &istiosecurity.Operation{ + Hosts: []string{"toystore.kuadrant.io"}, + Paths: []string{"/toy*"}, + }, + }, + }, + }, + { + To: []*istiosecurity.Rule_To{ + { + Operation: &istiosecurity.Operation{ + Hosts: []string{"toystore.kuadrant.io"}, + Paths: []string{"/foo"}, + }, + }, + }, + }, + }, + }, + { + name: "Multiple hosts", + hostnames: []gatewayapiv1beta1.Hostname{"toystore.kuadrant.io", "gamestore.kuadrant.io"}, + rule: gatewayapiv1beta1.HTTPRouteRule{ + Matches: []gatewayapiv1beta1.HTTPRouteMatch{ + { + Path: &gatewayapiv1beta1.HTTPPathMatch{ + Type: ptr.To(gatewayapiv1beta1.PathMatchType("PathPrefix")), + Value: ptr.To("/toy"), + }, + }, + }, + }, + expected: []*istiosecurity.Rule{ + { + To: []*istiosecurity.Rule_To{ + { + Operation: &istiosecurity.Operation{ + Hosts: []string{"toystore.kuadrant.io", "gamestore.kuadrant.io"}, + Paths: []string{"/toy*"}, + }, + }, + }, + }, + }, + }, + { + name: "Catch-all host is ignored", + hostnames: []gatewayapiv1beta1.Hostname{"toystore.kuadrant.io", "*"}, + rule: gatewayapiv1beta1.HTTPRouteRule{ + Matches: []gatewayapiv1beta1.HTTPRouteMatch{ + { + Path: &gatewayapiv1beta1.HTTPPathMatch{ + Type: ptr.To(gatewayapiv1beta1.PathMatchType("PathPrefix")), + Value: ptr.To("/toy"), + }, + }, + }, + }, + expected: []*istiosecurity.Rule{ + { + To: []*istiosecurity.Rule_To{ + { + Operation: &istiosecurity.Operation{ + Hosts: []string{"toystore.kuadrant.io"}, + Paths: []string{"/toy*"}, + }, + }, + }, + }, + }, + }, + { + name: "Method", + rule: gatewayapiv1beta1.HTTPRouteRule{ + Matches: []gatewayapiv1beta1.HTTPRouteMatch{ + { + Method: ptr.To(gatewayapiv1beta1.HTTPMethod("GET")), + }, + }, + }, + expected: []*istiosecurity.Rule{ + { + To: []*istiosecurity.Rule_To{ + { + Operation: &istiosecurity.Operation{ + Methods: []string{"GET"}, + }, + }, + }, + }, + }, + }, + { + name: "PathMatchExact", + rule: gatewayapiv1beta1.HTTPRouteRule{ + Matches: []gatewayapiv1beta1.HTTPRouteMatch{ + { + Path: &gatewayapiv1beta1.HTTPPathMatch{ + Type: ptr.To(gatewayapiv1beta1.PathMatchType("Exact")), + Value: ptr.To("/toy"), + }, + }, + }, + }, + expected: []*istiosecurity.Rule{ + { + To: []*istiosecurity.Rule_To{ + { + Operation: &istiosecurity.Operation{ + Paths: []string{"/toy"}, + }, + }, + }, + }, + }, + }, + { + name: "PathMatchPrefix", + rule: gatewayapiv1beta1.HTTPRouteRule{ + Matches: []gatewayapiv1beta1.HTTPRouteMatch{ + { + Path: &gatewayapiv1beta1.HTTPPathMatch{ + Type: ptr.To(gatewayapiv1beta1.PathMatchType("PathPrefix")), + Value: ptr.To("/toy"), + }, + }, + }, + }, + expected: []*istiosecurity.Rule{ + { + To: []*istiosecurity.Rule_To{ + { + Operation: &istiosecurity.Operation{ + Paths: []string{"/toy*"}, + }, + }, + }, + }, + }, + }, + { + name: "PathMatchRegularExpression", + rule: gatewayapiv1beta1.HTTPRouteRule{ + Matches: []gatewayapiv1beta1.HTTPRouteMatch{ + { + Path: &gatewayapiv1beta1.HTTPPathMatch{ + Type: ptr.To(gatewayapiv1beta1.PathMatchType("RegularExpression")), + Value: ptr.To("/toy"), + }, + }, + }, + }, + expected: []*istiosecurity.Rule{ + { + To: []*istiosecurity.Rule_To{ + { + Operation: &istiosecurity.Operation{}, + }, + }, + }, + }, + }, + { + name: "Single header match", + rule: gatewayapiv1beta1.HTTPRouteRule{ + Matches: []gatewayapiv1beta1.HTTPRouteMatch{ + { + Headers: []gatewayapiv1beta1.HTTPHeaderMatch{ + { + Type: ptr.To(gatewayapiv1beta1.HeaderMatchType("Exact")), + Name: "x-foo", + Value: "a-value", + }, + }, + }, + }, + }, + expected: []*istiosecurity.Rule{ + { + When: []*istiosecurity.Condition{ + { + Key: "request.headers[x-foo]", + Values: []string{"a-value"}, + }, + }, + }, + }, + }, + { + name: "Multiple header matches", + rule: gatewayapiv1beta1.HTTPRouteRule{ + Matches: []gatewayapiv1beta1.HTTPRouteMatch{ + { + Headers: []gatewayapiv1beta1.HTTPHeaderMatch{ + { + Type: ptr.To(gatewayapiv1beta1.HeaderMatchType("Exact")), + Name: "x-foo", + Value: "a-value", + }, + { + Type: ptr.To(gatewayapiv1beta1.HeaderMatchType("Exact")), + Name: "x-bar", + Value: "other-value", + }, + }, + }, + }, + }, + expected: []*istiosecurity.Rule{ + { + When: []*istiosecurity.Condition{ + { + Key: "request.headers[x-foo]", + Values: []string{"a-value"}, + }, + { + Key: "request.headers[x-bar]", + Values: []string{"other-value"}, + }, + }, + }, + }, + }, + { + name: "HeaderMatchRegularExpression", + rule: gatewayapiv1beta1.HTTPRouteRule{ + Matches: []gatewayapiv1beta1.HTTPRouteMatch{ + { + Headers: []gatewayapiv1beta1.HTTPHeaderMatch{ + { + Type: ptr.To(gatewayapiv1beta1.HeaderMatchType("RegularExpression")), + Name: "x-foo", + Value: "^a+.*$", + }, + }, + }, + }, + }, + expected: []*istiosecurity.Rule{ + { + When: []*istiosecurity.Condition{}, + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := istioAuthorizationPolicyRulesFromHTTPRouteRule(tc.rule, tc.hostnames) + if len(result) != len(tc.expected) { + t.Errorf("Expected %d rule, got %d", len(tc.expected), len(result)) + } + for i := range result { + if !reflect.DeepEqual(result[i], tc.expected[i]) { + t.Errorf("Expected rule %d to be %v, got %v", i, tc.expected[i], result[i]) + } + } + }) + } +} diff --git a/controllers/authpolicy_status.go b/controllers/authpolicy_status.go index a185b1b2e..45b26a92a 100644 --- a/controllers/authpolicy_status.go +++ b/controllers/authpolicy_status.go @@ -7,6 +7,7 @@ import ( "github.com/go-logr/logr" "golang.org/x/exp/slices" "k8s.io/apimachinery/pkg/api/errors" + apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ctrl "sigs.k8s.io/controller-runtime" @@ -32,7 +33,7 @@ func (r *AuthPolicyReconciler) reconcileStatus(ctx context.Context, ap *api.Auth Name: authConfigName(apKey), } authConfig := &authorinoapi.AuthConfig{} - if err := r.GetResource(ctx, authConfigKey, authConfig); err != nil { + if err := r.GetResource(ctx, authConfigKey, authConfig); err != nil && !apierrors.IsNotFound(err) { return ctrl.Result{}, err } diff --git a/controllers/helper_test.go b/controllers/helper_test.go index 6ee41f06c..92799c556 100644 --- a/controllers/helper_test.go +++ b/controllers/helper_test.go @@ -12,9 +12,11 @@ import ( "time" kuadrantv1beta1 "github.com/kuadrant/kuadrant-operator/api/v1beta1" + "github.com/kuadrant/kuadrant-operator/pkg/common" "github.com/google/uuid" . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" @@ -24,8 +26,10 @@ import ( "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/yaml" "k8s.io/client-go/kubernetes/scheme" + "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" logf "sigs.k8s.io/controller-runtime/pkg/log" + gatewayapiv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" ) func ApplyKuadrantCR(namespace string) { @@ -177,3 +181,102 @@ func CreateOrUpdateK8SObject(obj runtime.Object, k8sClient client.Client) error return k8sClient.Update(context.Background(), k8sObjCopy) } + +func testBuildBasicGateway(gwName, ns string) *gatewayapiv1beta1.Gateway { + return &gatewayapiv1beta1.Gateway{ + TypeMeta: metav1.TypeMeta{ + Kind: "Gateway", + APIVersion: gatewayapiv1beta1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: gwName, + Namespace: ns, + Labels: map[string]string{"app": "rlptest"}, + Annotations: map[string]string{"networking.istio.io/service-type": string(corev1.ServiceTypeClusterIP)}, + }, + Spec: gatewayapiv1beta1.GatewaySpec{ + GatewayClassName: "istio", + Listeners: []gatewayapiv1beta1.Listener{ + { + Name: "default", + Port: gatewayapiv1beta1.PortNumber(80), + Protocol: "HTTP", + }, + }, + }, + } +} + +func testBuildBasicHttpRoute(routeName, gwName, ns string, hostnames []string) *gatewayapiv1beta1.HTTPRoute { + return &gatewayapiv1beta1.HTTPRoute{ + TypeMeta: metav1.TypeMeta{ + Kind: "HTTPRoute", + APIVersion: gatewayapiv1beta1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: routeName, + Namespace: ns, + Labels: map[string]string{"app": "rlptest"}, + }, + Spec: gatewayapiv1beta1.HTTPRouteSpec{ + CommonRouteSpec: gatewayapiv1beta1.CommonRouteSpec{ + ParentRefs: []gatewayapiv1beta1.ParentReference{ + { + Name: gatewayapiv1beta1.ObjectName(gwName), + Namespace: ptr.To(gatewayapiv1beta1.Namespace(ns)), + }, + }, + }, + Hostnames: common.Map(hostnames, func(hostname string) gatewayapiv1beta1.Hostname { return gatewayapiv1beta1.Hostname(hostname) }), + Rules: []gatewayapiv1beta1.HTTPRouteRule{ + { + Matches: []gatewayapiv1beta1.HTTPRouteMatch{ + { + Path: &gatewayapiv1beta1.HTTPPathMatch{ + Type: ptr.To(gatewayapiv1beta1.PathMatchPathPrefix), + Value: ptr.To("/toy"), + }, + Method: ptr.To(gatewayapiv1beta1.HTTPMethod("GET")), + }, + }, + }, + }, + }, + } +} + +func testBuildMultipleRulesHttpRoute(routeName, gwName, ns string, hostnames []string) *gatewayapiv1beta1.HTTPRoute { + route := testBuildBasicHttpRoute(routeName, gwName, ns, hostnames) + route.Spec.Rules = []gatewayapiv1beta1.HTTPRouteRule{ + { // POST|DELETE /admin* + Matches: []gatewayapiv1beta1.HTTPRouteMatch{ + { + Path: &gatewayapiv1beta1.HTTPPathMatch{ + Type: ptr.To(gatewayapiv1beta1.PathMatchType("PathPrefix")), + Value: ptr.To("/admin"), + }, + Method: ptr.To(gatewayapiv1beta1.HTTPMethod("POST")), + }, + { + Path: &gatewayapiv1beta1.HTTPPathMatch{ + Type: ptr.To(gatewayapiv1beta1.PathMatchType("PathPrefix")), + Value: ptr.To("/admin"), + }, + Method: ptr.To(gatewayapiv1beta1.HTTPMethod("DELETE")), + }, + }, + }, + { // GET /private* + Matches: []gatewayapiv1beta1.HTTPRouteMatch{ + { + Path: &gatewayapiv1beta1.HTTPPathMatch{ + Type: ptr.To(gatewayapiv1beta1.PathMatchType("PathPrefix")), + Value: ptr.To("/private"), + }, + Method: ptr.To(gatewayapiv1beta1.HTTPMethod("GET")), + }, + }, + }, + } + return route +} diff --git a/controllers/httprouteparentrefs_eventmapper.go b/controllers/httprouteparentrefs_eventmapper.go new file mode 100644 index 000000000..0fcebcddb --- /dev/null +++ b/controllers/httprouteparentrefs_eventmapper.go @@ -0,0 +1,84 @@ +package controllers + +import ( + "context" + "fmt" + + "github.com/go-logr/logr" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + gatewayapiv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" + + api "github.com/kuadrant/kuadrant-operator/api/v1beta2" + "github.com/kuadrant/kuadrant-operator/pkg/common" +) + +// HTTPRouteParentRefsEventMapper is an EventHandler that maps HTTPRoute events to policy events, +// by going through the parentRefs of the route and finding all policies that target one of its +// parent resources, thus yielding events for those policies. +type HTTPRouteParentRefsEventMapper struct { + Logger logr.Logger + Client client.Client +} + +func (m *HTTPRouteParentRefsEventMapper) MapToRateLimitPolicy(obj client.Object) []reconcile.Request { + return m.mapToPolicyRequest(obj, "ratelimitpolicy", &api.RateLimitPolicyList{}) +} + +func (m *HTTPRouteParentRefsEventMapper) MapToAuthPolicy(obj client.Object) []reconcile.Request { + return m.mapToPolicyRequest(obj, "authpolicy", &api.AuthPolicyList{}) +} + +func (m *HTTPRouteParentRefsEventMapper) mapToPolicyRequest(obj client.Object, policyKind string, policyList client.ObjectList) []reconcile.Request { + logger := m.Logger.V(1).WithValues( + "object", client.ObjectKeyFromObject(obj), + "policyKind", policyKind, + ) + + route, ok := obj.(*gatewayapiv1beta1.HTTPRoute) + if !ok { + logger.Info("mapToPolicyRequest:", "error", fmt.Sprintf("%T is not a *gatewayapiv1beta1.HTTPRoute", obj)) + return []reconcile.Request{} + } + + requests := make([]reconcile.Request, 0) + + for _, parentRef := range route.Spec.ParentRefs { + // skips if parentRef is not a Gateway + if (parentRef.Group != nil && *parentRef.Group != "gateway.networking.k8s.io") || (parentRef.Kind != nil && *parentRef.Kind != "Gateway") { + continue + } + // list policies in the same namespace as the parent gateway of the route + parentRefNamespace := parentRef.Namespace + if parentRefNamespace == nil { + ns := gatewayapiv1beta1.Namespace(route.GetNamespace()) + parentRefNamespace = &ns + } + if err := m.Client.List(context.Background(), policyList, &client.ListOptions{Namespace: string(*parentRefNamespace)}); err != nil { + logger.Error(err, "failed to list policies") + } + // triggers the reconciliation of any policy that targets the parent gateway of the route + policies, ok := policyList.(common.KuadrantPolicyList) + if !ok { + logger.Info("mapToPolicyRequest:", "error", fmt.Sprintf("%T is not a KuadrantPolicyList", policyList)) + continue + } + for _, policy := range policies.GetItems() { + targetRef := policy.GetTargetRef() + if !common.IsTargetRefGateway(targetRef) { + continue + } + targetRefNamespace := targetRef.Namespace + if targetRefNamespace == nil { + ns := gatewayapiv1beta1.Namespace(policy.GetNamespace()) + targetRefNamespace = &ns + } + if *parentRefNamespace == *targetRefNamespace && parentRef.Name == targetRef.Name { + obj, _ := policy.(client.Object) + requests = append(requests, reconcile.Request{NamespacedName: client.ObjectKeyFromObject(obj)}) + } + } + } + + return requests +} diff --git a/controllers/ratelimitpolicy_controller_test.go b/controllers/ratelimitpolicy_controller_test.go index 98978539b..54e711508 100644 --- a/controllers/ratelimitpolicy_controller_test.go +++ b/controllers/ratelimitpolicy_controller_test.go @@ -11,7 +11,6 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" istioclientgoextensionv1alpha1 "istio.io/client-go/pkg/apis/extensions/v1alpha1" - corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -28,69 +27,6 @@ import ( "k8s.io/utils/ptr" ) -func testBuildBasicGateway(gwName, ns string) *gatewayapiv1beta1.Gateway { - return &gatewayapiv1beta1.Gateway{ - TypeMeta: metav1.TypeMeta{ - Kind: "Gateway", - APIVersion: gatewayapiv1beta1.GroupVersion.String(), - }, - ObjectMeta: metav1.ObjectMeta{ - Name: gwName, - Namespace: ns, - Labels: map[string]string{"app": "rlptest"}, - Annotations: map[string]string{"networking.istio.io/service-type": string(corev1.ServiceTypeClusterIP)}, - }, - Spec: gatewayapiv1beta1.GatewaySpec{ - GatewayClassName: "istio", - Listeners: []gatewayapiv1beta1.Listener{ - { - Name: "default", - Port: gatewayapiv1beta1.PortNumber(80), - Protocol: "HTTP", - }, - }, - }, - } -} - -func testBuildBasicHttpRoute(routeName, gwName, ns string, hostnames []string) *gatewayapiv1beta1.HTTPRoute { - return &gatewayapiv1beta1.HTTPRoute{ - TypeMeta: metav1.TypeMeta{ - Kind: "HTTPRoute", - APIVersion: gatewayapiv1beta1.GroupVersion.String(), - }, - ObjectMeta: metav1.ObjectMeta{ - Name: routeName, - Namespace: ns, - Labels: map[string]string{"app": "rlptest"}, - }, - Spec: gatewayapiv1beta1.HTTPRouteSpec{ - CommonRouteSpec: gatewayapiv1beta1.CommonRouteSpec{ - ParentRefs: []gatewayapiv1beta1.ParentReference{ - { - Name: gatewayapiv1beta1.ObjectName(gwName), - Namespace: ptr.To(gatewayapiv1beta1.Namespace(ns)), - }, - }, - }, - Hostnames: common.Map(hostnames, func(hostname string) gatewayapiv1beta1.Hostname { return gatewayapiv1beta1.Hostname(hostname) }), - Rules: []gatewayapiv1beta1.HTTPRouteRule{ - { - Matches: []gatewayapiv1beta1.HTTPRouteMatch{ - { - Path: &gatewayapiv1beta1.HTTPPathMatch{ - Type: ptr.To(gatewayapiv1beta1.PathMatchPathPrefix), - Value: ptr.To("/toy"), - }, - Method: ptr.To(gatewayapiv1beta1.HTTPMethod("GET")), - }, - }, - }, - }, - }, - } -} - var _ = Describe("RateLimitPolicy controller", func() { var ( testNamespace string diff --git a/examples/toystore/authpolicy.yaml b/examples/toystore/authpolicy.yaml index 6ca7ec6c7..3e84b3a11 100644 --- a/examples/toystore/authpolicy.yaml +++ b/examples/toystore/authpolicy.yaml @@ -8,13 +8,13 @@ spec: group: gateway.networking.k8s.io kind: HTTPRoute name: toystore - routes: # TODO(@guicassolato): replace for routeSelectors when available - - methods: - - DELETE - - POST - paths: - - "/admin*" rules: + routeSelectors: + - matches: + - path: + type: Exact + value: "/admin/toy" + method: DELETE authentication: "apikey": apiKey: @@ -43,9 +43,6 @@ spec: group: gateway.networking.k8s.io kind: Gateway name: istio-ingressgateway - routes: # TODO(@guicassolato): replace with routeSelectors when available - - hosts: - - "*.toystore.com" rules: authentication: "apikey": diff --git a/pkg/common/common.go b/pkg/common/common.go index 0f0e0d160..8f4d50772 100644 --- a/pkg/common/common.go +++ b/pkg/common/common.go @@ -45,6 +45,10 @@ type KuadrantPolicy interface { GetRulesHostnames() []string } +type KuadrantPolicyList interface { + GetItems() []KuadrantPolicy +} + // GetEmptySliceIfNil returns a provided slice, or an empty slice of the same type if the input slice is nil. func GetEmptySliceIfNil[T any](val []T) []T { if val == nil { diff --git a/pkg/common/gatewayapi_utils.go b/pkg/common/gatewayapi_utils.go index 3f842dc26..af79def89 100644 --- a/pkg/common/gatewayapi_utils.go +++ b/pkg/common/gatewayapi_utils.go @@ -26,11 +26,11 @@ type HTTPRouteRule struct { } func IsTargetRefHTTPRoute(targetRef gatewayapiv1alpha2.PolicyTargetReference) bool { - return targetRef.Kind == ("HTTPRoute") + return targetRef.Group == ("gateway.networking.k8s.io") && targetRef.Kind == ("HTTPRoute") } func IsTargetRefGateway(targetRef gatewayapiv1alpha2.PolicyTargetReference) bool { - return targetRef.Kind == ("Gateway") + return targetRef.Group == ("gateway.networking.k8s.io") && targetRef.Kind == ("Gateway") } func RouteHTTPMethodToRuleMethod(httpMethod *gatewayapiv1beta1.HTTPMethod) []string { @@ -540,6 +540,33 @@ func TargetHostnames(targetNetworkObject client.Object) ([]string, error) { return hosts, nil } +// HostnamesFromHTTPRoute returns an array of all hostnames specified in a HTTPRoute or inherited from its parent Gateways +func HostnamesFromHTTPRoute(ctx context.Context, route *gatewayapiv1beta1.HTTPRoute, cli client.Client) ([]string, error) { + if len(route.Spec.Hostnames) > 0 { + return RouteHostnames(route), nil + } + + hosts := []string{} + + for _, ref := range route.Spec.ParentRefs { + if (ref.Kind != nil && *ref.Kind != "Gateway") || (ref.Group != nil && *ref.Group != "gateway.networking.k8s.io") { + continue + } + gw := &gatewayapiv1beta1.Gateway{} + ns := route.Namespace + if ref.Namespace != nil { + ns = string(*ref.Namespace) + } + if err := cli.Get(ctx, types.NamespacedName{Namespace: ns, Name: string(ref.Name)}, gw); err != nil { + return nil, err + } + gwHostanmes := HostnamesToStrings(GatewayWrapper{Gateway: gw}.Hostnames()) + hosts = append(hosts, gwHostanmes...) + } + + return hosts, nil +} + // ValidateHierarchicalRules returns error if the policy rules hostnames fail to match the target network hosts func ValidateHierarchicalRules(policy KuadrantPolicy, targetNetworkObject client.Object) error { targetHostnames, err := TargetHostnames(targetNetworkObject) diff --git a/pkg/rlptools/wasm_utils.go b/pkg/rlptools/wasm_utils.go index 0c943700c..ea3c5e59d 100644 --- a/pkg/rlptools/wasm_utils.go +++ b/pkg/rlptools/wasm_utils.go @@ -14,7 +14,6 @@ import ( gatewayapiv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" kuadrantv1beta2 "github.com/kuadrant/kuadrant-operator/api/v1beta2" - "github.com/kuadrant/kuadrant-operator/pkg/common" "github.com/kuadrant/kuadrant-operator/pkg/rlptools/wasm" ) @@ -72,7 +71,7 @@ func conditionsFromLimit(limit *kuadrantv1beta2.Limit, route *gatewayapiv1beta1. // build conditions from the rules selected by the route selectors for idx := range limit.RouteSelectors { routeSelector := limit.RouteSelectors[idx] - hostnamesForConditions := hostnamesForConditions(route, &routeSelector) + hostnamesForConditions := routeSelector.HostnamesForConditions(route) for _, rule := range routeSelector.SelectRules(route) { routeConditions = append(routeConditions, conditionsFromRule(rule, hostnamesForConditions)...) } @@ -82,8 +81,9 @@ func conditionsFromLimit(limit *kuadrantv1beta2.Limit, route *gatewayapiv1beta1. } } else { // build conditions from all rules if no route selectors are defined + hostnamesForConditions := (&kuadrantv1beta2.RouteSelector{}).HostnamesForConditions(route) for _, rule := range route.Spec.Rules { - routeConditions = append(routeConditions, conditionsFromRule(rule, hostnamesForConditions(route, nil))...) + routeConditions = append(routeConditions, conditionsFromRule(rule, hostnamesForConditions)...) } } @@ -115,22 +115,6 @@ func conditionsFromLimit(limit *kuadrantv1beta2.Limit, route *gatewayapiv1beta1. return whenConditions, nil } -// hostnamesForConditions allows avoiding building conditions for hostnames that are excluded by the selector -// or when the hostname is irrelevant (i.e. matches all hostnames) -func hostnamesForConditions(route *gatewayapiv1beta1.HTTPRoute, routeSelector *kuadrantv1beta2.RouteSelector) []gatewayapiv1beta1.Hostname { - hostnames := route.Spec.Hostnames - - if routeSelector != nil && len(routeSelector.Hostnames) > 0 { - hostnames = common.Intersection(routeSelector.Hostnames, hostnames) - } - - if common.SameElements(hostnames, route.Spec.Hostnames) { - return []gatewayapiv1beta1.Hostname{"*"} - } - - return hostnames -} - // conditionsFromRule builds a list of conditions from a rule and a list of hostnames // each combination of a rule match and hostname yields one condition // rules that specify no explicit match are assumed to match all request (i.e. implicit catch-all rule)