diff --git a/README.md b/README.md index 9ec93f82e..c8dd8694b 100644 --- a/README.md +++ b/README.md @@ -62,17 +62,17 @@ to operate the cluster (Istio's) ingress gateway to provide API management with The kuadrant control plane owns the following [Custom Resource Definitions, CRDs](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/): -| CRD | Description | Example | -|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------| -| RateLimitPolicy CRD [\[doc\]](https://github.com/Kuadrant/kuadrant-operator/blob/main/doc/rate-limiting.md) [[reference]](https://github.com/Kuadrant/kuadrant-operator/blob/main/doc/ratelimitpolicy-reference.md) | Enable access control on workloads based on HTTP rate limiting | [RateLimitPolicy CR](https://raw.githubusercontent.com/Kuadrant/kuadrant-operator/main/config/samples/kuadrant_v1beta1_kuadrant.yaml) | -| [AuthPolicy CRD](https://github.com/Kuadrant/kuadrant-operator/blob/main/apis/apim/v1alpha1/authpolicy_types.go) | Enable AuthN and AuthZ based access control on workloads | [AuthPolicy CR](https://github.com/Kuadrant/kuadrant-operator/blob/main/config/samples/kuadrant_v1beta1_ratelimitpolicy.yaml) | +| CRD | Description | Example | +|-----------------------------------------------------------------------------------------------------|----------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------| +| AuthPolicy CRD [\[doc\]](doc/auth.md) [[reference]](doc/reference/authpolicy.md) | Enable AuthN and AuthZ based access control on workloads | [AuthPolicy CR](https://github.com/Kuadrant/kuadrant-operator/blob/main/examples/toystore/authpolicy.yaml) | +| RateLimitPolicy CRD [\[doc\]](doc/rate-limiting.md) [[reference]](doc/reference/ratelimitpolicy.md) | Enable access control on workloads based on HTTP rate limiting | [RateLimitPolicy CR](https://raw.githubusercontent.com/Kuadrant/kuadrant-operator/main/examples/toystore/ratelimitpolicy_httproute.yaml) | Additionally, Kuadrant provides the following CRDs | CRD | Owner | Description | Example | |--------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------|-------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------| | [Kuadrant CRD](https://github.com/Kuadrant/kuadrant-operator/blob/main/api/v1beta1/kuadrant_types.go) | [Kuadrant Operator](https://github.com/Kuadrant/kuadrant-operator) | Represents an instance of kuadrant | [Kuadrant CR](https://github.com/Kuadrant/kuadrant-operator/blob/main/config/samples/kuadrant_v1beta1_kuadrant.yaml) | -| [Limitador CRD](doc/ratelimitpolicy-reference.md) | [Limitador Operator](https://github.com/Kuadrant/limitador-operator) | Represents an instance of Limitador | [Limitador CR](https://github.com/Kuadrant/limitador-operator/blob/main/config/samples/limitador_v1alpha1_limitador.yaml) | +| [Limitador CRD](https://github.com/Kuadrant/limitador-operator/blob/main/api/v1alpha1/limitador_types.go) | [Limitador Operator](https://github.com/Kuadrant/limitador-operator) | Represents an instance of Limitador | [Limitador CR](https://github.com/Kuadrant/limitador-operator/blob/main/config/samples/limitador_v1alpha1_limitador.yaml) | | [Authorino CRD](https://github.com/Kuadrant/authorino-operator#the-authorino-custom-resource-definition-crd) | [Authorino Operator](https://github.com/Kuadrant/authorino-operator) | Represents an instance of Authorino | [Authorino CR](https://github.com/Kuadrant/authorino-operator/blob/main/config/samples/authorino-operator_v1beta1_authorino.yaml) | Kuadrant Architecture @@ -146,7 +146,7 @@ EOF * Expose the service/API using the kubernetes Gateway API, ie [HTTPRoute](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRoute) object. * Write and apply the Kuadrant's [RateLimitPolicy](doc/rate-limiting.md) and/or - [AuthPolicy](api/v1beta1/authpolicy_types.go) custom resources targeting the HTTPRoute resource + [AuthPolicy](doc/auth.md) custom resources targeting the HTTPRoute resource to have your API protected. #### If you are a *Cluster Operator* @@ -154,7 +154,7 @@ EOF * (Optionally) deploy istio ingress gateway using the [Gateway](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1beta1.Gateway) resource. * Write and apply the Kuadrant's [RateLimitPolicy](doc/rate-limiting.md) and/or - [AuthPolicy](api/v1beta1/authpolicy_types.go) custom resources targeting the Gateway resource + [AuthPolicy](doc/auth.md) custom resources targeting the Gateway resource to have your gateway traffic protected. ## User guides @@ -177,7 +177,7 @@ Docs can be found on the [Kuadrant website](https://kuadrant.io/). The [Development guide](doc/development.md) describes how to build the kuadrant operator and how to test your changes before submitting a patch or opening a PR. -Join us on the [#kuadrant](https://kubernetes.slack.com/archives/C05J0D0V525) channel in the Kubernetes Slack workspace, +Join us on the [#kuadrant](https://kubernetes.slack.com/archives/C05J0D0V525) channel in the Kubernetes Slack workspace, for live discussions about the roadmap and more. ## Licensing diff --git a/api/v1beta1/authpolicy_types.go b/api/v1beta1/authpolicy_types.go deleted file mode 100644 index d17abb350..000000000 --- a/api/v1beta1/authpolicy_types.go +++ /dev/null @@ -1,151 +0,0 @@ -package v1beta1 - -import ( - "fmt" - - "github.com/go-logr/logr" - "github.com/google/go-cmp/cmp" - authorinov1beta1 "github.com/kuadrant/authorino/api/v1beta1" - 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" -) - -type AuthSchemeSpec struct { - // Named sets of JSON patterns that can be referred in `when` conditionals and in JSON-pattern matching policy rules. - Patterns map[string]authorinov1beta1.JSONPatternExpressions `json:"patterns,omitempty"` - - // Conditions for the AuthConfig to be enforced. - // If omitted, the AuthConfig will be enforced for all requests. - // If present, all conditions must match for the AuthConfig to be enforced; otherwise, Authorino skips the AuthConfig and returns immediately with status OK. - Conditions []authorinov1beta1.JSONPattern `json:"when,omitempty"` - - // List of identity sources/authentication modes. - // At least one config of this list MUST evaluate to a valid identity for a request to be successful in the identity verification phase. - Identity []*authorinov1beta1.Identity `json:"identity,omitempty"` - - // List of metadata source configs. - // Authorino fetches JSON content from sources on this list on every request. - Metadata []*authorinov1beta1.Metadata `json:"metadata,omitempty"` - - // Authorization is the list of authorization policies. - // All policies in this list MUST evaluate to "true" for a request be successful in the authorization phase. - Authorization []*authorinov1beta1.Authorization `json:"authorization,omitempty"` - - // List of response configs. - // Authorino gathers data from the auth pipeline to build custom responses for the client. - Response []*authorinov1beta1.Response `json:"response,omitempty"` - - // Custom denial response codes, statuses and headers to override default 40x's. - DenyWith *authorinov1beta1.DenyWith `json:"denyWith,omitempty"` -} - -type AuthPolicySpec struct { - // TargetRef identifies an API object to apply policy to. - TargetRef gatewayapiv1alpha2.PolicyTargetReference `json:"targetRef"` - - // Rule describe the requests that will be routed to external authorization provider - AuthRules []AuthRule `json:"rules,omitempty"` - - // AuthSchemes are embedded Authorino's AuthConfigs - AuthScheme AuthSchemeSpec `json:"authScheme,omitempty"` -} - -type AuthRule struct { - Hosts []string `json:"hosts,omitempty"` - Methods []string `json:"methods,omitempty"` - Paths []string `json:"paths,omitempty"` -} - -type AuthPolicyStatus struct { - // ObservedGeneration reflects the generation of the most recently observed spec. - // +optional - ObservedGeneration int64 `json:"observedGeneration,omitempty"` - - // Represents the observations of a foo's current state. - // Known .status.conditions.type are: "Available" - // +patchMergeKey=type - // +patchStrategy=merge - // +listType=map - // +listMapKey=type - Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"` -} - -func (s *AuthPolicyStatus) Equals(other *AuthPolicyStatus, logger logr.Logger) bool { - if s.ObservedGeneration != other.ObservedGeneration { - diff := cmp.Diff(s.ObservedGeneration, other.ObservedGeneration) - logger.V(1).Info("ObservedGeneration not equal", "difference", diff) - return false - } - - // Marshalling sorts by condition type - currentMarshaledJSON, _ := common.ConditionMarshal(s.Conditions) - otherMarshaledJSON, _ := common.ConditionMarshal(other.Conditions) - if string(currentMarshaledJSON) != string(otherMarshaledJSON) { - diff := cmp.Diff(string(currentMarshaledJSON), string(otherMarshaledJSON)) - logger.V(1).Info("Conditions not equal", "difference", diff) - return false - } - - return true -} - -// +kubebuilder:object:root=true -// +kubebuilder:subresource:status -type AuthPolicy struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - - Spec AuthPolicySpec `json:"spec,omitempty"` - Status AuthPolicyStatus `json:"status,omitempty"` -} - -//+kubebuilder:object:root=true - -// AuthPolicyList contains a list of AuthPolicy -type AuthPolicyList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - Items []AuthPolicy `json:"items"` -} - -func init() { - SchemeBuilder.Register(&AuthPolicy{}, &AuthPolicyList{}) -} - -func (ap *AuthPolicy) Validate() error { - if ap.Spec.TargetRef.Group != ("gateway.networking.k8s.io") { - return fmt.Errorf("invalid targetRef.Group %s. The only supported group is gateway.networking.k8s.io", ap.Spec.TargetRef.Group) - } - - switch kind := ap.Spec.TargetRef.Kind; kind { - case - "HTTPRoute", - "Gateway": - default: - return fmt.Errorf("invalid targetRef.Kind %s. The only supported kinds are HTTPRoute and Gateway", kind) - } - - if ap.Spec.TargetRef.Namespace != nil && string(*ap.Spec.TargetRef.Namespace) != ap.Namespace { - return fmt.Errorf("invalid targetRef.Namespace %s. Currently only supporting references to the same namespace", *ap.Spec.TargetRef.Namespace) - } - return nil -} - -func (ap *AuthPolicy) GetTargetRef() gatewayapiv1alpha2.PolicyTargetReference { - return ap.Spec.TargetRef -} - -func (ap *AuthPolicy) GetWrappedNamespace() gatewayapiv1beta1.Namespace { - return gatewayapiv1beta1.Namespace(ap.Namespace) -} - -func (ap *AuthPolicy) GetRulesHostnames() (ruleHosts []string) { - ruleHosts = make([]string, 0) - for _, rule := range ap.Spec.AuthRules { - ruleHosts = append(ruleHosts, rule.Hosts...) - } - return -} diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 8d3cf2e94..ce406a60e 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -21,231 +21,10 @@ limitations under the License. package v1beta1 import ( - apiv1beta1 "github.com/kuadrant/authorino/api/v1beta1" "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AuthPolicy) DeepCopyInto(out *AuthPolicy) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) - in.Status.DeepCopyInto(&out.Status) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthPolicy. -func (in *AuthPolicy) DeepCopy() *AuthPolicy { - if in == nil { - return nil - } - out := new(AuthPolicy) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *AuthPolicy) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AuthPolicyList) DeepCopyInto(out *AuthPolicyList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]AuthPolicy, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthPolicyList. -func (in *AuthPolicyList) DeepCopy() *AuthPolicyList { - if in == nil { - return nil - } - out := new(AuthPolicyList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *AuthPolicyList) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AuthPolicySpec) DeepCopyInto(out *AuthPolicySpec) { - *out = *in - in.TargetRef.DeepCopyInto(&out.TargetRef) - if in.AuthRules != nil { - in, out := &in.AuthRules, &out.AuthRules - *out = make([]AuthRule, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - in.AuthScheme.DeepCopyInto(&out.AuthScheme) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthPolicySpec. -func (in *AuthPolicySpec) DeepCopy() *AuthPolicySpec { - if in == nil { - return nil - } - out := new(AuthPolicySpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AuthPolicyStatus) DeepCopyInto(out *AuthPolicyStatus) { - *out = *in - if in.Conditions != nil { - in, out := &in.Conditions, &out.Conditions - *out = make([]v1.Condition, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthPolicyStatus. -func (in *AuthPolicyStatus) DeepCopy() *AuthPolicyStatus { - if in == nil { - return nil - } - out := new(AuthPolicyStatus) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AuthRule) DeepCopyInto(out *AuthRule) { - *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.Paths != nil { - in, out := &in.Paths, &out.Paths - *out = make([]string, len(*in)) - copy(*out, *in) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthRule. -func (in *AuthRule) DeepCopy() *AuthRule { - if in == nil { - return nil - } - out := new(AuthRule) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AuthSchemeSpec) DeepCopyInto(out *AuthSchemeSpec) { - *out = *in - if in.Patterns != nil { - in, out := &in.Patterns, &out.Patterns - *out = make(map[string]apiv1beta1.JSONPatternExpressions, len(*in)) - for key, val := range *in { - var outVal []apiv1beta1.JSONPatternExpression - if val == nil { - (*out)[key] = nil - } else { - inVal := (*in)[key] - in, out := &inVal, &outVal - *out = make(apiv1beta1.JSONPatternExpressions, len(*in)) - copy(*out, *in) - } - (*out)[key] = outVal - } - } - if in.Conditions != nil { - in, out := &in.Conditions, &out.Conditions - *out = make([]apiv1beta1.JSONPattern, len(*in)) - copy(*out, *in) - } - if in.Identity != nil { - in, out := &in.Identity, &out.Identity - *out = make([]*apiv1beta1.Identity, len(*in)) - for i := range *in { - if (*in)[i] != nil { - in, out := &(*in)[i], &(*out)[i] - *out = new(apiv1beta1.Identity) - (*in).DeepCopyInto(*out) - } - } - } - if in.Metadata != nil { - in, out := &in.Metadata, &out.Metadata - *out = make([]*apiv1beta1.Metadata, len(*in)) - for i := range *in { - if (*in)[i] != nil { - in, out := &(*in)[i], &(*out)[i] - *out = new(apiv1beta1.Metadata) - (*in).DeepCopyInto(*out) - } - } - } - if in.Authorization != nil { - in, out := &in.Authorization, &out.Authorization - *out = make([]*apiv1beta1.Authorization, len(*in)) - for i := range *in { - if (*in)[i] != nil { - in, out := &(*in)[i], &(*out)[i] - *out = new(apiv1beta1.Authorization) - (*in).DeepCopyInto(*out) - } - } - } - if in.Response != nil { - in, out := &in.Response, &out.Response - *out = make([]*apiv1beta1.Response, len(*in)) - for i := range *in { - if (*in)[i] != nil { - in, out := &(*in)[i], &(*out)[i] - *out = new(apiv1beta1.Response) - (*in).DeepCopyInto(*out) - } - } - } - if in.DenyWith != nil { - in, out := &in.DenyWith, &out.DenyWith - *out = new(apiv1beta1.DenyWith) - (*in).DeepCopyInto(*out) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthSchemeSpec. -func (in *AuthSchemeSpec) DeepCopy() *AuthSchemeSpec { - if in == nil { - return nil - } - out := new(AuthSchemeSpec) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Kuadrant) DeepCopyInto(out *Kuadrant) { *out = *in diff --git a/api/v1beta2/authpolicy_types.go b/api/v1beta2/authpolicy_types.go new file mode 100644 index 000000000..d2569624e --- /dev/null +++ b/api/v1beta2/authpolicy_types.go @@ -0,0 +1,333 @@ +package v1beta2 + +import ( + "fmt" + + "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" + + authorinoapi "github.com/kuadrant/authorino/api/v1beta2" + "github.com/kuadrant/kuadrant-operator/pkg/common" +) + +type AuthSchemeSpec struct { + // Authentication configs. + // At least one config MUST evaluate to a valid identity object for the auth request to be successful. + // +optional + 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]MetadataSpec `json:"metadata,omitempty"` + + // Authorization policies. + // All policies MUST evaluate to "allowed = true" for the auth request be successful. + // +optional + Authorization map[string]AuthorizationSpec `json:"authorization,omitempty"` + + // Response items. + // Authorino builds custom responses to the client of the auth request. + // +optional + 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]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:""` +} + +// +kubebuilder:validation:XValidation:rule="self.targetRef.kind != 'Gateway' || !has(self.routeSelectors)",message="route selectors not supported when targeting a Gateway" +// +kubebuilder:validation:XValidation:rule="self.targetRef.kind != 'Gateway' || !has(self.rules.authentication) || !self.rules.authentication.exists(x, has(self.rules.authentication[x].routeSelectors))",message="route selectors not supported when targeting a Gateway" +// +kubebuilder:validation:XValidation:rule="self.targetRef.kind != 'Gateway' || !has(self.rules.metadata) || !self.rules.metadata.exists(x, has(self.rules.metadata[x].routeSelectors))",message="route selectors not supported when targeting a Gateway" +// +kubebuilder:validation:XValidation:rule="self.targetRef.kind != 'Gateway' || !has(self.rules.authorization) || !self.rules.authorization.exists(x, has(self.rules.authorization[x].routeSelectors))",message="route selectors not supported when targeting a Gateway" +// +kubebuilder:validation:XValidation:rule="self.targetRef.kind != 'Gateway' || !has(self.rules.response) || !has(self.rules.response.success) || self.rules.response.success.headers.exists(x, has(self.rules.response.success.headers[x].routeSelectors))",message="route selectors not supported when targeting a Gateway" +// +kubebuilder:validation:XValidation:rule="self.targetRef.kind != 'Gateway' || !has(self.rules.response) || !has(self.rules.response.success) || self.rules.response.success.dynamicMetadata.exists(x, has(self.rules.response.success.dynamicMetadata[x].routeSelectors))",message="route selectors not supported when targeting a Gateway" +// +kubebuilder:validation:XValidation:rule="self.targetRef.kind != 'Gateway' || !has(self.rules.callbacks) || !self.rules.callbacks.exists(x, has(self.rules.callbacks[x].routeSelectors))",message="route selectors not supported when targeting a Gateway" +type AuthPolicySpec struct { + // TargetRef identifies an API object to apply policy to. + // +kubebuilder:validation:XValidation:rule="self.group == 'gateway.networking.k8s.io'",message="Invalid targetRef.group. The only supported value is 'gateway.networking.k8s.io'" + // +kubebuilder:validation:XValidation:rule="self.kind == 'HTTPRoute' || self.kind == 'Gateway'",message="Invalid targetRef.kind. The only supported values are 'HTTPRoute' and 'Gateway'" + TargetRef gatewayapiv1alpha2.PolicyTargetReference `json:"targetRef"` + + // Top-level route selectors. + // 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"` + + // Named sets of patterns that can be referred in `when` conditions and in pattern-matching authorization policy rules. + // +optional + NamedPatterns map[string]authorinoapi.PatternExpressions `json:"patterns,omitempty"` + + // Overall conditions for the AuthPolicy to be enforced. + // If omitted, the AuthPolicy will be enforced at all requests to the protected routes. + // If present, all conditions must match for the AuthPolicy to be enforced; otherwise, the authorization service skips the AuthPolicy and returns to the auth request with status OK. + // +optional + Conditions []authorinoapi.PatternExpressionOrRef `json:"when,omitempty"` + + // The auth rules of the policy. + // See Authorino's AuthConfig CRD for more details. + AuthScheme AuthSchemeSpec `json:"rules,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 { + // ObservedGeneration reflects the generation of the most recently observed spec. + // +optional + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + + // Represents the observations of a foo's current state. + // Known .status.conditions.type are: "Available" + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"` +} + +func (s *AuthPolicyStatus) Equals(other *AuthPolicyStatus, logger logr.Logger) bool { + if s.ObservedGeneration != other.ObservedGeneration { + diff := cmp.Diff(s.ObservedGeneration, other.ObservedGeneration) + logger.V(1).Info("ObservedGeneration not equal", "difference", diff) + return false + } + + // Marshalling sorts by condition type + currentMarshaledJSON, _ := common.ConditionMarshal(s.Conditions) + otherMarshaledJSON, _ := common.ConditionMarshal(other.Conditions) + if string(currentMarshaledJSON) != string(otherMarshaledJSON) { + diff := cmp.Diff(string(currentMarshaledJSON), string(otherMarshaledJSON)) + logger.V(1).Info("Conditions not equal", "difference", diff) + return false + } + + return true +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:metadata:labels="gateway.networking.k8s.io/policy=inherited" +type AuthPolicy struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec AuthPolicySpec `json:"spec,omitempty"` + 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, + } +} + +func (ap *AuthPolicy) Validate() error { + if ap.Spec.TargetRef.Group != ("gateway.networking.k8s.io") { + return fmt.Errorf("invalid targetRef.Group %s. The only supported group is gateway.networking.k8s.io", ap.Spec.TargetRef.Group) + } + + switch kind := ap.Spec.TargetRef.Kind; kind { + case + "HTTPRoute", + "Gateway": + default: + return fmt.Errorf("invalid targetRef.Kind %s. The only supported kinds are HTTPRoute and Gateway", kind) + } + + if ap.Spec.TargetRef.Namespace != nil && string(*ap.Spec.TargetRef.Namespace) != ap.Namespace { + return fmt.Errorf("invalid targetRef.Namespace %s. Currently only supporting references to the same namespace", *ap.Spec.TargetRef.Namespace) + } + + // prevents usage of routeSelectors in a gateway AuthPolicy + if ap.Spec.TargetRef.Kind == ("Gateway") { + containRouteSelectors := func(config map[string]RouteSelectorsGetter) bool { + if config == nil { + return false + } + for _, config := range config { + if len(config.GetRouteSelectors()) > 0 { + return true + } + } + return false + } + configs := []map[string]RouteSelectorsGetter{ + {"": ap.Spec}, + toRouteSelectorGetterMap(ap.Spec.AuthScheme.Authentication), + toRouteSelectorGetterMap(ap.Spec.AuthScheme.Metadata), + toRouteSelectorGetterMap(ap.Spec.AuthScheme.Authorization), + toRouteSelectorGetterMap(ap.Spec.AuthScheme.Callbacks), + } + if r := ap.Spec.AuthScheme.Response; r != nil { + configs = append(configs, toRouteSelectorGetterMap(r.Success.Headers), toRouteSelectorGetterMap(r.Success.DynamicMetadata)) + } + for _, config := range configs { + if containRouteSelectors(config) { + return fmt.Errorf("route selectors not supported when targeting a Gateway") + } + } + } + + return nil +} + +func (ap *AuthPolicy) GetTargetRef() gatewayapiv1alpha2.PolicyTargetReference { + return ap.Spec.TargetRef +} + +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) + + 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 +} + +//+kubebuilder:object:root=true + +// AuthPolicyList contains a list of AuthPolicy +type AuthPolicyList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []AuthPolicy `json:"items"` +} + +func (l *AuthPolicyList) GetItems() []common.KuadrantPolicy { + return common.Map(l.Items, func(item AuthPolicy) common.KuadrantPolicy { + return &item + }) +} + +func init() { + SchemeBuilder.Register(&AuthPolicy{}, &AuthPolicyList{}) +} + +func toRouteSelectorGetterMap[T RouteSelectorsGetter](m map[string]T) map[string]RouteSelectorsGetter { + result := make(map[string]RouteSelectorsGetter) + for k, v := range m { + result[k] = v + } + return result +} diff --git a/api/v1beta2/authpolicy_types_test.go b/api/v1beta2/authpolicy_types_test.go new file mode 100644 index 000000000..9e5bb6b6f --- /dev/null +++ b/api/v1beta2/authpolicy_types_test.go @@ -0,0 +1,457 @@ +//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" + + authorinoapi "github.com/kuadrant/authorino/api/v1beta2" + "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 TestAuthPolicyValidate(t *testing.T) { + testCases := []struct { + name string + policy *AuthPolicy + valid bool + message string + }{ + { + name: "valid policy targeting a httproute", + policy: &AuthPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-policy", + Namespace: "my-namespace", + }, + Spec: AuthPolicySpec{ + TargetRef: gatewayapiv1alpha2.PolicyTargetReference{ + Group: "gateway.networking.k8s.io", + Kind: "HTTPRoute", + Name: "my-route", + }, + }, + }, + valid: true, + }, + { + name: "valid policy targeting a gateway", + policy: &AuthPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-policy", + Namespace: "my-namespace", + }, + Spec: AuthPolicySpec{ + TargetRef: gatewayapiv1alpha2.PolicyTargetReference{ + Group: "gateway.networking.k8s.io", + Kind: "Gateway", + Name: "my-gw", + }, + }, + }, + valid: true, + }, + { + name: "invalid targetRef group", + policy: &AuthPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-policy", + Namespace: "my-namespace", + }, + Spec: AuthPolicySpec{ + TargetRef: gatewayapiv1alpha2.PolicyTargetReference{ + Group: "not-gateway.networking.k8s.io.group", + Kind: "HTTPRoute", + Name: "my-non-gwapi-route", + }, + }, + }, + message: "invalid targetRef.Group not-gateway.networking.k8s.io.group. The only supported group is gateway.networking.k8s.io", + }, + { + name: "invalid targetRef kind", + policy: &AuthPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-policy", + Namespace: "my-namespace", + }, + Spec: AuthPolicySpec{ + TargetRef: gatewayapiv1alpha2.PolicyTargetReference{ + Group: "gateway.networking.k8s.io", + Kind: "TCPRoute", + Name: "my-tcp-route", + }, + }, + }, + message: "invalid targetRef.Kind TCPRoute. The only supported kinds are HTTPRoute and Gateway", + }, + { + name: "invalid usage of top-level route selectors with a gateway targetRef", + policy: &AuthPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-policy", + Namespace: "my-namespace", + }, + Spec: AuthPolicySpec{ + TargetRef: gatewayapiv1alpha2.PolicyTargetReference{ + Group: "gateway.networking.k8s.io", + Kind: "Gateway", + Name: "my-gw", + }, + RouteSelectors: []RouteSelector{ + { + Hostnames: []gatewayapiv1beta1.Hostname{"*.foo.io"}, + Matches: []gatewayapiv1beta1.HTTPRouteMatch{ + { + Path: &gatewayapiv1beta1.HTTPPathMatch{ + Value: ptr.To("/foo"), + }, + }, + }, + }, + }, + }, + }, + message: "route selectors not supported when targeting a Gateway", + }, + { + name: "invalid usage of config-level route selectors with a gateway targetRef", + policy: &AuthPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-policy", + Namespace: "my-namespace", + }, + Spec: AuthPolicySpec{ + TargetRef: gatewayapiv1alpha2.PolicyTargetReference{ + Group: "gateway.networking.k8s.io", + Kind: "Gateway", + Name: "my-gw", + }, + AuthScheme: AuthSchemeSpec{ + Authentication: map[string]AuthenticationSpec{ + "my-rule": { + AuthenticationSpec: authorinoapi.AuthenticationSpec{ + AuthenticationMethodSpec: authorinoapi.AuthenticationMethodSpec{ + AnonymousAccess: &authorinoapi.AnonymousAccessSpec{}, + }, + }, + CommonAuthRuleSpec: CommonAuthRuleSpec{ + RouteSelectors: []RouteSelector{ + { + Hostnames: []gatewayapiv1beta1.Hostname{"*.foo.io"}, + Matches: []gatewayapiv1beta1.HTTPRouteMatch{ + { + Path: &gatewayapiv1beta1.HTTPPathMatch{ + Value: ptr.To("/foo"), + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + message: "route selectors not supported when targeting a Gateway", + }, + { + name: "invalid targetRef namespace", + policy: &AuthPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-policy", + Namespace: "my-namespace", + }, + Spec: AuthPolicySpec{ + TargetRef: gatewayapiv1alpha2.PolicyTargetReference{ + Group: "gateway.networking.k8s.io", + Kind: "HTTPRoute", + Name: "my-route", + Namespace: ptr.To(gatewayapiv1beta1.Namespace("other-namespace")), + }, + AuthScheme: AuthSchemeSpec{ + Authentication: map[string]AuthenticationSpec{ + "my-rule": { + AuthenticationSpec: authorinoapi.AuthenticationSpec{ + AuthenticationMethodSpec: authorinoapi.AuthenticationMethodSpec{ + AnonymousAccess: &authorinoapi.AnonymousAccessSpec{}, + }, + }, + CommonAuthRuleSpec: CommonAuthRuleSpec{ + RouteSelectors: []RouteSelector{ + { + Hostnames: []gatewayapiv1beta1.Hostname{"*.foo.io"}, + Matches: []gatewayapiv1beta1.HTTPRouteMatch{ + { + Path: &gatewayapiv1beta1.HTTPPathMatch{ + Value: ptr.To("/foo"), + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + message: "invalid targetRef.Namespace other-namespace. Currently only supporting references to the same namespace", + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := tc.policy.Validate() + if tc.valid && result != nil { + t.Errorf("Expected policy to be valid, got %t", result) + } + if !tc.valid && result == nil { + t.Error("Expected policy to be invalid, got no validation error") + } + }) + } +} + +func testBuildRouteSelector() RouteSelector { + return RouteSelector{ + Hostnames: []gatewayapiv1beta1.Hostname{"toystore.kuadrant.io"}, + 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 b864d6e2f..e34640951 100644 --- a/api/v1beta2/zz_generated.deepcopy.go +++ b/api/v1beta2/zz_generated.deepcopy.go @@ -21,11 +21,277 @@ limitations under the License. package v1beta2 import ( + apiv1beta2 "github.com/kuadrant/authorino/api/v1beta2" "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/gateway-api/apis/v1beta1" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AuthPolicy) DeepCopyInto(out *AuthPolicy) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthPolicy. +func (in *AuthPolicy) DeepCopy() *AuthPolicy { + if in == nil { + return nil + } + out := new(AuthPolicy) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AuthPolicy) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AuthPolicyList) DeepCopyInto(out *AuthPolicyList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]AuthPolicy, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthPolicyList. +func (in *AuthPolicyList) DeepCopy() *AuthPolicyList { + if in == nil { + return nil + } + out := new(AuthPolicyList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AuthPolicyList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AuthPolicySpec) DeepCopyInto(out *AuthPolicySpec) { + *out = *in + in.TargetRef.DeepCopyInto(&out.TargetRef) + if in.RouteSelectors != nil { + in, out := &in.RouteSelectors, &out.RouteSelectors + *out = make([]RouteSelector, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.NamedPatterns != nil { + in, out := &in.NamedPatterns, &out.NamedPatterns + *out = make(map[string]apiv1beta2.PatternExpressions, len(*in)) + for key, val := range *in { + var outVal []apiv1beta2.PatternExpression + if val == nil { + (*out)[key] = nil + } else { + inVal := (*in)[key] + in, out := &inVal, &outVal + *out = make(apiv1beta2.PatternExpressions, len(*in)) + copy(*out, *in) + } + (*out)[key] = outVal + } + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]apiv1beta2.PatternExpressionOrRef, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + in.AuthScheme.DeepCopyInto(&out.AuthScheme) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthPolicySpec. +func (in *AuthPolicySpec) DeepCopy() *AuthPolicySpec { + if in == nil { + return nil + } + out := new(AuthPolicySpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AuthPolicyStatus) DeepCopyInto(out *AuthPolicyStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthPolicyStatus. +func (in *AuthPolicyStatus) DeepCopy() *AuthPolicyStatus { + if in == nil { + return nil + } + out := new(AuthPolicyStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AuthSchemeSpec) DeepCopyInto(out *AuthSchemeSpec) { + *out = *in + if in.Authentication != nil { + in, out := &in.Authentication, &out.Authentication + *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]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]AuthorizationSpec, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } + if in.Response != nil { + in, out := &in.Response, &out.Response + *out = new(ResponseSpec) + (*in).DeepCopyInto(*out) + } + if in.Callbacks != nil { + in, out := &in.Callbacks, &out.Callbacks + *out = make(map[string]CallbackSpec, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthSchemeSpec. +func (in *AuthSchemeSpec) DeepCopy() *AuthSchemeSpec { + if in == nil { + return nil + } + out := new(AuthSchemeSpec) + in.DeepCopyInto(out) + 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 @@ -63,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 @@ -182,6 +465,32 @@ func (in *RateLimitPolicyStatus) DeepCopy() *RateLimitPolicyStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ResponseSpec) DeepCopyInto(out *ResponseSpec) { + *out = *in + if in.Unauthenticated != nil { + in, out := &in.Unauthenticated, &out.Unauthenticated + *out = new(apiv1beta2.DenyWithSpec) + (*in).DeepCopyInto(*out) + } + 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 ResponseSpec. +func (in *ResponseSpec) DeepCopy() *ResponseSpec { + if in == nil { + return nil + } + out := new(ResponseSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RouteSelector) DeepCopyInto(out *RouteSelector) { *out = *in @@ -209,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 @@ -223,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 3b1d6b815..9efbcc92b 100644 --- a/bundle/manifests/kuadrant-operator.clusterserviceversion.yaml +++ b/bundle/manifests/kuadrant-operator.clusterserviceversion.yaml @@ -4,57 +4,6 @@ metadata: annotations: alm-examples: |- [ - { - "apiVersion": "kuadrant.io/v1beta1", - "kind": "AuthPolicy", - "metadata": { - "name": "toystore" - }, - "spec": { - "authScheme": { - "identity": [ - { - "apiKey": { - "allNamespaces": true, - "selector": { - "matchLabels": { - "app": "toystore" - } - } - }, - "credentials": { - "in": "authorization_header", - "keySelector": "APIKEY" - }, - "name": "friends" - } - ], - "response": [ - { - "json": { - "properties": [ - { - "name": "userID", - "valueFrom": { - "authJSON": "auth.identity.metadata.annotations.secret\\.kuadrant\\.io/user-id" - } - } - ] - }, - "name": "rate-limit-apikey", - "wrapper": "envoyDynamicMetadata", - "wrapperKey": "ext_auth_data" - } - ] - }, - "rules": null, - "targetRef": { - "group": "gateway.networking.k8s.io", - "kind": "HTTPRoute", - "name": "toystore" - } - } - }, { "apiVersion": "kuadrant.io/v1beta1", "kind": "Kuadrant", @@ -92,7 +41,7 @@ metadata: capabilities: Basic Install categories: Integration & Delivery containerImage: quay.io/kuadrant/kuadrant-operator:latest - createdAt: "2023-10-19T10:15:14Z" + createdAt: "2023-10-20T10:46:36Z" operators.operatorframework.io/builder: operator-sdk-v1.28.1 operators.operatorframework.io/project_layout: go.kubebuilder.io/v3 repository: https://github.com/Kuadrant/kuadrant-operator @@ -103,11 +52,9 @@ spec: apiservicedefinitions: {} customresourcedefinitions: owned: - - description: Enable AuthN and AuthZ based access control on workloads - displayName: AuthPolicy - kind: AuthPolicy + - kind: AuthPolicy name: authpolicies.kuadrant.io - version: v1beta1 + version: v1beta2 - description: Kuadrant is the Schema for the kuadrants API displayName: Kuadrant kind: Kuadrant diff --git a/bundle/manifests/kuadrant.io_authpolicies.yaml b/bundle/manifests/kuadrant.io_authpolicies.yaml index 7897bfbd2..971f8e947 100644 --- a/bundle/manifests/kuadrant.io_authpolicies.yaml +++ b/bundle/manifests/kuadrant.io_authpolicies.yaml @@ -6,6 +6,7 @@ metadata: creationTimestamp: null labels: app: kuadrant + gateway.networking.k8s.io/policy: inherited name: authpolicies.kuadrant.io spec: group: kuadrant.io @@ -16,7 +17,7 @@ spec: singular: authpolicy scope: Namespaced versions: - - name: v1beta1 + - name: v1beta2 schema: openAPIV3Schema: properties: @@ -34,226 +35,345 @@ spec: type: object spec: properties: - authScheme: - description: AuthSchemes are embedded Authorino's AuthConfigs - properties: - authorization: - description: Authorization is the list of authorization policies. - All policies in this list MUST evaluate to "true" for a request - be successful in the authorization phase. - items: - description: 'Authorization policy to be enforced. Apart from - "name", one of the following parameters is required and only - one of the following parameters is allowed: "opa", "json" - or "kubernetes".' - properties: - authzed: - description: Authzed authorization - properties: - endpoint: - description: Endpoint of the Authzed service. - type: string - insecure: - description: Insecure HTTP connection (i.e. disables - TLS verification) - type: boolean - permission: - description: The name of the permission (or relation) - on which to execute the check. + patterns: + additionalProperties: + items: + properties: + operator: + description: 'The binary operator to be applied to the content + fetched from the authorization JSON, for comparison with + "value". Possible values are: "eq" (equal to), "neq" (not + equal to), "incl" (includes; for arrays), "excl" (excludes; + for arrays), "matches" (regex)' + enum: + - eq + - neq + - incl + - excl + - matches + type: string + selector: + description: Path selector to fetch content from the authorization + JSON (e.g. 'request.method'). Any pattern supported by https://pkg.go.dev/github.com/tidwall/gjson + can be used. Authorino custom JSON path modifiers are also + supported. + type: string + value: + description: The value of reference for the comparison with + the content fetched from the authorization JSON. If used + with the "matches" operator, the value must compile to a + valid Golang regex. + type: string + type: object + type: array + description: Named sets of patterns that can be referred in `when` + conditions and in pattern-matching authorization policy rules. + type: object + 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: + 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: Static value + description: Value is the value of HTTP Header to + be matched. + maxLength: 4096 + minLength: 1 type: string - valueFrom: - description: Dynamic value - properties: - authJSON: - description: 'Selector to fetch a value from - the authorization JSON. It can be any path - pattern to fetch from the authorization JSON - (e.g. ''context.request.http.host'') or a - string template with variable placeholders - that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). - Any patterns supported by https://pkg.go.dev/github.com/tidwall/gjson - can be used. The following string modifiers - are available: @extract:{sep:" ",pos:0}, @replace{old:"",new:""}, - @case:upper|lower, @base64:encode|decode and - @strip.' - type: string - type: object + required: + - name + - value type: object - resource: - description: The resource on which to check the permission - or relation. + 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: - kind: - description: StaticOrDynamicValue is either a constant - static string value or a config for fetching a - value from a dynamic source (e.g. a path pattern - of authorization JSON) - properties: - value: - description: Static value - type: string - valueFrom: - description: Dynamic value - properties: - authJSON: - description: 'Selector to fetch a value - from the authorization JSON. It can be - any path pattern to fetch from the authorization - JSON (e.g. ''context.request.http.host'') - or a string template with variable placeholders - that resolve to patterns (e.g. "Hello, - {auth.identity.name}!"). Any patterns - supported by https://pkg.go.dev/github.com/tidwall/gjson - can be used. The following string modifiers - are available: @extract:{sep:" ",pos:0}, - @replace{old:"",new:""}, @case:upper|lower, - @base64:encode|decode and @strip.' - type: string - type: object - type: object name: - description: StaticOrDynamicValue is either a constant - static string value or a config for fetching a - value from a dynamic source (e.g. a path pattern - of authorization JSON) - properties: - value: - description: Static value - type: string - valueFrom: - description: Dynamic value - properties: - authJSON: - description: 'Selector to fetch a value - from the authorization JSON. It can be - any path pattern to fetch from the authorization - JSON (e.g. ''context.request.http.host'') - or a string template with variable placeholders - that resolve to patterns (e.g. "Hello, - {auth.identity.name}!"). Any patterns - supported by https://pkg.go.dev/github.com/tidwall/gjson - can be used. The following string modifiers - are available: @extract:{sep:" ",pos:0}, - @replace{old:"",new:""}, @case:upper|lower, - @base64:encode|decode and @strip.' - type: string - type: object - type: object - type: object - sharedSecretRef: - description: Reference to a Secret key whose value will - be used by Authorino to authenticate with the Authzed - service. - properties: - key: - description: The key of the secret to select from. Must - be a valid secret key. + 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 - name: - description: The name of the secret in the Authorino's - namespace to select from. + 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: - - key - name + - value type: object - subject: - description: The subject that will be checked for the - permission or relation. + maxItems: 16 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + type: object + type: array + type: object + type: array + rules: + description: The auth rules of the policy. See Authorino's AuthConfig + CRD for more details. + properties: + authentication: + additionalProperties: + properties: + anonymous: + description: Anonymous access. + type: object + apiKey: + description: Authentication based on API keys stored in + Kubernetes secrets. + properties: + allNamespaces: + default: false + description: Whether Authorino should look for API key + secrets in all namespaces or only in the same namespace + as the AuthConfig. Enabling this option in namespaced + Authorino instances has no effect. + type: boolean + selector: + description: Label selector used by Authorino to match + secrets from the cluster storing valid credentials + to authenticate to this service properties: - kind: - description: StaticOrDynamicValue is either a constant - static string value or a config for fetching a - value from a dynamic source (e.g. a path pattern - of authorization JSON) - properties: - value: - description: Static value - type: string - valueFrom: - description: Dynamic value - properties: - authJSON: - description: 'Selector to fetch a value - from the authorization JSON. It can be - any path pattern to fetch from the authorization - JSON (e.g. ''context.request.http.host'') - or a string template with variable placeholders - that resolve to patterns (e.g. "Hello, - {auth.identity.name}!"). Any patterns - supported by https://pkg.go.dev/github.com/tidwall/gjson - can be used. The following string modifiers - are available: @extract:{sep:" ",pos:0}, - @replace{old:"",new:""}, @case:upper|lower, - @base64:encode|decode and @strip.' - type: string - type: object - type: object - name: - description: StaticOrDynamicValue is either a constant - static string value or a config for fetching a - value from a dynamic source (e.g. a path pattern - of authorization JSON) - properties: - value: - description: Static value - type: string - valueFrom: - description: Dynamic value - properties: - authJSON: - description: 'Selector to fetch a value - from the authorization JSON. It can be - any path pattern to fetch from the authorization - JSON (e.g. ''context.request.http.host'') - or a string template with variable placeholders - that resolve to patterns (e.g. "Hello, - {auth.identity.name}!"). Any patterns - supported by https://pkg.go.dev/github.com/tidwall/gjson - can be used. The following string modifiers - are available: @extract:{sep:" ",pos:0}, - @replace{old:"",new:""}, @case:upper|lower, - @base64:encode|decode and @strip.' + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: A label selector requirement is a + selector that contains values, a key, and an + operator that relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. Valid operators are + In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string + values. If the operator is In or NotIn, + the values array must be non-empty. If the + operator is Exists or DoesNotExist, the + values array must be empty. This array is + replaced during a strategic merge patch. + items: type: string - type: object + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, + whose key field is "key", the operator is "In", + and the values array contains only "value". The + requirements are ANDed. type: object type: object + x-kubernetes-map-type: atomic required: - - endpoint + - selector type: object cache: - description: Caching options for the policy evaluation results - when enforcing this config. Omit it to avoid caching policy - evaluation results for this config. + description: Caching options for the resolved object returned + when applying this config. Omit it to avoid caching objects + for this config. properties: key: description: Key used to store the entry in the cache. - Cache entries from different metadata configs are - stored and managed separately regardless of the key. + The resolved key must be unique within the scope of + this particular config. properties: + selector: + description: 'Simple path selector to fetch content + from the authorization JSON (e.g. ''request.method'') + or a string template with variables that resolve + to patterns (e.g. "Hello, {auth.identity.name}!"). + Any pattern supported by https://pkg.go.dev/github.com/tidwall/gjson + can be used. The following Authorino custom modifiers + are supported: @extract:{sep:" ",pos:0}, @replace{old:"",new:""}, + @case:upper|lower, @base64:encode|decode and @strip.' + type: string value: description: Static value - type: string - valueFrom: - description: Dynamic value - properties: - authJSON: - description: 'Selector to fetch a value from - the authorization JSON. It can be any path - pattern to fetch from the authorization JSON - (e.g. ''context.request.http.host'') or a - string template with variable placeholders - that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). - Any patterns supported by https://pkg.go.dev/github.com/tidwall/gjson - can be used. The following string modifiers - are available: @extract:{sep:" ",pos:0}, @replace{old:"",new:""}, - @case:upper|lower, @base64:encode|decode and - @strip.' - type: string - type: object + x-kubernetes-preserve-unknown-fields: true type: object ttl: default: 60 @@ -263,273 +383,729 @@ spec: required: - key type: object - json: - description: JSON pattern matching authorization policy. + credentials: + description: Defines where credentials are required to be + passed in the request for authentication based on this + config. If omitted, it defaults to credentials passed + in the HTTP Authorization header and the "Bearer" prefix + prepended to the secret credential value. properties: - rules: - description: The rules that must all evaluate to "true" - for the request to be authorized. - items: - properties: - operator: - description: 'The binary operator to be applied - to the content fetched from the authorization - JSON, for comparison with "value". Possible - values are: "eq" (equal to), "neq" (not equal - to), "incl" (includes; for arrays), "excl" (excludes; - for arrays), "matches" (regex)' - enum: - - eq - - neq - - incl - - excl - - matches - type: string - patternRef: - description: Name of a named pattern - type: string - selector: - description: Any pattern supported by https://pkg.go.dev/github.com/tidwall/gjson. - The value is used to fetch content from the - input authorization JSON built by Authorino - along the identity and metadata phases. - type: string - value: - description: The value of reference for the comparison - with the content fetched from the authorization - JSON. If used with the "matches" operator, the - value must compile to a valid Golang regex. - type: string - type: object - type: array - required: - - rules + authorizationHeader: + properties: + prefix: + type: string + type: object + cookie: + properties: + name: + type: string + required: + - name + type: object + customHeader: + properties: + name: + type: string + required: + - name + type: object + queryString: + properties: + name: + type: string + required: + - name + type: object + type: object + defaults: + additionalProperties: + properties: + selector: + description: 'Simple path selector to fetch content + from the authorization JSON (e.g. ''request.method'') + or a string template with variables that resolve + to patterns (e.g. "Hello, {auth.identity.name}!"). + Any pattern supported by https://pkg.go.dev/github.com/tidwall/gjson + can be used. The following Authorino custom modifiers + are supported: @extract:{sep:" ",pos:0}, @replace{old:"",new:""}, + @case:upper|lower, @base64:encode|decode and @strip.' + type: string + value: + description: Static value + x-kubernetes-preserve-unknown-fields: true + type: object + description: Set default property values (claims) for the + resolved identity object, that are set before appending + the object to the authorization JSON. If the property + is already present in the resolved identity object, the + default value is ignored. It requires the resolved identity + object to always be a JSON object. Do not use this option + with identity objects of other JSON types (array, string, + etc). type: object - kubernetes: - description: Kubernetes authorization policy based on `SubjectAccessReview` - Path and Verb are inferred from the request. + jwt: + description: Authentication based on JWT tokens. properties: - groups: - description: Groups to test for. + issuerUrl: + description: URL of the issuer of the JWT. If `jwksUrl` + is omitted, Authorino will append the path to the + OpenID Connect Well-Known Discovery endpoint (i.e. + "/.well-known/openid-configuration") to this URL, + to discover the OIDC configuration where to obtain + the "jkws_uri" claim from. The value must coincide + with the value of the "iss" (issuer) claim of the + discovered OpenID Connect configuration. + type: string + ttl: + description: Decides how long to wait before refreshing + the JWKS (in seconds). If omitted, Authorino will + never refresh the JWKS. + type: integer + type: object + kubernetesTokenReview: + description: Authentication by Kubernetes token review. + properties: + audiences: + description: The list of audiences (scopes) that must + be claimed in a Kubernetes authentication token supplied + in the request, and reviewed by Authorino. If omitted, + Authorino will review tokens expecting the host name + of the requested protected service amongst the audiences. items: type: string type: array - resourceAttributes: - description: Use ResourceAttributes for checking permissions - on Kubernetes resources If omitted, it performs a - non-resource `SubjectAccessReview`, with verb and - path inferred from the request. + type: object + metrics: + default: false + description: Whether this config should generate individual + observability metrics + type: boolean + oauth2Introspection: + description: Authentication by OAuth2 token introspection. + properties: + credentialsRef: + description: Reference to a Kubernetes secret in the + same namespace, that stores client credentials to + the OAuth2 server. properties: - group: - description: StaticOrDynamicValue is either a constant - static string value or a config for fetching a - value from a dynamic source (e.g. a path pattern - of authorization JSON) + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, + uid?' + type: string + type: object + x-kubernetes-map-type: atomic + endpoint: + description: The full URL of the token introspection + endpoint. + type: string + tokenTypeHint: + description: The token type hint for the token introspection. + If omitted, it defaults to "access_token". + type: string + required: + - credentialsRef + - endpoint + type: object + overrides: + additionalProperties: + properties: + selector: + description: 'Simple path selector to fetch content + from the authorization JSON (e.g. ''request.method'') + or a string template with variables that resolve + to patterns (e.g. "Hello, {auth.identity.name}!"). + Any pattern supported by https://pkg.go.dev/github.com/tidwall/gjson + can be used. The following Authorino custom modifiers + are supported: @extract:{sep:" ",pos:0}, @replace{old:"",new:""}, + @case:upper|lower, @base64:encode|decode and @strip.' + type: string + value: + description: Static value + x-kubernetes-preserve-unknown-fields: true + type: object + description: Overrides the resolved identity object by setting + the additional properties (claims) specified in this config, + before appending the object to the authorization JSON. + It requires the resolved identity object to always be + a JSON object. Do not use this option with identity objects + of other JSON types (array, string, etc). + type: object + plain: + description: Identity object extracted from the context. + Use this method when authentication is performed beforehand + by a proxy and the resulting object passed to Authorino + as JSON in the auth request. + properties: + selector: + description: 'Simple path selector to fetch content + from the authorization JSON (e.g. ''request.method'') + or a string template with variables that resolve to + patterns (e.g. "Hello, {auth.identity.name}!"). Any + pattern supported by https://pkg.go.dev/github.com/tidwall/gjson + can be used. The following Authorino custom modifiers + are supported: @extract:{sep:" ",pos:0}, @replace{old:"",new:""}, + @case:upper|lower, @base64:encode|decode and @strip.' + type: string + required: + - selector + type: object + priority: + default: 0 + description: Priority group of the config. All configs 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: - value: - description: Static value + 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 - valueFrom: - description: Dynamic value + 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: - authJSON: - description: 'Selector to fetch a value - from the authorization JSON. It can be - any path pattern to fetch from the authorization - JSON (e.g. ''context.request.http.host'') - or a string template with variable placeholders - that resolve to patterns (e.g. "Hello, - {auth.identity.name}!"). Any patterns - supported by https://pkg.go.dev/github.com/tidwall/gjson - can be used. The following string modifiers - are available: @extract:{sep:" ",pos:0}, - @replace{old:"",new:""}, @case:upper|lower, - @base64:encode|decode and @strip.' + 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 - name: - description: StaticOrDynamicValue is either a constant - static string value or a config for fetching a - value from a dynamic source (e.g. a path pattern - of authorization JSON) + 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. + If present, all conditions must match for the config to + be enforced; otherwise, the config will be skipped. + items: + properties: + all: + description: A list of pattern expressions to be evaluated + as a logical AND. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + any: + description: A list of pattern expressions to be evaluated + as a logical OR. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + operator: + description: 'The binary operator to be applied to + the content fetched from the authorization JSON, + for comparison with "value". Possible values are: + "eq" (equal to), "neq" (not equal to), "incl" (includes; + for arrays), "excl" (excludes; for arrays), "matches" + (regex)' + enum: + - eq + - neq + - incl + - excl + - matches + type: string + patternRef: + description: Reference to a named set of pattern expressions + type: string + selector: + description: Path selector to fetch content from the + authorization JSON (e.g. 'request.method'). Any + pattern supported by https://pkg.go.dev/github.com/tidwall/gjson + can be used. Authorino custom JSON path modifiers + are also supported. + type: string + value: + description: The value of reference for the comparison + with the content fetched from the authorization + JSON. If used with the "matches" operator, the value + must compile to a valid Golang regex. + type: string + type: object + type: array + x509: + description: Authentication based on client X.509 certificates. + The certificates presented by the clients must be signed + by a trusted CA whose certificates are stored in Kubernetes + secrets. + properties: + allNamespaces: + default: false + description: Whether Authorino should look for TLS secrets + in all namespaces or only in the same namespace as + the AuthConfig. Enabling this option in namespaced + Authorino instances has no effect. + type: boolean + selector: + description: Label selector used by Authorino to match + secrets from the cluster storing trusted CA certificates + to validate clients trying to authenticate to this + service + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: A label selector requirement is a + selector that contains values, a key, and an + operator that relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. Valid operators are + In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string + values. If the operator is In or NotIn, + the values array must be non-empty. If the + operator is Exists or DoesNotExist, the + values array must be empty. This array is + replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, + whose key field is "key", the operator is "In", + and the values array contains only "value". The + requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + required: + - selector + type: object + type: object + description: Authentication configs. At least one config MUST + evaluate to a valid identity object for the auth request to + be successful. + type: object + authorization: + additionalProperties: + properties: + cache: + description: Caching options for the resolved object returned + when applying this config. Omit it to avoid caching objects + for this config. + properties: + key: + description: Key used to store the entry in the cache. + The resolved key must be unique within the scope of + this particular config. + properties: + selector: + description: 'Simple path selector to fetch content + from the authorization JSON (e.g. ''request.method'') + or a string template with variables that resolve + to patterns (e.g. "Hello, {auth.identity.name}!"). + Any pattern supported by https://pkg.go.dev/github.com/tidwall/gjson + can be used. The following Authorino custom modifiers + are supported: @extract:{sep:" ",pos:0}, @replace{old:"",new:""}, + @case:upper|lower, @base64:encode|decode and @strip.' + type: string + value: + description: Static value + x-kubernetes-preserve-unknown-fields: true + type: object + ttl: + default: 60 + description: Duration (in seconds) of the external data + in the cache before pulled again from the source. + type: integer + required: + - key + type: object + kubernetesSubjectAccessReview: + description: Authorization by Kubernetes SubjectAccessReview + properties: + groups: + description: Groups the user must be a member of or, + if `user` is omitted, the groups to check for authorization + in the Kubernetes RBAC. + items: + type: string + type: array + resourceAttributes: + description: Use resourceAttributes to check permissions + on Kubernetes resources. If omitted, it performs a + non-resource SubjectAccessReview, with verb and path + inferred from the request. + properties: + group: + description: API group of the resource. Use '*' + for all API groups. properties: + selector: + description: 'Simple path selector to fetch + content from the authorization JSON (e.g. + ''request.method'') or a string template with + variables that resolve to patterns (e.g. "Hello, + {auth.identity.name}!"). Any pattern supported + by https://pkg.go.dev/github.com/tidwall/gjson + can be used. The following Authorino custom + modifiers are supported: @extract:{sep:" ",pos:0}, + @replace{old:"",new:""}, @case:upper|lower, + @base64:encode|decode and @strip.' + type: string value: description: Static value + x-kubernetes-preserve-unknown-fields: true + type: object + name: + description: Resource name Omit it to check for + authorization on all resources of the specified + kind. + properties: + selector: + description: 'Simple path selector to fetch + content from the authorization JSON (e.g. + ''request.method'') or a string template with + variables that resolve to patterns (e.g. "Hello, + {auth.identity.name}!"). Any pattern supported + by https://pkg.go.dev/github.com/tidwall/gjson + can be used. The following Authorino custom + modifiers are supported: @extract:{sep:" ",pos:0}, + @replace{old:"",new:""}, @case:upper|lower, + @base64:encode|decode and @strip.' type: string - valueFrom: - description: Dynamic value - properties: - authJSON: - description: 'Selector to fetch a value - from the authorization JSON. It can be - any path pattern to fetch from the authorization - JSON (e.g. ''context.request.http.host'') - or a string template with variable placeholders - that resolve to patterns (e.g. "Hello, - {auth.identity.name}!"). Any patterns - supported by https://pkg.go.dev/github.com/tidwall/gjson - can be used. The following string modifiers - are available: @extract:{sep:" ",pos:0}, - @replace{old:"",new:""}, @case:upper|lower, - @base64:encode|decode and @strip.' - type: string - type: object + value: + description: Static value + x-kubernetes-preserve-unknown-fields: true type: object namespace: - description: StaticOrDynamicValue is either a constant - static string value or a config for fetching a - value from a dynamic source (e.g. a path pattern - of authorization JSON) + description: Namespace where the user must have + permissions on the resource. properties: + selector: + description: 'Simple path selector to fetch + content from the authorization JSON (e.g. + ''request.method'') or a string template with + variables that resolve to patterns (e.g. "Hello, + {auth.identity.name}!"). Any pattern supported + by https://pkg.go.dev/github.com/tidwall/gjson + can be used. The following Authorino custom + modifiers are supported: @extract:{sep:" ",pos:0}, + @replace{old:"",new:""}, @case:upper|lower, + @base64:encode|decode and @strip.' + type: string value: description: Static value - type: string - valueFrom: - description: Dynamic value - properties: - authJSON: - description: 'Selector to fetch a value - from the authorization JSON. It can be - any path pattern to fetch from the authorization - JSON (e.g. ''context.request.http.host'') - or a string template with variable placeholders - that resolve to patterns (e.g. "Hello, - {auth.identity.name}!"). Any patterns - supported by https://pkg.go.dev/github.com/tidwall/gjson - can be used. The following string modifiers - are available: @extract:{sep:" ",pos:0}, - @replace{old:"",new:""}, @case:upper|lower, - @base64:encode|decode and @strip.' - type: string - type: object + x-kubernetes-preserve-unknown-fields: true type: object resource: - description: StaticOrDynamicValue is either a constant - static string value or a config for fetching a - value from a dynamic source (e.g. a path pattern - of authorization JSON) + description: Resource kind Use '*' for all resource + kinds. properties: + selector: + description: 'Simple path selector to fetch + content from the authorization JSON (e.g. + ''request.method'') or a string template with + variables that resolve to patterns (e.g. "Hello, + {auth.identity.name}!"). Any pattern supported + by https://pkg.go.dev/github.com/tidwall/gjson + can be used. The following Authorino custom + modifiers are supported: @extract:{sep:" ",pos:0}, + @replace{old:"",new:""}, @case:upper|lower, + @base64:encode|decode and @strip.' + type: string value: description: Static value - type: string - valueFrom: - description: Dynamic value - properties: - authJSON: - description: 'Selector to fetch a value - from the authorization JSON. It can be - any path pattern to fetch from the authorization - JSON (e.g. ''context.request.http.host'') - or a string template with variable placeholders - that resolve to patterns (e.g. "Hello, - {auth.identity.name}!"). Any patterns - supported by https://pkg.go.dev/github.com/tidwall/gjson - can be used. The following string modifiers - are available: @extract:{sep:" ",pos:0}, - @replace{old:"",new:""}, @case:upper|lower, - @base64:encode|decode and @strip.' - type: string - type: object + x-kubernetes-preserve-unknown-fields: true type: object subresource: - description: StaticOrDynamicValue is either a constant - static string value or a config for fetching a - value from a dynamic source (e.g. a path pattern - of authorization JSON) + description: Subresource kind properties: + selector: + description: 'Simple path selector to fetch + content from the authorization JSON (e.g. + ''request.method'') or a string template with + variables that resolve to patterns (e.g. "Hello, + {auth.identity.name}!"). Any pattern supported + by https://pkg.go.dev/github.com/tidwall/gjson + can be used. The following Authorino custom + modifiers are supported: @extract:{sep:" ",pos:0}, + @replace{old:"",new:""}, @case:upper|lower, + @base64:encode|decode and @strip.' + type: string value: description: Static value - type: string - valueFrom: - description: Dynamic value - properties: - authJSON: - description: 'Selector to fetch a value - from the authorization JSON. It can be - any path pattern to fetch from the authorization - JSON (e.g. ''context.request.http.host'') - or a string template with variable placeholders - that resolve to patterns (e.g. "Hello, - {auth.identity.name}!"). Any patterns - supported by https://pkg.go.dev/github.com/tidwall/gjson - can be used. The following string modifiers - are available: @extract:{sep:" ",pos:0}, - @replace{old:"",new:""}, @case:upper|lower, - @base64:encode|decode and @strip.' - type: string - type: object + x-kubernetes-preserve-unknown-fields: true type: object verb: - description: StaticOrDynamicValue is either a constant - static string value or a config for fetching a - value from a dynamic source (e.g. a path pattern - of authorization JSON) + description: Verb to check for authorization on + the resource. Use '*' for all verbs. properties: + selector: + description: 'Simple path selector to fetch + content from the authorization JSON (e.g. + ''request.method'') or a string template with + variables that resolve to patterns (e.g. "Hello, + {auth.identity.name}!"). Any pattern supported + by https://pkg.go.dev/github.com/tidwall/gjson + can be used. The following Authorino custom + modifiers are supported: @extract:{sep:" ",pos:0}, + @replace{old:"",new:""}, @case:upper|lower, + @base64:encode|decode and @strip.' + type: string value: description: Static value - type: string - valueFrom: - description: Dynamic value - properties: - authJSON: - description: 'Selector to fetch a value - from the authorization JSON. It can be - any path pattern to fetch from the authorization - JSON (e.g. ''context.request.http.host'') - or a string template with variable placeholders - that resolve to patterns (e.g. "Hello, - {auth.identity.name}!"). Any patterns - supported by https://pkg.go.dev/github.com/tidwall/gjson - can be used. The following string modifiers - are available: @extract:{sep:" ",pos:0}, - @replace{old:"",new:""}, @case:upper|lower, - @base64:encode|decode and @strip.' - type: string - type: object + x-kubernetes-preserve-unknown-fields: true type: object type: object user: - description: User to test for. If without "Groups", - then is it interpreted as "What if User were not a - member of any groups" + description: User to check for authorization in the + Kubernetes RBAC. Omit it to check for group authorization + only. properties: + selector: + description: 'Simple path selector to fetch content + from the authorization JSON (e.g. ''request.method'') + or a string template with variables that resolve + to patterns (e.g. "Hello, {auth.identity.name}!"). + Any pattern supported by https://pkg.go.dev/github.com/tidwall/gjson + can be used. The following Authorino custom modifiers + are supported: @extract:{sep:" ",pos:0}, @replace{old:"",new:""}, + @case:upper|lower, @base64:encode|decode and @strip.' + type: string value: description: Static value - type: string - valueFrom: - description: Dynamic value - properties: - authJSON: - description: 'Selector to fetch a value from - the authorization JSON. It can be any path - pattern to fetch from the authorization JSON - (e.g. ''context.request.http.host'') or a - string template with variable placeholders - that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). - Any patterns supported by https://pkg.go.dev/github.com/tidwall/gjson - can be used. The following string modifiers - are available: @extract:{sep:" ",pos:0}, @replace{old:"",new:""}, - @case:upper|lower, @base64:encode|decode and - @strip.' - type: string - type: object + x-kubernetes-preserve-unknown-fields: true type: object - required: - - user type: object metrics: default: false - description: Whether this authorization config should generate - individual observability metrics + description: Whether this config should generate individual + observability metrics type: boolean - name: - description: Name of the authorization policy. It can be - used to refer to the resolved authorization object in - other configs. - type: string opa: - description: Open Policy Agent (OPA) authorization policy. + description: Open Policy Agent (OPA) Rego policy. properties: allValues: default: false @@ -540,9 +1116,73 @@ spec: all Rego rules can affect performance of OPA policies during reconciliation (policy precompile) and at runtime. type: boolean - externalRegistry: - description: External registry of OPA policies. + externalPolicy: + description: 'Settings for fetching the OPA policy from + an external registry. Use it alternatively to ''rego''. + For the configurations of the HTTP request, the following + options are not implemented: ''method'', ''body'', + ''bodyParameters'', ''contentType'', ''headers'', + ''oauth2''. Use it only with: ''url'', ''sharedSecret'', + ''credentials''.' properties: + body: + description: Raw body of the HTTP request. Supersedes + 'bodyParameters'; use either one or the other. + Use it with method=POST; for GET requests, set + parameters as query string in the 'endpoint' (placeholders + can be used). + properties: + selector: + description: 'Simple path selector to fetch + content from the authorization JSON (e.g. + ''request.method'') or a string template with + variables that resolve to patterns (e.g. "Hello, + {auth.identity.name}!"). Any pattern supported + by https://pkg.go.dev/github.com/tidwall/gjson + can be used. The following Authorino custom + modifiers are supported: @extract:{sep:" ",pos:0}, + @replace{old:"",new:""}, @case:upper|lower, + @base64:encode|decode and @strip.' + type: string + value: + description: Static value + x-kubernetes-preserve-unknown-fields: true + type: object + bodyParameters: + additionalProperties: + properties: + selector: + description: 'Simple path selector to fetch + content from the authorization JSON (e.g. + ''request.method'') or a string template + with variables that resolve to patterns + (e.g. "Hello, {auth.identity.name}!"). Any + pattern supported by https://pkg.go.dev/github.com/tidwall/gjson + can be used. The following Authorino custom + modifiers are supported: @extract:{sep:" + ",pos:0}, @replace{old:"",new:""}, @case:upper|lower, + @base64:encode|decode and @strip.' + type: string + value: + description: Static value + x-kubernetes-preserve-unknown-fields: true + type: object + description: Custom parameters to encode in the + body of the HTTP request. Superseded by 'body'; + use either one or the other. Use it with method=POST; + for GET requests, set parameters as query string + in the 'endpoint' (placeholders can be used). + type: object + contentType: + default: application/x-www-form-urlencoded + description: Content-Type of the request body. Shapes + how 'bodyParameters' are encoded. Use it with + method=POST; for GET requests, Content-Type is + automatically set to 'text/plain'. + enum: + - application/x-www-form-urlencoded + - application/json + type: string credentials: description: Defines where client credentials will be passed in the request to the service. If omitted, @@ -550,48 +1190,131 @@ spec: HTTP Authorization header and the "Bearer" prefix expected prepended to the secret value. properties: - in: - default: authorization_header - description: The location in the request where - client credentials shall be passed on requests - authenticating with this identity source/authentication - mode. - enum: - - authorization_header - - custom_header - - query - - cookie - type: string - keySelector: - description: Used in conjunction with the `in` - parameter. When used with `authorization_header`, - the value is the prefix of the client credentials - string, separated by a white-space, in the - HTTP Authorization header (e.g. "Bearer", - "Basic"). When used with `custom_header`, - `query` or `cookie`, the value is the name - of the HTTP header, query string parameter - or cookie key, respectively. - type: string - required: - - keySelector - type: object - endpoint: - description: Endpoint of the HTTP external registry. - The endpoint must respond with either plain/text - or application/json content-type. In the latter - case, the JSON returned in the body must include - a path `result.raw`, where the raw Rego policy - will be extracted from. This complies with the - specification of the OPA REST API (https://www.openpolicyagent.org/docs/latest/rest-api/#get-a-policy). - type: string - sharedSecretRef: - description: Reference to a Secret key whose value - will be passed by Authorino in the request. The - HTTP service can use the shared secret to authenticate - the origin of the request. - properties: - key: + authorizationHeader: + properties: + prefix: + type: string + type: object + cookie: + properties: + name: + type: string + required: + - name + type: object + customHeader: + properties: + name: + type: string + required: + - name + type: object + queryString: + properties: + name: + type: string + required: + - name + type: object + type: object + headers: + additionalProperties: + properties: + selector: + description: 'Simple path selector to fetch + content from the authorization JSON (e.g. + ''request.method'') or a string template + with variables that resolve to patterns + (e.g. "Hello, {auth.identity.name}!"). Any + pattern supported by https://pkg.go.dev/github.com/tidwall/gjson + can be used. The following Authorino custom + modifiers are supported: @extract:{sep:" + ",pos:0}, @replace{old:"",new:""}, @case:upper|lower, + @base64:encode|decode and @strip.' + type: string + value: + description: Static value + x-kubernetes-preserve-unknown-fields: true + type: object + description: Custom headers in the HTTP request. + type: object + method: + default: GET + description: 'HTTP verb used in the request to the + service. Accepted values: GET (default), POST. + When the request method is POST, the authorization + JSON is passed in the body of the request.' + enum: + - GET + - POST + - PUT + - PATCH + - DELETE + - HEAD + - OPTIONS + - CONNECT + - TRACE + type: string + oauth2: + description: Authentication with the HTTP service + by OAuth2 Client Credentials grant. + properties: + cache: + default: true + description: Caches and reuses the token until + expired. Set it to false to force fetch the + token at every authorization request regardless + of expiration. + type: boolean + clientId: + description: OAuth2 Client ID. + type: string + clientSecretRef: + description: Reference to a Kuberentes Secret + key that stores that OAuth2 Client Secret. + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + description: The name of the secret in the + Authorino's namespace to select from. + type: string + required: + - key + - name + type: object + extraParams: + additionalProperties: + type: string + description: Optional extra parameters for the + requests to the token URL. + type: object + scopes: + description: Optional scopes for the client + credentials grant, if supported by he OAuth2 + server. + items: + type: string + type: array + tokenUrl: + description: Token endpoint URL of the OAuth2 + resource server. + type: string + required: + - clientId + - clientSecretRef + - tokenUrl + type: object + sharedSecretRef: + description: Reference to a Secret key whose value + will be passed by Authorino in the request. The + HTTP service can use the shared secret to authenticate + the origin of the request. Ignored if used together + with oauth2. + properties: + key: description: The key of the secret to select from. Must be a valid secret key. type: string @@ -608,8 +1331,18 @@ spec: data in the cache before pulled again from the source. type: integer + url: + description: Endpoint URL of the HTTP service. The + value can include variable placeholders in the + format "{selector}", where "selector" is any pattern + supported by https://pkg.go.dev/github.com/tidwall/gjson + and selects value from the authorization JSON. + E.g. https://ext-auth-server.io/metadata?p={request.path} + type: string + required: + - url type: object - inlineRego: + rego: description: Authorization policy as a Rego language document. The Rego document must include the "allow" condition, set by Authorino to "false" by default @@ -618,19 +1351,448 @@ spec: in line 1. type: string type: object + patternMatching: + description: Pattern-matching authorization rules. + properties: + patterns: + items: + properties: + all: + description: A list of pattern expressions to + be evaluated as a logical AND. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + any: + description: A list of pattern expressions to + be evaluated as a logical OR. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + operator: + description: 'The binary operator to be applied + to the content fetched from the authorization + JSON, for comparison with "value". Possible + values are: "eq" (equal to), "neq" (not equal + to), "incl" (includes; for arrays), "excl" (excludes; + for arrays), "matches" (regex)' + enum: + - eq + - neq + - incl + - excl + - matches + type: string + patternRef: + description: Reference to a named set of pattern + expressions + type: string + selector: + description: Path selector to fetch content from + the authorization JSON (e.g. 'request.method'). + Any pattern supported by https://pkg.go.dev/github.com/tidwall/gjson + can be used. Authorino custom JSON path modifiers + are also supported. + type: string + value: + description: The value of reference for the comparison + with the content fetched from the authorization + JSON. If used with the "matches" operator, the + value must compile to a valid Golang regex. + type: string + type: object + type: array + required: + - patterns + type: object priority: default: 0 description: Priority group of the config. All configs 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 + spicedb: + description: Authorization decision delegated to external + Authzed/SpiceDB server. + properties: + endpoint: + description: Hostname and port number to the GRPC interface + of the SpiceDB server (e.g. spicedb:50051). + type: string + insecure: + description: Insecure HTTP connection (i.e. disables + TLS verification) + type: boolean + permission: + description: The name of the permission (or relation) + on which to execute the check. + properties: + selector: + description: 'Simple path selector to fetch content + from the authorization JSON (e.g. ''request.method'') + or a string template with variables that resolve + to patterns (e.g. "Hello, {auth.identity.name}!"). + Any pattern supported by https://pkg.go.dev/github.com/tidwall/gjson + can be used. The following Authorino custom modifiers + are supported: @extract:{sep:" ",pos:0}, @replace{old:"",new:""}, + @case:upper|lower, @base64:encode|decode and @strip.' + type: string + value: + description: Static value + x-kubernetes-preserve-unknown-fields: true + type: object + resource: + description: The resource on which to check the permission + or relation. + properties: + kind: + properties: + selector: + description: 'Simple path selector to fetch + content from the authorization JSON (e.g. + ''request.method'') or a string template with + variables that resolve to patterns (e.g. "Hello, + {auth.identity.name}!"). Any pattern supported + by https://pkg.go.dev/github.com/tidwall/gjson + can be used. The following Authorino custom + modifiers are supported: @extract:{sep:" ",pos:0}, + @replace{old:"",new:""}, @case:upper|lower, + @base64:encode|decode and @strip.' + type: string + value: + description: Static value + x-kubernetes-preserve-unknown-fields: true + type: object + name: + properties: + selector: + description: 'Simple path selector to fetch + content from the authorization JSON (e.g. + ''request.method'') or a string template with + variables that resolve to patterns (e.g. "Hello, + {auth.identity.name}!"). Any pattern supported + by https://pkg.go.dev/github.com/tidwall/gjson + can be used. The following Authorino custom + modifiers are supported: @extract:{sep:" ",pos:0}, + @replace{old:"",new:""}, @case:upper|lower, + @base64:encode|decode and @strip.' + type: string + value: + description: Static value + x-kubernetes-preserve-unknown-fields: true + type: object + type: object + sharedSecretRef: + description: Reference to a Secret key whose value will + be used by Authorino to authenticate with the Authzed + service. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: The name of the secret in the Authorino's + namespace to select from. + type: string + required: + - key + - name + type: object + subject: + description: The subject that will be checked for the + permission or relation. + properties: + kind: + properties: + selector: + description: 'Simple path selector to fetch + content from the authorization JSON (e.g. + ''request.method'') or a string template with + variables that resolve to patterns (e.g. "Hello, + {auth.identity.name}!"). Any pattern supported + by https://pkg.go.dev/github.com/tidwall/gjson + can be used. The following Authorino custom + modifiers are supported: @extract:{sep:" ",pos:0}, + @replace{old:"",new:""}, @case:upper|lower, + @base64:encode|decode and @strip.' + type: string + value: + description: Static value + x-kubernetes-preserve-unknown-fields: true + type: object + name: + properties: + selector: + description: 'Simple path selector to fetch + content from the authorization JSON (e.g. + ''request.method'') or a string template with + variables that resolve to patterns (e.g. "Hello, + {auth.identity.name}!"). Any pattern supported + by https://pkg.go.dev/github.com/tidwall/gjson + can be used. The following Authorino custom + modifiers are supported: @extract:{sep:" ",pos:0}, + @replace{old:"",new:""}, @case:upper|lower, + @base64:encode|decode and @strip.' + type: string + value: + description: Static value + x-kubernetes-preserve-unknown-fields: true + type: object + type: object + required: + - endpoint + type: object when: - description: Conditions for Authorino to enforce this authorization - policy. If omitted, the config will be enforced for all - requests. If present, all conditions must match for the - config to be enforced; otherwise, the config will be skipped. + description: Conditions for Authorino to enforce this config. + If omitted, the config will be enforced for all requests. + If present, all conditions must match for the config to + be enforced; otherwise, the config will be skipped. items: properties: + all: + description: A list of pattern expressions to be evaluated + as a logical AND. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + any: + description: A list of pattern expressions to be evaluated + as a logical OR. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array operator: description: 'The binary operator to be applied to the content fetched from the authorization JSON, @@ -646,13 +1808,14 @@ spec: - matches type: string patternRef: - description: Name of a named pattern + description: Reference to a named set of pattern expressions type: string selector: - description: Any pattern supported by https://pkg.go.dev/github.com/tidwall/gjson. - The value is used to fetch content from the input - authorization JSON built by Authorino along the - identity and metadata phases. + description: Path selector to fetch content from the + authorization JSON (e.g. 'request.method'). Any + pattern supported by https://pkg.go.dev/github.com/tidwall/gjson + can be used. Authorino custom JSON path modifiers + are also supported. type: string value: description: The value of reference for the comparison @@ -662,541 +1825,511 @@ spec: type: string type: object type: array - required: - - name type: object - type: array - denyWith: - description: Custom denial response codes, statuses and headers - to override default 40x's. - properties: - unauthenticated: - description: Denial status customization when the request - is unauthenticated. - properties: - body: - description: HTTP response body to override the default - denial body. - properties: - value: - description: Static value - type: string - valueFrom: - description: Dynamic value - properties: - authJSON: - description: 'Selector to fetch a value from the - authorization JSON. It can be any path pattern - to fetch from the authorization JSON (e.g. ''context.request.http.host'') - or a string template with variable placeholders - that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). - Any patterns supported by https://pkg.go.dev/github.com/tidwall/gjson - can be used. The following string modifiers - are available: @extract:{sep:" ",pos:0}, @replace{old:"",new:""}, - @case:upper|lower, @base64:encode|decode and - @strip.' - type: string - type: object - type: object - code: - description: HTTP status code to override the default - denial status code. - format: int64 - maximum: 599 - minimum: 300 - type: integer - headers: - description: HTTP response headers to override the default - denial headers. - items: + description: Authorization policies. All policies MUST evaluate + to "allowed = true" for the auth request be successful. + type: object + callbacks: + additionalProperties: + properties: + cache: + description: Caching options for the resolved object returned + when applying this config. Omit it to avoid caching objects + for this config. + properties: + key: + description: Key used to store the entry in the cache. + The resolved key must be unique within the scope of + this particular config. properties: - name: - description: The name of the JSON property + selector: + description: 'Simple path selector to fetch content + from the authorization JSON (e.g. ''request.method'') + or a string template with variables that resolve + to patterns (e.g. "Hello, {auth.identity.name}!"). + Any pattern supported by https://pkg.go.dev/github.com/tidwall/gjson + can be used. The following Authorino custom modifiers + are supported: @extract:{sep:" ",pos:0}, @replace{old:"",new:""}, + @case:upper|lower, @base64:encode|decode and @strip.' type: string value: - description: Static value of the JSON property + description: Static value x-kubernetes-preserve-unknown-fields: true - valueFrom: - description: Dynamic value of the JSON property - properties: - authJSON: - description: 'Selector to fetch a value from - the authorization JSON. It can be any path - pattern to fetch from the authorization JSON - (e.g. ''context.request.http.host'') or a - string template with variable placeholders - that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). - Any patterns supported by https://pkg.go.dev/github.com/tidwall/gjson - can be used. The following string modifiers - are available: @extract:{sep:" ",pos:0}, @replace{old:"",new:""}, - @case:upper|lower, @base64:encode|decode and - @strip.' - type: string - type: object - required: - - name type: object - type: array - message: - description: HTTP message to override the default denial - message. - properties: - value: - description: Static value - type: string - valueFrom: - description: Dynamic value - properties: - authJSON: - description: 'Selector to fetch a value from the - authorization JSON. It can be any path pattern - to fetch from the authorization JSON (e.g. ''context.request.http.host'') - or a string template with variable placeholders - that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). - Any patterns supported by https://pkg.go.dev/github.com/tidwall/gjson - can be used. The following string modifiers - are available: @extract:{sep:" ",pos:0}, @replace{old:"",new:""}, - @case:upper|lower, @base64:encode|decode and - @strip.' - type: string - type: object - type: object - type: object - unauthorized: - description: Denial status customization when the request - is unauthorized. - properties: - body: - description: HTTP response body to override the default - denial body. - properties: - value: - description: Static value - type: string - valueFrom: - description: Dynamic value + ttl: + default: 60 + description: Duration (in seconds) of the external data + in the cache before pulled again from the source. + type: integer + required: + - key + type: object + http: + description: Settings of the external HTTP request + properties: + body: + description: Raw body of the HTTP request. Supersedes + 'bodyParameters'; use either one or the other. Use + it with method=POST; for GET requests, set parameters + as query string in the 'endpoint' (placeholders can + be used). + properties: + selector: + description: 'Simple path selector to fetch content + from the authorization JSON (e.g. ''request.method'') + or a string template with variables that resolve + to patterns (e.g. "Hello, {auth.identity.name}!"). + Any pattern supported by https://pkg.go.dev/github.com/tidwall/gjson + can be used. The following Authorino custom modifiers + are supported: @extract:{sep:" ",pos:0}, @replace{old:"",new:""}, + @case:upper|lower, @base64:encode|decode and @strip.' + type: string + value: + description: Static value + x-kubernetes-preserve-unknown-fields: true + type: object + bodyParameters: + additionalProperties: properties: - authJSON: - description: 'Selector to fetch a value from the - authorization JSON. It can be any path pattern - to fetch from the authorization JSON (e.g. ''context.request.http.host'') - or a string template with variable placeholders - that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). - Any patterns supported by https://pkg.go.dev/github.com/tidwall/gjson - can be used. The following string modifiers - are available: @extract:{sep:" ",pos:0}, @replace{old:"",new:""}, - @case:upper|lower, @base64:encode|decode and - @strip.' + selector: + description: 'Simple path selector to fetch content + from the authorization JSON (e.g. ''request.method'') + or a string template with variables that resolve + to patterns (e.g. "Hello, {auth.identity.name}!"). + Any pattern supported by https://pkg.go.dev/github.com/tidwall/gjson + can be used. The following Authorino custom + modifiers are supported: @extract:{sep:" ",pos:0}, + @replace{old:"",new:""}, @case:upper|lower, + @base64:encode|decode and @strip.' type: string + value: + description: Static value + x-kubernetes-preserve-unknown-fields: true type: object - type: object - code: - description: HTTP status code to override the default - denial status code. - format: int64 - maximum: 599 - minimum: 300 - type: integer - headers: - description: HTTP response headers to override the default - denial headers. - items: + description: Custom parameters to encode in the body + of the HTTP request. Superseded by 'body'; use either + one or the other. Use it with method=POST; for GET + requests, set parameters as query string in the 'endpoint' + (placeholders can be used). + type: object + contentType: + default: application/x-www-form-urlencoded + description: Content-Type of the request body. Shapes + how 'bodyParameters' are encoded. Use it with method=POST; + for GET requests, Content-Type is automatically set + to 'text/plain'. + enum: + - application/x-www-form-urlencoded + - application/json + type: string + credentials: + description: Defines where client credentials will be + passed in the request to the service. If omitted, + it defaults to client credentials passed in the HTTP + Authorization header and the "Bearer" prefix expected + prepended to the secret value. properties: - name: - description: The name of the JSON property - type: string - value: - description: Static value of the JSON property - x-kubernetes-preserve-unknown-fields: true - valueFrom: - description: Dynamic value of the JSON property + authorizationHeader: properties: - authJSON: - description: 'Selector to fetch a value from - the authorization JSON. It can be any path - pattern to fetch from the authorization JSON - (e.g. ''context.request.http.host'') or a - string template with variable placeholders - that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). - Any patterns supported by https://pkg.go.dev/github.com/tidwall/gjson - can be used. The following string modifiers - are available: @extract:{sep:" ",pos:0}, @replace{old:"",new:""}, - @case:upper|lower, @base64:encode|decode and - @strip.' + prefix: type: string type: object - required: - - name + cookie: + properties: + name: + type: string + required: + - name + type: object + customHeader: + properties: + name: + type: string + required: + - name + type: object + queryString: + properties: + name: + type: string + required: + - name + type: object type: object - type: array - message: - description: HTTP message to override the default denial - message. - properties: - value: - description: Static value - type: string - valueFrom: - description: Dynamic value + headers: + additionalProperties: properties: - authJSON: - description: 'Selector to fetch a value from the - authorization JSON. It can be any path pattern - to fetch from the authorization JSON (e.g. ''context.request.http.host'') - or a string template with variable placeholders - that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). - Any patterns supported by https://pkg.go.dev/github.com/tidwall/gjson - can be used. The following string modifiers - are available: @extract:{sep:" ",pos:0}, @replace{old:"",new:""}, - @case:upper|lower, @base64:encode|decode and - @strip.' + selector: + description: 'Simple path selector to fetch content + from the authorization JSON (e.g. ''request.method'') + or a string template with variables that resolve + to patterns (e.g. "Hello, {auth.identity.name}!"). + Any pattern supported by https://pkg.go.dev/github.com/tidwall/gjson + can be used. The following Authorino custom + modifiers are supported: @extract:{sep:" ",pos:0}, + @replace{old:"",new:""}, @case:upper|lower, + @base64:encode|decode and @strip.' type: string + value: + description: Static value + x-kubernetes-preserve-unknown-fields: true type: object - type: object - type: object - type: object - identity: - description: List of identity sources/authentication modes. At - least one config of this list MUST evaluate to a valid identity - for a request to be successful in the identity verification - phase. - items: - description: 'The identity source/authentication mode config. - Apart from "name", one of the following parameters is required - and only one of the following parameters is allowed: "oicd", - "apiKey" or "kubernetes".' - properties: - anonymous: - type: object - apiKey: - properties: - allNamespaces: - default: false - description: Whether Authorino should look for API key - secrets in all namespaces or only in the same namespace - as the AuthConfig. Enabling this option in namespaced - Authorino instances has no effect. - type: boolean - selector: - description: Label selector used by Authorino to match - secrets from the cluster storing valid credentials - to authenticate to this service - properties: - matchExpressions: - description: matchExpressions is a list of label - selector requirements. The requirements are ANDed. - items: - description: A label selector requirement is a - selector that contains values, a key, and an - operator that relates the key and values. - properties: - key: - description: key is the label key that the - selector applies to. - type: string - operator: - description: operator represents a key's relationship - to a set of values. Valid operators are - In, NotIn, Exists and DoesNotExist. - type: string - values: - description: values is an array of string - values. If the operator is In or NotIn, - the values array must be non-empty. If the - operator is Exists or DoesNotExist, the - values array must be empty. This array is - replaced during a strategic merge patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchLabels: - additionalProperties: - type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator is "In", - and the values array contains only "value". The - requirements are ANDed. - type: object + description: Custom headers in the HTTP request. type: object - x-kubernetes-map-type: atomic - required: - - selector - type: object - cache: - description: Caching options for the identity resolved when - applying this config. Omit it to avoid caching identity - objects for this config. - properties: - key: - description: Key used to store the entry in the cache. - Cache entries from different metadata configs are - stored and managed separately regardless of the key. + method: + default: GET + description: 'HTTP verb used in the request to the service. + Accepted values: GET (default), POST. When the request + method is POST, the authorization JSON is passed in + the body of the request.' + enum: + - GET + - POST + - PUT + - PATCH + - DELETE + - HEAD + - OPTIONS + - CONNECT + - TRACE + type: string + oauth2: + description: Authentication with the HTTP service by + OAuth2 Client Credentials grant. properties: - value: - description: Static value + cache: + default: true + description: Caches and reuses the token until expired. + Set it to false to force fetch the token at every + authorization request regardless of expiration. + type: boolean + clientId: + description: OAuth2 Client ID. type: string - valueFrom: - description: Dynamic value + clientSecretRef: + description: Reference to a Kuberentes Secret key + that stores that OAuth2 Client Secret. properties: - authJSON: - description: 'Selector to fetch a value from - the authorization JSON. It can be any path - pattern to fetch from the authorization JSON - (e.g. ''context.request.http.host'') or a - string template with variable placeholders - that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). - Any patterns supported by https://pkg.go.dev/github.com/tidwall/gjson - can be used. The following string modifiers - are available: @extract:{sep:" ",pos:0}, @replace{old:"",new:""}, - @case:upper|lower, @base64:encode|decode and - @strip.' + key: + description: The key of the secret to select + from. Must be a valid secret key. type: string + name: + description: The name of the secret in the Authorino's + namespace to select from. + type: string + required: + - key + - name type: object - type: object - ttl: - default: 60 - description: Duration (in seconds) of the external data - in the cache before pulled again from the source. - type: integer - required: - - key - type: object - credentials: - description: Defines where client credentials are required - to be passed in the request for this identity source/authentication - mode. If omitted, it defaults to client credentials passed - in the HTTP Authorization header and the "Bearer" prefix - expected prepended to the credentials value (token, API - key, etc). - properties: - in: - default: authorization_header - description: The location in the request where client - credentials shall be passed on requests authenticating - with this identity source/authentication mode. - enum: - - authorization_header - - custom_header - - query - - cookie - type: string - keySelector: - description: Used in conjunction with the `in` parameter. - When used with `authorization_header`, the value is - the prefix of the client credentials string, separated - by a white-space, in the HTTP Authorization header - (e.g. "Bearer", "Basic"). When used with `custom_header`, - `query` or `cookie`, the value is the name of the - HTTP header, query string parameter or cookie key, - respectively. - type: string - required: - - keySelector - type: object - extendedProperties: - description: Extends the resolved identity object with additional - custom properties before appending to the authorization - JSON. It requires the resolved identity object to always - be of the JSON type 'object'. Other JSON types (array, - string, etc) will break. - items: - properties: - name: - description: The name of the JSON property - type: string - overwrite: - default: false - description: Whether the value should overwrite the - value of an existing property with the same name. - type: boolean - value: - description: Static value of the JSON property - x-kubernetes-preserve-unknown-fields: true - valueFrom: - description: Dynamic value of the JSON property - properties: - authJSON: - description: 'Selector to fetch a value from the - authorization JSON. It can be any path pattern - to fetch from the authorization JSON (e.g. ''context.request.http.host'') - or a string template with variable placeholders - that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). - Any patterns supported by https://pkg.go.dev/github.com/tidwall/gjson - can be used. The following string modifiers - are available: @extract:{sep:" ",pos:0}, @replace{old:"",new:""}, - @case:upper|lower, @base64:encode|decode and - @strip.' - type: string - type: object - required: - - name - type: object - type: array - kubernetes: - properties: - audiences: - description: The list of audiences (scopes) that must - be claimed in a Kubernetes authentication token supplied - in the request, and reviewed by Authorino. If omitted, - Authorino will review tokens expecting the host name - of the requested protected service amongst the audiences. - items: - type: string - type: array - type: object - metrics: - default: false - description: Whether this identity config should generate - individual observability metrics - type: boolean - mtls: - properties: - allNamespaces: - default: false - description: Whether Authorino should look for TLS secrets - in all namespaces or only in the same namespace as - the AuthConfig. Enabling this option in namespaced - Authorino instances has no effect. - type: boolean - selector: - description: Label selector used by Authorino to match - secrets from the cluster storing trusted CA certificates - to validate clients trying to authenticate to this - service - properties: - matchExpressions: - description: matchExpressions is a list of label - selector requirements. The requirements are ANDed. - items: - description: A label selector requirement is a - selector that contains values, a key, and an - operator that relates the key and values. - properties: - key: - description: key is the label key that the - selector applies to. - type: string - operator: - description: operator represents a key's relationship - to a set of values. Valid operators are - In, NotIn, Exists and DoesNotExist. - type: string - values: - description: values is an array of string - values. If the operator is In or NotIn, - the values array must be non-empty. If the - operator is Exists or DoesNotExist, the - values array must be empty. This array is - replaced during a strategic merge patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchLabels: + extraParams: additionalProperties: type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator is "In", - and the values array contains only "value". The - requirements are ANDed. + description: Optional extra parameters for the requests + to the token URL. type: object - type: object - x-kubernetes-map-type: atomic - required: - - selector - type: object - name: - description: The name of this identity source/authentication - mode. It usually identifies a source of identities or - group of users/clients of the protected service. It can - be used to refer to the resolved identity object in other - configs. - type: string - oauth2: - properties: - credentialsRef: - description: Reference to a Kubernetes secret in the - same namespace, that stores client credentials to - the OAuth2 server. + scopes: + description: Optional scopes for the client credentials + grant, if supported by he OAuth2 server. + items: + type: string + type: array + tokenUrl: + description: Token endpoint URL of the OAuth2 resource + server. + type: string + required: + - clientId + - clientSecretRef + - tokenUrl + type: object + sharedSecretRef: + description: Reference to a Secret key whose value will + be passed by Authorino in the request. The HTTP service + can use the shared secret to authenticate the origin + of the request. Ignored if used together with oauth2. properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string name: - description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - TODO: Add other useful fields. apiVersion, kind, - uid?' + description: The name of the secret in the Authorino's + namespace to select from. type: string + required: + - key + - name type: object - x-kubernetes-map-type: atomic - tokenIntrospectionUrl: - description: The full URL of the token introspection - endpoint. - type: string - tokenTypeHint: - description: The token type hint for the token introspection. - If omitted, it defaults to "access_token". - type: string - required: - - credentialsRef - - tokenIntrospectionUrl - type: object - oidc: - properties: - endpoint: - description: Endpoint of the OIDC issuer. Authorino - will append to this value the well-known path to the - OpenID Connect discovery endpoint (i.e. "/.well-known/openid-configuration"), - used to automatically discover the OpenID Connect - configuration, whose set of claims is expected to - include (among others) the "jkws_uri" claim. The value - must coincide with the value of the "iss" (issuer) - claim of the discovered OpenID Connect configuration. + url: + description: Endpoint URL of the HTTP service. The value + can include variable placeholders in the format "{selector}", + where "selector" is any pattern supported by https://pkg.go.dev/github.com/tidwall/gjson + and selects value from the authorization JSON. E.g. + https://ext-auth-server.io/metadata?p={request.path} type: string - ttl: - description: Decides how long to wait before refreshing - the OIDC configuration (in seconds). - type: integer required: - - endpoint - type: object - plain: - properties: - authJSON: - description: 'Selector to fetch a value from the authorization - JSON. It can be any path pattern to fetch from the - authorization JSON (e.g. ''context.request.http.host'') - or a string template with variable placeholders that - resolve to patterns (e.g. "Hello, {auth.identity.name}!"). - Any patterns supported by https://pkg.go.dev/github.com/tidwall/gjson - can be used. The following string modifiers are available: - @extract:{sep:" ",pos:0}, @replace{old:"",new:""}, - @case:upper|lower, @base64:encode|decode and @strip.' - type: string + - url type: object + metrics: + default: false + description: Whether this config should generate individual + observability metrics + type: boolean priority: default: 0 description: Priority group of the config. All configs 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 identity - config. If omitted, the config will be enforced for all - requests. If present, all conditions must match for the - config to be enforced; otherwise, the config will be skipped. + description: Conditions for Authorino to enforce this config. + If omitted, the config will be enforced for all requests. + If present, all conditions must match for the config to + be enforced; otherwise, the config will be skipped. items: properties: + all: + description: A list of pattern expressions to be evaluated + as a logical AND. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + any: + description: A list of pattern expressions to be evaluated + as a logical OR. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array operator: description: 'The binary operator to be applied to the content fetched from the authorization JSON, @@ -1212,13 +2345,14 @@ spec: - matches type: string patternRef: - description: Name of a named pattern + description: Reference to a named set of pattern expressions type: string selector: - description: Any pattern supported by https://pkg.go.dev/github.com/tidwall/gjson. - The value is used to fetch content from the input - authorization JSON built by Authorino along the - identity and metadata phases. + description: Path selector to fetch content from the + authorization JSON (e.g. 'request.method'). Any + pattern supported by https://pkg.go.dev/github.com/tidwall/gjson + can be used. Authorino custom JSON path modifiers + are also supported. type: string value: description: The value of reference for the comparison @@ -1229,47 +2363,38 @@ spec: type: object type: array required: - - name + - http type: object - type: array + description: Callback functions. Authorino sends callbacks at + the end of the auth pipeline to the endpoints specified in this + config. + type: object metadata: - description: List of metadata source configs. Authorino fetches - JSON content from sources on this list on every request. - items: - description: 'The metadata config. Apart from "name", one of - the following parameters is required and only one of the following - parameters is allowed: "http", userInfo" or "uma".' + additionalProperties: properties: cache: - description: Caching options for the external metadata fetched - when applying this config. Omit it to avoid caching metadata - from this source. + description: Caching options for the resolved object returned + when applying this config. Omit it to avoid caching objects + for this config. properties: key: description: Key used to store the entry in the cache. - Cache entries from different metadata configs are - stored and managed separately regardless of the key. + The resolved key must be unique within the scope of + this particular config. properties: + selector: + description: 'Simple path selector to fetch content + from the authorization JSON (e.g. ''request.method'') + or a string template with variables that resolve + to patterns (e.g. "Hello, {auth.identity.name}!"). + Any pattern supported by https://pkg.go.dev/github.com/tidwall/gjson + can be used. The following Authorino custom modifiers + are supported: @extract:{sep:" ",pos:0}, @replace{old:"",new:""}, + @case:upper|lower, @base64:encode|decode and @strip.' + type: string value: description: Static value - type: string - valueFrom: - description: Dynamic value - properties: - authJSON: - description: 'Selector to fetch a value from - the authorization JSON. It can be any path - pattern to fetch from the authorization JSON - (e.g. ''context.request.http.host'') or a - string template with variable placeholders - that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). - Any patterns supported by https://pkg.go.dev/github.com/tidwall/gjson - can be used. The following string modifiers - are available: @extract:{sep:" ",pos:0}, @replace{old:"",new:""}, - @case:upper|lower, @base64:encode|decode and - @strip.' - type: string - type: object + x-kubernetes-preserve-unknown-fields: true type: object ttl: default: 60 @@ -1280,8 +2405,7 @@ spec: - key type: object http: - description: Generic HTTP interface to obtain authorization - metadata from a HTTP service. + description: External source of auth metadata via HTTP request properties: body: description: Raw body of the HTTP request. Supersedes @@ -1290,62 +2414,44 @@ spec: as query string in the 'endpoint' (placeholders can be used). properties: + selector: + description: 'Simple path selector to fetch content + from the authorization JSON (e.g. ''request.method'') + or a string template with variables that resolve + to patterns (e.g. "Hello, {auth.identity.name}!"). + Any pattern supported by https://pkg.go.dev/github.com/tidwall/gjson + can be used. The following Authorino custom modifiers + are supported: @extract:{sep:" ",pos:0}, @replace{old:"",new:""}, + @case:upper|lower, @base64:encode|decode and @strip.' + type: string value: description: Static value - type: string - valueFrom: - description: Dynamic value - properties: - authJSON: - description: 'Selector to fetch a value from - the authorization JSON. It can be any path - pattern to fetch from the authorization JSON - (e.g. ''context.request.http.host'') or a - string template with variable placeholders - that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). - Any patterns supported by https://pkg.go.dev/github.com/tidwall/gjson - can be used. The following string modifiers - are available: @extract:{sep:" ",pos:0}, @replace{old:"",new:""}, - @case:upper|lower, @base64:encode|decode and - @strip.' - type: string - type: object + x-kubernetes-preserve-unknown-fields: true type: object bodyParameters: - description: Custom parameters to encode in the body - of the HTTP request. Superseded by 'body'; use either - one or the other. Use it with method=POST; for GET - requests, set parameters as query string in the 'endpoint' - (placeholders can be used). - items: + additionalProperties: properties: - name: - description: The name of the JSON property + selector: + description: 'Simple path selector to fetch content + from the authorization JSON (e.g. ''request.method'') + or a string template with variables that resolve + to patterns (e.g. "Hello, {auth.identity.name}!"). + Any pattern supported by https://pkg.go.dev/github.com/tidwall/gjson + can be used. The following Authorino custom + modifiers are supported: @extract:{sep:" ",pos:0}, + @replace{old:"",new:""}, @case:upper|lower, + @base64:encode|decode and @strip.' type: string value: - description: Static value of the JSON property + description: Static value x-kubernetes-preserve-unknown-fields: true - valueFrom: - description: Dynamic value of the JSON property - properties: - authJSON: - description: 'Selector to fetch a value from - the authorization JSON. It can be any path - pattern to fetch from the authorization - JSON (e.g. ''context.request.http.host'') - or a string template with variable placeholders - that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). - Any patterns supported by https://pkg.go.dev/github.com/tidwall/gjson - can be used. The following string modifiers - are available: @extract:{sep:" ",pos:0}, - @replace{old:"",new:""}, @case:upper|lower, - @base64:encode|decode and @strip.' - type: string - type: object - required: - - name type: object - type: array + description: Custom parameters to encode in the body + of the HTTP request. Superseded by 'body'; use either + one or the other. Use it with method=POST; for GET + requests, set parameters as query string in the 'endpoint' + (placeholders can be used). + type: object contentType: default: application/x-www-form-urlencoded description: Content-Type of the request body. Shapes @@ -1363,68 +2469,53 @@ spec: Authorization header and the "Bearer" prefix expected prepended to the secret value. properties: - in: - default: authorization_header - description: The location in the request where client - credentials shall be passed on requests authenticating - with this identity source/authentication mode. - enum: - - authorization_header - - custom_header - - query - - cookie - type: string - keySelector: - description: Used in conjunction with the `in` parameter. - When used with `authorization_header`, the value - is the prefix of the client credentials string, - separated by a white-space, in the HTTP Authorization - header (e.g. "Bearer", "Basic"). When used with - `custom_header`, `query` or `cookie`, the value - is the name of the HTTP header, query string parameter - or cookie key, respectively. - type: string - required: - - keySelector + authorizationHeader: + properties: + prefix: + type: string + type: object + cookie: + properties: + name: + type: string + required: + - name + type: object + customHeader: + properties: + name: + type: string + required: + - name + type: object + queryString: + properties: + name: + type: string + required: + - name + type: object type: object - endpoint: - description: Endpoint of the HTTP service. The endpoint - accepts variable placeholders in the format "{selector}", - where "selector" is any pattern supported by https://pkg.go.dev/github.com/tidwall/gjson - and selects value from the authorization JSON. E.g. - https://ext-auth-server.io/metadata?p={context.request.http.path} - type: string headers: - description: Custom headers in the HTTP request. - items: + additionalProperties: properties: - name: - description: The name of the JSON property + selector: + description: 'Simple path selector to fetch content + from the authorization JSON (e.g. ''request.method'') + or a string template with variables that resolve + to patterns (e.g. "Hello, {auth.identity.name}!"). + Any pattern supported by https://pkg.go.dev/github.com/tidwall/gjson + can be used. The following Authorino custom + modifiers are supported: @extract:{sep:" ",pos:0}, + @replace{old:"",new:""}, @case:upper|lower, + @base64:encode|decode and @strip.' type: string value: - description: Static value of the JSON property + description: Static value x-kubernetes-preserve-unknown-fields: true - valueFrom: - description: Dynamic value of the JSON property - properties: - authJSON: - description: 'Selector to fetch a value from - the authorization JSON. It can be any path - pattern to fetch from the authorization - JSON (e.g. ''context.request.http.host'') - or a string template with variable placeholders - that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). - Any patterns supported by https://pkg.go.dev/github.com/tidwall/gjson - can be used. The following string modifiers - are available: @extract:{sep:" ",pos:0}, - @replace{old:"",new:""}, @case:upper|lower, - @base64:encode|decode and @strip.' - type: string - type: object - required: - - name type: object - type: array + description: Custom headers in the HTTP request. + type: object method: default: GET description: 'HTTP verb used in the request to the service. @@ -1434,6 +2525,13 @@ spec: enum: - GET - POST + - PUT + - PATCH + - DELETE + - HEAD + - OPTIONS + - CONNECT + - TRACE type: string oauth2: description: Authentication with the HTTP service by @@ -1503,25 +2601,254 @@ spec: - key - name type: object + url: + description: Endpoint URL of the HTTP service. The value + can include variable placeholders in the format "{selector}", + where "selector" is any pattern supported by https://pkg.go.dev/github.com/tidwall/gjson + and selects value from the authorization JSON. E.g. + https://ext-auth-server.io/metadata?p={request.path} + type: string required: - - endpoint + - url type: object metrics: default: false - description: Whether this metadata config should generate - individual observability metrics + description: Whether this config should generate individual + observability metrics type: boolean - name: - description: The name of the metadata source. It can be - used to refer to the resolved metadata object in other - configs. - type: string priority: default: 0 description: Priority group of the config. All configs 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 uma: description: User-Managed Access (UMA) source of resource data. @@ -1549,225 +2876,37 @@ spec: type: object userInfo: description: OpendID Connect UserInfo linked to an OIDC - identity config of this same spec. + authentication config specified in this same AuthConfig. properties: identitySource: - description: The name of an OIDC identity source included - in the "identity" section and whose OpenID Connect - configuration discovered includes the OIDC "userinfo_endpoint" - claim. - type: string - required: - - identitySource - type: object - when: - description: Conditions for Authorino to apply this metadata - config. If omitted, the config will be applied for all - requests. If present, all conditions must match for the - config to be applied; otherwise, the config will be skipped. - items: - properties: - operator: - description: 'The binary operator to be applied to - the content fetched from the authorization JSON, - for comparison with "value". Possible values are: - "eq" (equal to), "neq" (not equal to), "incl" (includes; - for arrays), "excl" (excludes; for arrays), "matches" - (regex)' - enum: - - eq - - neq - - incl - - excl - - matches - type: string - patternRef: - description: Name of a named pattern - type: string - selector: - description: Any pattern supported by https://pkg.go.dev/github.com/tidwall/gjson. - The value is used to fetch content from the input - authorization JSON built by Authorino along the - identity and metadata phases. - type: string - value: - description: The value of reference for the comparison - with the content fetched from the authorization - JSON. If used with the "matches" operator, the value - must compile to a valid Golang regex. - type: string - type: object - type: array - required: - - name - type: object - type: array - patterns: - additionalProperties: - items: - properties: - operator: - description: 'The binary operator to be applied to the - content fetched from the authorization JSON, for comparison - with "value". Possible values are: "eq" (equal to), - "neq" (not equal to), "incl" (includes; for arrays), - "excl" (excludes; for arrays), "matches" (regex)' - enum: - - eq - - neq - - incl - - excl - - matches - type: string - selector: - description: Any pattern supported by https://pkg.go.dev/github.com/tidwall/gjson. - The value is used to fetch content from the input authorization - JSON built by Authorino along the identity and metadata - phases. - type: string - value: - description: The value of reference for the comparison - with the content fetched from the authorization JSON. - If used with the "matches" operator, the value must - compile to a valid Golang regex. - type: string - type: object - type: array - description: Named sets of JSON patterns that can be referred - in `when` conditionals and in JSON-pattern matching policy rules. - type: object - response: - description: List of response configs. Authorino gathers data - from the auth pipeline to build custom responses for the client. - items: - description: 'Dynamic response to return to the client. Apart - from "name", one of the following parameters is required and - only one of the following parameters is allowed: "wristband" - or "json".' - properties: - cache: - description: Caching options for dynamic responses built - when applying this config. Omit it to avoid caching dynamic - responses for this config. - properties: - key: - description: Key used to store the entry in the cache. - Cache entries from different metadata configs are - stored and managed separately regardless of the key. - properties: - value: - description: Static value - type: string - valueFrom: - description: Dynamic value - properties: - authJSON: - description: 'Selector to fetch a value from - the authorization JSON. It can be any path - pattern to fetch from the authorization JSON - (e.g. ''context.request.http.host'') or a - string template with variable placeholders - that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). - Any patterns supported by https://pkg.go.dev/github.com/tidwall/gjson - can be used. The following string modifiers - are available: @extract:{sep:" ",pos:0}, @replace{old:"",new:""}, - @case:upper|lower, @base64:encode|decode and - @strip.' - type: string - type: object - type: object - ttl: - default: 60 - description: Duration (in seconds) of the external data - in the cache before pulled again from the source. - type: integer - required: - - key - type: object - json: - properties: - properties: - description: List of JSON property-value pairs to be - added to the dynamic response. - items: - properties: - name: - description: The name of the JSON property - type: string - value: - description: Static value of the JSON property - x-kubernetes-preserve-unknown-fields: true - valueFrom: - description: Dynamic value of the JSON property - properties: - authJSON: - description: 'Selector to fetch a value from - the authorization JSON. It can be any path - pattern to fetch from the authorization - JSON (e.g. ''context.request.http.host'') - or a string template with variable placeholders - that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). - Any patterns supported by https://pkg.go.dev/github.com/tidwall/gjson - can be used. The following string modifiers - are available: @extract:{sep:" ",pos:0}, - @replace{old:"",new:""}, @case:upper|lower, - @base64:encode|decode and @strip.' - type: string - type: object - required: - - name - type: object - type: array - required: - - properties - type: object - metrics: - default: false - description: Whether this response config should generate - individual observability metrics - type: boolean - name: - description: Name of the custom response. It can be used - to refer to the resolved response object in other configs. - type: string - plain: - description: StaticOrDynamicValue is either a constant static - string value or a config for fetching a value from a dynamic - source (e.g. a path pattern of authorization JSON) - properties: - value: - description: Static value - type: string - valueFrom: - description: Dynamic value - properties: - authJSON: - description: 'Selector to fetch a value from the - authorization JSON. It can be any path pattern - to fetch from the authorization JSON (e.g. ''context.request.http.host'') - or a string template with variable placeholders - that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). - Any patterns supported by https://pkg.go.dev/github.com/tidwall/gjson - can be used. The following string modifiers are - available: @extract:{sep:" ",pos:0}, @replace{old:"",new:""}, - @case:upper|lower, @base64:encode|decode and @strip.' - type: string - type: object - type: object - priority: - default: 0 - description: Priority group of the config. All configs in - the same priority group are evaluated concurrently; consecutive - priority groups are evaluated sequentially. - type: integer + description: The name of an OIDC-enabled JWT authentication + config whose OpenID Connect configuration discovered + includes the OIDC "userinfo_endpoint" claim. + type: string + required: + - identitySource + type: object when: - description: Conditions for Authorino to enforce this custom - response config. If omitted, the config will be enforced - for all requests. If present, all conditions must match - for the config to be enforced; otherwise, the config will - be skipped. + description: Conditions for Authorino to enforce this config. + If omitted, the config will be enforced for all requests. + If present, all conditions must match for the config to + be enforced; otherwise, the config will be skipped. items: properties: + all: + description: A list of pattern expressions to be evaluated + as a logical AND. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + any: + description: A list of pattern expressions to be evaluated + as a logical OR. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array operator: description: 'The binary operator to be applied to the content fetched from the authorization JSON, @@ -1783,13 +2922,14 @@ spec: - matches type: string patternRef: - description: Name of a named pattern + description: Reference to a named set of pattern expressions type: string selector: - description: Any pattern supported by https://pkg.go.dev/github.com/tidwall/gjson. - The value is used to fetch content from the input - authorization JSON built by Authorino along the - identity and metadata phases. + description: Path selector to fetch content from the + authorization JSON (e.g. 'request.method'). Any + pattern supported by https://pkg.go.dev/github.com/tidwall/gjson + can be used. Authorino custom JSON path modifiers + are also supported. type: string value: description: The value of reference for the comparison @@ -1799,160 +2939,1155 @@ spec: type: string type: object type: array - wrapper: - default: httpHeader - description: How Authorino wraps the response. Use "httpHeader" - (default) to wrap the response in an HTTP header; or "envoyDynamicMetadata" - to wrap the response as Envoy Dynamic Metadata - enum: - - httpHeader - - envoyDynamicMetadata - type: string - wrapperKey: - description: The name of key used in the wrapped response - (name of the HTTP header or property of the Envoy Dynamic - Metadata JSON). If omitted, it will be set to the name - of the configuration. - type: string - wristband: - properties: - customClaims: - description: Any claims to be added to the wristband - token apart from the standard JWT claims (iss, iat, - exp) added by default. - items: - properties: - name: - description: The name of the JSON property - type: string - value: - description: Static value of the JSON property - x-kubernetes-preserve-unknown-fields: true - valueFrom: - description: Dynamic value of the JSON property + type: object + description: Metadata sources. Authorino fetches auth metadata + as JSON from sources specified in this config. + type: object + response: + description: Response items. Authorino builds custom responses + to the client of the auth request. + properties: + success: + description: 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. + properties: + dynamicMetadata: + additionalProperties: + properties: + cache: + description: Caching options for the resolved object + returned when applying this config. Omit it to + avoid caching objects for this config. + properties: + key: + description: Key used to store the entry in + the cache. The resolved key must be unique + within the scope of this particular config. + properties: + selector: + description: 'Simple path selector to fetch + content from the authorization JSON (e.g. + ''request.method'') or a string template + with variables that resolve to patterns + (e.g. "Hello, {auth.identity.name}!"). + Any pattern supported by https://pkg.go.dev/github.com/tidwall/gjson + can be used. The following Authorino custom + modifiers are supported: @extract:{sep:" + ",pos:0}, @replace{old:"",new:""}, @case:upper|lower, + @base64:encode|decode and @strip.' + type: string + value: + description: Static value + x-kubernetes-preserve-unknown-fields: true + type: object + ttl: + default: 60 + description: Duration (in seconds) of the external + data in the cache before pulled again from + the source. + type: integer + required: + - key + type: object + json: + description: JSON object Specify it as the list + of properties of the object, whose values can + combine static values and values selected from + the authorization JSON. + properties: properties: - authJSON: - description: 'Selector to fetch a value from - the authorization JSON. It can be any path - pattern to fetch from the authorization - JSON (e.g. ''context.request.http.host'') - or a string template with variable placeholders - that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). - Any patterns supported by https://pkg.go.dev/github.com/tidwall/gjson - can be used. The following string modifiers - are available: @extract:{sep:" ",pos:0}, - @replace{old:"",new:""}, @case:upper|lower, - @base64:encode|decode and @strip.' + additionalProperties: + properties: + selector: + description: 'Simple path selector to + fetch content from the authorization + JSON (e.g. ''request.method'') or a + string template with variables that + resolve to patterns (e.g. "Hello, {auth.identity.name}!"). + Any pattern supported by https://pkg.go.dev/github.com/tidwall/gjson + can be used. The following Authorino + custom modifiers are supported: @extract:{sep:" + ",pos:0}, @replace{old:"",new:""}, @case:upper|lower, + @base64:encode|decode and @strip.' + type: string + value: + description: Static value + x-kubernetes-preserve-unknown-fields: true + type: object + type: object + required: + - properties + type: object + key: + description: The key used to add the custom response + item (name of the HTTP header or root property + of the Dynamic Metadata object). If omitted, it + will be set to the name of the response config. + type: string + metrics: + default: false + description: Whether this config should generate + individual observability metrics + type: boolean + plain: + description: Plain text content + properties: + selector: + description: 'Simple path selector to fetch + content from the authorization JSON (e.g. + ''request.method'') or a string template with + variables that resolve to patterns (e.g. "Hello, + {auth.identity.name}!"). Any pattern supported + by https://pkg.go.dev/github.com/tidwall/gjson + can be used. The following Authorino custom + modifiers are supported: @extract:{sep:" ",pos:0}, + @replace{old:"",new:""}, @case:upper|lower, + @base64:encode|decode and @strip.' + type: string + value: + description: Static value + x-kubernetes-preserve-unknown-fields: true + type: object + priority: + default: 0 + description: Priority group of the config. All configs + 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 + for all requests. If present, all conditions must + match for the config to be enforced; otherwise, + the config will be skipped. + items: + properties: + all: + description: A list of pattern expressions + to be evaluated as a logical AND. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + any: + description: A list of pattern expressions + to be evaluated as a logical OR. + items: + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + operator: + description: 'The binary operator to be applied + to the content fetched from the authorization + JSON, for comparison with "value". Possible + values are: "eq" (equal to), "neq" (not + equal to), "incl" (includes; for arrays), + "excl" (excludes; for arrays), "matches" + (regex)' + enum: + - eq + - neq + - incl + - excl + - matches + type: string + patternRef: + description: Reference to a named set of pattern + expressions + type: string + selector: + description: Path selector to fetch content + from the authorization JSON (e.g. 'request.method'). + Any pattern supported by https://pkg.go.dev/github.com/tidwall/gjson + can be used. Authorino custom JSON path + modifiers are also supported. + type: string + value: + description: The value of reference for the + comparison with the content fetched from + the authorization JSON. If used with the + "matches" operator, the value must compile + to a valid Golang regex. type: string type: object - required: - - name - type: object - type: array - issuer: - description: 'The endpoint to the Authorino service - that issues the wristband (format: ://:/, - where = /://:/, where + = /://:/, where + = /://:/, - where = /://:/, where + = /://:/, where + = / 0 { + authConfig.Spec.NamedPatterns = namedPatterns + } + + // 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.Conditions) > 0 { + authConfig.Spec.Conditions = append(ap.Spec.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 + }), + }, } } - hostnames := make([]string, 0, len(uniqueHostnamesMap)) - for k := range uniqueHostnamesMap { - hostnames = append(hostnames, k) + // 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 hostnames, nil + return mergeConditionsFromRouteSelectorsIntoConfigs(ap, route, authConfig) } // authConfigName returns the name of Authorino AuthConfig CR. @@ -119,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 fa280d055..1c872749d 100644 --- a/controllers/authpolicy_controller.go +++ b/controllers/authpolicy_controller.go @@ -13,7 +13,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/source" gatewayapiv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" - api "github.com/kuadrant/kuadrant-operator/api/v1beta1" + api "github.com/kuadrant/kuadrant-operator/api/v1beta2" "github.com/kuadrant/kuadrant-operator/pkg/common" "github.com/kuadrant/kuadrant-operator/pkg/reconcilers" ) @@ -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 74318cea2..fbe3a39f4 100644 --- a/controllers/authpolicy_controller_test.go +++ b/controllers/authpolicy_controller_test.go @@ -4,492 +4,1252 @@ package controllers import ( "context" + "encoding/json" "path/filepath" + "strings" "time" - authorinov1beta1 "github.com/kuadrant/authorino/api/v1beta1" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" secv1beta1resources "istio.io/client-go/pkg/apis/security/v1beta1" 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" gatewayapiv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" - kuadrantv1beta1 "github.com/kuadrant/kuadrant-operator/api/v1beta1" + authorinoapi "github.com/kuadrant/authorino/api/v1beta2" + api "github.com/kuadrant/kuadrant-operator/api/v1beta2" "github.com/kuadrant/kuadrant-operator/pkg/common" ) 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 - } - - if !common.IsHTTPRouteAccepted(existingRoute) { - logf.Log.V(1).Info("[WARN] route not accepted") - return false - } - - return true + err := k8sClient.Get(context.Background(), client.ObjectKeyFromObject(route), existingRoute) + return err == nil && common.IsHTTPRouteAccepted(existingRoute) }, 15*time.Second, 5*time.Second).Should(BeTrue()) + }) - 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 := &kuadrantv1beta1.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), + It("Attaches policy to the Gateway", func() { + policy := &api.AuthPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gw-auth", 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 := &authorinov1beta1.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: "Gateway", + Name: testGatewayName, + Namespace: ptr.To(gatewayapiv1beta1.Namespace(testNamespace)), + }, + AuthScheme: testBasicAuthScheme(), + }, } + policy.Spec.AuthScheme.Authentication["apiKey"].ApiKey.Selector.MatchLabels["admin"] = "yes" - // 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 Istio's AuthorizationPolicy existence - iapKey := types.NamespacedName{ - Name: istioAuthorizationPolicyName(CustomGatewayName, authpolicies[idx].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) - if err != nil && apierrors.IsNotFound(err) { - return true - } - return false - }, 2*time.Minute, 5*time.Second).Should(BeTrue()) + // check policy status + Eventually(testPolicyIsReady(policy), 30*time.Second, 5*time.Second).Should(BeTrue()) - // check Authorino's AuthConfig existence - acKey := types.NamespacedName{ - Name: authConfigName(client.ObjectKeyFromObject(authpolicies[idx])), - Namespace: testNamespace, - } - Eventually(func() bool { - err := k8sClient.Get(context.Background(), acKey, &authorinov1beta1.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()) - } + // 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*"})) + + // 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, + }, + 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(), policy) + logf.Log.V(1).Info("Creating AuthPolicy", "key", client.ObjectKeyFromObject(policy).String(), "error", err) Expect(err).ToNot(HaveOccurred()) - typedNamespace := gatewayapiv1beta1.Namespace(testNamespace) - policy := &kuadrantv1beta1.AuthPolicy{ + // 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{"*.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, }, - Spec: kuadrantv1beta1.AuthPolicySpec{ + 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)), }, - AuthRules: []kuadrantv1beta1.AuthRule{ - { - Hosts: []string{"*.admin.toystore.com"}, - Methods: []string{"DELETE", "POST"}, - Paths: []string{"/admin*"}, - }, + 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{ { - Methods: []string{"GET"}, - Paths: []string{"/private*"}, + Method: ptr.To(gatewayapiv1beta1.HTTPMethod("POST")), }, }, + }, + } + err = k8sClient.Create(context.Background(), otherRoute) + Expect(err).ToNot(HaveOccurred()) + + // 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(), policy) + err = k8sClient.Create(context.Background(), gwPolicy) + logf.Log.V(1).Info("Creating AuthPolicy", "key", client.ObjectKeyFromObject(gwPolicy).String(), "error", err) Expect(err).ToNot(HaveOccurred()) - kapKey := client.ObjectKey{Name: "toystore", Namespace: testNamespace} - // Check KAP status is available + // 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 { - existingKAP := &kuadrantv1beta1.AuthPolicy{} - err := k8sClient.Get(context.Background(), kapKey, existingKAP) + 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(), + }, + } + + 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()) + + // check policy status + Eventually(func() bool { + existingPolicy := &api.AuthPolicy{} + err := k8sClient.Get(context.Background(), client.ObjectKeyFromObject(gwPolicy), existingPolicy) if err != nil { return false } - if !meta.IsStatusConditionTrue(existingKAP.Status.Conditions, "Available") { - return false - } - - return true + condition := meta.FindStatusCondition(existingPolicy.Status.Conditions, APAvailableConditionType) + return condition != nil && condition.Reason == "AuthSchemeNotReady" }, 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 := &authorinov1beta1.AuthConfig{} - authCKey := types.NamespacedName{Name: authConfigName(kapKey), Namespace: testNamespace} + // check istio authorizationpolicy + iapKey := types.NamespacedName{Name: istioAuthorizationPolicyName(testGatewayName, gwPolicy.Spec.TargetRef), Namespace: testNamespace} Eventually(func() bool { - err := k8sClient.Get(context.Background(), authCKey, existingAuthC) - 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.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()) - 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("Rejects policy with only unmatching top-level route selectors while trying to configure 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)), + }, + RouteSelectors: []api.RouteSelector{ + { // does not select any HTTPRouteRule + Matches: []gatewayapiv1alpha2.HTTPRouteMatch{ + { + Method: ptr.To(gatewayapiv1alpha2.HTTPMethod("DELETE")), + }, + }, + }, + }, + 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) + 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(), key, existingIAP) - return err == nil + 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()) - 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 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()) - Context("All rules with subdomains", 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} + Eventually(func() bool { + err := k8sClient.Get(context.Background(), authConfigKey, &authorinoapi.AuthConfig{}) + return apierrors.IsNotFound(err) + }, 30*time.Second, 5*time.Second).Should(BeTrue()) + }) - typedNamespace := gatewayapiv1beta1.Namespace(testNamespace) - policy := &kuadrantv1beta1.AuthPolicy{ + 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: kuadrantv1beta1.AuthPolicySpec{ + 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)), }, - AuthRules: []kuadrantv1beta1.AuthRule{ - { - 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(), + }, + } + config := policy.Spec.AuthScheme.Authentication["apiKey"] + config.RouteSelectors = []api.RouteSelector{ + { // does not select any HTTPRouteRule + Matches: []gatewayapiv1alpha2.HTTPRouteMatch{ { - Hosts: []string{"*.a.toystore.com", "*.b.toystore.com"}, - Methods: []string{"GET"}, - Paths: []string{"/private*"}, + Method: ptr.To(gatewayapiv1alpha2.HTTPMethod("DELETE")), }, }, - AuthScheme: testBasicAuthScheme(), }, } + policy.Spec.AuthScheme.Authentication["apiKey"] = config - 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 := &kuadrantv1beta1.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()) + + 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*"})) - 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 the list of subdomains with unique elements", func() { - // Check authconfig's hosts - kapKey := client.ObjectKey{Name: "toystore", Namespace: testNamespace} - existingAuthC := &authorinov1beta1.AuthConfig{} - authCKey := types.NamespacedName{Name: authConfigName(kapKey), Namespace: testNamespace} + 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(), + }, + } + + 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(), authCKey, existingAuthC) - 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()) - Expect(existingAuthC.Spec.Hosts).To(HaveLen(2)) - Expect(existingAuthC.Spec.Hosts).To(ContainElements("*.a.toystore.com", "*.b.toystore.com")) + }) + + 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)), + }, + 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", + }, + }, + }, + AuthScheme: api.AuthSchemeSpec{ + 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()) + + // 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("No rules", 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) - policy := &kuadrantv1beta1.AuthPolicy{ + 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", Namespace: testNamespace, }, - Spec: kuadrantv1beta1.AuthPolicySpec{ + 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)), }, - AuthRules: nil, 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 := &kuadrantv1beta1.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)) + 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*"})) - 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(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("authconfig's hosts should be route's hostnames", func() { - // Check authconfig's hosts - kapKey := client.ObjectKey{Name: "toystore", Namespace: testNamespace} - existingAuthC := &authorinov1beta1.AuthConfig{} - authCKey := types.NamespacedName{Name: authConfigName(kapKey), Namespace: testNamespace} + 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"}, + }, + { // 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) + 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(Equal([]string{"*.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[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*"})) + + // 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.*")) }) - }) -}) -func testBasicAuthScheme() kuadrantv1beta1.AuthSchemeSpec { - return kuadrantv1beta1.AuthSchemeSpec{ - Identity: []*authorinov1beta1.Identity{ - { - Name: "apiKey", - APIKey: &authorinov1beta1.Identity_APIKey{ - Selector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - "app": "toystore", - }, + 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(), }, - Credentials: authorinov1beta1.Credentials{ - In: "authorization_header", - KeySelector: "APIKEY", + } + 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 -func authPolicies(namespace string) []*kuadrantv1beta1.AuthPolicy { - typedNamespace := gatewayapiv1beta1.Namespace(namespace) - routePolicy := &kuadrantv1beta1.AuthPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: "target-route", - Namespace: namespace, - }, - Spec: kuadrantv1beta1.AuthPolicySpec{ - TargetRef: gatewayapiv1alpha2.PolicyTargetReference{ - Group: "gateway.networking.k8s.io", - Kind: "HTTPRoute", - Name: CustomHTTPRouteName, - Namespace: &typedNamespace, - }, - AuthRules: []kuadrantv1beta1.AuthRule{ + 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)) + // 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*"})) + + // 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.*")) + }) + + It("Mixes route selectors into other conditions", 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: 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{ { - Hosts: []string{"*.toystore.com"}, - Methods: []string{"DELETE", "POST"}, - Paths: []string{"/admin*"}, + PatternExpression: authorinoapi.PatternExpression{ + Selector: "context.source.address.Address.SocketAddress.address", + Operator: authorinoapi.PatternExpressionOperator("matches"), + Value: `192\.168\.0\..*`, + }, }, - }, - AuthScheme: kuadrantv1beta1.AuthSchemeSpec{ - Identity: []*authorinov1beta1.Identity{ - { - Name: "apiKey", - APIKey: &authorinov1beta1.Identity_APIKey{ + } + 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 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(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]api.AuthenticationSpec{ + "apiKey": { + AuthenticationSpec: authorinoapi.AuthenticationSpec{ + AuthenticationMethodSpec: authorinoapi.AuthenticationMethodSpec{ + ApiKey: &authorinoapi.ApiKeyAuthenticationSpec{ Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "app": "toystore", }, }, }, - Credentials: authorinov1beta1.Credentials{ - In: authorinov1beta1.Credentials_In( - "authorization_header", - ), - KeySelector: "APIKEY", + }, + Credentials: authorinoapi.Credentials{ + AuthorizationHeader: &authorinoapi.Prefixed{ + Prefix: "APIKEY", }, }, }, }, }, } - 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.AuthRules = []kuadrantv1beta1.AuthRule{ - // Must be different from the other KAP targeting the route, otherwise authconfigs will not be ready - {Hosts: []string{"*.com"}}, - } - gatewayPolicy.Spec.AuthScheme.Identity[0].APIKey.Selector.MatchLabels["admin"] = "yes" +} - return []*kuadrantv1beta1.AuthPolicy{routePolicy, gatewayPolicy} +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") + } } 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 3706b30f2..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" @@ -17,7 +17,7 @@ import ( istiosecurity "istio.io/api/security/v1beta1" istio "istio.io/client-go/pkg/apis/security/v1beta1" - api "github.com/kuadrant/kuadrant-operator/api/v1beta1" + api "github.com/kuadrant/kuadrant-operator/api/v1beta2" "github.com/kuadrant/kuadrant-operator/pkg/common" "github.com/kuadrant/kuadrant-operator/pkg/reconcilers" "k8s.io/utils/env" @@ -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.AuthRules, 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.AuthRule, 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 9fc238bd1..e936f1a10 100644 --- a/controllers/authpolicy_status.go +++ b/controllers/authpolicy_status.go @@ -5,20 +5,21 @@ import ( "fmt" "github.com/go-logr/logr" - authorinov1beta1 "github.com/kuadrant/authorino/api/v1beta1" - kuadrantv1beta1 "github.com/kuadrant/kuadrant-operator/api/v1beta1" "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" "sigs.k8s.io/controller-runtime/pkg/client" + + authorinoapi "github.com/kuadrant/authorino/api/v1beta2" + api "github.com/kuadrant/kuadrant-operator/api/v1beta2" ) const APAvailableConditionType string = "Available" // reconcileStatus makes sure status block of AuthPolicy is up-to-date. -func (r *AuthPolicyReconciler) reconcileStatus(ctx context.Context, ap *kuadrantv1beta1.AuthPolicy, specErr error) (ctrl.Result, error) { +func (r *AuthPolicyReconciler) reconcileStatus(ctx context.Context, ap *api.AuthPolicy, specErr error) (ctrl.Result, error) { logger, _ := logr.FromContext(ctx) logger.V(1).Info("Reconciling AuthPolicy status", "spec error", specErr) @@ -30,8 +31,8 @@ func (r *AuthPolicyReconciler) reconcileStatus(ctx context.Context, ap *kuadrant Namespace: ap.Namespace, Name: authConfigName(apKey), } - authConfig := &authorinov1beta1.AuthConfig{} - if err := r.GetResource(ctx, authConfigKey, authConfig); err != nil { + authConfig := &authorinoapi.AuthConfig{} + if err := r.GetResource(ctx, authConfigKey, authConfig); err != nil && !apierrors.IsNotFound(err) { return ctrl.Result{}, err } @@ -61,7 +62,7 @@ func (r *AuthPolicyReconciler) reconcileStatus(ctx context.Context, ap *kuadrant updateErr := r.Client().Status().Update(ctx, ap) if updateErr != nil { // Ignore conflicts, resource might just be outdated. - if errors.IsConflict(updateErr) { + if apierrors.IsConflict(updateErr) { logger.Info("Failed to update status: resource might just be outdated") return ctrl.Result{Requeue: true}, nil } @@ -71,8 +72,8 @@ func (r *AuthPolicyReconciler) reconcileStatus(ctx context.Context, ap *kuadrant return ctrl.Result{}, nil } -func (r *AuthPolicyReconciler) calculateStatus(ap *kuadrantv1beta1.AuthPolicy, specErr error, authConfigReady bool) *kuadrantv1beta1.AuthPolicyStatus { - newStatus := &kuadrantv1beta1.AuthPolicyStatus{ +func (r *AuthPolicyReconciler) calculateStatus(ap *api.AuthPolicy, specErr error, authConfigReady bool) *api.AuthPolicyStatus { + newStatus := &api.AuthPolicyStatus{ Conditions: slices.Clone(ap.Status.Conditions), ObservedGeneration: ap.Status.ObservedGeneration, } 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/kuadrant_controller.go b/controllers/kuadrant_controller.go index 08a529c2c..5798a3f11 100644 --- a/controllers/kuadrant_controller.go +++ b/controllers/kuadrant_controller.go @@ -487,7 +487,8 @@ func (r *KuadrantReconciler) reconcileAuthorino(ctx context.Context, kObj *kuadr Namespace: kObj.Namespace, }, Spec: authorinov1beta1.AuthorinoSpec{ - ClusterWide: true, + ClusterWide: true, + SupersedingHostSubsets: true, Listener: authorinov1beta1.Listener{ Tls: authorinov1beta1.Tls{ Enabled: &tmpFalse, 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/controllers/suite_test.go b/controllers/suite_test.go index d6b3d999e..a842045fb 100644 --- a/controllers/suite_test.go +++ b/controllers/suite_test.go @@ -23,8 +23,8 @@ import ( "path/filepath" "testing" - authorinoopv1beta1 "github.com/kuadrant/authorino-operator/api/v1beta1" - authorinov1beta1 "github.com/kuadrant/authorino/api/v1beta1" + authorinoopapi "github.com/kuadrant/authorino-operator/api/v1beta1" + authorinoapi "github.com/kuadrant/authorino/api/v1beta2" limitadorv1alpha1 "github.com/kuadrant/limitador-operator/api/v1alpha1" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -86,10 +86,10 @@ var _ = BeforeSuite(func() { err = gatewayapiv1beta1.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) - err = authorinoopv1beta1.AddToScheme(scheme.Scheme) + err = authorinoopapi.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) - err = authorinov1beta1.AddToScheme(scheme.Scheme) + err = authorinoapi.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) err = istioapis.AddToScheme(scheme.Scheme) diff --git a/doc/auth.md b/doc/auth.md new file mode 100644 index 000000000..ae2796287 --- /dev/null +++ b/doc/auth.md @@ -0,0 +1,323 @@ +# Kuadrant Auth + +A Kuadrant AuthPolicy custom resource: + +1. Targets Gateway API networking resources such as [HTTPRoutes](https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRoute) and [Gateways](https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.Gateway), using these resources to obtain additional context, i.e., which traffic workload (HTTP attributes, hostnames, user attributes, etc) to enforce auth. +2. Supports targeting subsets (sections) of a network resource to apply the auth rules to. +3. Abstracts the details of the underlying external authorization protocol and configuration resources, that have a much broader remit and surface area. +4. Enables cluster operators to set defaults that govern behavior at the lower levels of the network, until a more specific policy is applied. + +## How it works + +### Envoy's External Authorization Protocol + +Kuadrant's Auth implementation relies on the Envoy's [External Authorization](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/ext_authz_filter) protocol. The workflow per request goes: +1. On incoming request, the gateway checks the matching rules for enforcing the auth rules, as stated in the AuthPolicy custom resources and targeted Gateway API networking objects +2. If the request matches, the gateway sends one [CheckRequest](https://www.envoyproxy.io/docs/envoy/latest/api-v3/service/auth/v3/external_auth.proto#envoy-v3-api-msg-service-auth-v3-checkrequest) to the external auth service ("Authorino"). +3. The external auth service responds with a [CheckResponse](https://www.envoyproxy.io/docs/envoy/latest/api-v3/service/auth/v3/external_auth.proto#service-auth-v3-checkresponse) back to the gateway with either an `OK` or `DENIED` response code. + +An AuthPolicy and its targeted Gateway API networking resource contain all the statements to configure both the ingress gateway and the external auth service. + +### The AuthPolicy custom resource + +#### Overview + +The `AuthPolicy` spec includes the following parts: + +* A reference to an existing Gateway API resource (`spec.targetRef`) +* Authentication/authorization scheme (`spec.rules`) +* Top-level route selectors (`spec.routeSelectors`) +* Top-level additional conditions (`spec.when`) +* List of named patterns (`spec.patterns`) + +The auth scheme specify rules for: +* Authentication (`spec.rules.authentication`) +* External auth metadata fetching (`spec.rules.metadata`) +* Authorization (`spec.rules.authorization`) +* Custom response items (`spec.rules.response`) +* Callbacks (`spec.rules.callbacks`) + +Each auth rule can declare specific `routeSelectors` and `when` conditions for the rule to apply. + +#### High-level example and field definition + +```yaml +apiVersion: kuadrant.io/v1beta2 +kind: AuthPolicy +metadata: + name: my-auth-policy +spec: + # Reference to an existing networking resource to attach the policy to. + # It can be a Gateway API HTTPRoute or Gateway resource. + # It can only refer to objects in the same namespace as the AuthPolicy. + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute / Gateway + name: myroute / mygateway + + # Selectors of HTTPRouteRules within the targeted HTTPRoute that activate the AuthPolicy. + # Each element contains a HTTPRouteMatch object that will be used to select HTTPRouteRules that include at least + # one identical HTTPRouteMatch. + # The HTTPRouteMatch part does not have to be fully identical, but the what's stated in the selector must be + # identically stated in the HTTPRouteRule. + # Do not use it on AuthPolicies that target a Gateway. + routeSelectors: + - matches: + - path: + type: PathPrefix + value: "/admin" + + # Additional dynamic conditions to trigger the AuthPolicy. + # Use it for filtering attributes not supported by HTTPRouteRule or with AuthPolicies that target a Gateway. + # Check out https://github.com/Kuadrant/architecture/blob/main/rfcs/0002-well-known-attributes.md to learn more + # about the Well-known Attributes that can be used in this field. + when: […] + + # The auth rules to apply to the network traffic routed through the targeted resource + rules: + # Authentication rules to enforce. + # At least one config must evaluate to a valid identity object for the auth request to be successful. + # If omitted or empty, anonymous access is assumed. + authentication: + "my-authn-rule": + # The authentication method of this rule. + # One-of: apiKey, jwt, oauth2Introspection, kubernetesTokenReview, x509, plain, anonymous. + apiKey: {…} + + # Where credentials are required to be passed in the request for authentication based on this rule. + # One-of: authorizationHeader, customHeader, queryString, cookie. + credentials: + authorizationHeader: + prefix: APIKEY + + # Rule-level route selectors. + routeSelectors: […] + + # Rule-level additional conditions. + when: […] + + # Configs for caching the resolved object returned out of evaluating this auth rule. + cache: {…} + + # Rules for fetching auth metadata from external sources. + metadata: + "my-external-source": + # The method for fetching metadata from the external source. + # One-of: http: userInfo, uma. + http: {…} + + # Authorization rules to enforce. + # All policies must allow access for the auth request be successful. + authorization: + "my-authz-rule": + # The authorization method of this rule. + # One-of: patternMatching, opa, kubernetesSubjectAccessReview, spicedb. + opa: {…} + + # Customizations to the authorization response. + response: + # Custom denial status and other HTTP attributes for unauthenticated requests. + unauthenticated: {…} + + # Custom denial status and other HTTP attributes for unauhtorized requests. + unauthorized: {…} + + # Custom response items when access is granted. + success: + # Custom response items wrapped as HTTP headers to be injected in the request + headers: + "my-custom-header": + # One-of: plain, json, wristband. + plain: {…} + + # Custom response items wrapped as envoy dynamic metadata. + dynamicMetadata: + # One-of: plain, json, wristband. + "my-custom-dyn-metadata": + json: {…} + + # Rules for post-authorization callback requests to external services. + # Triggered regardless of the result of the authorization request. + callbacks: + "my-webhook": + http: {…} +``` + +Check out the [API reference](reference/authpolicy.md) for a full specification of the AuthPolicy CRD. + +## Using the AuthPolicy + +### Targeting a HTTPRoute networking resource + +When an AuthPolicy targets a HTTPRoute, the policy is enforced to all traffic routed according to the rules and hostnames specified in the HTTPRoute, across all Gateways referenced in the `spec.parentRefs` field of the HTTPRoute. + +The targeted HTTPRoute's rules and/or hostnames to which the policy must be enforced can be filtered to specific subsets, by specifying the [`routeSelectors`](reference/route-selectors.md#the-routeselectors-field) field of the AuthPolicy spec. + +Target a HTTPRoute by setting the `spec.targetRef` field of the AuthPolicy as follows: + +```yaml +apiVersion: kuadrant.io/v1beta2 +kind: AuthPolicy +metadata: + name: my-route-auth +spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: + rules: {…} +``` + +``` +┌───────────────────┐ ┌────────────────────┐ +│ (Infra namespace) │ │ (App namespace) │ +│ │ │ │ +│ ┌─────────┐ │ parentRefs │ ┌───────────┐ │ +│ │ Gateway │◄─────┼─────────────┼──┤ HTTPRoute │ │ +│ └─────────┘ │ │ └───────────┘ │ +│ │ │ ▲ │ +│ │ │ │ │ +│ │ │ │ │ +│ │ │ │ targetRef │ +│ │ │ │ │ +│ │ │ ┌─────┴──────┐ │ +│ │ │ │ AuthPolicy │ │ +│ │ │ └────────────┘ │ +│ │ │ │ +└───────────────────┘ └────────────────────┘ +``` + +#### Multiple HTTPRoutes with the same hostname + +Kuadrant currently does not support concurrent AuthPolicies targeting the exact same hostname. + +In case 2 or more AuthPolicies target resources that specify the same exact hostnames, the first AuthPolicy created claims the hostname; all the other AuthPolicies will not be enforced for the conflicting hostname and their status will be reported as not ready. + +This limitation only applies to identical hostnames. Different AuthPolicies can still be declared for different hostnames, as well as sets and strict subsets of hostnames. + +#### Hostnames and wildcards + +If an AuthPolicy targets a route defined for `*.com` and another AuthPolicy targets another route for `api.com`, the Kuadrant control plane will not merge these two AuthPolicies. Rather, it will mimic the behavior of gateway implementation by which the "most specific hostname wins", thus enforcing only the corresponding applicable policies and auth rules. + +E.g., a request coming for `api.com` will be protected according to the rules from the AuthPolicy that targets the route for `api.com`; while a request for `other.com` will be protected with the rules from the AuthPolicy targeting the route for `*.com`. + +Example with 3 AuthPolicies and 3 HTTPRoutes: +- AuthPolicy A → HTTPRoute A (`a.toystore.com`) +- AuthPolicy B → HTTPRoute B (`b.toystore.com`) +- AuthPolicy W → HTTPRoute W (`*.toystore.com`) + +Expected behavior: +- Request to `a.toystore.com` → AuthPolicy A will be enforced +- Request to `b.toystore.com` → AuthPolicy B will be enforced +- Request to `other.toystore.com` → AuthPolicy W will be enforced + +### Targeting a Gateway networking resource + +When an AuthPolicy targets a Gateway, the policy will be enforced to all HTTP traffic hitting the gateway, unless a more specific AuthPolicy targeting a matching HTTPRoute exists. + +Any new HTTPRoute referrencing the gateway as parent will be automatically covered by the AuthPolicy that targets the Gateway, as well as changes in the existing HTTPRoutes. + +This effectively provides cluster operators with the ability to set _defaults_ to protect the infrastructure against unplanned and malicious network traffic attempt, such as by setting preemptive "deny-all" policies for hostnames and hostname wildcards. + +Target a Gateway HTTPRoute by setting the `spec.targetRef` field of the AuthPolicy as follows: + +```yaml +apiVersion: kuadrant.io/v1beta2 +kind: AuthPolicy +metadata: + name: my-gw-auth +spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: + rules: {…} +``` + +``` +┌───────────────────┐ ┌────────────────────┐ +│ (Infra namespace) │ │ (App namespace) │ +│ │ │ │ +│ ┌─────────┐ │ parentRefs │ ┌───────────┐ │ +│ │ Gateway │◄─────┼─────────────┼──┤ HTTPRoute │ │ +│ └─────────┘ │ │ └───────────┘ │ +│ ▲ │ │ ▲ │ +│ │ │ │ │ │ +│ │ │ │ │ │ +│ │ targetRef │ │ │ targetRef │ +│ │ │ │ │ │ +│ ┌─────┴──────┐ │ │ ┌─────┴──────┐ │ +│ │ AuthPolicy │ │ │ │ AuthPolicy │ │ +│ └────────────┘ │ │ └────────────┘ │ +│ │ │ │ +└───────────────────┘ └────────────────────┘ +``` + +#### Overlapping Gateway and HTTPRoute AuthPolicies + +Gateway-targeted AuthPolicies will serve as a default to protect all traffic routed through the gateway until a more specific HTTPRoute-targeted AuthPolicy exists, in which case the HTTPRoute AuthPolicy prevails. + +Example with 4 AuthPolicies, 3 HTTPRoutes and 1 Gateway (plus 2 HTTPRoute and 2 Gateways without AuthPolicies attached): +- AuthPolicy A → HTTPRoute A (`a.toystore.com`) → Gateway G (`*.com`) +- AuthPolicy B → HTTPRoute B (`b.toystore.com`) → Gateway G (`*.com`) +- AuthPolicy W → HTTPRoute W (`*.toystore.com`) → Gateway G (`*.com`) +- AuthPolicy G → Gateway G (`*.com`) + +Expected behavior: +- Request to `a.toystore.com` → AuthPolicy A will be enforced +- Request to `b.toystore.com` → AuthPolicy B will be enforced +- Request to `other.toystore.com` → AuthPolicy W will be enforced +- Request to `other.com` (suppose a route exists) → AuthPolicy G will be enforced +- Request to `yet-another.net` (suppose a route and gateway exist) → No AuthPolicy will be enforced + +### Route selectors + +Route selectors allow targeting sections of a HTTPRoute, by specifying sets of HTTPRouteMatches and/or hostnames that make the policy controller look up within the HTTPRoute spec for compatible declarations, and select the corresponding HTTPRouteRules and hostnames, to then build conditions that activate the policy or policy rule. + +Check out [Route selectors](reference/route-selectors.md) for a full description, semantics and API reference. + +#### `when` conditions + +`when` conditions can be used to scope an AuthPolicy or auth rule within an AuthPolicy (i.e. to filter the traffic to which a policy or policy rule applies) without any coupling to the underlying network topology, i.e. without making direct references to HTTPRouteRules via [`routeSelectors`](reference/route-selectors.md#the-routeselectors-field). + +Use `when` conditions to conditionally activate policies and policy rules based on attributes that cannot be expressed in the HTTPRoutes' `spec.hostnames` and `spec.rules.matches` fields, or in general in AuthPolicies that target a Gateway. + +`when` conditions in an AuthPolicy are compatible with Authorino [conditions](https://docs.kuadrant.io/authorino/docs/features/#common-feature-conditions-when), thus supporting complex boolean expressions with AND and OR operators, as well as grouping. + +The selectors within the `when` conditions of an AuthPolicy are a subset of Kuadrant's Well-known Attributes ([RFC 0002](https://github.com/Kuadrant/architecture/blob/main/rfcs/0002-well-known-attributes.md)). Check out the reference for the full list of supported selectors. + +Authorino [JSON path string modifiers](https://docs.kuadrant.io/authorino/docs/features/#string-modifiers) can also be applied to the selectors within the `when` conditions of an AuthPolicy. + +### Examples + +Check out the following user guides for examples of protecting services with Kuadrant: +* [Enforcing authentication & authorization with Kuadrant AuthPolicy, for app developers and platform engineers](user-guides/auth-for-app-devs-and-platform-engineers.md) +* [Authenticated Rate Limiting for Application Developers](user-guides/authenticated-rl-for-app-developers.md) +* [Authenticated Rate Limiting with JWTs and Kubernetes RBAC](user-guides/authenticated-rl-with-jwt-and-k8s-authnz.md) + +### Known limitations + +* One HTTPRoute can only be targeted by one AuthPolicy. +* One Gateway can only be targeted by one AuthPolicy. +* AuthPolicies can only target HTTPRoutes/Gateways defined within the same namespace of the AuthPolicy. +* 2+ AuthPolicies cannot target network resources that define/inherit the same exact hotname. + +## Implementation details + +Under the hood, for each AuthPolicy, Kuadrant creates an Istio [`AuthorizationPolicy`](https://istio.io/latest/docs/reference/config/security/authorization-policy) and an Authorino [`AuthConfig`](https://docs.kuadrant.io/authorino/docs/architecture/#the-authorino-authconfig-custom-resource-definition-crd) custom resources. + +Only requests that matches the rules in the Istio `AuthorizationPolicy` cause an authorization request to be sent to the external authorization service ("Authorino"), i.e., only requests directed to the HTTPRouteRules targeted by the AuthPolicy (directly or indirectly), according to the declared top-level route selectors (if present), or all requests for which a matching HTTPRouteRule exists (otherwise). + +Authorino looks up for the auth scheme (`AuthConfig` custom resource) to enforce using the provided hostname of the original request as key. It then checks again if the request matches at least one of the selected HTTPRouteRules, in which case it enforces the auth scheme. + +
+ Exception to the rule + + Due to limitations imposed by the Istio `AuthorizationPolicy`, there are a few patterns of HTTPRouteRules that cannot be translated to filters for the external authorization request. Therefore, the following patterns used in HTTPRouteMatches of top-level route selectors of an AuthPolicy will not be included in the Istio AuthorizationPolicy rules that trigger the check request with Authorino: `PathMatchRegularExpression`, `HeaderMatchRegularExpression`, and `HTTPQueryParamMatch`. + + As a consequence to the above, requests that do not match these rules and otherwise would not be checked with Authorino will result in a request to the external authorization service. Authorino nonetheless will still verify those patterns and ensure the auth scheme is enforced only when it matches a selected HTTPRouteRule. Users of Kuadrant may observe an unnecessary call to the authorization service in those cases where the request is out of the scope of the AuthPolicy and therefore always authorized. +
+ +### Internal custom resources and namespaces + +While the Istio `AuthorizationPolicy` needs to be created in the same namespace as the gateway workload, the Authorino `AuthConfig` is created in the namespace of the `AuthPolicy` itself. This allows to simplify references such as to Kubernetes Secrets referred in the AuthPolicy, as well as the RBAC to support the architecture. diff --git a/doc/proposals/authpolicy-crd.md b/doc/proposals/authpolicy-crd.md deleted file mode 100644 index f53bd7aeb..000000000 --- a/doc/proposals/authpolicy-crd.md +++ /dev/null @@ -1,128 +0,0 @@ -# AuthPolicy Proposal -Authors: Rahul Anand (rahanand@redhat.com), Craig Brookes (cbrookes@redhat.com) - -## Introduction -Istio offers an [AuthorizationPolicy](https://istio.io/latest/docs/reference/config/security/authorization-policy/) resource which requires it to be applied in the namespace of the workload. This means that all the configuration is completely decoupled from routing logic like hostnames and paths. For managed gateway scenario, users need to either ask cluster operator to apply their policies in the gateway's namespace (which is not scalable) or use sidecars/personal gateway for their workloads in their own namepsace which is not optimal. - -The new [GatewayAPI](https://gateway-api.sigs.k8s.io/) defines a standard [policy attachment mechanism](https://gateway-api.sigs.k8s.io/v1alpha2/references/policy-attachment/) for hierarchical effect of vendor specific policies. We believe creating a new CRD with concepts from Gateway API that solves use cases of Istio's AuthorizationPolicy plus its limitations as described above. - -## Goals - -With `targetRef` from policy attachment concept, following are the goals: -- Application developer should be able to target [`HTTPRoute`](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.HTTPRoute) object in their own namespace. This will define authorization policy at the hostname/domain/vHost level. -- Cluster operator should be able to target [`Gateway`](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.Gateway) object along with HTTPRoute in the gateway's namespace. This will define policy at the listener level. -- To reduce context sharing at the gateway and external authorization provider, action type and auth-provider are defaulted to `CUSTOM` and authorino respectively. - -## Proposed Solution -Following is the proposed new CRD that combines policy attachment concepts with Istio's AuthorizationPolicy: - -```yaml -apiVersion: kuadrant.io/v1beta1 -kind: AuthPolicy -metadata: - name: toystore -spec: - targetRef: - group: # Only takes gateway.networking.k8s.io - kind: HTTPRoute | Gateway - name: toystore - rules: - - hosts: ["*.toystore.com"] - methods: ["GET", "POST"] - paths: ["/admin"] - authScheme: # Embedded AuthConfigs - hosts: ["admin.toystore.com"] - identity: - - name: idp-users - oidc: - endpoint: https://my-idp.com/auth/realm - authorization: - - name: check-claim - json: - rules: - - selector: auth.identity.group - operator: eq - value: allowed-users -status: - conditions: - - lastTransitionTime: "2022-06-06T11:03:04Z" - message: HTTPRoute/Gateway is protected/Error - reason: HTTPRouteProtected/GatewayProtected/Error - status: "True" | "False" - type: Available - observedGeneration: 1 -``` - -### Target Reference -`targetRef` field is taken from [policy attachment's target reference API](https://gateway-api.sigs.k8s.io/v1alpha2/references/policy-attachment/#target-reference-api). It can only target one resource at a time. Fields included inside: -- `Group` is the group of the target resource. Only valid option is `gateway.networking.k8s.io`. -- `Kind` is kind of the target resource. Only valid options are `HTTPRoute` and `Gateway`. -- `Name` is the name of the target resource. -- `Namespace` is the namespace of the referent. Currently only local objects can be referred so value is ignored. - -### Rule objects -`rules` field describe the requests that will be routed to external authorization provider (like authorino). It includes: -- `hosts`: a host is matched over `Host` request header or `SNI` if TLS is used. - -**Note**: Each rule's host in a route level policy must match at least one hostname regex described in [HTTPRoute's `hostnames`](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.Hostname) but Gateway level policies have no such restriction. -``` - targetRef - HTTPRoute ◄───────────────────────── AuthPolicy - hostnames: ["*.toystore.com"] rules: - ┌────────────────────────────┐ - Rejected Rule: │- hosts: ["*.carstore.com"] │ - Regex mismatch │ methods: ["GET", "DELETE"]│ - └────────────────────────────┘ - - ┌───────────────────────────────┐ - Accepted Rule: │- hosts: ["admin.toystore.com"]│ - Regex match │ methods: ["POST", "DELETE"] │ - └───────────────────────────────┘ -``` - -- `paths`: a path matches over request path like `/admin/`. -- `methods`: a method matches over request method like `DELETE`. - -Fields in a rule object are ANDed together but inner fields follow OR semantics. For example, -```yaml -hosts: ["*.toystore.com"] -methods: ["GET", "POST"] -paths: ["/admin"] -``` -The above rule matches if the host matches`*.toystore.com` AND the method is `POST` OR `GET`; AND path is `/admin` - -Internally, All the rules in a AuthPolicy are translated into list of [`Operations`](https://istio.io/latest/docs/reference/config/security/authorization-policy/#Operation) under a single [Istio's AuthorizationPolicy](https://istio.io/latest/docs/reference/config/security/authorization-policy/) with CUSTOM [action](https://istio.io/latest/docs/reference/config/security/authorization-policy/#AuthorizationPolicy-Action) type and [external authorization provider](https://istio.io/latest/docs/reference/config/security/authorization-policy/#AuthorizationPolicy-ExtensionProvider) as authorino. - -### AuthScheme object -AuthScheme is embedded form of [Authorino's AuthConfig](https://github.com/Kuadrant/authorino/blob/main/docs/architecture.md#the-authorino-authconfig-custom-resource-definition-crd). Applying an AuthPolicy resource with AuthScheme defined, would create an AuthConfig in the Gateway's namespace. - -**Note**: Following the heirarchial constraints, `spec.AuthScheme.Hosts` must match at least one `spec.Hosts` for AuthPolicy to be validated. - -The example AuthPolicy showed above will create the following AuthConfig: - -```yaml -apiVersion: authorino.kuadrant.io/v1beta1 -kind: AuthConfig -metadata: - name: default-toystore-1 -spec: - hosts: - - "admin.toystore.com" - identity: - - name: idp-users - oidc: - endpoint: https://my-idp.com/auth/realm - authorization: - - name: check-claim - json: - rules: - - selector: auth.identity.group - operator: eq - value: allowed-users -``` - -Overall control structure looks like the following between the developer and the kuadrant operator: -![](images/authpolicy-control-structure.png) - -## Checklist -- Issue tracking this proposal: https://github.com/Kuadrant/kuadrant-operator/issues/130 diff --git a/doc/rate-limiting.md b/doc/rate-limiting.md index 6cb90b689..25aa329d5 100644 --- a/doc/rate-limiting.md +++ b/doc/rate-limiting.md @@ -2,10 +2,10 @@ A Kuadrant RateLimitPolicy custom resource, often abbreviated "RLP": -1. Allows it to target Gateway API networking resources such as [HTTPRoutes](https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRoute) and [Gateways](https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.Gateway), using these resources to obtain additional context, i.e., which traffic workload (HTTP attributes, hostnames, user attributes, etc) to rate limit. -2. Allows to specify which specific subsets of the targeted network resource to apply the limits to. +1. Targets Gateway API networking resources such as [HTTPRoutes](https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRoute) and [Gateways](https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.Gateway), using these resources to obtain additional context, i.e., which traffic workload (HTTP attributes, hostnames, user attributes, etc) to rate limit. +2. Supports targeting subsets (sections) of a network resource to apply the limits to. 3. Abstracts the details of the underlying Rate Limit protocol and configuration resources, that have a much broader remit and surface area. -4. Supports cluster operators to set overrides (soon) and defaults that govern what can be done at the lower levels. +4. Enables cluster operators to set defaults that govern behavior at the lower levels of the network, until a more specific policy is applied. ## How it works @@ -77,7 +77,7 @@ spec: routeSelectors: […] # (optional) additional dynamic conditions to trigger the limit. - # use it for filterring attributes not supported by HTTPRouteRule or with RateLimitPolicies that target a Gateway + # use it for filtering attributes not supported by HTTPRouteRule or with RateLimitPolicies that target a Gateway # check out Kuadrant RFC 0002 (https://github.com/Kuadrant/architecture/blob/main/rfcs/0002-well-known-attributes.md) to learn more about the Well-known Attributes that can be used in this field when: […] ``` @@ -88,7 +88,7 @@ spec: When a RLP targets a HTTPRoute, the policy is enforced to all traffic routed according to the rules and hostnames specified in the HTTPRoute, across all Gateways referenced in the `spec.parentRefs` field of the HTTPRoute. -The targeted HTTPRoute's rules and/or hostnames to which the policy must be enforced can be filtered to specific subsets, by specifying the `routeSelectors` field of the limit definition. +The targeted HTTPRoute's rules and/or hostnames to which the policy must be enforced can be filtered to specific subsets, by specifying the [`routeSelectors`](reference/route-selectors.md#the-routeselectors-field) field of the limit definition. Target a HTTPRoute by setting the `spec.targetRef` field of the RLP as follows: @@ -172,7 +172,7 @@ Expected behavior: ### Limit definition A limit will be activated whenever a request comes in and the request matches: -- any of the route rules selected by the limit (via `routeSelectors` or implicit "catch-all" selector), and +- any of the route rules selected by the limit (via [`routeSelectors`](reference/route-selectors.md#the-routeselectors-field) or implicit "catch-all" selector), and - all of the `when` conditions specified in the limit. A limit can define: @@ -226,36 +226,19 @@ spec: | `admin.toystore.com` | 250rps | | `other.toystore.com` | 5000rps | -#### Route selectors +### Route selectors -The `routeSelectors` field of the limit definition allows to specify **selectors of routes** (or parts of a route), that _transitively induce a set of conditions for a limit to be enforced_. It is defined as a set of route matching rules, where these rules must exist, partially or identically stated within the HTTPRouteRules of the HTTPRoute that is targeted by the RLP. +Route selectors allow targeting sections of a HTTPRoute, by specifying sets of HTTPRouteMatches and/or hostnames that make the policy controller look up within the HTTPRoute spec for compatible declarations, and select the corresponding HTTPRouteRules and hostnames, to then build conditions that activate the policy or policy rule. -The field is typed as a list of objects based on a special type defined from Gateway API's [HTTPRouteMatch](https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPPathMatch) type (`matches` subfield of the route selector object), and an additional field `hostnames`. - -Route selectors matches and the HTTPRoute's HTTPRouteMatches are pairwise compared to select or not select HTTPRouteRules that should activate a limit. To decide whether the route selector selects a HTTPRouteRule or not, for each pair of route selector HTTPRouteMatch and HTTPRoute HTTPRouteMatch: -1. The route selector selects the HTTPRoute's HTTPRouteRule if the HTTPRouteRule contains at least one HTTPRouteMatch that specifies fields that are literally identical to all the fields specified by at least one HTTPRouteMatch of the route selector. -2. A HTTPRouteMatch within a HTTPRouteRule may include other fields that are not specified in a route selector match, and yet the route selector match selects the HTTPRouteRule if all fields of the route selector match are identically included in the HTTPRouteRule's HTTPRouteMatch; the opposite is NOT true. -3. Each field `path` of a HTTPRouteMatch, as well as each field `method` of a HTTPRouteMatch, as well as each element of the fields `headers` and `queryParams` of a HTTPRouteMatch, is atomic – this is true for the HTTPRouteMatches within a HTTPRouteRule, as well as for HTTPRouteMatches of a route selector. - -Additionally, at least one hostname specified in a route selector must identically match one of the hostnames specified (or inherited, when omitted) by the targeted HTTPRoute. - -The semantics of the route selectors allows to assertively relate limit definitions to routing rules, with benefits for identifying the subsets of the network that are covered by a limit, while preventing unreachable definitions, as well as the overhead associated with the maintenance of such rules across multiple resources throughout time, according to network topology beneath. Moreover, the requirement of not having to be a full copy of the targeted HTTPRouteRule matches, but only partially identical, helps prevent repetition to some degree, as well as it enables to more easily define limits that scope across multiple HTTPRouteRules (by specifying less rules in the selector). - -A few rules and corner cases to keep in mind while using the RLP's `routeSelectors`: -1. **The golden rule –** The route selectors in a RLP are **not** to be read strictly as the route matching rules that activate a limit, but as selectors of the route rules that activate the limit. -2. Due to (1) above, this can lead to cases, e.g., where a route selector that states `matches: [{ method: POST }]` selects a HTTPRouteRule that defines `matches: [{ method: POST }, { method: GET }]`, effectively causing the limit to be activated on requests to the HTTP method `POST`, but **also** to the HTTP method `GET`. -3. The requirement for the route selector match to state patterns that are identical to the patterns stated by the HTTPRouteRule (partially or entirely) makes, e.g., a route selector such as `matches: { path: { type: PathPrefix, value: /foo } }` to select a HTTPRouteRule that defines `matches: { path: { type: PathPrefix, value: /foo }, method: GET }`, but **not** to select a HTTPRouteRule that only defines `matches: { method: GET }`, even though the latter includes technically all HTTP paths; **nor** it selects a HTTPRouteRule that only defines `matches: { path: { type: Exact, value: /foo } }`, even though all requests to the exact path `/foo` are also technically requests to `/foo*`. -4. The atomicity property of fields of the route selectors makes, e.g., a route selector such as `matches: { path: { value: /foo } }` to select a HTTPRouteRule that defines `matches: { path: { value: /foo } }`, but **not** to select a HTTPRouteRule that only defines `matches: { path: { type: PathPrefix, value: /foo } }`. (This case may actually never happen because `PathPrefix` is the default value for `path.type` and will be set automatically by the Kubernetes API server.) - -Due to the nature of route selectors of defining pointers to HTTPRouteRules, the `routeSelectors` field is not supported in a RLP that targets a Gateway resource. +Check out [Route selectors](reference/route-selectors.md) for a full description, semantics and API reference. #### `when` conditions -`when` conditions can be used to scope a limit (i.e. to filter the traffic to which a limit definition applies) without any coupling to the underlying network topology, i.e. without making direct references to HTTPRouteRules via `routeSelectors`. +`when` conditions can be used to scope a limit (i.e. to filter the traffic to which a limit definition applies) without any coupling to the underlying network topology, i.e. without making direct references to HTTPRouteRules via [`routeSelectors`](reference/route-selectors.md#the-routeselectors-field). -The syntax of the `when` conditions selectors comply with Kuadrant's [Well-known Attributes (RFC 0002)](https://github.com/Kuadrant/architecture/blob/main/rfcs/0002-well-known-attributes.md). +Use `when` conditions to conditionally activate limits based on attributes that cannot be expressed in the HTTPRoutes' `spec.hostnames` and `spec.rules.matches` fields, or in general in RLPs that target a Gateway. -Use the `when` conditions to conditionally activate limits based on attributes that cannot be expressed in the HTTPRoutes' `spec.hostnames` and `spec.rules.matches` fields, or in general in RLPs that target a Gateway. +The selectors within the `when` conditions of a RateLimitPolicy are a subset of Kuadrant's Well-known Attributes ([RFC 0002](https://github.com/Kuadrant/architecture/blob/main/rfcs/0002-well-known-attributes.md)). Check out the reference for the full list of supported selectors. ### Examples diff --git a/doc/reference/authpolicy.md b/doc/reference/authpolicy.md new file mode 100644 index 000000000..7a8e0d85f --- /dev/null +++ b/doc/reference/authpolicy.md @@ -0,0 +1,151 @@ +# The AuthPolicy Custom Resource Definition (CRD) + +- [AuthPolicy](#authpolicy) +- [AuthPolicySpec](#authpolicyspec) + - [AuthScheme](#authscheme) + - [AuthRuleCommon](#authrulecommon) + - [AuthenticationRule](#authenticationrule) + - [MetadataRule](#metadatarule) + - [AuthorizationRule](#authorizationrule) + - [ResponseSpec](#responsespec) + - [SuccessResponseSpec](#successresponsespec) + - [SuccessResponseItem](#successresponseitem) + - [CallbackRule](#callbackrule) + - [NamedPattern](#namedpattern) +- [AuthPolicyStatus](#authpolicystatus) + - [ConditionSpec](#conditionspec) + +## AuthPolicy + +| **Field** | **Type** | **Required** | **Description** | +|-----------|---------------------------------------|:------------:|-------------------------------------------------| +| `spec` | [AuthPolicySpec](#authpolicyspec) | Yes | The specfication for AuthPolicy custom resource | +| `status` | [AuthPolicyStatus](#authpolicystatus) | No | The status for the custom resource | + +## AuthPolicySpec + +| **Field** | **Type** | **Required** | **Description** | +|------------------|---------------------------------------------------------------------------------------------------------------------------------------------|--------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `targetRef` | [PolicyTargetReference](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.PolicyTargetReference) | Yes | Reference to a Kuberentes resource that the policy attaches to | +| `rules` | [AuthScheme](#authscheme) | No | Authentication/authorization rules | +| `routeSelectors` | [][RouteSelector](route-selectors.md#routeselector) | No | List of selectors of HTTPRouteRules whose matching rules activate the policy. At least one HTTPRouteRule must be selected to activate the policy. If omitted, all HTTPRouteRules of the targeted HTTPRoute activate the policy. Do not use it in policies targeting a Gateway. | +| `patterns` | Map | No | Named patterns of lists of `selector`, `operator` and `value` tuples, to be reused in `when` conditions and pattern-matching authorization rules. | +| `when` | [][PatternExpressionOrRef](https://docs.kuadrant.io/authorino/docs/features/#common-feature-conditions-when) | No | List of additional dynamic conditions (expressions) to activate the policy. Use it for filtering attributes that cannot be expressed in the targeted HTTPRoute's `spec.hostnames` and `spec.rules.matches` fields, or when targeting a Gateway. | + +### AuthScheme + +| **Field** | **Type** | **Required** | **Description** | +|------------------|--------------------------------------------------------|:------------:|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `authentication` | Map | No | Authentication rules. At least one config MUST evaluate to a valid identity object for the auth request to be successful. If omitted or empty, anonymous access is assumed. | +| `metadata` | Map | No | Rules for fetching auth metadata from external sources. | +| `authorization` | Map | No | Authorization rules. All policies MUST allow access for the auth request be successful. | +| `response` | [ResponseSpec](#responsespec) | No | Customizations to the response to the authorization request. Use it to set custom values for unauthenticated, unauthorized, and/or success access request. | +| `callbacks` | Map | No | Rules for post-authorization callback requests to external services. Triggered regardless of the result of the authorization request. | + +#### AuthRuleCommon + +| **Field** | **Type** | **Required** | **Description** | +|-------------------------|--------------------------------------------------------------------------------------------------------------|:------------:|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `routeSelectors` | [][RouteSelector](route-selectors.md#routeselector) | No | List of selectors of HTTPRouteRules whose matching rules activate the auth rule. At least one HTTPRouteRule must be selected to activate the auth rule. If omitted, the auth rule is activated at all requests where the policy is enforced. Do not use it in policies targeting a Gateway. | +| `when` | [][PatternExpressionOrRef](https://docs.kuadrant.io/authorino/docs/features/#common-feature-conditions-when) | No | List of additional dynamic conditions (expressions) to activate the auth rule. Use it for filtering attributes that cannot be expressed in the targeted HTTPRoute's `spec.hostnames` and `spec.rules.matches` fields, or when targeting a Gateway. | +| `cache` | [Caching spec](https://docs.kuadrant.io/authorino/docs/features/#common-feature-caching-cache) | No | Caching options for the resolved object returned when applying this auth rule. (Default: disabled) | +| `priority` | Integer | No | Priority group of the auth rule. All rules in the same priority group are evaluated concurrently; consecutive priority groups are evaluated sequentially. (Default: `0`) | +| `metrics` | Boolean | No | Whether the auth rule emits individual observability metrics. (Default: `false`) | + +#### AuthenticationRule + +| **Field** | **Type** | **Required** | **Description** | +|-------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|:------------:|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `apiKey` | [API Key authentication spec](https://docs.kuadrant.io/authorino/docs/features/#api-key-authenticationapikey) | No | Authentication based on API keys stored in Kubernetes secrets. Use one of: `apiKey`, `jwt`, `oauth2Introspection`, `kubernetesTokenReview`, `x509`, `plain`, `anonymous`. | +| `kubernetesTokenReview` | [KubernetesTokenReview spec](https://docs.kuadrant.io/authorino/docs/features/#kubernetes-tokenreview-authenticationkubernetestokenreview) | No | Authentication by Kubernetes token review. Use one of: `apiKey`, `jwt`, `oauth2Introspection`, `kubernetesTokenReview`, `x509`, `plain`, `anonymous`. | +| `jwt` | [JWT verification spec](https://docs.kuadrant.io/authorino/docs/features/#jwt-verification-authenticationjwt) | No | Authentication based on JSON Web Tokens (JWT). Use one of: `apiKey`, `jwt`, `oauth2Introspection`, `kubernetesTokenReview`, `x509`, `plain`, `anonymous`. | +| `oauth2Introspection` | [OAuth2 Token Introscpection spec](https://docs.kuadrant.io/authorino/docs/features/#oauth-20-introspection-authenticationoauth2introspection) | No | Authentication by OAuth2 token introspection. Use one of: `apiKey`, `jwt`, `oauth2Introspection`, `kubernetesTokenReview`, `x509`, `plain`, `anonymous`. | +| `x509` | [X.509 authentication spec](https://docs.kuadrant.io/authorino/docs/features/#x509-client-certificate-authentication-authenticationx509) | No | Authentication based on client X.509 certificates. The certificates presented by the clients must be signed by a trusted CA whose certificates are stored in Kubernetes secrets. Use one of: `apiKey`, `jwt`, `oauth2Introspection`, `kubernetesTokenReview`, `x509`, `plain`, `anonymous`. | +| `plain` | [Plain identity object spec](https://docs.kuadrant.io/authorino/docs/features/#plain-authenticationplain) | No | Identity object extracted from the context. Use this method when authentication is performed beforehand by a proxy and the resulting object passed to Authorino as JSON in the auth request. Use one of: `apiKey`, `jwt`, `oauth2Introspection`, `kubernetesTokenReview`, `x509`, `plain`, `anonymous`. | +| `anonymous` | [Anonymous access](https://docs.kuadrant.io/authorino/docs/features/#anonymous-access-authenticationanonymous) | No | Anonymous access. Use one of: `apiKey`, `jwt`, `oauth2Introspection`, `kubernetesTokenReview`, `x509`, `plain`, `anonymous`. | +| `credentials` | [Auth credentials spec](https://docs.kuadrant.io/authorino/docs/features/#extra-auth-credentials-authenticationcredentials) | No | Customizations to where credentials are required to be passed in the request for authentication based on this auth rule. Defaults to HTTP Authorization header with prefix "Bearer". | +| `overrides` | [Identity extension spec](https://docs.kuadrant.io/authorino/docs/features/#extra-identity-extension-authenticationdefaults-and-authenticationoverrides) | No | JSON overrides to set to the resolved identity object. Do not use it with identity objects of other JSON types (array, string, etc). | +| `defaults` | [Identity extension spec](https://docs.kuadrant.io/authorino/docs/features/#extra-identity-extension-authenticationdefaults-and-authenticationoverrides) | No | JSON defaults to set to the resolved identity object. Do not use it with identity objects of other JSON types (array, string, etc). | +| _(inline)_ | [AuthRuleCommon](#authrulecommon) | No | | + +#### MetadataRule + +| **Field** | **Type** | **Required** | **Description** | +|-------------|-----------------------------------------------------------------------------------------------------------------------------------|:------------:|-----------------------------------------------------------------------------------------------------------------------------------------| +| `http` | [HTTP GET/GET-by-POST external metadata spec](https://docs.kuadrant.io/authorino/docs/features/#http-getget-by-post-metadatahttp) | No | External source of auth metadata via HTTP request. Use one of: `http`, `userInfo`, `uma`. | +| `userInfo` | [OIDC UserInfo spec](https://docs.kuadrant.io/authorino/docs/features/#oidc-userinfo-metadatauserinfo) | No | OpendID Connect UserInfo linked to an OIDC authentication rule declared in this same AuthPolicy. Use one of: `http`, `userInfo`, `uma`. | +| `uma` | [UMA metadata spec](https://docs.kuadrant.io/authorino/docs/features/#user-managed-access-uma-resource-registry-metadatauma) | No | User-Managed Access (UMA) source of resource data. Use one of: `http`, `userInfo`, `uma`. | +| _(inline)_ | [AuthRuleCommon](#authrulecommon) | No | | + +#### AuthorizationRule + +| **Field** | **Type** | **Required** | **Description** | +|---------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------|:------------:|--------------------------------------------------------------------------------------------------------------------------------------------------------| +| `patternMatching` | [Pattern-matching authorization spec](https://docs.kuadrant.io/authorino/docs/features/#pattern-matching-authorization-authorizationpatternmatching) | No | Pattern-matching authorization rules. Use one of: `patternMatching`, `opa`, `kubernetesSubjectAccessReview`, `spicedb`. | +| `opa` | [OPA authorization spec](https://docs.kuadrant.io/authorino/docs/features/#open-policy-agent-opa-rego-policies-authorizationopa) | No | Open Policy Agent (OPA) Rego policy. Use one of: `patternMatching`, `opa`, `kubernetesSubjectAccessReview`, `spicedb`. | +| `kubernetesSubjectAccessReview` | [Kubernetes SubjectAccessReview spec](https://docs.kuadrant.io/authorino/docs/features/#kubernetes-subjectaccessreview-authorizationkubernetessubjectaccessreview) | No | Authorization by Kubernetes SubjectAccessReview. Use one of: `patternMatching`, `opa`, `kubernetesSubjectAccessReview`, `spicedb`. | +| `spicedb` | [SpiceDB authorization spec](https://docs.kuadrant.io/authorino/docs/features/#spicedb-authorizationspicedb) | No | Authorization decision delegated to external Authzed/SpiceDB server. Use one of: `patternMatching`, `opa`, `kubernetesSubjectAccessReview`, `spicedb`. | +| _(inline)_ | [AuthRuleCommon](#authrulecommon) | No | | + +#### ResponseSpec + +| **Field** | **Type** | **Required** | **Description** | +|-------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------|:------------:|------------------------------------------------------------------------------------------------------------------------------------| +| `unauthenticated` | [Custom denial status spec](https://docs.kuadrant.io/authorino/docs/features/#custom-denial-status-responseunauthenticated-and-responseunauthorized) | No | Customizations on the denial status and other HTTP attributes when the request is unauthenticated. (Default: `401 Unauthorized`) | +| `unauthorized` | [Custom denial status spec](https://docs.kuadrant.io/authorino/docs/features/#custom-denial-status-responseunauthenticated-and-responseunauthorized) | No | Customizations on the denial status and other HTTP attributes when the request is unauthorized. (Default: `403 Forbidden`) | +| `success` | [SuccessResponseSpec](#successresponsespec) | No | Response items to be included in the auth response when the request is authenticated and authorized. | + +##### SuccessResponseSpec + +| **Field** | **Type** | **Required** | **Description** | +|-------------------|----------------------------------------------------------|:------------:|---------------------------------------------------------------------------------------------------------------------------------------------------| +| `headers` | Map | No | Custom success response items wrapped as HTTP headers to be injected in the request. | +| `dynamicMetadata` | Map | No | Custom success response items wrapped as Envoy Dynamic Metadata. Use it to pass data along to other proxy filters, such as the rate-limit filter. | + +###### SuccessResponseItem + +| **Field** | **Type** | **Required** | **Description** | +|-------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------|:------------:|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `plain` | [Plain text response item](https://docs.kuadrant.io/authorino/docs/features/#plain-text-responsesuccessheadersdynamicmetadataplain) | No | Plain text content. Use one of: `plain`, `json`, `wristband`. | +| `json` | [JSON injection response item](https://docs.kuadrant.io/authorino/docs/features/#json-injection-responsesuccessheadersdynamicmetadatajson) | No | Specification of a JSON object. Use one of: `plain`, `json`, `wristband`. | +| `wristband` | [Festival Wristband token response item](https://docs.kuadrant.io/authorino/docs/features/#festival-wristband-tokens-responsesuccessheadersdynamicmetadatawristband) | No | Specification of a JSON object. Use one of: `plain`, `json`, `wristband`. | +| `key` | String | No | The key used to add the custom response item (name of the HTTP header or root property of the Dynamic Metadata object). Defaults to the name of the response item if omitted. | + +#### CallbackRule + +| **Field** | **Type** | **Required** | **Description** | +|------------------|----------------------------------------------------------------------------------------------------------------|:------------:|-----------------------------------------------------------------| +| `http` | [HTTP endpoints callback spec](https://docs.kuadrant.io/authorino/docs/features/#http-endpoints-callbackshttp) | No | HTTP endpoint settings to build the callback request (webhook). | +| _(inline)_ | [AuthRuleCommon](#authrulecommon) | No | | + +### NamedPattern + +| **Field** | **Type** | **Required** | **Description** | +|------------|----------|:------------:|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `selector` | String | Yes | A valid [Well-known attribute](https://github.com/Kuadrant/architecture/blob/main/rfcs/0002-well-known-attributes.md) whose resolved value in the data plane will be compared to `value`, using the `operator`. | +| `operator` | String | Yes | The binary operator to be applied to the resolved value specified by the selector. One of: `eq` (equal to), `neq` (not equal to), `incl` (includes; for arrays), `excl` (excludes; for arrays), `matches` (regex). | +| `value` | String | Yes | The static value to be compared to the one resolved from the selector. | + +## AuthPolicyStatus + +| **Field** | **Type** | **Description** | +|----------------------|-----------------------------------|-------------------------------------------------------------------------------------------------------------------------------------| +| `observedGeneration` | String | Number of the last observed generation of the resource. Use it to check if the status info is up to date with latest resource spec. | +| `conditions` | [][ConditionSpec](#conditionspec) | List of conditions that define that status of the resource. | + +### ConditionSpec + +* The *lastTransitionTime* field provides a timestamp for when the entity last transitioned from one status to another. +* The *message* field is a human-readable message indicating details about the transition. +* The *reason* field is a unique, one-word, CamelCase reason for the condition’s last transition. +* The *status* field is a string, with possible values **True**, **False**, and **Unknown**. +* The *type* field is a string with the following possible values: + * Available: the resource has successfully configured; + +| **Field** | **Type** | **Description** | +|----------------------|-----------|------------------------------| +| `type` | String | Condition Type | +| `status` | String | Status: True, False, Unknown | +| `reason` | String | Condition state reason | +| `message` | String | Condition state description | +| `lastTransitionTime` | Timestamp | Last transition timestamp | diff --git a/doc/ratelimitpolicy-reference.md b/doc/reference/ratelimitpolicy.md similarity index 70% rename from doc/ratelimitpolicy-reference.md rename to doc/reference/ratelimitpolicy.md index e5ee83ebc..711b08569 100644 --- a/doc/ratelimitpolicy-reference.md +++ b/doc/reference/ratelimitpolicy.md @@ -4,7 +4,6 @@ - [RateLimitPolicySpec](#ratelimitpolicyspec) - [Limit](#limit) - [RateLimit](#ratelimit) - - [RouteSelector](#routeselector) - [WhenCondition](#whencondition) - [RateLimitPolicyStatus](#ratelimitpolicystatus) - [ConditionSpec](#conditionspec) @@ -25,12 +24,12 @@ ### Limit -| **Field** | **Type** | **Required** | **Description** | -|------------------|-----------------------------------|:------------:|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `rates` | [][RateLimit](#ratelimit) | No | List of rate limits associated with the limit definition | -| `counters` | []String | No | List of rate limit counter qualifiers. Items must be a valid [Well-known attribute](https://github.com/Kuadrant/architecture/blob/main/rfcs/0002-well-known-attributes.md). Each distinct value resolved in the data plane starts a separate counter for each rate limit. | -| `routeSelectors` | [][RouteSelector](#routeselector) | No | List of selectors of HTTPRouteRules whose matching rules activate the limit. At least one HTTPRouteRule must be selected to activate the limit. If omitted, all HTTPRouteRules of the targeted HTTPRoute activate the limit. Do not use it in policies targeting a Gateway. | -| `when` | [][WhenCondition](#whencondition) | No | List of additional dynamic conditions (expressions) to activate the limit. All expression must evaluate to true for the limit to be applied. Use it for filterring attributes that cannot be expressed in the targeted HTTPRoute's `spec.hostnames` and `spec.rules.matches` fields, or when targeting a Gateway. | +| **Field** | **Type** | **Required** | **Description** | +|------------------|-----------------------------------------------------|:------------:|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `rates` | [][RateLimit](#ratelimit) | No | List of rate limits associated with the limit definition | +| `counters` | []String | No | List of rate limit counter qualifiers. Items must be a valid [Well-known attribute](https://github.com/Kuadrant/architecture/blob/main/rfcs/0002-well-known-attributes.md). Each distinct value resolved in the data plane starts a separate counter for each rate limit. | +| `routeSelectors` | [][RouteSelector](route-selectors.md#routeselector) | No | List of selectors of HTTPRouteRules whose matching rules activate the limit. At least one HTTPRouteRule must be selected to activate the limit. If omitted, all HTTPRouteRules of the targeted HTTPRoute activate the limit. Do not use it in policies targeting a Gateway. | +| `when` | [][WhenCondition](#whencondition) | No | List of additional dynamic conditions (expressions) to activate the limit. All expression must evaluate to true for the limit to be applied. Use it for filtering attributes that cannot be expressed in the targeted HTTPRoute's `spec.hostnames` and `spec.rules.matches` fields, or when targeting a Gateway. | #### RateLimit @@ -40,15 +39,6 @@ | `duration` | Number | Yes | The period of time in the specified unit that the limit applies | | `unit` | String | Yes | Unit of time for the duration of the limit. One-of: "second", "minute", "hour", "day". | -#### RouteSelector - -| **Field** | **Type** | **Required** | **Description** | -|-------------|--------------------------------------------------------------------------------------------------------------------------------|:------------:|-----------------------------------------------------------------------------| -| `hostnames` | [][Hostname](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1beta1.Hostname) | No | List of hostnames of the HTTPRoute that activate the limit | -| `matches` | [][HTTPRouteMatch](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRouteMatch) | No | List of selectors of HTTPRouteRules whose matching rules activate the limit | - -Check out _Kuadrant Rate Limiting > [Route selectors](rate-limiting.md#route-selectors)_ for the semantics of how route selectors work. - #### WhenCondition | **Field** | **Type** | **Required** | **Description** | diff --git a/doc/reference/route-selectors.md b/doc/reference/route-selectors.md new file mode 100644 index 000000000..45f499611 --- /dev/null +++ b/doc/reference/route-selectors.md @@ -0,0 +1,41 @@ +# Route selectors + +The route selectors of a policy spec or policy rule (limit definition or auth rule) allow to specify **selectors of routes** or parts of a route, that _transitively induce a set of conditions for a policy or policy rule to be enforced_. It is defined as a set of HTTP route matching rules, where these matching rules must exist, partially or identically stated within the HTTPRouteRules of the HTTPRoute that is targeted by the policy. + +## The `routeSelectors` field + +The `routeSelectors` field can be found in policy specs and policy rules (limit definition or auth rule). + +| **Field** | **Type** | **Required** | **Description** | +|------------------|-----------------------------------|:------------:|------------------------------------------------------------------------------------------------------| +| `routeSelectors` | [][RouteSelector](#routeselector) | No | List of route selectors of HTTPRouteRules whose HTTPRouteMatches activate the policy or policy rule. | + +### RouteSelector + +Each `RouteSelector` is an object composed of a set of [HTTPRouteMatch](https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPPathMatch) objects (from Gateway API), and an additional `hostnames` field. + +| **Field** | **Type** | **Required** | **Description** | +|-------------|--------------------------------------------------------------------------------------------------------------------------------|:------------:|---------------------------------------------------------------------------------------------| +| `matches` | [][HTTPRouteMatch](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRouteMatch) | No | List of selectors of HTTPRouteRules whose matching rules activate the policy or policy rule | +| `hostnames` | [][Hostname](https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1beta1.Hostname) | No | List of hostnames of the HTTPRoute that activate the policy or policy rule | + +## Mechanics of the route selectors + +Route selectors matches and the HTTPRoute's HTTPRouteMatches are pairwise compared to select or not select HTTPRouteRules that should activate a policy rule. To decide whether the route selector selects a HTTPRouteRule or not, for each pair of route selector HTTPRouteMatch and HTTPRoute HTTPRouteMatch: +1. The route selector selects the HTTPRoute's HTTPRouteRule if the HTTPRouteRule contains at least one HTTPRouteMatch that specifies fields that are literally identical to all the fields specified by at least one HTTPRouteMatch of the route selector. +2. A HTTPRouteMatch within a HTTPRouteRule may include other fields that are not specified in a route selector match, and yet the route selector match selects the HTTPRouteRule if all fields of the route selector match are identically included in the HTTPRouteRule's HTTPRouteMatch; the opposite is NOT true. +3. Each field `path` of a HTTPRouteMatch, as well as each field `method` of a HTTPRouteMatch, as well as each element of the fields `headers` and `queryParams` of a HTTPRouteMatch, is atomic – this is true for the HTTPRouteMatches within a HTTPRouteRule, as well as for HTTPRouteMatches of a route selector. + +Additionally, at least one hostname specified in a route selector must identically match one of the hostnames specified (or inherited, when omitted) by the targeted HTTPRoute. + +The semantics of the route selectors allows to assertively relate policy rule definitions to routing rules, with benefits for identifying the subsets of the network that are covered by a policy rule, while preventing unreachable definitions, as well as the overhead associated with the maintenance of such rules across multiple resources throughout time, according to network topology beneath. Moreover, the requirement of not having to be a full copy of the targeted HTTPRouteRule matches, but only partially identical, helps prevent repetition to some degree, as well as it enables to more easily define policy rules that scope across multiple HTTPRouteRules (by specifying less rules in the selector). + +## Golden rules and corner cases + +A few rules and corner cases to keep in mind while using the RLP's `routeSelectors`: +1. **The golden rule –** The route selectors in a policy or policy rule are **not** to be interpreted as the route matching rules that activate the policy or policy rule, but as **selectors of the route rules** that activate the policy or policy rule. +2. Due to (1) above, this can lead to cases, e.g., where a route selector that states `matches: [{ method: POST }]` selects a HTTPRouteRule that defines `matches: [{ method: POST }, { method: GET }]`, effectively causing the policy or policy rule to be activated on requests to the HTTP method `POST`, but **also** to the HTTP method `GET`. +3. The requirement for the route selector match to state patterns that are identical to the patterns stated by the HTTPRouteRule (partially or entirely) makes, e.g., a route selector such as `matches: { path: { type: PathPrefix, value: /foo } }` to select a HTTPRouteRule that defines `matches: { path: { type: PathPrefix, value: /foo }, method: GET }`, but **not** to select a HTTPRouteRule that only defines `matches: { method: GET }`, even though the latter includes technically all HTTP paths; **nor** it selects a HTTPRouteRule that only defines `matches: { path: { type: Exact, value: /foo } }`, even though all requests to the exact path `/foo` are also technically requests to `/foo*`. +4. The atomicity property of fields of the route selectors makes, e.g., a route selector such as `matches: { path: { value: /foo } }` to select a HTTPRouteRule that defines `matches: { path: { value: /foo } }`, but **not** to select a HTTPRouteRule that only defines `matches: { path: { type: PathPrefix, value: /foo } }`. (This case may actually never happen because `PathPrefix` is the default value for `path.type` and will be set automatically by the Kubernetes API server.) + +Due to the nature of route selectors of defining pointers to HTTPRouteRules, the `routeSelectors` field is not supported in a RLP that targets a Gateway resource. diff --git a/doc/user-guides/auth-for-app-devs-and-platform-engineers.md b/doc/user-guides/auth-for-app-devs-and-platform-engineers.md new file mode 100644 index 000000000..4e1b13551 --- /dev/null +++ b/doc/user-guides/auth-for-app-devs-and-platform-engineers.md @@ -0,0 +1,285 @@ +# Enforcing authentication & authorization with Kuadrant AuthPolicy + +This guide walks you through the process of setting up a local Kubernetes cluster with Kuadrant where you will protect [Gateway API](gateway-api.sigs.k8s.io/) endpoints by declaring Kuadrant AuthPolicy custom resources. + +Two AuthPolicies will be declared: + +| Use case | AuthPolicy | +|--------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **App developer** | 1 AuthPolicy targeting a HTTPRoute that routes traffic to a sample Toy Store application, and enforces API key authentication to all requests in this route, as well as requires API key owners to be mapped to `groups:admins` metadata to access a specific HTTPRouteRule of the route. | +| **Platform engineer use-case** | 1 AuthPolicy targeting the `istio-ingressgateway` Gateway that enforces a trivial "deny-all" policy that locks down any other HTTPRoute attached to the Gateway. | + +Topology: + +``` + ┌───────────────┐ + │ (AuthPolicy) │ + │ gw-auth │ + └───────┬───────┘ + │ + ▼ + ┌──────────────────────┐ + │ (Gateway) │ + │ istio-ingressgateway │ + ┌────►│ │◄───┐ + │ │ * │ │ + │ └──────────────────────┘ │ + │ │ + ┌────────┴─────────┐ ┌────────┴─────────┐ + │ (HTTPRoute) │ │ (HTTPRoute) │ + │ toystore │ │ other │ + │ │ │ │ + │ api.toystore.com │ │ *.other-apps.com │ + └──────────────────┘ └──────────────────┘ + ▲ + │ + ┌───────┴───────┐ + │ (AuthPolicy) │ + │ toystore │ + └───────────────┘ +``` + +## Requisites + +- [Docker](https://docker.io) + +## Run the guide ① → ④ + +### ① Setup (Persona: _Cluster admin_) + +Clone the repo: + +```sh +git clone git@github.com:Kuadrant/kuadrant-operator.git && cd kuadrant-operator +``` + +Run the following command to create a local Kubernetes cluster with [Kind](https://kind.sigs.k8s.io/), install & deploy Kuadrant: + +```sh +make local-setup +``` + +Request an instance of Kuadrant in the `kuadrant-system` namespace: + +```sh +kubectl -n kuadrant-system apply -f - < 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)