diff --git a/.golangci.yaml b/.golangci.yaml index 6f5fd666c..7461076ef 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -7,8 +7,8 @@ linters: - errorlint - revive - gosec - - prealloc - stylecheck + - prealloc - tparallel - unconvert - unparam diff --git a/api/v1/merge_strategies.go b/api/v1/merge_strategies.go index 09977500f..779c9f157 100644 --- a/api/v1/merge_strategies.go +++ b/api/v1/merge_strategies.go @@ -32,9 +32,20 @@ const ( PolicyRuleMergeStrategy = "merge" ) -type MergeableRule struct { - Spec any - Source string +// NewMergeableRule creates a new MergeableRule with a default source if the rule does not have one. +func NewMergeableRule(rule MergeableRule, defaultSource string) MergeableRule { + if rule.GetSource() == "" { + return rule.WithSource(defaultSource) + } + return rule +} + +// MergeableRule is a policy rule that contains a spec which can be traced back to its source, +// i.e. to the policy where the rule spec was defined. +type MergeableRule interface { + GetSpec() any + GetSource() string + WithSource(string) MergeableRule } // +kubebuilder:object:generate=false @@ -58,13 +69,11 @@ func AtomicDefaultsMergeStrategy(source, target machinery.Policy) machinery.Poli return source } - mergeableTargetPolicy := target.(MergeablePolicy) - - if !mergeableTargetPolicy.Empty() { - return mergeableTargetPolicy.DeepCopyObject().(machinery.Policy) + if mergeableTarget := target.(MergeablePolicy); !mergeableTarget.Empty() { + return copyMergeablePolicy(mergeableTarget) } - return source.(MergeablePolicy).DeepCopyObject().(machinery.Policy) + return copyMergeablePolicy(source.(MergeablePolicy)) } var _ machinery.MergeStrategy = AtomicDefaultsMergeStrategy @@ -75,7 +84,7 @@ func AtomicOverridesMergeStrategy(source, _ machinery.Policy) machinery.Policy { if source == nil { return nil } - return source.(MergeablePolicy).DeepCopyObject().(machinery.Policy) + return copyMergeablePolicy(source.(MergeablePolicy)) } var _ machinery.MergeStrategy = AtomicOverridesMergeStrategy @@ -94,15 +103,16 @@ func PolicyRuleDefaultsMergeStrategy(source, target machinery.Policy) machinery. targetMergeablePolicy := target.(MergeablePolicy) // copy rules from the target - rules := targetMergeablePolicy.Rules() + rules := lo.MapValues(targetMergeablePolicy.Rules(), mapRuleWithSourceFunc(target)) // add extra rules from the source for ruleID, rule := range sourceMergeablePolicy.Rules() { if _, ok := targetMergeablePolicy.Rules()[ruleID]; !ok { - rules[ruleID] = MergeableRule{ - Spec: rule.Spec, - Source: source.GetLocator(), + origin := rule.GetSource() + if origin == "" { + origin = source.GetLocator() } + rules[ruleID] = rule.WithSource(origin) } } @@ -121,12 +131,16 @@ func PolicyRuleOverridesMergeStrategy(source, target machinery.Policy) machinery targetMergeablePolicy := target.(MergeablePolicy) // copy rules from the source - rules := sourceMergeablePolicy.Rules() + rules := lo.MapValues(sourceMergeablePolicy.Rules(), mapRuleWithSourceFunc(source)) // add extra rules from the target for ruleID, rule := range targetMergeablePolicy.Rules() { if _, ok := sourceMergeablePolicy.Rules()[ruleID]; !ok { - rules[ruleID] = rule + origin := rule.GetSource() + if origin == "" { + origin = target.GetLocator() + } + rules[ruleID] = rule.WithSource(origin) } } @@ -198,3 +212,15 @@ func PathID(path []machinery.Targetable) string { return strings.TrimPrefix(k8stypes.NamespacedName{Namespace: t.GetNamespace(), Name: t.GetName()}.String(), string(k8stypes.Separator)) }), "|") } + +func mapRuleWithSourceFunc(source machinery.Policy) func(MergeableRule, string) MergeableRule { + return func(rule MergeableRule, _ string) MergeableRule { + return rule.WithSource(source.GetLocator()) + } +} + +func copyMergeablePolicy(policy MergeablePolicy) MergeablePolicy { + dup := policy.DeepCopyObject().(MergeablePolicy) + dup.SetRules(lo.MapValues(dup.Rules(), mapRuleWithSourceFunc(policy))) + return dup +} diff --git a/api/v1beta1/topology.go b/api/v1beta1/topology.go index a2cf74945..69cf954d9 100644 --- a/api/v1beta1/topology.go +++ b/api/v1beta1/topology.go @@ -1,23 +1,28 @@ package v1beta1 import ( - authorinov1beta1 "github.com/kuadrant/authorino-operator/api/v1beta1" + authorinooperatorv1beta1 "github.com/kuadrant/authorino-operator/api/v1beta1" + authorinov1beta2 "github.com/kuadrant/authorino/api/v1beta2" limitadorv1alpha1 "github.com/kuadrant/limitador-operator/api/v1alpha1" "github.com/kuadrant/policy-machinery/controller" "github.com/kuadrant/policy-machinery/machinery" "github.com/samber/lo" "k8s.io/apimachinery/pkg/runtime/schema" - gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" + gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" ) var ( - AuthorinoGroupKind = schema.GroupKind{Group: authorinov1beta1.GroupVersion.Group, Kind: "Authorino"} - KuadrantGroupKind = schema.GroupKind{Group: GroupVersion.Group, Kind: "Kuadrant"} - LimitadorGroupKind = schema.GroupKind{Group: limitadorv1alpha1.GroupVersion.Group, Kind: "Limitador"} + KuadrantGroupKind = schema.GroupKind{Group: GroupVersion.Group, Kind: "Kuadrant"} + LimitadorGroupKind = schema.GroupKind{Group: limitadorv1alpha1.GroupVersion.Group, Kind: "Limitador"} + AuthorinoGroupKind = schema.GroupKind{Group: authorinooperatorv1beta1.GroupVersion.Group, Kind: "Authorino"} + AuthConfigGroupKind = schema.GroupKind{Group: authorinov1beta2.GroupVersion.Group, Kind: "AuthConfig"} - AuthorinosResource = authorinov1beta1.GroupVersion.WithResource("authorinos") - KuadrantsResource = GroupVersion.WithResource("kuadrants") - LimitadorsResource = limitadorv1alpha1.GroupVersion.WithResource("limitadors") + KuadrantsResource = GroupVersion.WithResource("kuadrants") + LimitadorsResource = limitadorv1alpha1.GroupVersion.WithResource("limitadors") + AuthorinosResource = authorinooperatorv1beta1.GroupVersion.WithResource("authorinos") + AuthConfigsResource = authorinov1beta2.GroupVersion.WithResource("authconfigs") + + AuthConfigHTTPRouteRuleAnnotation = machinery.HTTPRouteRuleGroupKind.String() ) var _ machinery.Object = &Kuadrant{} @@ -31,7 +36,7 @@ func LinkKuadrantToGatewayClasses(objs controller.Store) machinery.LinkFunc { return machinery.LinkFunc{ From: KuadrantGroupKind, - To: schema.GroupKind{Group: gwapiv1.GroupVersion.Group, Kind: "GatewayClass"}, + To: schema.GroupKind{Group: gatewayapiv1.GroupVersion.Group, Kind: "GatewayClass"}, Func: func(_ machinery.Object) []machinery.Object { parents := make([]machinery.Object, len(kuadrants)) for _, parent := range kuadrants { @@ -69,3 +74,22 @@ func LinkKuadrantToAuthorino(objs controller.Store) machinery.LinkFunc { }, } } + +func LinkHTTPRouteRuleToAuthConfig(objs controller.Store) machinery.LinkFunc { + httpRoutes := lo.Map(objs.FilterByGroupKind(machinery.HTTPRouteGroupKind), controller.ObjectAs[*gatewayapiv1.HTTPRoute]) + httpRouteRules := lo.FlatMap(lo.Map(httpRoutes, func(r *gatewayapiv1.HTTPRoute, _ int) *machinery.HTTPRoute { + return &machinery.HTTPRoute{HTTPRoute: r} + }), machinery.HTTPRouteRulesFromHTTPRouteFunc) + + return machinery.LinkFunc{ + From: machinery.HTTPRouteRuleGroupKind, + To: AuthConfigGroupKind, + Func: func(child machinery.Object) []machinery.Object { + return lo.FilterMap(httpRouteRules, func(httpRouteRule *machinery.HTTPRouteRule, _ int) (machinery.Object, bool) { + authConfig := child.(*controller.RuntimeObject).Object.(*authorinov1beta2.AuthConfig) + annotations := authConfig.GetAnnotations() + return httpRouteRule, annotations != nil && annotations[AuthConfigHTTPRouteRuleAnnotation] == httpRouteRule.GetLocator() + }) + }, + } +} diff --git a/api/v1beta3/authpolicy_types.go b/api/v1beta3/authpolicy_types.go index 032ba57fa..bbae0f157 100644 --- a/api/v1beta3/authpolicy_types.go +++ b/api/v1beta3/authpolicy_types.go @@ -1,137 +1,602 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + package v1beta3 import ( - "context" + "fmt" + "strings" "github.com/go-logr/logr" "github.com/google/go-cmp/cmp" - authorinoapi "github.com/kuadrant/authorino/api/v1beta2" + authorinov1beta2 "github.com/kuadrant/authorino/api/v1beta2" + "github.com/kuadrant/policy-machinery/machinery" + "github.com/samber/lo" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" - "sigs.k8s.io/controller-runtime/pkg/client" gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + kuadrantv1 "github.com/kuadrant/kuadrant-operator/api/v1" kuadrantgatewayapi "github.com/kuadrant/kuadrant-operator/pkg/library/gatewayapi" "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" "github.com/kuadrant/kuadrant-operator/pkg/library/utils" ) -var ( - AuthPolicyGVK schema.GroupVersionKind = schema.GroupVersionKind{ - Group: GroupVersion.Group, - Version: GroupVersion.Version, - Kind: "AuthPolicy", - } -) - const ( + // TODO: remove after fixing the integration tests that still depend on these AuthPolicyBackReferenceAnnotationName = "kuadrant.io/authpolicies" AuthPolicyDirectReferenceAnnotationName = "kuadrant.io/authpolicy" ) +var ( + AuthPolicyGroupKind = schema.GroupKind{Group: SchemeGroupVersion.Group, Kind: "AuthPolicy"} + AuthPoliciesResource = SchemeGroupVersion.WithResource("authpolicies") +) + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:metadata:labels="gateway.networking.k8s.io/policy=inherited" +// +kubebuilder:printcolumn:name="Accepted",type=string,JSONPath=`.status.conditions[?(@.type=="Accepted")].status`,description="AuthPolicy Accepted",priority=2 +// +kubebuilder:printcolumn:name="Enforced",type=string,JSONPath=`.status.conditions[?(@.type=="Enforced")].status`,description="AuthPolicy Enforced",priority=2 +// +kubebuilder:printcolumn:name="TargetKind",type="string",JSONPath=".spec.targetRef.kind",description="Kind of the object to which the policy aaplies",priority=2 +// +kubebuilder:printcolumn:name="TargetName",type="string",JSONPath=".spec.targetRef.name",description="Name of the object to which the policy applies",priority=2 +// +kubebuilder:printcolumn:name="TargetSection",type="string",JSONPath=".spec.targetRef.sectionName",description="Name of the section within the object to which the policy applies ",priority=2 +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" + +// AuthPolicy enables authentication and authorization for service workloads in a Gateway API network +type AuthPolicy struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec AuthPolicySpec `json:"spec,omitempty"` + Status AuthPolicyStatus `json:"status,omitempty"` +} + +var _ machinery.Policy = &AuthPolicy{} + +func (p *AuthPolicy) GetNamespace() string { + return p.Namespace +} + +func (p *AuthPolicy) GetName() string { + return p.Name +} + +func (p *AuthPolicy) GetLocator() string { + return machinery.LocatorFromObject(p) +} + +// TODO: remove +func (p *AuthPolicy) IsAtomicOverride() bool { + return p.Spec.Overrides != nil && p.Spec.Overrides.Strategy == kuadrantv1.AtomicMergeStrategy +} + +// DEPRECATED: Use GetTargetRefs instead +func (p *AuthPolicy) GetTargetRef() gatewayapiv1alpha2.LocalPolicyTargetReference { + return p.Spec.TargetRef.LocalPolicyTargetReference +} + +func (p *AuthPolicy) GetTargetRefs() []machinery.PolicyTargetReference { + return []machinery.PolicyTargetReference{ + machinery.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReferenceWithSectionName: p.Spec.TargetRef, + PolicyNamespace: p.Namespace, + }, + } +} + +func (p *AuthPolicy) GetMergeStrategy() machinery.MergeStrategy { + if spec := p.Spec.Defaults; spec != nil { + return kuadrantv1.DefaultsMergeStrategy(spec.Strategy) + } + if spec := p.Spec.Overrides; spec != nil { + return kuadrantv1.OverridesMergeStrategy(spec.Strategy) + } + return kuadrantv1.AtomicDefaultsMergeStrategy +} + +func (p *AuthPolicy) Merge(other machinery.Policy) machinery.Policy { + source, ok := other.(*AuthPolicy) + if !ok { + return p + } + return source.GetMergeStrategy()(source, p) +} + +var _ kuadrantv1.MergeablePolicy = &AuthPolicy{} + +func (p *AuthPolicy) Empty() bool { + return p.Spec.Proper().AuthScheme == nil +} + +func (p *AuthPolicy) Rules() map[string]kuadrantv1.MergeableRule { + rules := make(map[string]kuadrantv1.MergeableRule) + policyLocator := p.GetLocator() + spec := p.Spec.Proper() + + for ruleID := range spec.NamedPatterns { + rule := spec.NamedPatterns[ruleID] + rules[fmt.Sprintf("patterns#%s", ruleID)] = kuadrantv1.NewMergeableRule(&rule, policyLocator) + } + + for ruleID := range spec.Conditions { + rule := spec.Conditions[ruleID] + rules[fmt.Sprintf("conditions#%d", ruleID)] = kuadrantv1.NewMergeableRule(&rule, policyLocator) + } + + if spec.AuthScheme == nil { + return rules + } + + for ruleID := range spec.AuthScheme.Authentication { + rule := spec.AuthScheme.Authentication[ruleID] + rules[fmt.Sprintf("authentication#%s", ruleID)] = kuadrantv1.NewMergeableRule(&rule, policyLocator) + } + + for ruleID := range spec.AuthScheme.Metadata { + rule := spec.AuthScheme.Metadata[ruleID] + rules[fmt.Sprintf("metadata#%s", ruleID)] = kuadrantv1.NewMergeableRule(&rule, policyLocator) + } + + for ruleID := range spec.AuthScheme.Authorization { + rule := spec.AuthScheme.Authorization[ruleID] + rules[fmt.Sprintf("authorization#%s", ruleID)] = kuadrantv1.NewMergeableRule(&rule, policyLocator) + } + + for ruleID := range spec.AuthScheme.Callbacks { + rule := spec.AuthScheme.Callbacks[ruleID] + rules[fmt.Sprintf("callbacks#%s", ruleID)] = kuadrantv1.NewMergeableRule(&rule, policyLocator) + } + + if spec.AuthScheme.Response == nil { + return rules + } + + if rule := spec.AuthScheme.Response.Unauthenticated; rule != nil { + rules["response.unauthenticated#"] = kuadrantv1.NewMergeableRule(rule, policyLocator) + } + if rule := spec.AuthScheme.Response.Unauthorized; rule != nil { + rules["response.unauthorized#"] = kuadrantv1.NewMergeableRule(rule, policyLocator) + } + + for ruleID := range spec.AuthScheme.Response.Success.Headers { + rule := spec.AuthScheme.Response.Success.Headers[ruleID] + rules[fmt.Sprintf("response.success.headers#%s", ruleID)] = kuadrantv1.NewMergeableRule(&rule, policyLocator) + } + + for ruleID := range spec.AuthScheme.Response.Success.DynamicMetadata { + rule := spec.AuthScheme.Response.Success.DynamicMetadata[ruleID] + rules[fmt.Sprintf("response.success.metadata#%s", ruleID)] = kuadrantv1.NewMergeableRule(&rule, policyLocator) + } + + return rules +} + +func (p *AuthPolicy) SetRules(rules map[string]kuadrantv1.MergeableRule) { + // clear all rules of the policy before setting new ones + p.Spec.Proper().NamedPatterns = nil + p.Spec.Proper().Conditions = nil + p.Spec.Proper().AuthScheme = nil + + ensureNamedPatterns := func() { + if p.Spec.Proper().NamedPatterns == nil { + p.Spec.Proper().NamedPatterns = make(map[string]MergeablePatternExpressions) + } + } + + ensureAuthScheme := func() { + if p.Spec.Proper().AuthScheme == nil { + p.Spec.Proper().AuthScheme = &AuthSchemeSpec{} + } + } + + ensureAuthentication := func() { + ensureAuthScheme() + if p.Spec.Proper().AuthScheme.Authentication == nil { + p.Spec.Proper().AuthScheme.Authentication = make(map[string]MergeableAuthenticationSpec) + } + } + + ensureMetadata := func() { + ensureAuthScheme() + if p.Spec.Proper().AuthScheme.Metadata == nil { + p.Spec.Proper().AuthScheme.Metadata = make(map[string]MergeableMetadataSpec) + } + } + + ensureAuthorization := func() { + ensureAuthScheme() + if p.Spec.Proper().AuthScheme.Authorization == nil { + p.Spec.Proper().AuthScheme.Authorization = make(map[string]MergeableAuthorizationSpec) + } + } + + ensureResponse := func() { + ensureAuthScheme() + if p.Spec.Proper().AuthScheme.Response == nil { + p.Spec.Proper().AuthScheme.Response = &MergeableResponseSpec{} + } + } + + ensureResponseSuccessHeaders := func() { + ensureResponse() + if p.Spec.Proper().AuthScheme.Response.Success.Headers == nil { + p.Spec.Proper().AuthScheme.Response.Success.Headers = make(map[string]MergeableHeaderSuccessResponseSpec) + } + } + + ensureResponseSuccessDynamicMetadata := func() { + ensureResponse() + if p.Spec.Proper().AuthScheme.Response.Success.DynamicMetadata == nil { + p.Spec.Proper().AuthScheme.Response.Success.DynamicMetadata = make(map[string]MergeableSuccessResponseSpec) + } + } + + ensureCallbacks := func() { + ensureAuthScheme() + if p.Spec.Proper().AuthScheme.Callbacks == nil { + p.Spec.Proper().AuthScheme.Callbacks = make(map[string]MergeableCallbackSpec) + } + } + + for id := range rules { + rule := rules[id] + parts := strings.SplitN(id, "#", 2) + group := parts[0] + ruleID := parts[len(parts)-1] + + if strings.HasPrefix(group, "response.") { + ensureResponse() + } + + switch group { + case "patterns": + ensureNamedPatterns() + p.Spec.Proper().NamedPatterns[ruleID] = *rule.(*MergeablePatternExpressions) + case "conditions": + p.Spec.Proper().Conditions = append(p.Spec.Proper().Conditions, *rule.(*MergeablePatternExpressionOrRef)) + case "authentication": + ensureAuthentication() + p.Spec.Proper().AuthScheme.Authentication[ruleID] = *rule.(*MergeableAuthenticationSpec) + case "metadata": + ensureMetadata() + p.Spec.Proper().AuthScheme.Metadata[ruleID] = *rule.(*MergeableMetadataSpec) + case "authorization": + ensureAuthorization() + p.Spec.Proper().AuthScheme.Authorization[ruleID] = *rule.(*MergeableAuthorizationSpec) + case "response.unauthenticated": + ensureResponse() + p.Spec.Proper().AuthScheme.Response.Unauthenticated = rule.(*MergeableDenyWithSpec) + case "response.unauthorized": + ensureResponse() + p.Spec.Proper().AuthScheme.Response.Unauthorized = rule.(*MergeableDenyWithSpec) + case "response.success.headers": + ensureResponseSuccessHeaders() + p.Spec.Proper().AuthScheme.Response.Success.Headers[ruleID] = *rule.(*MergeableHeaderSuccessResponseSpec) + case "response.success.metadata": + ensureResponseSuccessDynamicMetadata() + p.Spec.Proper().AuthScheme.Response.Success.DynamicMetadata[ruleID] = *rule.(*MergeableSuccessResponseSpec) + case "callbacks": + ensureCallbacks() + p.Spec.Proper().AuthScheme.Callbacks[ruleID] = *rule.(*MergeableCallbackSpec) + } + } +} + +// DEPRECATED. impl: kuadrant.Policy +func (p *AuthPolicy) GetStatus() kuadrantgatewayapi.PolicyStatus { + return &p.Status +} + +// DEPRECATED. impl: kuadrant.Policy +func (p *AuthPolicy) PolicyClass() kuadrantgatewayapi.PolicyClass { + return kuadrantgatewayapi.InheritedPolicy +} + +// DEPRECATED. impl: kuadrant.Policy +func (p *AuthPolicy) GetWrappedNamespace() gatewayapiv1.Namespace { + return gatewayapiv1.Namespace(p.GetNamespace()) +} + +// DEPRECATED. impl: kuadrant.Policy +func (p *AuthPolicy) GetRulesHostnames() []string { + return []string{} +} + +// DEPRECATED. impl: kuadrant.Policy +func (p *AuthPolicy) Kind() string { + return AuthPolicyGroupKind.Kind +} + +// TODO: remove +func (p *AuthPolicy) BackReferenceAnnotationName() string { + return AuthPolicyBackReferenceAnnotationName +} + +// TODO: remove +func (p *AuthPolicy) DirectReferenceAnnotationName() string { + return AuthPolicyDirectReferenceAnnotationName +} + +// TODO: remove +func (p *AuthPolicy) TargetProgrammedGatewaysOnly() bool { + return true +} + +// +kubebuilder:validation:XValidation:rule="!(has(self.defaults) && (has(self.patterns) || has(self.when) || has(self.rules)))",message="Implicit and explicit defaults are mutually exclusive" +// +kubebuilder:validation:XValidation:rule="!(has(self.overrides) && (has(self.patterns) || has(self.when) || has(self.rules)))",message="Implicit defaults and explicit overrides are mutually exclusive" +// +kubebuilder:validation:XValidation:rule="!(has(self.overrides) && has(self.defaults))",message="Explicit overrides and explicit defaults are mutually exclusive" +type AuthPolicySpec struct { + // Reference to the object to which this policy applies. + // +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.LocalPolicyTargetReferenceWithSectionName `json:"targetRef"` + + // Rules to apply as defaults. Can be overridden by more specific policiy rules lower in the hierarchy and by less specific policy overrides. + // Use one of: defaults, overrides, or bare set of policy rules (implicit defaults). + // +optional + Defaults *MergeableAuthPolicySpec `json:"defaults,omitempty"` + + // Rules to apply as overrides. Override all policy rules lower in the hierarchy. Can be overridden by less specific policy overrides. + // Use one of: defaults, overrides, or bare set of policy rules (implicit defaults). + // +optional + Overrides *MergeableAuthPolicySpec `json:"overrides,omitempty"` + + // Bare set of policy rules (implicit defaults). + // Use one of: defaults, overrides, or bare set of policy rules (implicit defaults). + AuthPolicySpecProper `json:""` +} + +func (s *AuthPolicySpec) Proper() *AuthPolicySpecProper { + if s.Defaults != nil { + return &s.Defaults.AuthPolicySpecProper + } + + if s.Overrides != nil { + return &s.Overrides.AuthPolicySpecProper + } + + return &s.AuthPolicySpecProper +} + +type MergeableAuthPolicySpec struct { + // Strategy defines the merge strategy to apply when merging this policy with other policies. + // +kubebuilder:validation:Enum=atomic;merge + // +kubebuilder:default=atomic + Strategy string `json:"strategy,omitempty"` + + AuthPolicySpecProper `json:""` +} + +// AuthPolicySpecProper contains common shared fields for defaults and overrides +type AuthPolicySpecProper struct { + // Named sets of patterns that can be referred in `when` conditions and in pattern-matching authorization policy rules. + // +optional + NamedPatterns map[string]MergeablePatternExpressions `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 []MergeablePatternExpressionOrRef `json:"when,omitempty"` + + // The auth rules of the policy. + // See Authorino's AuthConfig CRD for more details. + AuthScheme *AuthSchemeSpec `json:"rules,omitempty"` +} + 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]authorinoapi.AuthenticationSpec `json:"authentication,omitempty"` + // +kubebuilder:validation:MaxProperties=10 + Authentication map[string]MergeableAuthenticationSpec `json:"authentication,omitempty"` // Metadata sources. // Authorino fetches auth metadata as JSON from sources specified in this config. // +optional - Metadata map[string]authorinoapi.MetadataSpec `json:"metadata,omitempty"` + // +kubebuilder:validation:MaxProperties=10 + Metadata map[string]MergeableMetadataSpec `json:"metadata,omitempty"` // Authorization policies. // All policies MUST evaluate to "allowed = true" for the auth request be successful. // +optional - Authorization map[string]authorinoapi.AuthorizationSpec `json:"authorization,omitempty"` + // +kubebuilder:validation:MaxProperties=10 + Authorization map[string]MergeableAuthorizationSpec `json:"authorization,omitempty"` // Response items. // Authorino builds custom responses to the client of the auth request. // +optional - Response *ResponseSpec `json:"response,omitempty"` + Response *MergeableResponseSpec `json:"response,omitempty"` // Callback functions. // Authorino sends callbacks at the end of the auth pipeline to the endpoints specified in this config. // +optional - Callbacks map[string]authorinoapi.CallbackSpec `json:"callbacks,omitempty"` + // +kubebuilder:validation:MaxProperties=10 + Callbacks map[string]MergeableCallbackSpec `json:"callbacks,omitempty"` +} + +type MergeablePatternExpressions struct { + authorinov1beta2.PatternExpressions `json:"allOf"` + Source string `json:"-"` +} + +func (r *MergeablePatternExpressions) GetSpec() any { return r.PatternExpressions } +func (r *MergeablePatternExpressions) GetSource() string { return r.Source } +func (r *MergeablePatternExpressions) WithSource(source string) kuadrantv1.MergeableRule { + r.Source = source + return r +} + +type MergeablePatternExpressionOrRef struct { + authorinov1beta2.PatternExpressionOrRef `json:",inline"` + Source string `json:"-"` } -type ResponseSpec struct { +func (r *MergeablePatternExpressionOrRef) GetSpec() any { return r.PatternExpressionOrRef } +func (r *MergeablePatternExpressionOrRef) GetSource() string { return r.Source } +func (r *MergeablePatternExpressionOrRef) WithSource(source string) kuadrantv1.MergeableRule { + r.Source = source + return r +} +func (r *MergeablePatternExpressionOrRef) ToWhenConditions(namedPatterns map[string]MergeablePatternExpressions) []WhenCondition { + if ref := r.PatternRef.Name; ref != "" { + if pattern, ok := namedPatterns[ref]; ok { + return lo.Map(pattern.PatternExpressions, func(p authorinov1beta2.PatternExpression, _ int) WhenCondition { + return WhenCondition{ + Selector: ContextSelector(p.Selector), + Operator: WhenConditionOperator(p.Operator), + Value: p.Value, + } + }) + } + } + + if allOf := r.All; len(allOf) > 0 { + return lo.Map(allOf, func(p authorinov1beta2.UnstructuredPatternExpressionOrRef, _ int) WhenCondition { + return WhenCondition{ + Selector: ContextSelector(p.Selector), + Operator: WhenConditionOperator(p.Operator), + Value: p.Value, + } + }) + } + + // FIXME: anyOf cannot be represented in the current schema of the wasm config + + return []WhenCondition{ + { + Selector: ContextSelector(r.Selector), + Operator: WhenConditionOperator(r.Operator), + Value: r.Value, + }, + } +} + +type MergeableAuthenticationSpec struct { + authorinov1beta2.AuthenticationSpec `json:",inline"` + Source string `json:"-"` +} + +func (r *MergeableAuthenticationSpec) GetSpec() any { return r.AuthenticationSpec } +func (r *MergeableAuthenticationSpec) GetSource() string { return r.Source } +func (r *MergeableAuthenticationSpec) WithSource(source string) kuadrantv1.MergeableRule { + r.Source = source + return r +} + +type MergeableMetadataSpec struct { + authorinov1beta2.MetadataSpec `json:",inline"` + Source string `json:"-"` +} + +func (r *MergeableMetadataSpec) GetSpec() any { return r.MetadataSpec } +func (r *MergeableMetadataSpec) GetSource() string { return r.Source } +func (r *MergeableMetadataSpec) WithSource(source string) kuadrantv1.MergeableRule { + r.Source = source + return r +} + +type MergeableAuthorizationSpec struct { + authorinov1beta2.AuthorizationSpec `json:",inline"` + Source string `json:"-"` +} + +func (r *MergeableAuthorizationSpec) GetSpec() any { return r.AuthorizationSpec } +func (r *MergeableAuthorizationSpec) GetSource() string { return r.Source } +func (r *MergeableAuthorizationSpec) WithSource(source string) kuadrantv1.MergeableRule { + r.Source = source + return r +} + +// Settings of the custom auth response. +type MergeableResponseSpec 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"` + Unauthenticated *MergeableDenyWithSpec `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"` + Unauthorized *MergeableDenyWithSpec `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"` + Success MergeableWrappedSuccessResponseSpec `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]authorinoapi.SuccessResponseSpec `json:"dynamicMetadata,omitempty"` +type MergeableDenyWithSpec struct { + authorinov1beta2.DenyWithSpec `json:",inline"` + Source string `json:"-"` } -type HeaderSuccessResponseSpec struct { - authorinoapi.SuccessResponseSpec `json:""` +func (r *MergeableDenyWithSpec) GetSpec() any { return r.DenyWithSpec } +func (r *MergeableDenyWithSpec) GetSource() string { return r.Source } +func (r *MergeableDenyWithSpec) WithSource(source string) kuadrantv1.MergeableRule { + r.Source = source + return r } -// Mutual Exclusivity Validation -// +kubebuilder:validation:XValidation:rule="!(has(self.defaults) && (has(self.patterns) || has(self.when) || has(self.rules)))",message="Implicit and explicit defaults are mutually exclusive" -// +kubebuilder:validation:XValidation:rule="!(has(self.overrides) && (has(self.patterns) || has(self.when) || has(self.rules)))",message="Implicit defaults and explicit overrides are mutually exclusive" -// +kubebuilder:validation:XValidation:rule="!(has(self.overrides) && has(self.defaults))",message="Explicit overrides and explicit defaults are mutually exclusive" -// +kubebuilder:validation:XValidation:rule="!(has(self.overrides) && self.targetRef.kind == 'HTTPRoute')",message="Overrides are not allowed for policies targeting a HTTPRoute resource" -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.LocalPolicyTargetReference `json:"targetRef"` +type MergeableWrappedSuccessResponseSpec struct { + // Custom headers to inject in the request. + Headers map[string]MergeableHeaderSuccessResponseSpec `json:"headers,omitempty"` - // Defaults define explicit default values for this policy and for policies inheriting this policy. - // Defaults are mutually exclusive with implicit defaults defined by AuthPolicyCommonSpec. - // +optional - Defaults *AuthPolicyCommonSpec `json:"defaults,omitempty"` + // Custom data made available to other filters managed by Kuadrant (i.e. Rate Limit) + DynamicMetadata map[string]MergeableSuccessResponseSpec `json:"filters,omitempty"` +} - // Overrides define explicit override values for this policy. - // Overrides are mutually exclusive with explicit and implicit defaults defined by AuthPolicyCommonSpec. - // +optional - Overrides *AuthPolicyCommonSpec `json:"overrides,omitempty"` +type MergeableHeaderSuccessResponseSpec struct { + authorinov1beta2.HeaderSuccessResponseSpec `json:",inline"` + Source string `json:"-"` +} - // AuthPolicyCommonSpec defines implicit default values for this policy and for policies inheriting this policy. - // AuthPolicyCommonSpec is mutually exclusive with explicit defaults defined by Defaults. - AuthPolicyCommonSpec `json:""` +func (r *MergeableHeaderSuccessResponseSpec) GetSpec() any { return r.HeaderSuccessResponseSpec } +func (r *MergeableHeaderSuccessResponseSpec) GetSource() string { return r.Source } +func (r *MergeableHeaderSuccessResponseSpec) WithSource(source string) kuadrantv1.MergeableRule { + r.Source = source + return r } -// AuthPolicyCommonSpec contains common shared fields for defaults and overrides -type AuthPolicyCommonSpec struct { - // 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"` +type MergeableSuccessResponseSpec struct { + authorinov1beta2.SuccessResponseSpec `json:",inline"` + Source string `json:"-"` +} - // 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"` +func (r *MergeableSuccessResponseSpec) GetSpec() any { return r.SuccessResponseSpec } +func (r *MergeableSuccessResponseSpec) GetSource() string { return r.Source } +func (r *MergeableSuccessResponseSpec) WithSource(source string) kuadrantv1.MergeableRule { + r.Source = source + return r +} - // The auth rules of the policy. - // See Authorino's AuthConfig CRD for more details. - AuthScheme *AuthSchemeSpec `json:"rules,omitempty"` +type MergeableCallbackSpec struct { + authorinov1beta2.CallbackSpec `json:",inline"` + Source string `json:"-"` +} + +func (r *MergeableCallbackSpec) GetSpec() any { return r.CallbackSpec } +func (r *MergeableCallbackSpec) GetSource() string { return r.Source } +func (r *MergeableCallbackSpec) WithSource(source string) kuadrantv1.MergeableRule { + r.Source = source + return r } type AuthPolicyStatus struct { @@ -171,82 +636,6 @@ func (s *AuthPolicyStatus) GetConditions() []metav1.Condition { return s.Conditions } -var _ kuadrant.Policy = &AuthPolicy{} -var _ kuadrant.Referrer = &AuthPolicy{} - -// +kubebuilder:object:root=true -// +kubebuilder:subresource:status -// +kubebuilder:metadata:labels="gateway.networking.k8s.io/policy=inherited" -// +kubebuilder:printcolumn:name="Accepted",type=string,JSONPath=`.status.conditions[?(@.type=="Accepted")].status`,description="AuthPolicy Accepted",priority=2 -// +kubebuilder:printcolumn:name="Enforced",type=string,JSONPath=`.status.conditions[?(@.type=="Enforced")].status`,description="AuthPolicy Enforced",priority=2 -// +kubebuilder:printcolumn:name="TargetRefKind",type="string",JSONPath=".spec.targetRef.kind",description="Type of the referenced Gateway API resource",priority=2 -// +kubebuilder:printcolumn:name="TargetRefName",type="string",JSONPath=".spec.targetRef.name",description="Name of the referenced Gateway API resource",priority=2 -// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" - -// AuthPolicy enables authentication and authorization for service workloads in a Gateway API network -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) IsAtomicOverride() bool { - return ap.Spec.Overrides != nil -} - -func (ap *AuthPolicy) GetTargetRef() gatewayapiv1alpha2.LocalPolicyTargetReference { - return ap.Spec.TargetRef -} - -func (ap *AuthPolicy) GetStatus() kuadrantgatewayapi.PolicyStatus { - return &ap.Status -} - -func (ap *AuthPolicy) GetWrappedNamespace() gatewayapiv1.Namespace { - return gatewayapiv1.Namespace(ap.Namespace) -} - -// GetRulesHostnames -// in v1beta2 this returned the list of route selectors -// in v1beta3 this should work with section name, once implemented. -func (ap *AuthPolicy) GetRulesHostnames() []string { - return make([]string, 0) -} - -func (ap *AuthPolicy) Kind() string { - return NewAuthPolicyType().GetGVK().Kind -} - -func (ap *AuthPolicy) TargetProgrammedGatewaysOnly() bool { - return true -} - -func (ap *AuthPolicy) PolicyClass() kuadrantgatewayapi.PolicyClass { - return kuadrantgatewayapi.InheritedPolicy -} - -func (ap *AuthPolicy) BackReferenceAnnotationName() string { - return NewAuthPolicyType().BackReferenceAnnotationName() -} - -func (ap *AuthPolicy) DirectReferenceAnnotationName() string { - return NewAuthPolicyType().DirectReferenceAnnotationName() -} - -func (ap *AuthPolicySpec) CommonSpec() *AuthPolicyCommonSpec { - if ap.Defaults != nil { - return ap.Defaults - } - - if ap.Overrides != nil { - return ap.Overrides - } - - return &ap.AuthPolicyCommonSpec -} - //+kubebuilder:object:root=true // AuthPolicyList contains a list of AuthPolicy @@ -256,47 +645,13 @@ type AuthPolicyList struct { Items []AuthPolicy `json:"items"` } +// DEPRECATED. impl: kuadrant.PolicyList func (l *AuthPolicyList) GetItems() []kuadrant.Policy { return utils.Map(l.Items, func(item AuthPolicy) kuadrant.Policy { return &item }) } -type authPolicyType struct{} - -func NewAuthPolicyType() kuadrantgatewayapi.PolicyType { - return &authPolicyType{} -} - -func (a authPolicyType) GetGVK() schema.GroupVersionKind { - return AuthPolicyGVK -} -func (a authPolicyType) GetInstance() client.Object { - return &AuthPolicy{ - TypeMeta: metav1.TypeMeta{ - Kind: AuthPolicyGVK.Kind, - APIVersion: GroupVersion.String(), - }, - } -} - -func (a authPolicyType) GetList(ctx context.Context, cl client.Client, listOpts ...client.ListOption) ([]kuadrantgatewayapi.Policy, error) { - list := &AuthPolicyList{} - err := cl.List(ctx, list, listOpts...) - if err != nil { - return nil, err - } - return utils.Map(list.Items, func(p AuthPolicy) kuadrantgatewayapi.Policy { return &p }), nil -} - -func (a authPolicyType) BackReferenceAnnotationName() string { - return AuthPolicyBackReferenceAnnotationName -} - -func (a authPolicyType) DirectReferenceAnnotationName() string { - return AuthPolicyDirectReferenceAnnotationName -} - func init() { SchemeBuilder.Register(&AuthPolicy{}, &AuthPolicyList{}) } diff --git a/api/v1beta3/ratelimitpolicy_types.go b/api/v1beta3/ratelimitpolicy_types.go index 4c4adecdf..0d058291b 100644 --- a/api/v1beta3/ratelimitpolicy_types.go +++ b/api/v1beta3/ratelimitpolicy_types.go @@ -17,8 +17,6 @@ limitations under the License. package v1beta3 import ( - "encoding/json" - "github.com/kuadrant/policy-machinery/machinery" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" @@ -123,32 +121,26 @@ func (p *RateLimitPolicy) Empty() bool { func (p *RateLimitPolicy) Rules() map[string]kuadrantv1.MergeableRule { rules := make(map[string]kuadrantv1.MergeableRule) + policyLocator := p.GetLocator() for ruleID := range p.Spec.Proper().Limits { limit := p.Spec.Proper().Limits[ruleID] - origin := limit.Origin - if origin == "" { - origin = p.GetLocator() - } - rules[ruleID] = kuadrantv1.MergeableRule{ - Spec: limit, - Source: origin, - } + rules[ruleID] = kuadrantv1.NewMergeableRule(&limit, policyLocator) } return rules } func (p *RateLimitPolicy) SetRules(rules map[string]kuadrantv1.MergeableRule) { - if len(rules) > 0 && p.Spec.Proper().Limits == nil { + // clear all rules of the policy before setting new ones + p.Spec.Proper().Limits = nil + + if len(rules) > 0 { p.Spec.Proper().Limits = make(map[string]Limit) } for ruleID := range rules { - rule := rules[ruleID] - limit := rule.Spec.(Limit) - limit.Origin = rule.Source - p.Spec.Proper().Limits[ruleID] = limit + p.Spec.Proper().Limits[ruleID] = *rules[ruleID].(*Limit) } } @@ -211,44 +203,6 @@ type RateLimitPolicySpec struct { RateLimitPolicySpecProper `json:""` } -// UnmarshalJSON unmarshals the RateLimitPolicySpec from JSON byte array. -// This should not be needed, but runtime.DefaultUnstructuredConverter.FromUnstructured does not work well with embedded structs. -func (s *RateLimitPolicySpec) UnmarshalJSON(j []byte) error { - targetRef := struct { - gatewayapiv1alpha2.LocalPolicyTargetReferenceWithSectionName `json:"targetRef"` - }{} - if err := json.Unmarshal(j, &targetRef); err != nil { - return err - } - s.TargetRef = targetRef.LocalPolicyTargetReferenceWithSectionName - - defaults := &struct { - *MergeableRateLimitPolicySpec `json:"defaults,omitempty"` - }{} - if err := json.Unmarshal(j, defaults); err != nil { - return err - } - s.Defaults = defaults.MergeableRateLimitPolicySpec - - overrides := &struct { - *MergeableRateLimitPolicySpec `json:"overrides,omitempty"` - }{} - if err := json.Unmarshal(j, overrides); err != nil { - return err - } - s.Overrides = overrides.MergeableRateLimitPolicySpec - - proper := struct { - RateLimitPolicySpecProper `json:""` - }{} - if err := json.Unmarshal(j, &proper); err != nil { - return err - } - s.RateLimitPolicySpecProper = proper.RateLimitPolicySpecProper - - return nil -} - func (s *RateLimitPolicySpec) Proper() *RateLimitPolicySpecProper { if s.Defaults != nil { return &s.Defaults.RateLimitPolicySpecProper @@ -293,8 +247,8 @@ type Limit struct { // +optional Rates []Rate `json:"rates,omitempty"` - // origin stores the resource where the limit is originally defined (internal use) - Origin string `json:"-"` + // Source stores the locator of the policy where the limit is orignaly defined (internal use) + Source string `json:"-"` } func (l Limit) CountersAsStringList() []string { @@ -304,6 +258,21 @@ func (l Limit) CountersAsStringList() []string { return utils.Map(l.Counters, func(counter ContextSelector) string { return string(counter) }) } +var _ kuadrantv1.MergeableRule = &Limit{} + +func (l *Limit) GetSpec() any { + return l +} + +func (l *Limit) GetSource() string { + return l.Source +} + +func (l *Limit) WithSource(source string) kuadrantv1.MergeableRule { + l.Source = source + return l +} + // +kubebuilder:validation:Enum:=second;minute;hour;day type TimeUnit string diff --git a/api/v1beta3/topology.go b/api/v1beta3/topology.go deleted file mode 100644 index 7543ae1f7..000000000 --- a/api/v1beta3/topology.go +++ /dev/null @@ -1,39 +0,0 @@ -package v1beta3 - -// Contains of this file allow the AuthPolicy and RateLimitPolicy to adhere to the machinery.Policy interface - -import ( - "github.com/kuadrant/policy-machinery/machinery" - "k8s.io/apimachinery/pkg/runtime/schema" -) - -var ( - AuthPoliciesResource = GroupVersion.WithResource("authpolicies") - - AuthPolicyGroupKind = schema.GroupKind{Group: GroupVersion.Group, Kind: "AuthPolicy"} -) - -var _ machinery.Policy = &AuthPolicy{} - -func (ap *AuthPolicy) GetTargetRefs() []machinery.PolicyTargetReference { - return []machinery.PolicyTargetReference{ - machinery.LocalPolicyTargetReference{ - LocalPolicyTargetReference: ap.Spec.TargetRef, - PolicyNamespace: ap.Namespace, - }, - } -} - -func (ap *AuthPolicy) GetMergeStrategy() machinery.MergeStrategy { - return func(policy machinery.Policy, _ machinery.Policy) machinery.Policy { - return policy - } -} - -func (ap *AuthPolicy) Merge(other machinery.Policy) machinery.Policy { - return other -} - -func (ap *AuthPolicy) GetLocator() string { - return machinery.LocatorFromObject(ap) -} diff --git a/api/v1beta3/zz_generated.deepcopy.go b/api/v1beta3/zz_generated.deepcopy.go index ad41e4958..e54e1a628 100644 --- a/api/v1beta3/zz_generated.deepcopy.go +++ b/api/v1beta3/zz_generated.deepcopy.go @@ -53,49 +53,6 @@ func (in *AuthPolicy) DeepCopyObject() runtime.Object { return nil } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AuthPolicyCommonSpec) DeepCopyInto(out *AuthPolicyCommonSpec) { - *out = *in - if in.NamedPatterns != nil { - in, out := &in.NamedPatterns, &out.NamedPatterns - *out = make(map[string]v1beta2.PatternExpressions, len(*in)) - for key, val := range *in { - var outVal []v1beta2.PatternExpression - if val == nil { - (*out)[key] = nil - } else { - inVal := (*in)[key] - in, out := &inVal, &outVal - *out = make(v1beta2.PatternExpressions, len(*in)) - copy(*out, *in) - } - (*out)[key] = outVal - } - } - if in.Conditions != nil { - in, out := &in.Conditions, &out.Conditions - *out = make([]v1beta2.PatternExpressionOrRef, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - if in.AuthScheme != nil { - in, out := &in.AuthScheme, &out.AuthScheme - *out = new(AuthSchemeSpec) - (*in).DeepCopyInto(*out) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthPolicyCommonSpec. -func (in *AuthPolicyCommonSpec) DeepCopy() *AuthPolicyCommonSpec { - if in == nil { - return nil - } - out := new(AuthPolicyCommonSpec) - in.DeepCopyInto(out) - return out -} - // 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 @@ -131,18 +88,18 @@ func (in *AuthPolicyList) DeepCopyObject() runtime.Object { // 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 - out.TargetRef = in.TargetRef + in.TargetRef.DeepCopyInto(&out.TargetRef) if in.Defaults != nil { in, out := &in.Defaults, &out.Defaults - *out = new(AuthPolicyCommonSpec) + *out = new(MergeableAuthPolicySpec) (*in).DeepCopyInto(*out) } if in.Overrides != nil { in, out := &in.Overrides, &out.Overrides - *out = new(AuthPolicyCommonSpec) + *out = new(MergeableAuthPolicySpec) (*in).DeepCopyInto(*out) } - in.AuthPolicyCommonSpec.DeepCopyInto(&out.AuthPolicyCommonSpec) + in.AuthPolicySpecProper.DeepCopyInto(&out.AuthPolicySpecProper) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthPolicySpec. @@ -155,6 +112,40 @@ func (in *AuthPolicySpec) DeepCopy() *AuthPolicySpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AuthPolicySpecProper) DeepCopyInto(out *AuthPolicySpecProper) { + *out = *in + if in.NamedPatterns != nil { + in, out := &in.NamedPatterns, &out.NamedPatterns + *out = make(map[string]MergeablePatternExpressions, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]MergeablePatternExpressionOrRef, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.AuthScheme != nil { + in, out := &in.AuthScheme, &out.AuthScheme + *out = new(AuthSchemeSpec) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthPolicySpecProper. +func (in *AuthPolicySpecProper) DeepCopy() *AuthPolicySpecProper { + if in == nil { + return nil + } + out := new(AuthPolicySpecProper) + 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 @@ -182,33 +173,33 @@ func (in *AuthSchemeSpec) DeepCopyInto(out *AuthSchemeSpec) { *out = *in if in.Authentication != nil { in, out := &in.Authentication, &out.Authentication - *out = make(map[string]v1beta2.AuthenticationSpec, len(*in)) + *out = make(map[string]MergeableAuthenticationSpec, 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]v1beta2.MetadataSpec, len(*in)) + *out = make(map[string]MergeableMetadataSpec, 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]v1beta2.AuthorizationSpec, len(*in)) + *out = make(map[string]MergeableAuthorizationSpec, len(*in)) for key, val := range *in { (*out)[key] = *val.DeepCopy() } } if in.Response != nil { in, out := &in.Response, &out.Response - *out = new(ResponseSpec) + *out = new(MergeableResponseSpec) (*in).DeepCopyInto(*out) } if in.Callbacks != nil { in, out := &in.Callbacks, &out.Callbacks - *out = make(map[string]v1beta2.CallbackSpec, len(*in)) + *out = make(map[string]MergeableCallbackSpec, len(*in)) for key, val := range *in { (*out)[key] = *val.DeepCopy() } @@ -225,22 +216,6 @@ func (in *AuthSchemeSpec) DeepCopy() *AuthSchemeSpec { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *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 @@ -271,6 +246,154 @@ 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 *MergeableAuthPolicySpec) DeepCopyInto(out *MergeableAuthPolicySpec) { + *out = *in + in.AuthPolicySpecProper.DeepCopyInto(&out.AuthPolicySpecProper) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MergeableAuthPolicySpec. +func (in *MergeableAuthPolicySpec) DeepCopy() *MergeableAuthPolicySpec { + if in == nil { + return nil + } + out := new(MergeableAuthPolicySpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MergeableAuthenticationSpec) DeepCopyInto(out *MergeableAuthenticationSpec) { + *out = *in + in.AuthenticationSpec.DeepCopyInto(&out.AuthenticationSpec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MergeableAuthenticationSpec. +func (in *MergeableAuthenticationSpec) DeepCopy() *MergeableAuthenticationSpec { + if in == nil { + return nil + } + out := new(MergeableAuthenticationSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MergeableAuthorizationSpec) DeepCopyInto(out *MergeableAuthorizationSpec) { + *out = *in + in.AuthorizationSpec.DeepCopyInto(&out.AuthorizationSpec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MergeableAuthorizationSpec. +func (in *MergeableAuthorizationSpec) DeepCopy() *MergeableAuthorizationSpec { + if in == nil { + return nil + } + out := new(MergeableAuthorizationSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MergeableCallbackSpec) DeepCopyInto(out *MergeableCallbackSpec) { + *out = *in + in.CallbackSpec.DeepCopyInto(&out.CallbackSpec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MergeableCallbackSpec. +func (in *MergeableCallbackSpec) DeepCopy() *MergeableCallbackSpec { + if in == nil { + return nil + } + out := new(MergeableCallbackSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MergeableDenyWithSpec) DeepCopyInto(out *MergeableDenyWithSpec) { + *out = *in + in.DenyWithSpec.DeepCopyInto(&out.DenyWithSpec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MergeableDenyWithSpec. +func (in *MergeableDenyWithSpec) DeepCopy() *MergeableDenyWithSpec { + if in == nil { + return nil + } + out := new(MergeableDenyWithSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MergeableHeaderSuccessResponseSpec) DeepCopyInto(out *MergeableHeaderSuccessResponseSpec) { + *out = *in + in.HeaderSuccessResponseSpec.DeepCopyInto(&out.HeaderSuccessResponseSpec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MergeableHeaderSuccessResponseSpec. +func (in *MergeableHeaderSuccessResponseSpec) DeepCopy() *MergeableHeaderSuccessResponseSpec { + if in == nil { + return nil + } + out := new(MergeableHeaderSuccessResponseSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MergeableMetadataSpec) DeepCopyInto(out *MergeableMetadataSpec) { + *out = *in + in.MetadataSpec.DeepCopyInto(&out.MetadataSpec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MergeableMetadataSpec. +func (in *MergeableMetadataSpec) DeepCopy() *MergeableMetadataSpec { + if in == nil { + return nil + } + out := new(MergeableMetadataSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MergeablePatternExpressionOrRef) DeepCopyInto(out *MergeablePatternExpressionOrRef) { + *out = *in + in.PatternExpressionOrRef.DeepCopyInto(&out.PatternExpressionOrRef) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MergeablePatternExpressionOrRef. +func (in *MergeablePatternExpressionOrRef) DeepCopy() *MergeablePatternExpressionOrRef { + if in == nil { + return nil + } + out := new(MergeablePatternExpressionOrRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MergeablePatternExpressions) DeepCopyInto(out *MergeablePatternExpressions) { + *out = *in + if in.PatternExpressions != nil { + in, out := &in.PatternExpressions, &out.PatternExpressions + *out = make(v1beta2.PatternExpressions, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MergeablePatternExpressions. +func (in *MergeablePatternExpressions) DeepCopy() *MergeablePatternExpressions { + if in == nil { + return nil + } + out := new(MergeablePatternExpressions) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *MergeableRateLimitPolicySpec) DeepCopyInto(out *MergeableRateLimitPolicySpec) { *out = *in @@ -287,6 +410,77 @@ func (in *MergeableRateLimitPolicySpec) DeepCopy() *MergeableRateLimitPolicySpec return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MergeableResponseSpec) DeepCopyInto(out *MergeableResponseSpec) { + *out = *in + if in.Unauthenticated != nil { + in, out := &in.Unauthenticated, &out.Unauthenticated + *out = new(MergeableDenyWithSpec) + (*in).DeepCopyInto(*out) + } + if in.Unauthorized != nil { + in, out := &in.Unauthorized, &out.Unauthorized + *out = new(MergeableDenyWithSpec) + (*in).DeepCopyInto(*out) + } + in.Success.DeepCopyInto(&out.Success) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MergeableResponseSpec. +func (in *MergeableResponseSpec) DeepCopy() *MergeableResponseSpec { + if in == nil { + return nil + } + out := new(MergeableResponseSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MergeableSuccessResponseSpec) DeepCopyInto(out *MergeableSuccessResponseSpec) { + *out = *in + in.SuccessResponseSpec.DeepCopyInto(&out.SuccessResponseSpec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MergeableSuccessResponseSpec. +func (in *MergeableSuccessResponseSpec) DeepCopy() *MergeableSuccessResponseSpec { + if in == nil { + return nil + } + out := new(MergeableSuccessResponseSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MergeableWrappedSuccessResponseSpec) DeepCopyInto(out *MergeableWrappedSuccessResponseSpec) { + *out = *in + if in.Headers != nil { + in, out := &in.Headers, &out.Headers + *out = make(map[string]MergeableHeaderSuccessResponseSpec, 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]MergeableSuccessResponseSpec, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MergeableWrappedSuccessResponseSpec. +func (in *MergeableWrappedSuccessResponseSpec) DeepCopy() *MergeableWrappedSuccessResponseSpec { + if in == nil { + return nil + } + out := new(MergeableWrappedSuccessResponseSpec) + 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 @@ -432,32 +626,6 @@ 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(v1beta2.DenyWithSpec) - (*in).DeepCopyInto(*out) - } - if in.Unauthorized != nil { - in, out := &in.Unauthorized, &out.Unauthorized - *out = new(v1beta2.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 *WhenCondition) DeepCopyInto(out *WhenCondition) { *out = *in @@ -472,32 +640,3 @@ 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]v1beta2.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 6d94036ae..d1715ffaf 100644 --- a/bundle/manifests/kuadrant-operator.clusterserviceversion.yaml +++ b/bundle/manifests/kuadrant-operator.clusterserviceversion.yaml @@ -106,7 +106,7 @@ metadata: capabilities: Basic Install categories: Integration & Delivery containerImage: quay.io/kuadrant/kuadrant-operator:latest - createdAt: "2024-11-04T15:47:12Z" + createdAt: "2024-11-05T09:44:13Z" description: A Kubernetes Operator to manage the lifecycle of the Kuadrant system operators.operatorframework.io/builder: operator-sdk-v1.32.0 operators.operatorframework.io/project_layout: go.kubebuilder.io/v3 @@ -308,18 +308,6 @@ spec: - patch - update - watch - - apiGroups: - - gateway.envoyproxy.io - resources: - - securitypolicies - verbs: - - create - - delete - - get - - list - - patch - - update - - watch - apiGroups: - gateway.networking.k8s.io resources: @@ -371,18 +359,6 @@ spec: - get - patch - update - - apiGroups: - - gateway.networking.k8s.io - resources: - - referencegrants - verbs: - - create - - delete - - get - - list - - patch - - update - - watch - apiGroups: - install.istio.io resources: @@ -610,18 +586,6 @@ spec: - patch - update - watch - - apiGroups: - - security.istio.io - resources: - - authorizationpolicies - verbs: - - create - - delete - - get - - list - - patch - - update - - watch serviceAccountName: kuadrant-operator-controller-manager deployments: - label: diff --git a/bundle/manifests/kuadrant.io_authpolicies.yaml b/bundle/manifests/kuadrant.io_authpolicies.yaml index 21d8e80c8..2c2f1df2f 100644 --- a/bundle/manifests/kuadrant.io_authpolicies.yaml +++ b/bundle/manifests/kuadrant.io_authpolicies.yaml @@ -28,14 +28,19 @@ spec: name: Enforced priority: 2 type: string - - description: Type of the referenced Gateway API resource + - description: Kind of the object to which the policy aaplies jsonPath: .spec.targetRef.kind - name: TargetRefKind + name: TargetKind priority: 2 type: string - - description: Name of the referenced Gateway API resource + - description: Name of the object to which the policy applies jsonPath: .spec.targetRef.name - name: TargetRefName + name: TargetName + priority: 2 + type: string + - description: 'Name of the section within the object to which the policy applies ' + jsonPath: .spec.targetRef.sectionName + name: TargetSection priority: 2 type: string - jsonPath: .metadata.creationTimestamp @@ -65,41 +70,45 @@ spec: metadata: type: object spec: - description: Mutual Exclusivity Validation properties: defaults: description: |- - Defaults define explicit default values for this policy and for policies inheriting this policy. - Defaults are mutually exclusive with implicit defaults defined by AuthPolicyCommonSpec. + Rules to apply as defaults. Can be overridden by more specific policiy rules lower in the hierarchy and by less specific policy overrides. + Use one of: defaults, overrides, or bare set of policy rules (implicit defaults). properties: 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 + properties: + allOf: + 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 + required: + - allOf + type: object description: Named sets of patterns that can be referred in `when` conditions and in pattern-matching authorization policy rules. type: object @@ -478,6 +487,7 @@ spec: description: |- Authentication configs. At least one config MUST evaluate to a valid identity object for the auth request to be successful. + maxProperties: 10 type: object authorization: additionalProperties: @@ -1065,6 +1075,7 @@ spec: description: |- Authorization policies. All policies MUST evaluate to "allowed = true" for the auth request be successful. + maxProperties: 10 type: object callbacks: additionalProperties: @@ -1352,6 +1363,7 @@ spec: description: |- Callback functions. Authorino sends callbacks at the end of the auth pipeline to the endpoints specified in this config. + maxProperties: 10 type: object metadata: additionalProperties: @@ -1681,6 +1693,7 @@ spec: description: |- Metadata sources. Authorino fetches auth metadata as JSON from sources specified in this config. + maxProperties: 10 type: object response: description: |- @@ -1692,10 +1705,8 @@ spec: 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: + filters: additionalProperties: - description: Settings of the success custom response - item. properties: cache: description: |- @@ -1890,10 +1901,8 @@ spec: - signingKeyRefs type: object type: object - description: |- - 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 + description: Custom data made available to other filters + managed by Kuadrant (i.e. Rate Limit) type: object headers: additionalProperties: @@ -2091,9 +2100,7 @@ spec: - signingKeyRefs type: object type: object - description: |- - 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. + description: Custom headers to inject in the request. type: object type: object unauthenticated: @@ -2214,6 +2221,14 @@ spec: type: object type: object type: object + strategy: + default: atomic + description: Strategy defines the merge strategy to apply when + merging this policy with other policies. + enum: + - atomic + - merge + type: string when: description: |- Overall conditions for the AuthPolicy to be enforced. @@ -2265,37 +2280,42 @@ spec: type: object overrides: description: |- - Overrides define explicit override values for this policy. - Overrides are mutually exclusive with explicit and implicit defaults defined by AuthPolicyCommonSpec. + Rules to apply as overrides. Override all policy rules lower in the hierarchy. Can be overridden by less specific policy overrides. + Use one of: defaults, overrides, or bare set of policy rules (implicit defaults). properties: 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 + properties: + allOf: + 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 + required: + - allOf + type: object description: Named sets of patterns that can be referred in `when` conditions and in pattern-matching authorization policy rules. type: object @@ -2674,6 +2694,7 @@ spec: description: |- Authentication configs. At least one config MUST evaluate to a valid identity object for the auth request to be successful. + maxProperties: 10 type: object authorization: additionalProperties: @@ -3261,6 +3282,7 @@ spec: description: |- Authorization policies. All policies MUST evaluate to "allowed = true" for the auth request be successful. + maxProperties: 10 type: object callbacks: additionalProperties: @@ -3548,6 +3570,7 @@ spec: description: |- Callback functions. Authorino sends callbacks at the end of the auth pipeline to the endpoints specified in this config. + maxProperties: 10 type: object metadata: additionalProperties: @@ -3877,6 +3900,7 @@ spec: description: |- Metadata sources. Authorino fetches auth metadata as JSON from sources specified in this config. + maxProperties: 10 type: object response: description: |- @@ -3888,10 +3912,8 @@ spec: 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: + filters: additionalProperties: - description: Settings of the success custom response - item. properties: cache: description: |- @@ -4086,10 +4108,8 @@ spec: - signingKeyRefs type: object type: object - description: |- - 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 + description: Custom data made available to other filters + managed by Kuadrant (i.e. Rate Limit) type: object headers: additionalProperties: @@ -4287,9 +4307,7 @@ spec: - signingKeyRefs type: object type: object - description: |- - 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. + description: Custom headers to inject in the request. type: object type: object unauthenticated: @@ -4410,6 +4428,14 @@ spec: type: object type: object type: object + strategy: + default: atomic + description: Strategy defines the merge strategy to apply when + merging this policy with other policies. + enum: + - atomic + - merge + type: string when: description: |- Overall conditions for the AuthPolicy to be enforced. @@ -4461,32 +4487,37 @@ spec: type: object 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 + properties: + allOf: + 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 + required: + - allOf + type: object description: Named sets of patterns that can be referred in `when` conditions and in pattern-matching authorization policy rules. type: object @@ -4861,6 +4892,7 @@ spec: description: |- Authentication configs. At least one config MUST evaluate to a valid identity object for the auth request to be successful. + maxProperties: 10 type: object authorization: additionalProperties: @@ -5444,6 +5476,7 @@ spec: description: |- Authorization policies. All policies MUST evaluate to "allowed = true" for the auth request be successful. + maxProperties: 10 type: object callbacks: additionalProperties: @@ -5728,6 +5761,7 @@ spec: description: |- Callback functions. Authorino sends callbacks at the end of the auth pipeline to the endpoints specified in this config. + maxProperties: 10 type: object metadata: additionalProperties: @@ -6053,6 +6087,7 @@ spec: description: |- Metadata sources. Authorino fetches auth metadata as JSON from sources specified in this config. + maxProperties: 10 type: object response: description: |- @@ -6064,10 +6099,8 @@ spec: 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: + filters: additionalProperties: - description: Settings of the success custom response - item. properties: cache: description: |- @@ -6261,10 +6294,8 @@ spec: - signingKeyRefs type: object type: object - description: |- - 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 + description: Custom data made available to other filters + managed by Kuadrant (i.e. Rate Limit) type: object headers: additionalProperties: @@ -6461,9 +6492,7 @@ spec: - signingKeyRefs type: object type: object - description: |- - 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. + description: Custom headers to inject in the request. type: object type: object unauthenticated: @@ -6585,7 +6614,7 @@ spec: type: object type: object targetRef: - description: TargetRef identifies an API object to apply policy to. + description: Reference to the object to which this policy applies. properties: group: description: Group is the group of the target resource. @@ -6603,6 +6632,25 @@ spec: maxLength: 253 minLength: 1 type: string + sectionName: + description: |- + SectionName is the name of a section within the target resource. When + unspecified, this targetRef targets the entire resource. In the following + resources, SectionName is interpreted as the following: + + + * Gateway: Listener name + * HTTPRoute: HTTPRouteRule name + * Service: Port name + + + If a SectionName is specified, but does not exist on the targeted object, + the Policy must fail to attach, and the policy implementation should record + a `ResolvedRefs` or similar Condition in the Policy's status. + 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 required: - group - kind @@ -6674,9 +6722,6 @@ spec: || has(self.rules)))' - message: Explicit overrides and explicit defaults are mutually exclusive rule: '!(has(self.overrides) && has(self.defaults))' - - message: Overrides are not allowed for policies targeting a HTTPRoute - resource - rule: '!(has(self.overrides) && self.targetRef.kind == ''HTTPRoute'')' status: properties: conditions: diff --git a/charts/kuadrant-operator/templates/manifests.yaml b/charts/kuadrant-operator/templates/manifests.yaml index 1b01a5cd6..bae2b9073 100644 --- a/charts/kuadrant-operator/templates/manifests.yaml +++ b/charts/kuadrant-operator/templates/manifests.yaml @@ -28,14 +28,19 @@ spec: name: Enforced priority: 2 type: string - - description: Type of the referenced Gateway API resource + - description: Kind of the object to which the policy aaplies jsonPath: .spec.targetRef.kind - name: TargetRefKind + name: TargetKind priority: 2 type: string - - description: Name of the referenced Gateway API resource + - description: Name of the object to which the policy applies jsonPath: .spec.targetRef.name - name: TargetRefName + name: TargetName + priority: 2 + type: string + - description: 'Name of the section within the object to which the policy applies ' + jsonPath: .spec.targetRef.sectionName + name: TargetSection priority: 2 type: string - jsonPath: .metadata.creationTimestamp @@ -65,41 +70,45 @@ spec: metadata: type: object spec: - description: Mutual Exclusivity Validation properties: defaults: description: |- - Defaults define explicit default values for this policy and for policies inheriting this policy. - Defaults are mutually exclusive with implicit defaults defined by AuthPolicyCommonSpec. + Rules to apply as defaults. Can be overridden by more specific policiy rules lower in the hierarchy and by less specific policy overrides. + Use one of: defaults, overrides, or bare set of policy rules (implicit defaults). properties: 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 + properties: + allOf: + 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 + required: + - allOf + type: object description: Named sets of patterns that can be referred in `when` conditions and in pattern-matching authorization policy rules. type: object @@ -478,6 +487,7 @@ spec: description: |- Authentication configs. At least one config MUST evaluate to a valid identity object for the auth request to be successful. + maxProperties: 10 type: object authorization: additionalProperties: @@ -1065,6 +1075,7 @@ spec: description: |- Authorization policies. All policies MUST evaluate to "allowed = true" for the auth request be successful. + maxProperties: 10 type: object callbacks: additionalProperties: @@ -1352,6 +1363,7 @@ spec: description: |- Callback functions. Authorino sends callbacks at the end of the auth pipeline to the endpoints specified in this config. + maxProperties: 10 type: object metadata: additionalProperties: @@ -1681,6 +1693,7 @@ spec: description: |- Metadata sources. Authorino fetches auth metadata as JSON from sources specified in this config. + maxProperties: 10 type: object response: description: |- @@ -1692,10 +1705,8 @@ spec: 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: + filters: additionalProperties: - description: Settings of the success custom response - item. properties: cache: description: |- @@ -1890,10 +1901,8 @@ spec: - signingKeyRefs type: object type: object - description: |- - 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 + description: Custom data made available to other filters + managed by Kuadrant (i.e. Rate Limit) type: object headers: additionalProperties: @@ -2091,9 +2100,7 @@ spec: - signingKeyRefs type: object type: object - description: |- - 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. + description: Custom headers to inject in the request. type: object type: object unauthenticated: @@ -2214,6 +2221,14 @@ spec: type: object type: object type: object + strategy: + default: atomic + description: Strategy defines the merge strategy to apply when + merging this policy with other policies. + enum: + - atomic + - merge + type: string when: description: |- Overall conditions for the AuthPolicy to be enforced. @@ -2265,37 +2280,42 @@ spec: type: object overrides: description: |- - Overrides define explicit override values for this policy. - Overrides are mutually exclusive with explicit and implicit defaults defined by AuthPolicyCommonSpec. + Rules to apply as overrides. Override all policy rules lower in the hierarchy. Can be overridden by less specific policy overrides. + Use one of: defaults, overrides, or bare set of policy rules (implicit defaults). properties: 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 + properties: + allOf: + 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 + required: + - allOf + type: object description: Named sets of patterns that can be referred in `when` conditions and in pattern-matching authorization policy rules. type: object @@ -2674,6 +2694,7 @@ spec: description: |- Authentication configs. At least one config MUST evaluate to a valid identity object for the auth request to be successful. + maxProperties: 10 type: object authorization: additionalProperties: @@ -3261,6 +3282,7 @@ spec: description: |- Authorization policies. All policies MUST evaluate to "allowed = true" for the auth request be successful. + maxProperties: 10 type: object callbacks: additionalProperties: @@ -3548,6 +3570,7 @@ spec: description: |- Callback functions. Authorino sends callbacks at the end of the auth pipeline to the endpoints specified in this config. + maxProperties: 10 type: object metadata: additionalProperties: @@ -3877,6 +3900,7 @@ spec: description: |- Metadata sources. Authorino fetches auth metadata as JSON from sources specified in this config. + maxProperties: 10 type: object response: description: |- @@ -3888,10 +3912,8 @@ spec: 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: + filters: additionalProperties: - description: Settings of the success custom response - item. properties: cache: description: |- @@ -4086,10 +4108,8 @@ spec: - signingKeyRefs type: object type: object - description: |- - 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 + description: Custom data made available to other filters + managed by Kuadrant (i.e. Rate Limit) type: object headers: additionalProperties: @@ -4287,9 +4307,7 @@ spec: - signingKeyRefs type: object type: object - description: |- - 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. + description: Custom headers to inject in the request. type: object type: object unauthenticated: @@ -4410,6 +4428,14 @@ spec: type: object type: object type: object + strategy: + default: atomic + description: Strategy defines the merge strategy to apply when + merging this policy with other policies. + enum: + - atomic + - merge + type: string when: description: |- Overall conditions for the AuthPolicy to be enforced. @@ -4461,32 +4487,37 @@ spec: type: object 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 + properties: + allOf: + 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 + required: + - allOf + type: object description: Named sets of patterns that can be referred in `when` conditions and in pattern-matching authorization policy rules. type: object @@ -4861,6 +4892,7 @@ spec: description: |- Authentication configs. At least one config MUST evaluate to a valid identity object for the auth request to be successful. + maxProperties: 10 type: object authorization: additionalProperties: @@ -5444,6 +5476,7 @@ spec: description: |- Authorization policies. All policies MUST evaluate to "allowed = true" for the auth request be successful. + maxProperties: 10 type: object callbacks: additionalProperties: @@ -5728,6 +5761,7 @@ spec: description: |- Callback functions. Authorino sends callbacks at the end of the auth pipeline to the endpoints specified in this config. + maxProperties: 10 type: object metadata: additionalProperties: @@ -6053,6 +6087,7 @@ spec: description: |- Metadata sources. Authorino fetches auth metadata as JSON from sources specified in this config. + maxProperties: 10 type: object response: description: |- @@ -6064,10 +6099,8 @@ spec: 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: + filters: additionalProperties: - description: Settings of the success custom response - item. properties: cache: description: |- @@ -6261,10 +6294,8 @@ spec: - signingKeyRefs type: object type: object - description: |- - 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 + description: Custom data made available to other filters + managed by Kuadrant (i.e. Rate Limit) type: object headers: additionalProperties: @@ -6461,9 +6492,7 @@ spec: - signingKeyRefs type: object type: object - description: |- - 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. + description: Custom headers to inject in the request. type: object type: object unauthenticated: @@ -6585,7 +6614,7 @@ spec: type: object type: object targetRef: - description: TargetRef identifies an API object to apply policy to. + description: Reference to the object to which this policy applies. properties: group: description: Group is the group of the target resource. @@ -6603,6 +6632,25 @@ spec: maxLength: 253 minLength: 1 type: string + sectionName: + description: |- + SectionName is the name of a section within the target resource. When + unspecified, this targetRef targets the entire resource. In the following + resources, SectionName is interpreted as the following: + + + * Gateway: Listener name + * HTTPRoute: HTTPRouteRule name + * Service: Port name + + + If a SectionName is specified, but does not exist on the targeted object, + the Policy must fail to attach, and the policy implementation should record + a `ResolvedRefs` or similar Condition in the Policy's status. + 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 required: - group - kind @@ -6674,9 +6722,6 @@ spec: || has(self.rules)))' - message: Explicit overrides and explicit defaults are mutually exclusive rule: '!(has(self.overrides) && has(self.defaults))' - - message: Overrides are not allowed for policies targeting a HTTPRoute - resource - rule: '!(has(self.overrides) && self.targetRef.kind == ''HTTPRoute'')' status: properties: conditions: @@ -8571,18 +8616,6 @@ rules: - patch - update - watch -- apiGroups: - - gateway.envoyproxy.io - resources: - - securitypolicies - verbs: - - create - - delete - - get - - list - - patch - - update - - watch - apiGroups: - gateway.networking.k8s.io resources: @@ -8634,18 +8667,6 @@ rules: - get - patch - update -- apiGroups: - - gateway.networking.k8s.io - resources: - - referencegrants - verbs: - - create - - delete - - get - - list - - patch - - update - - watch - apiGroups: - install.istio.io resources: @@ -8873,18 +8894,6 @@ rules: - patch - update - watch -- apiGroups: - - security.istio.io - resources: - - authorizationpolicies - verbs: - - create - - delete - - get - - list - - patch - - update - - watch --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding diff --git a/config/crd/bases/kuadrant.io_authpolicies.yaml b/config/crd/bases/kuadrant.io_authpolicies.yaml index 307d7eca4..ad5eecf34 100644 --- a/config/crd/bases/kuadrant.io_authpolicies.yaml +++ b/config/crd/bases/kuadrant.io_authpolicies.yaml @@ -27,14 +27,19 @@ spec: name: Enforced priority: 2 type: string - - description: Type of the referenced Gateway API resource + - description: Kind of the object to which the policy aaplies jsonPath: .spec.targetRef.kind - name: TargetRefKind + name: TargetKind priority: 2 type: string - - description: Name of the referenced Gateway API resource + - description: Name of the object to which the policy applies jsonPath: .spec.targetRef.name - name: TargetRefName + name: TargetName + priority: 2 + type: string + - description: 'Name of the section within the object to which the policy applies ' + jsonPath: .spec.targetRef.sectionName + name: TargetSection priority: 2 type: string - jsonPath: .metadata.creationTimestamp @@ -64,41 +69,45 @@ spec: metadata: type: object spec: - description: Mutual Exclusivity Validation properties: defaults: description: |- - Defaults define explicit default values for this policy and for policies inheriting this policy. - Defaults are mutually exclusive with implicit defaults defined by AuthPolicyCommonSpec. + Rules to apply as defaults. Can be overridden by more specific policiy rules lower in the hierarchy and by less specific policy overrides. + Use one of: defaults, overrides, or bare set of policy rules (implicit defaults). properties: 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 + properties: + allOf: + 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 + required: + - allOf + type: object description: Named sets of patterns that can be referred in `when` conditions and in pattern-matching authorization policy rules. type: object @@ -477,6 +486,7 @@ spec: description: |- Authentication configs. At least one config MUST evaluate to a valid identity object for the auth request to be successful. + maxProperties: 10 type: object authorization: additionalProperties: @@ -1064,6 +1074,7 @@ spec: description: |- Authorization policies. All policies MUST evaluate to "allowed = true" for the auth request be successful. + maxProperties: 10 type: object callbacks: additionalProperties: @@ -1351,6 +1362,7 @@ spec: description: |- Callback functions. Authorino sends callbacks at the end of the auth pipeline to the endpoints specified in this config. + maxProperties: 10 type: object metadata: additionalProperties: @@ -1680,6 +1692,7 @@ spec: description: |- Metadata sources. Authorino fetches auth metadata as JSON from sources specified in this config. + maxProperties: 10 type: object response: description: |- @@ -1691,10 +1704,8 @@ spec: 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: + filters: additionalProperties: - description: Settings of the success custom response - item. properties: cache: description: |- @@ -1889,10 +1900,8 @@ spec: - signingKeyRefs type: object type: object - description: |- - 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 + description: Custom data made available to other filters + managed by Kuadrant (i.e. Rate Limit) type: object headers: additionalProperties: @@ -2090,9 +2099,7 @@ spec: - signingKeyRefs type: object type: object - description: |- - 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. + description: Custom headers to inject in the request. type: object type: object unauthenticated: @@ -2213,6 +2220,14 @@ spec: type: object type: object type: object + strategy: + default: atomic + description: Strategy defines the merge strategy to apply when + merging this policy with other policies. + enum: + - atomic + - merge + type: string when: description: |- Overall conditions for the AuthPolicy to be enforced. @@ -2264,37 +2279,42 @@ spec: type: object overrides: description: |- - Overrides define explicit override values for this policy. - Overrides are mutually exclusive with explicit and implicit defaults defined by AuthPolicyCommonSpec. + Rules to apply as overrides. Override all policy rules lower in the hierarchy. Can be overridden by less specific policy overrides. + Use one of: defaults, overrides, or bare set of policy rules (implicit defaults). properties: 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 + properties: + allOf: + 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 + required: + - allOf + type: object description: Named sets of patterns that can be referred in `when` conditions and in pattern-matching authorization policy rules. type: object @@ -2673,6 +2693,7 @@ spec: description: |- Authentication configs. At least one config MUST evaluate to a valid identity object for the auth request to be successful. + maxProperties: 10 type: object authorization: additionalProperties: @@ -3260,6 +3281,7 @@ spec: description: |- Authorization policies. All policies MUST evaluate to "allowed = true" for the auth request be successful. + maxProperties: 10 type: object callbacks: additionalProperties: @@ -3547,6 +3569,7 @@ spec: description: |- Callback functions. Authorino sends callbacks at the end of the auth pipeline to the endpoints specified in this config. + maxProperties: 10 type: object metadata: additionalProperties: @@ -3876,6 +3899,7 @@ spec: description: |- Metadata sources. Authorino fetches auth metadata as JSON from sources specified in this config. + maxProperties: 10 type: object response: description: |- @@ -3887,10 +3911,8 @@ spec: 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: + filters: additionalProperties: - description: Settings of the success custom response - item. properties: cache: description: |- @@ -4085,10 +4107,8 @@ spec: - signingKeyRefs type: object type: object - description: |- - 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 + description: Custom data made available to other filters + managed by Kuadrant (i.e. Rate Limit) type: object headers: additionalProperties: @@ -4286,9 +4306,7 @@ spec: - signingKeyRefs type: object type: object - description: |- - 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. + description: Custom headers to inject in the request. type: object type: object unauthenticated: @@ -4409,6 +4427,14 @@ spec: type: object type: object type: object + strategy: + default: atomic + description: Strategy defines the merge strategy to apply when + merging this policy with other policies. + enum: + - atomic + - merge + type: string when: description: |- Overall conditions for the AuthPolicy to be enforced. @@ -4460,32 +4486,37 @@ spec: type: object 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 + properties: + allOf: + 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 + required: + - allOf + type: object description: Named sets of patterns that can be referred in `when` conditions and in pattern-matching authorization policy rules. type: object @@ -4860,6 +4891,7 @@ spec: description: |- Authentication configs. At least one config MUST evaluate to a valid identity object for the auth request to be successful. + maxProperties: 10 type: object authorization: additionalProperties: @@ -5443,6 +5475,7 @@ spec: description: |- Authorization policies. All policies MUST evaluate to "allowed = true" for the auth request be successful. + maxProperties: 10 type: object callbacks: additionalProperties: @@ -5727,6 +5760,7 @@ spec: description: |- Callback functions. Authorino sends callbacks at the end of the auth pipeline to the endpoints specified in this config. + maxProperties: 10 type: object metadata: additionalProperties: @@ -6052,6 +6086,7 @@ spec: description: |- Metadata sources. Authorino fetches auth metadata as JSON from sources specified in this config. + maxProperties: 10 type: object response: description: |- @@ -6063,10 +6098,8 @@ spec: 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: + filters: additionalProperties: - description: Settings of the success custom response - item. properties: cache: description: |- @@ -6260,10 +6293,8 @@ spec: - signingKeyRefs type: object type: object - description: |- - 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 + description: Custom data made available to other filters + managed by Kuadrant (i.e. Rate Limit) type: object headers: additionalProperties: @@ -6460,9 +6491,7 @@ spec: - signingKeyRefs type: object type: object - description: |- - 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. + description: Custom headers to inject in the request. type: object type: object unauthenticated: @@ -6584,7 +6613,7 @@ spec: type: object type: object targetRef: - description: TargetRef identifies an API object to apply policy to. + description: Reference to the object to which this policy applies. properties: group: description: Group is the group of the target resource. @@ -6602,6 +6631,25 @@ spec: maxLength: 253 minLength: 1 type: string + sectionName: + description: |- + SectionName is the name of a section within the target resource. When + unspecified, this targetRef targets the entire resource. In the following + resources, SectionName is interpreted as the following: + + + * Gateway: Listener name + * HTTPRoute: HTTPRouteRule name + * Service: Port name + + + If a SectionName is specified, but does not exist on the targeted object, + the Policy must fail to attach, and the policy implementation should record + a `ResolvedRefs` or similar Condition in the Policy's status. + 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 required: - group - kind @@ -6673,9 +6721,6 @@ spec: || has(self.rules)))' - message: Explicit overrides and explicit defaults are mutually exclusive rule: '!(has(self.overrides) && has(self.defaults))' - - message: Overrides are not allowed for policies targeting a HTTPRoute - resource - rule: '!(has(self.overrides) && self.targetRef.kind == ''HTTPRoute'')' status: properties: conditions: diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 75da3957c..dc819ef96 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -158,18 +158,6 @@ rules: - patch - update - watch -- apiGroups: - - gateway.envoyproxy.io - resources: - - securitypolicies - verbs: - - create - - delete - - get - - list - - patch - - update - - watch - apiGroups: - gateway.networking.k8s.io resources: @@ -221,18 +209,6 @@ rules: - get - patch - update -- apiGroups: - - gateway.networking.k8s.io - resources: - - referencegrants - verbs: - - create - - delete - - get - - list - - patch - - update - - watch - apiGroups: - install.istio.io resources: @@ -460,15 +436,3 @@ rules: - patch - update - watch -- apiGroups: - - security.istio.io - resources: - - authorizationpolicies - verbs: - - create - - delete - - get - - list - - patch - - update - - watch diff --git a/controllers/auth_policies_validator.go b/controllers/auth_policies_validator.go new file mode 100644 index 000000000..69c72be83 --- /dev/null +++ b/controllers/auth_policies_validator.go @@ -0,0 +1,60 @@ +package controllers + +import ( + "context" + "sync" + + "github.com/kuadrant/policy-machinery/controller" + "github.com/kuadrant/policy-machinery/machinery" + "github.com/samber/lo" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/utils/ptr" + + kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" + kuadrant "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" +) + +type AuthPolicyValidator struct{} + +// AuthPolicyValidator subscribes to events with potential to flip the validity of auth policies +func (r *AuthPolicyValidator) Subscription() controller.Subscription { + return controller.Subscription{ + ReconcileFunc: r.Validate, + Events: []controller.ResourceEventMatcher{ + {Kind: &machinery.GatewayGroupKind}, + {Kind: &machinery.HTTPRouteGroupKind}, + {Kind: &kuadrantv1beta3.AuthPolicyGroupKind, EventType: ptr.To(controller.CreateEvent)}, + {Kind: &kuadrantv1beta3.AuthPolicyGroupKind, EventType: ptr.To(controller.UpdateEvent)}, + }, + } +} + +func (r *AuthPolicyValidator) Validate(ctx context.Context, _ []controller.ResourceEvent, topology *machinery.Topology, _ error, state *sync.Map) error { + logger := controller.LoggerFromContext(ctx).WithName("AuthPolicyValidator") + + policies := topology.Policies().Items(func(o machinery.Object) bool { + return o.GroupVersionKind().GroupKind() == kuadrantv1beta3.AuthPolicyGroupKind + }) + + logger.V(1).Info("validating auth policies", "policies", len(policies)) + defer logger.V(1).Info("finished validating auth policies") + + state.Store(StateAuthPolicyValid, lo.SliceToMap(policies, func(policy machinery.Policy) (string, error) { + var err error + if len(policy.GetTargetRefs()) > 0 && len(topology.Targetables().Children(policy)) == 0 { + ref := policy.GetTargetRefs()[0] + var res schema.GroupResource + switch ref.GroupVersionKind().Kind { + case machinery.GatewayGroupKind.Kind: + res = controller.GatewaysResource.GroupResource() + case machinery.HTTPRouteGroupKind.Kind: + res = controller.HTTPRoutesResource.GroupResource() + } + err = kuadrant.NewErrPolicyTargetNotFound(kuadrantv1beta3.AuthPolicyGroupKind.Kind, ref, apierrors.NewNotFound(res, ref.GetName())) + } + return policy.GetLocator(), err + })) + + return nil +} diff --git a/controllers/auth_policy_status_updater.go b/controllers/auth_policy_status_updater.go new file mode 100644 index 000000000..113713d9a --- /dev/null +++ b/controllers/auth_policy_status_updater.go @@ -0,0 +1,282 @@ +package controllers + +import ( + "context" + "fmt" + "slices" + "sync" + + envoygatewayv1alpha1 "github.com/envoyproxy/gateway/api/v1alpha1" + authorinooperatorv1beta1 "github.com/kuadrant/authorino-operator/api/v1beta1" + authorinov1beta2 "github.com/kuadrant/authorino/api/v1beta2" + "github.com/kuadrant/policy-machinery/controller" + "github.com/kuadrant/policy-machinery/machinery" + "github.com/samber/lo" + "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + k8stypes "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/dynamic" + "k8s.io/utils/ptr" + gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" + gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + + kuadrantv1 "github.com/kuadrant/kuadrant-operator/api/v1" + kuadrantv1beta1 "github.com/kuadrant/kuadrant-operator/api/v1beta1" + kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" + "github.com/kuadrant/kuadrant-operator/pkg/common" + kuadrantenvoygateway "github.com/kuadrant/kuadrant-operator/pkg/envoygateway" + kuadrantistio "github.com/kuadrant/kuadrant-operator/pkg/istio" + kuadrantgatewayapi "github.com/kuadrant/kuadrant-operator/pkg/library/gatewayapi" + "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" +) + +type AuthPolicyStatusUpdater struct { + client *dynamic.DynamicClient +} + +// AuthPolicyStatusUpdater reconciles to events with impact to change the status of AuthPolicy resources +func (r *AuthPolicyStatusUpdater) Subscription() controller.Subscription { + return controller.Subscription{ + ReconcileFunc: r.UpdateStatus, + Events: []controller.ResourceEventMatcher{ + {Kind: &kuadrantv1beta1.KuadrantGroupKind}, + {Kind: &machinery.GatewayClassGroupKind}, + {Kind: &machinery.GatewayGroupKind}, + {Kind: &machinery.HTTPRouteGroupKind}, + {Kind: &kuadrantv1beta3.AuthPolicyGroupKind}, + {Kind: &kuadrantv1beta1.AuthConfigGroupKind}, + {Kind: &kuadrantistio.EnvoyFilterGroupKind}, + {Kind: &kuadrantistio.WasmPluginGroupKind}, + {Kind: &kuadrantenvoygateway.EnvoyPatchPolicyGroupKind}, + {Kind: &kuadrantenvoygateway.EnvoyExtensionPolicyGroupKind}, + }, + } +} + +func (r *AuthPolicyStatusUpdater) UpdateStatus(ctx context.Context, _ []controller.ResourceEvent, topology *machinery.Topology, _ error, state *sync.Map) error { + logger := controller.LoggerFromContext(ctx).WithName("AuthPolicyStatusUpdater") + + policies := lo.FilterMap(topology.Policies().Items(), func(item machinery.Policy, index int) (*kuadrantv1beta3.AuthPolicy, bool) { + p, ok := item.(*kuadrantv1beta3.AuthPolicy) + return p, ok + }) + + policyAcceptedFunc := authPolicyAcceptedStatusFunc(state) + + logger.V(1).Info("updating authpolicy statuses", "policies", len(policies)) + defer logger.V(1).Info("finished updating authpolicy statuses") + + for _, policy := range policies { + if policy.GetDeletionTimestamp() != nil { + logger.V(1).Info("authpolicy is marked for deletion, skipping", "name", policy.Name, "namespace", policy.Namespace) + continue + } + + // copy initial conditions, otherwise status will always be updated + newStatus := &kuadrantv1beta3.AuthPolicyStatus{ + Conditions: slices.Clone(policy.Status.Conditions), + ObservedGeneration: policy.Status.ObservedGeneration, + } + + accepted, err := policyAcceptedFunc(policy) + meta.SetStatusCondition(&newStatus.Conditions, *kuadrant.AcceptedCondition(policy, err)) + + // do not set enforced condition if Accepted condition is false + if !accepted { + meta.RemoveStatusCondition(&newStatus.Conditions, string(kuadrant.PolicyConditionEnforced)) + } else { + enforcedCond := r.enforcedCondition(policy, topology, state) + meta.SetStatusCondition(&newStatus.Conditions, *enforcedCond) + } + + equalStatus := equality.Semantic.DeepEqual(newStatus, policy.Status) + if equalStatus && policy.Generation == policy.Status.ObservedGeneration { + logger.V(1).Info("policy status unchanged, skipping update") + continue + } + newStatus.ObservedGeneration = policy.Generation + policy.Status = *newStatus + + obj, err := controller.Destruct(policy) + if err != nil { + logger.Error(err, "unable to destruct policy") // should never happen + continue + } + + _, err = r.client.Resource(kuadrantv1beta3.AuthPoliciesResource).Namespace(policy.GetNamespace()).UpdateStatus(ctx, obj, metav1.UpdateOptions{}) + if err != nil { + logger.Error(err, "unable to update status for authpolicy", "name", policy.GetName(), "namespace", policy.GetNamespace()) + // TODO: handle error + } + } + + return nil +} + +func (r *AuthPolicyStatusUpdater) enforcedCondition(policy *kuadrantv1beta3.AuthPolicy, topology *machinery.Topology, state *sync.Map) *metav1.Condition { + policyKind := kuadrantv1beta3.AuthPolicyGroupKind.Kind + + effectivePolicies, ok := state.Load(StateEffectiveAuthPolicies) + if !ok { + return kuadrant.EnforcedCondition(policy, kuadrant.NewErrUnknown(policyKind, ErrMissingStateEffectiveAuthPolicies), false) + } + + type affectedGateway struct { + gateway *machinery.Gateway + gatewayClass *machinery.GatewayClass + } + + // check the state of the rules of the policy in the effective policies + policyRuleKeys := lo.Keys(policy.Rules()) + overridingPolicies := map[string][]string{} // policyRuleKey → locators of policies overriding the policy rule + affectedGateways := map[string]affectedGateway{} // Gateway locator → {GatewayClass, Gateway} + affectedHTTPRouteRules := map[string]*machinery.HTTPRouteRule{} // pathID → HTTPRouteRule + setAffectedObjects := func(pathID string, gatewayClass *machinery.GatewayClass, gateway *machinery.Gateway, httpRouteRule *machinery.HTTPRouteRule) { + affectedGateways[gateway.GetLocator()] = affectedGateway{ + gateway: gateway, + gatewayClass: gatewayClass, + } + affectedHTTPRouteRules[pathID] = httpRouteRule + } + for pathID, effectivePolicy := range effectivePolicies.(EffectiveAuthPolicies) { + if len(kuadrantv1.PoliciesInPath(effectivePolicy.Path, func(p machinery.Policy) bool { return p.GetLocator() == policy.GetLocator() })) == 0 { + continue + } + gatewayClass, gateway, listener, httpRoute, httpRouteRule, _ := common.ObjectsInRequestPath(effectivePolicy.Path) + if !kuadrantgatewayapi.IsListenerReady(listener.Listener, gateway.Gateway) || !kuadrantgatewayapi.IsHTTPRouteReady(httpRoute.HTTPRoute, gateway.Gateway, gatewayClass.GatewayClass.Spec.ControllerName) { + continue + } + effectivePolicyRules := effectivePolicy.Spec.Rules() + if len(effectivePolicyRules) > 0 { + for _, policyRuleKey := range policyRuleKeys { + if effectivePolicyRule, ok := effectivePolicyRules[policyRuleKey]; !ok || (ok && effectivePolicyRule.GetSource() != policy.GetLocator()) { // policy rule has been overridden by another policy + var overriddenBy string + if ok { // TODO(guicassolato): !ok → we cannot tell which policy is overriding the rule, this information is lost when the policy rule is dropped during an atomic override + overriddenBy = effectivePolicyRule.GetSource() + } + overridingPolicies[policyRuleKey] = append(overridingPolicies[policyRuleKey], overriddenBy) + continue + } + // policy rule is in the effective policy, track the Gateway and the HTTPRouteRule affected by the policy + setAffectedObjects(pathID, gatewayClass, gateway, httpRouteRule) + } + continue + } + // effective policy has no rules, track the Gateway and the HTTPRouteRule affected by the policy + setAffectedObjects(pathID, gatewayClass, gateway, httpRouteRule) + } + + if len(affectedGateways) == 0 { // no rules of the policy found in the effective policies + if len(overridingPolicies) == 0 { // no rules of the policy have been overridden by any other policy + return kuadrant.EnforcedCondition(policy, kuadrant.NewErrNoRoutes(policyKind), false) + } + // all rules of the policy have been overridden by at least one other policy + overridingPoliciesKeys := lo.FilterMap(lo.Uniq(lo.Flatten(lo.Values(overridingPolicies))), func(policyLocator string, _ int) (k8stypes.NamespacedName, bool) { + policyKey, err := common.NamespacedNameFromLocator(policyLocator) + return policyKey, err == nil + }) + return kuadrant.EnforcedCondition(policy, kuadrant.NewErrOverridden(policyKind, overridingPoliciesKeys), false) + } + + var componentsToSync []string + + // check the status of Authorino + authorino, err := GetAuthorinoFromTopology(topology) + if err != nil { + return kuadrant.EnforcedCondition(policy, kuadrant.NewErrUnknown(policyKind, err), false) + } + if !meta.IsStatusConditionTrue(lo.Map(authorino.Status.Conditions, authorinoOperatorConditionToProperConditionFunc), string(authorinooperatorv1beta1.ConditionReady)) { + componentsToSync = append(componentsToSync, kuadrantv1beta1.AuthorinoGroupKind.Kind) + } + + // check status of the authconfigs + isAuthConfigReady := authConfigReadyStatusFunc(state) + for pathID, httpRouteRule := range affectedHTTPRouteRules { + authConfigName := AuthConfigNameForPath(pathID) + authConfig, found := lo.Find(topology.Objects().Children(httpRouteRule), func(authConfig machinery.Object) bool { + return authConfig.GroupVersionKind().GroupKind() == kuadrantv1beta1.AuthConfigGroupKind && authConfig.GetName() == authConfigName + }) + if !found || !isAuthConfigReady(authConfig.(*controller.RuntimeObject).Object.(*authorinov1beta2.AuthConfig)) { + componentsToSync = append(componentsToSync, fmt.Sprintf("%s (%s)", kuadrantv1beta1.AuthConfigGroupKind.Kind, authConfigName)) + } + } + + // check the status of the gateways' configuration resources + for _, g := range affectedGateways { + switch g.gatewayClass.Spec.ControllerName { + case istioGatewayControllerName: + // EnvoyFilter + istioAuthClustersModifiedGateways, _ := state.Load(StateIstioAuthClustersModified) + componentsToSync = append(componentsToSync, gatewayComponentsToSync(g.gateway, kuadrantistio.EnvoyFilterGroupKind, istioAuthClustersModifiedGateways, topology, func(obj machinery.Object) bool { + // return meta.IsStatusConditionTrue(lo.Map(obj.(*controller.RuntimeObject).Object.(*istioclientgonetworkingv1alpha3.EnvoyFilter).Status.Conditions, kuadrantistio.ConditionToProperConditionFunc), "Ready") + return true // Istio won't ever populate the status stanza of EnvoyFilter resources, so we cannot expect to find a given a condition there + })...) + // WasmPlugin + istioExtensionsModifiedGateways, _ := state.Load(StateIstioExtensionsModified) + componentsToSync = append(componentsToSync, gatewayComponentsToSync(g.gateway, kuadrantistio.WasmPluginGroupKind, istioExtensionsModifiedGateways, topology, func(obj machinery.Object) bool { + // return meta.IsStatusConditionTrue(lo.Map(obj.(*controller.RuntimeObject).Object.(*istioclientgoextensionv1alpha1.WasmPlugin).Status.Conditions, kuadrantistio.ConditionToProperConditionFunc), "Ready") + return true // Istio won't ever populate the status stanza of WasmPlugin resources, so we cannot expect to find a given a condition there + })...) + case envoyGatewayGatewayControllerName: + gatewayAncestor := gatewayapiv1.ParentReference{Name: gatewayapiv1.ObjectName(g.gateway.GetName()), Namespace: ptr.To(gatewayapiv1.Namespace(g.gateway.GetNamespace()))} + // EnvoyPatchPolicy + envoyGatewayAuthClustersModifiedGateways, _ := state.Load(StateEnvoyGatewayAuthClustersModified) + componentsToSync = append(componentsToSync, gatewayComponentsToSync(g.gateway, kuadrantenvoygateway.EnvoyPatchPolicyGroupKind, envoyGatewayAuthClustersModifiedGateways, topology, func(obj machinery.Object) bool { + return meta.IsStatusConditionTrue(kuadrantgatewayapi.PolicyStatusConditionsFromAncestor(obj.(*controller.RuntimeObject).Object.(*envoygatewayv1alpha1.EnvoyPatchPolicy).Status, envoyGatewayGatewayControllerName, gatewayAncestor, gatewayapiv1.Namespace(obj.GetNamespace())), string(envoygatewayv1alpha1.PolicyConditionProgrammed)) + })...) + // EnvoyExtensionPolicy + envoyGatewayExtensionsModifiedGateways, _ := state.Load(StateEnvoyGatewayExtensionsModified) + componentsToSync = append(componentsToSync, gatewayComponentsToSync(g.gateway, kuadrantenvoygateway.EnvoyExtensionPolicyGroupKind, envoyGatewayExtensionsModifiedGateways, topology, func(obj machinery.Object) bool { + return meta.IsStatusConditionTrue(kuadrantgatewayapi.PolicyStatusConditionsFromAncestor(obj.(*controller.RuntimeObject).Object.(*envoygatewayv1alpha1.EnvoyExtensionPolicy).Status, envoyGatewayGatewayControllerName, gatewayAncestor, gatewayapiv1.Namespace(obj.GetNamespace())), string(gatewayapiv1alpha2.PolicyConditionAccepted)) + })...) + default: + componentsToSync = append(componentsToSync, fmt.Sprintf("%s (%s/%s)", machinery.GatewayGroupKind.Kind, g.gateway.GetNamespace(), g.gateway.GetName())) + } + } + + if len(componentsToSync) > 0 { + return kuadrant.EnforcedCondition(policy, kuadrant.NewErrOutOfSync(policyKind, componentsToSync), false) + } + + return kuadrant.EnforcedCondition(policy, nil, len(overridingPolicies) == 0) +} + +func authorinoOperatorConditionToProperConditionFunc(condition authorinooperatorv1beta1.Condition, _ int) metav1.Condition { + return metav1.Condition{ + Type: string(condition.Type), + Status: metav1.ConditionStatus(condition.Status), + Reason: condition.Reason, + Message: condition.Message, + } +} + +func authorinoConditionToProperConditionFunc(cond authorinov1beta2.AuthConfigStatusCondition, _ int) metav1.Condition { + return metav1.Condition{ + Type: string(cond.Type), + Status: metav1.ConditionStatus(cond.Status), + Reason: cond.Reason, + Message: cond.Message, + } +} + +func authConfigReadyStatusFunc(state *sync.Map) func(authConfig *authorinov1beta2.AuthConfig) bool { + modifiedAuthConfigs, modified := state.Load(StateModifiedAuthConfigs) + if !modified { + return authConfigReadyStatus + } + modifiedAuthConfigsList := modifiedAuthConfigs.([]string) + return func(authConfig *authorinov1beta2.AuthConfig) bool { + if lo.Contains(modifiedAuthConfigsList, authConfig.GetName()) { + return false + } + return authConfigReadyStatus(authConfig) + } +} + +func authConfigReadyStatus(authConfig *authorinov1beta2.AuthConfig) bool { + if condition := meta.FindStatusCondition(lo.Map(authConfig.Status.Conditions, authorinoConditionToProperConditionFunc), string(authorinov1beta2.StatusConditionReady)); condition != nil { + return condition.Status == metav1.ConditionTrue + } + return false +} diff --git a/controllers/auth_workflow.go b/controllers/auth_workflow.go deleted file mode 100644 index 8eaf5f251..000000000 --- a/controllers/auth_workflow.go +++ /dev/null @@ -1,7 +0,0 @@ -package controllers - -import "github.com/kuadrant/policy-machinery/controller" - -func NewAuthWorkflow() *controller.Workflow { - return &controller.Workflow{} -} diff --git a/controllers/auth_workflow_helpers.go b/controllers/auth_workflow_helpers.go new file mode 100644 index 000000000..c428f9e43 --- /dev/null +++ b/controllers/auth_workflow_helpers.go @@ -0,0 +1,179 @@ +package controllers + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "sync" + + authorinooperatorv1beta1 "github.com/kuadrant/authorino-operator/api/v1beta1" + "github.com/kuadrant/policy-machinery/controller" + "github.com/kuadrant/policy-machinery/machinery" + "github.com/samber/lo" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + + kuadrantv1beta1 "github.com/kuadrant/kuadrant-operator/api/v1beta1" + kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" + "github.com/kuadrant/kuadrant-operator/pkg/common" + "github.com/kuadrant/kuadrant-operator/pkg/wasm" +) + +const authObjectLabelKey = "kuadrant.io/auth" + +var ( + StateAuthPolicyValid = "AuthPolicyValid" + StateEffectiveAuthPolicies = "EffectiveAuthPolicies" + StateModifiedAuthConfigs = "ModifiedAuthConfigs" + StateIstioAuthClustersModified = "IstioAuthClustersModified" + StateEnvoyGatewayAuthClustersModified = "EnvoyGatewayAuthClustersModified" + + ErrMissingAuthorino = fmt.Errorf("missing authorino object in the topology") + ErrMissingStateEffectiveAuthPolicies = fmt.Errorf("missing auth effective policies stored in the reconciliation state") +) + +//+kubebuilder:rbac:groups=kuadrant.io,resources=authpolicies,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=kuadrant.io,resources=authpolicies/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=kuadrant.io,resources=authpolicies/finalizers,verbs=update +//+kubebuilder:rbac:groups=authorino.kuadrant.io,resources=authconfigs,verbs=get;list;watch;create;update;patch;delete + +func GetAuthorinoFromTopology(topology *machinery.Topology) (*authorinooperatorv1beta1.Authorino, error) { + kuadrant, err := GetKuadrantFromTopology(topology) + if err != nil { + return nil, err + } + + authorinoObj, found := lo.Find(topology.Objects().Children(kuadrant), func(child machinery.Object) bool { + return child.GroupVersionKind().GroupKind() == kuadrantv1beta1.AuthorinoGroupKind + }) + if !found { + return nil, ErrMissingAuthorino + } + + authorino := authorinoObj.(*controller.RuntimeObject).Object.(*authorinooperatorv1beta1.Authorino) + return authorino, nil +} + +func AuthObjectLabels() labels.Set { + m := KuadrantManagedObjectLabels() + m[authObjectLabelKey] = "true" + return m +} + +func AuthClusterName(gatewayName string) string { + return fmt.Sprintf("kuadrant-auth-%s", gatewayName) +} + +func authClusterPatch(host string, port int) map[string]any { + return map[string]any{ + "name": common.KuadrantAuthClusterName, + "type": "STRICT_DNS", + "connect_timeout": "1s", + "lb_policy": "ROUND_ROBIN", + "http2_protocol_options": map[string]any{}, + "load_assignment": map[string]any{ + "cluster_name": common.KuadrantAuthClusterName, + "endpoints": []map[string]any{ + { + "lb_endpoints": []map[string]any{ + { + "endpoint": map[string]any{ + "address": map[string]any{ + "socket_address": map[string]any{ + "address": host, + "port_value": port, + }, + }, + }, + }, + }, + }, + }, + }, + } +} + +type authorinoServiceInfo struct { + Host string + Port int32 +} + +func authorinoServiceInfoFromAuthorino(authorino *authorinooperatorv1beta1.Authorino) authorinoServiceInfo { + info := authorinoServiceInfo{ + Host: fmt.Sprintf("%s-authorino-authorization.%s.svc.cluster.local", authorino.GetName(), authorino.GetNamespace()), + Port: int32(50051), // default authorino grpc authorization service port + } + if p := authorino.Spec.Listener.Ports.GRPC; p != nil { + info.Port = *p + } else if p := authorino.Spec.Listener.Port; p != nil { + info.Port = *p + } + return info +} + +func AuthConfigNameForPath(pathID string) string { + hash := sha256.Sum256([]byte(pathID)) + return hex.EncodeToString(hash[:]) +} + +func buildWasmActionsForAuth(pathID string, effectivePolicy EffectiveAuthPolicy) []wasm.Action { + action := wasm.Action{ + ServiceName: wasm.AuthServiceName, + Scope: AuthConfigNameForPath(pathID), + } + spec := effectivePolicy.Spec.Spec.Proper() + if conditions := wasm.PredicatesFromWhenConditions(lo.FlatMap(spec.Conditions, func(pattern kuadrantv1beta3.MergeablePatternExpressionOrRef, _ int) []kuadrantv1beta3.WhenCondition { + return pattern.ToWhenConditions(spec.NamedPatterns) + })...); len(conditions) > 0 { + action.Conditions = conditions + } + return []wasm.Action{action} +} + +func isAuthPolicyAcceptedAndNotDeletedFunc(state *sync.Map) func(machinery.Policy) bool { + f := isAuthPolicyAcceptedFunc(state) + return func(policy machinery.Policy) bool { + p, object := policy.(metav1.Object) + return object && f(policy) && p.GetDeletionTimestamp() == nil + } +} + +func isAuthPolicyAcceptedFunc(state *sync.Map) func(machinery.Policy) bool { + f := authPolicyAcceptedStatusFunc(state) + return func(policy machinery.Policy) bool { + accepted, _ := f(policy) + return accepted + } +} + +func authPolicyAcceptedStatusFunc(state *sync.Map) func(policy machinery.Policy) (bool, error) { + validatedPolicies, validated := state.Load(StateAuthPolicyValid) + if !validated { + return authPolicyAcceptedStatus + } + validatedPoliciesMap := validatedPolicies.(map[string]error) + return func(policy machinery.Policy) (bool, error) { + err, validated := validatedPoliciesMap[policy.GetLocator()] + if validated { + return err == nil, err + } + return authPolicyAcceptedStatus(policy) + } +} + +func authPolicyAcceptedStatus(policy machinery.Policy) (accepted bool, err error) { + p, ok := policy.(*kuadrantv1beta3.AuthPolicy) + if !ok { + return + } + if condition := meta.FindStatusCondition(p.Status.Conditions, string(gatewayapiv1alpha2.PolicyConditionAccepted)); condition != nil { + accepted = condition.Status == metav1.ConditionTrue + if !accepted { + err = fmt.Errorf(condition.Message) + } + return + } + return +} diff --git a/controllers/authconfigs_reconciler.go b/controllers/authconfigs_reconciler.go new file mode 100644 index 000000000..03fa021e8 --- /dev/null +++ b/controllers/authconfigs_reconciler.go @@ -0,0 +1,283 @@ +package controllers + +import ( + "context" + "errors" + "fmt" + "reflect" + "sync" + + authorinov1beta2 "github.com/kuadrant/authorino/api/v1beta2" + "github.com/kuadrant/policy-machinery/controller" + "github.com/kuadrant/policy-machinery/machinery" + "github.com/samber/lo" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + k8stypes "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/dynamic" + + kuadrantv1beta1 "github.com/kuadrant/kuadrant-operator/api/v1beta1" + kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" + "github.com/kuadrant/kuadrant-operator/pkg/common" + "github.com/kuadrant/kuadrant-operator/pkg/library/utils" +) + +type AuthConfigsReconciler struct { + client *dynamic.DynamicClient +} + +// AuthConfigsReconciler subscribes to events with potential to change Authorino AuthConfig custom resources +func (r *AuthConfigsReconciler) Subscription() controller.Subscription { + return controller.Subscription{ + ReconcileFunc: r.Reconcile, + Events: []controller.ResourceEventMatcher{ + {Kind: &kuadrantv1beta1.KuadrantGroupKind}, + {Kind: &machinery.GatewayClassGroupKind}, + {Kind: &machinery.GatewayGroupKind}, + {Kind: &machinery.HTTPRouteGroupKind}, + {Kind: &kuadrantv1beta3.AuthPolicyGroupKind}, + {Kind: &kuadrantv1beta1.AuthConfigGroupKind}, + }, + } +} + +func (r *AuthConfigsReconciler) Reconcile(ctx context.Context, _ []controller.ResourceEvent, topology *machinery.Topology, _ error, state *sync.Map) error { + logger := controller.LoggerFromContext(ctx).WithName("AuthConfigsReconciler") + + authorino, err := GetAuthorinoFromTopology(topology) + if err != nil { + if errors.Is(err, ErrMissingKuadrant) || errors.Is(err, ErrMissingAuthorino) { + logger.V(1).Info(err.Error()) + return nil + } + return err + } + authConfigsNamespace := authorino.GetNamespace() + + effectivePolicies, ok := state.Load(StateEffectiveAuthPolicies) + if !ok { + logger.Error(ErrMissingStateEffectiveAuthPolicies, "failed to reconcile authconfig objects") + return nil + } + effectivePoliciesMap := effectivePolicies.(EffectiveAuthPolicies) + + logger.V(1).Info("reconciling authconfig objects", "effectivePolicies", len(effectivePoliciesMap)) + defer logger.V(1).Info("finished reconciling authconfig objects") + + desiredAuthConfigs := make(map[k8stypes.NamespacedName]struct{}) + modifiedAuthConfigs := []string{} + + for pathID, effectivePolicy := range effectivePoliciesMap { + _, _, _, httpRoute, httpRouteRule, _ := common.ObjectsInRequestPath(effectivePolicy.Path) + httpRouteKey := k8stypes.NamespacedName{Name: httpRoute.GetName(), Namespace: httpRoute.GetNamespace()} + httpRouteRuleKey := httpRouteRule.Name + + authConfigName := AuthConfigNameForPath(pathID) + desiredAuthConfig := r.buildDesiredAuthConfig(effectivePolicy, authConfigName, authConfigsNamespace) + desiredAuthConfigs[k8stypes.NamespacedName{Name: desiredAuthConfig.GetName(), Namespace: desiredAuthConfig.GetNamespace()}] = struct{}{} + + resource := r.client.Resource(kuadrantv1beta1.AuthConfigsResource).Namespace(desiredAuthConfig.GetNamespace()) + + existingAuthConfigObj, found := lo.Find(topology.Objects().Children(httpRouteRule), func(child machinery.Object) bool { + return child.GroupVersionKind().GroupKind() == kuadrantv1beta1.AuthConfigGroupKind && child.GetName() == authConfigName && labels.Set(child.(*controller.RuntimeObject).GetLabels()).AsSelector().Matches(labels.Set(desiredAuthConfig.GetLabels())) + }) + + // create + if !found { + modifiedAuthConfigs = append(modifiedAuthConfigs, authConfigName) + desiredAuthConfigUnstructured, err := controller.Destruct(desiredAuthConfig) + if err != nil { + logger.Error(err, "failed to destruct authconfig object", "httpRoute", httpRouteKey.String(), "httpRouteRule", httpRouteRuleKey, "authconfig", desiredAuthConfig) + continue + } + + if _, err = resource.Create(ctx, desiredAuthConfigUnstructured, metav1.CreateOptions{}); err != nil { + logger.Error(err, "failed to create authconfig object", "httpRoute", httpRouteKey.String(), "httpRouteRule", httpRouteRuleKey, "authconfig", desiredAuthConfigUnstructured.Object) + // TODO: handle error + } + continue + } + + existingAuthConfig := existingAuthConfigObj.(*controller.RuntimeObject).Object.(*authorinov1beta2.AuthConfig) + + if equalAuthConfigs(existingAuthConfig, desiredAuthConfig) { + logger.V(1).Info("authconfig object is up to date, nothing to do") + continue + } + + modifiedAuthConfigs = append(modifiedAuthConfigs, authConfigName) + + // delete + if utils.IsObjectTaggedToDelete(desiredAuthConfig) && !utils.IsObjectTaggedToDelete(existingAuthConfig) { + if err := resource.Delete(ctx, existingAuthConfig.GetName(), metav1.DeleteOptions{}); err != nil { + logger.Error(err, "failed to delete wasmplugin object", "httpRoute", httpRouteKey.String(), "httpRouteRule", httpRouteRuleKey, "authconfig", fmt.Sprintf("%s/%s", existingAuthConfig.GetNamespace(), existingAuthConfig.GetName())) + // TODO: handle error + } + continue + } + + // update + existingAuthConfig.Spec = desiredAuthConfig.Spec + + existingAuthConfigUnstructured, err := controller.Destruct(existingAuthConfig) + if err != nil { + logger.Error(err, "failed to destruct authconfig object", "httpRoute", httpRouteKey.String(), "httpRouteRule", httpRouteRuleKey, "authconfig", existingAuthConfig) + continue + } + if _, err = resource.Update(ctx, existingAuthConfigUnstructured, metav1.UpdateOptions{}); err != nil { + logger.Error(err, "failed to update authconfig object", "httpRoute", httpRouteKey.String(), "httpRouteRule", httpRouteRuleKey, "authconfig", existingAuthConfigUnstructured.Object) + // TODO: handle error + } + } + + if len(modifiedAuthConfigs) > 0 { + state.Store(StateModifiedAuthConfigs, modifiedAuthConfigs) + } + + // cleanup authconfigs that are not in the effective policies + staleAuthConfigs := topology.Objects().Items(func(o machinery.Object) bool { + _, desired := desiredAuthConfigs[k8stypes.NamespacedName{Name: o.GetName(), Namespace: o.GetNamespace()}] + return o.GroupVersionKind().GroupKind() == kuadrantv1beta1.AuthConfigGroupKind && labels.Set(o.(*controller.RuntimeObject).GetLabels()).AsSelector().Matches(AuthObjectLabels()) && !desired + }) + for _, authConfig := range staleAuthConfigs { + if err := r.client.Resource(kuadrantv1beta1.AuthConfigsResource).Namespace(authConfig.GetNamespace()).Delete(ctx, authConfig.GetName(), metav1.DeleteOptions{}); err != nil { + logger.Error(err, "failed to delete authconfig object", "authconfig", fmt.Sprintf("%s/%s", authConfig.GetNamespace(), authConfig.GetName())) + // TODO: handle error + } + } + + return nil +} + +func (r *AuthConfigsReconciler) buildDesiredAuthConfig(effectivePolicy EffectiveAuthPolicy, name, namespace string) *authorinov1beta2.AuthConfig { + _, _, _, _, httpRouteRule, _ := common.ObjectsInRequestPath(effectivePolicy.Path) + + authConfig := &authorinov1beta2.AuthConfig{ + TypeMeta: metav1.TypeMeta{ + Kind: "AuthConfig", + APIVersion: authorinov1beta2.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Annotations: map[string]string{ + kuadrantv1beta1.AuthConfigHTTPRouteRuleAnnotation: httpRouteRule.GetLocator(), + }, + Labels: AuthObjectLabels(), + }, + Spec: authorinov1beta2.AuthConfigSpec{ + Hosts: []string{name}, + }, + } + + spec := effectivePolicy.Spec.Spec.Proper() + + // named patterns + if namedPatterns := spec.NamedPatterns; namedPatterns != nil { + authConfig.Spec.NamedPatterns = lo.MapValues(spec.NamedPatterns, func(v kuadrantv1beta3.MergeablePatternExpressions, _ string) authorinov1beta2.PatternExpressions { + return v.PatternExpressions + }) + } + + // top-level conditions + if conditions := spec.Conditions; conditions != nil { + authConfig.Spec.Conditions = lo.Map(spec.Conditions, func(v kuadrantv1beta3.MergeablePatternExpressionOrRef, _ int) authorinov1beta2.PatternExpressionOrRef { + return v.PatternExpressionOrRef + }) + } + + // return early if authScheme is nil + authScheme := spec.AuthScheme + if authScheme == nil { + return authConfig + } + + // authentication + if authentication := authScheme.Authentication; authentication != nil { + authConfig.Spec.Authentication = lo.MapValues(authentication, func(v kuadrantv1beta3.MergeableAuthenticationSpec, _ string) authorinov1beta2.AuthenticationSpec { + return v.AuthenticationSpec + }) + } + + // metadata + if metadata := authScheme.Metadata; metadata != nil { + authConfig.Spec.Metadata = lo.MapValues(metadata, func(v kuadrantv1beta3.MergeableMetadataSpec, _ string) authorinov1beta2.MetadataSpec { + return v.MetadataSpec + }) + } + + // authorization + if authorization := authScheme.Authorization; authorization != nil { + authConfig.Spec.Authorization = lo.MapValues(authorization, func(v kuadrantv1beta3.MergeableAuthorizationSpec, _ string) authorinov1beta2.AuthorizationSpec { + return v.AuthorizationSpec + }) + } + + // response + if response := authScheme.Response; response != nil { + var unauthenticated *authorinov1beta2.DenyWithSpec + if response.Unauthenticated != nil { + unauthenticated = &response.Unauthenticated.DenyWithSpec + } + + var unauthorized *authorinov1beta2.DenyWithSpec + if response.Unauthorized != nil { + unauthorized = &response.Unauthorized.DenyWithSpec + } + + authConfig.Spec.Response = &authorinov1beta2.ResponseSpec{ + Unauthenticated: unauthenticated, + Unauthorized: unauthorized, + Success: authorinov1beta2.WrappedSuccessResponseSpec{ + Headers: authorinoSpecsFromConfigs(response.Success.Headers, func(config kuadrantv1beta3.MergeableHeaderSuccessResponseSpec) authorinov1beta2.HeaderSuccessResponseSpec { + return authorinov1beta2.HeaderSuccessResponseSpec{SuccessResponseSpec: config.SuccessResponseSpec} + }), + DynamicMetadata: authorinoSpecsFromConfigs(response.Success.DynamicMetadata, func(config kuadrantv1beta3.MergeableSuccessResponseSpec) authorinov1beta2.SuccessResponseSpec { + return config.SuccessResponseSpec + }), + }, + } + } + + // callbacks + if callbacks := authScheme.Callbacks; callbacks != nil { + authConfig.Spec.Callbacks = lo.MapValues(callbacks, func(v kuadrantv1beta3.MergeableCallbackSpec, _ string) authorinov1beta2.CallbackSpec { + return v.CallbackSpec + }) + } + + return authConfig +} + +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 + } + + if len(specs) == 0 { + return nil + } + + return specs +} + +func equalAuthConfigs(existing, desired *authorinov1beta2.AuthConfig) bool { + // httprouterule back ref annotation + existingAnnotations := existing.GetAnnotations() + desiredAnnotations := desired.GetAnnotations() + if existingAnnotations == nil || desiredAnnotations == nil || existingAnnotations[kuadrantv1beta1.AuthConfigHTTPRouteRuleAnnotation] != desiredAnnotations[kuadrantv1beta1.AuthConfigHTTPRouteRuleAnnotation] { + return false + } + + // labels + existingLabels := existing.GetLabels() + desiredLabels := desired.GetLabels() + if len(existingLabels) != len(desiredLabels) || !labels.Set(existingLabels).AsSelector().Matches(labels.Set(desiredLabels)) { + return false + } + + // spec + return reflect.DeepEqual(existing.Spec, desired.Spec) +} diff --git a/controllers/authpolicy_authconfig.go b/controllers/authpolicy_authconfig.go deleted file mode 100644 index 92192fa6e..000000000 --- a/controllers/authpolicy_authconfig.go +++ /dev/null @@ -1,472 +0,0 @@ -package controllers - -import ( - "context" - "fmt" - "reflect" - "slices" - "strings" - - "github.com/go-logr/logr" - authorinoapi "github.com/kuadrant/authorino/api/v1beta2" - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "sigs.k8s.io/controller-runtime/pkg/client" - gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" - - kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" - "github.com/kuadrant/kuadrant-operator/pkg/common" - kuadrantgatewayapi "github.com/kuadrant/kuadrant-operator/pkg/library/gatewayapi" - "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" - "github.com/kuadrant/kuadrant-operator/pkg/library/utils" -) - -func (r *AuthPolicyReconciler) reconcileAuthConfigs(ctx context.Context, ap *kuadrantv1beta3.AuthPolicy, targetNetworkObject client.Object) error { - logger, err := logr.FromContext(ctx) - if err != nil { - return err - } - - authConfig, err := r.desiredAuthConfig(ctx, ap, targetNetworkObject) - if err != nil { - return err - } - - err = r.SetOwnerReference(ap, authConfig) - if err != nil { - return err - } - - err = r.ReconcileResource(ctx, &authorinoapi.AuthConfig{}, authConfig, authConfigBasicMutator) - if err != nil && !apierrors.IsAlreadyExists(err) { - logger.Error(err, "ReconcileResource failed to create/update AuthConfig resource") - return err - } - return nil -} - -func (r *AuthPolicyReconciler) desiredAuthConfig(ctx context.Context, ap *kuadrantv1beta3.AuthPolicy, targetNetworkObject client.Object) (*authorinoapi.AuthConfig, error) { - logger, _ := logr.FromContext(ctx) - logger = logger.WithName("desiredAuthConfig") - - authConfig := &authorinoapi.AuthConfig{ - TypeMeta: metav1.TypeMeta{ - Kind: "AuthConfig", - APIVersion: authorinoapi.GroupVersion.String(), - }, - ObjectMeta: metav1.ObjectMeta{ - Name: AuthConfigName(client.ObjectKeyFromObject(ap)), - Namespace: ap.Namespace, - }, - Spec: authorinoapi.AuthConfigSpec{}, - } - - var route *gatewayapiv1.HTTPRoute - var hosts []string - - switch obj := targetNetworkObject.(type) { - case *gatewayapiv1.HTTPRoute: - t, err := r.generateTopology(ctx) - if err != nil { - logger.V(1).Info("Failed to generate topology", "error", err) - return nil, err - } - - overrides := routeGatewayAuthOverrides(t, ap) - if len(overrides) != 0 { - logger.V(1).Info("targeted gateway has authpolicy with atomic overrides, skipping authorino authconfig for the HTTPRoute authpolicy") - utils.TagObjectToDelete(authConfig) - r.AffectedPolicyMap.SetAffectedPolicy(ap, overrides) - return authConfig, nil - } - route = obj - hosts, err = kuadrant.HostnamesFromHTTPRoute(ctx, obj, r.Client()) - if err != nil { - return nil, err - } - case *gatewayapiv1.Gateway: - // fake a single httproute with all rules from all httproutes accepted by the gateway, - // that do not have an authpolicy of its own, so we can generate wasm rules for those cases - gw := kuadrant.GatewayWrapper{Gateway: obj} - gwHostnames := gw.Hostnames() - if len(gwHostnames) == 0 { - gwHostnames = []gatewayapiv1.Hostname{"*"} - } - hosts = utils.HostnamesToStrings(gwHostnames) - - rules := make([]gatewayapiv1.HTTPRouteRule, 0) - routes := r.TargetRefReconciler.FetchAcceptedGatewayHTTPRoutes(ctx, obj) - for idx := range routes { - route := routes[idx] - // skip routes that have an authpolicy of its own and Gateway authpolicy does not define atomic overrides - if route.GetAnnotations()[common.AuthPolicyBackRefAnnotation] != "" && !ap.IsAtomicOverride() { - continue - } - rules = append(rules, route.Spec.Rules...) - } - if len(rules) == 0 { - logger.V(1).Info("no httproutes attached to the targeted gateway, skipping authorino authconfig for the gateway authpolicy") - utils.TagObjectToDelete(authConfig) - obj := targetNetworkObject.(*gatewayapiv1.Gateway) - gatewayWrapper := kuadrant.GatewayWrapper{Gateway: obj, Referrer: ap} - refs := gatewayWrapper.PolicyRefs() - filteredRef := utils.Filter(refs, func(key client.ObjectKey) bool { - return key != client.ObjectKeyFromObject(ap) - }) - - r.AffectedPolicyMap.SetAffectedPolicy(ap, filteredRef) - return authConfig, nil - } - route = &gatewayapiv1.HTTPRoute{ - Spec: gatewayapiv1.HTTPRouteSpec{ - Hostnames: gwHostnames, - Rules: rules, - }, - } - } - - // AuthPolicy is not Affected if we still need to create an AuthConfig for it - r.AffectedPolicyMap.RemoveAffectedPolicy(ap) - - // hosts - authConfig.Spec.Hosts = hosts - - commonSpec := ap.Spec.CommonSpec() - - // named patterns - if namedPatterns := commonSpec.NamedPatterns; len(namedPatterns) > 0 { - authConfig.Spec.NamedPatterns = namedPatterns - } - - conditionsFromHTTPRoute := authorinoConditionsFromHTTPRoute(route) - if len(conditionsFromHTTPRoute) > 0 || len(commonSpec.Conditions) > 0 { - authConfig.Spec.Conditions = append(commonSpec.Conditions, conditionsFromHTTPRoute...) - } - - // return early if authScheme is nil - if commonSpec.AuthScheme == nil { - return authConfig, nil - } - - // authentication - if authentication := commonSpec.AuthScheme.Authentication; len(authentication) > 0 { - authConfig.Spec.Authentication = authorinoSpecsFromConfigs(authentication, func(config authorinoapi.AuthenticationSpec) authorinoapi.AuthenticationSpec { - return config - }) - } - - // metadata - if metadata := commonSpec.AuthScheme.Metadata; len(metadata) > 0 { - authConfig.Spec.Metadata = authorinoSpecsFromConfigs(metadata, func(config authorinoapi.MetadataSpec) authorinoapi.MetadataSpec { return config }) - } - - // authorization - if authorization := commonSpec.AuthScheme.Authorization; len(authorization) > 0 { - authConfig.Spec.Authorization = authorinoSpecsFromConfigs(authorization, func(config authorinoapi.AuthorizationSpec) authorinoapi.AuthorizationSpec { - return config - }) - } - - // response - if response := commonSpec.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 kuadrantv1beta3.HeaderSuccessResponseSpec) authorinoapi.HeaderSuccessResponseSpec { - return authorinoapi.HeaderSuccessResponseSpec{SuccessResponseSpec: config.SuccessResponseSpec} - }), - DynamicMetadata: authorinoSpecsFromConfigs(response.Success.DynamicMetadata, func(config authorinoapi.SuccessResponseSpec) authorinoapi.SuccessResponseSpec { - return config - }), - }, - } - } - - // callbacks - if callbacks := commonSpec.AuthScheme.Callbacks; len(callbacks) > 0 { - authConfig.Spec.Callbacks = authorinoSpecsFromConfigs(callbacks, func(config authorinoapi.CallbackSpec) authorinoapi.CallbackSpec { return config }) - } - - return authConfig, nil -} - -// routeGatewayAuthOverrides returns the GW auth policies that has an override field set -func routeGatewayAuthOverrides(t *kuadrantgatewayapi.Topology, ap *kuadrantv1beta3.AuthPolicy) []client.ObjectKey { - affectedPolicies := getAffectedPolicies(t, ap) - - // Filter the policies where: - // 1. targets a gateway - // 2. is not the current AP that is being assessed - // 3. is an overriding policy - // 4. is not marked for deletion - affectedPolicies = utils.Filter(affectedPolicies, func(policy kuadrantgatewayapi.Policy) bool { - p, ok := policy.(*kuadrantv1beta3.AuthPolicy) - return ok && - p.DeletionTimestamp == nil && - kuadrantgatewayapi.IsTargetRefGateway(policy.GetTargetRef()) && - ap.GetUID() != policy.GetUID() && - p.IsAtomicOverride() - }) - - return utils.Map(affectedPolicies, func(policy kuadrantgatewayapi.Policy) client.ObjectKey { - return client.ObjectKeyFromObject(policy) - }) -} - -func getAffectedPolicies(t *kuadrantgatewayapi.Topology, ap *kuadrantv1beta3.AuthPolicy) []kuadrantgatewayapi.Policy { - topologyIndexes := kuadrantgatewayapi.NewTopologyIndexes(t) - var affectedPolicies []kuadrantgatewayapi.Policy - - // If AP is listed within the policies from gateway, it potentially can be overridden by it - for _, gw := range t.Gateways() { - policyList := topologyIndexes.PoliciesFromGateway(gw.Gateway) - if slices.Contains(utils.Map(policyList, func(p kuadrantgatewayapi.Policy) client.ObjectKey { - return client.ObjectKeyFromObject(p) - }), client.ObjectKeyFromObject(ap)) { - affectedPolicies = append(affectedPolicies, policyList...) - } - } - - return affectedPolicies -} - -// AuthConfigName returns the name of Authorino AuthConfig CR. -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 - } - - if len(specs) == 0 { - return nil - } - - return specs -} - -// authorinoConditionsFromHTTPRoute builds a list of Authorino conditions from an HTTPRoute, without using route selectors. -func authorinoConditionsFromHTTPRoute(route *gatewayapiv1.HTTPRoute) []authorinoapi.PatternExpressionOrRef { - conditions := []authorinoapi.PatternExpressionOrRef{} - hostnamesForConditions := []gatewayapiv1.Hostname{"*"} - 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 gatewayapiv1.HTTPRouteRule, hostnames []gatewayapiv1.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: utils.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(utils.Map(hostnames, func(hostname string) string { - return strings.ReplaceAll(strings.ReplaceAll(hostname, ".", `\.`), "*", ".*") - }), "|") -} - -func httpMethodRuleToAuthorinoCondition(method gatewayapiv1.HTTPMethod) authorinoapi.PatternExpressionOrRef { - return authorinoapi.PatternExpressionOrRef{ - PatternExpression: authorinoapi.PatternExpression{ - Selector: "request.method", - Operator: "eq", - Value: string(method), - }, - } -} - -func httpPathRuleToAuthorinoCondition(path gatewayapiv1.HTTPPathMatch) authorinoapi.PatternExpressionOrRef { - value := "/" - if path.Value != nil { - value = *path.Value - } - var operator string - - matchType := path.Type - if matchType == nil { - p := gatewayapiv1.PathMatchPathPrefix - matchType = &p // gateway api defaults to PathMatchPathPrefix - } - - switch *matchType { - case gatewayapiv1.PathMatchExact: - operator = "eq" - case gatewayapiv1.PathMatchPathPrefix: - operator = "matches" - value += ".*" - case gatewayapiv1.PathMatchRegularExpression: - operator = "matches" - } - - return authorinoapi.PatternExpressionOrRef{ - PatternExpression: authorinoapi.PatternExpression{ - Selector: `request.url_path`, - Operator: authorinoapi.PatternExpressionOperator(operator), - Value: value, - }, - } -} - -func httpHeadersRuleToAuthorinoConditions(headers []gatewayapiv1.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 gatewayapiv1.HTTPHeaderMatch) authorinoapi.PatternExpressionOrRef { - operator := "eq" // gateway api defaults to HeaderMatchExact - if header.Type != nil && *header.Type == gatewayapiv1.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 []gatewayapiv1.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 gatewayapiv1.HTTPQueryParamMatch) authorinoapi.PatternExpressionOrRef { - operator := "eq" // gateway api defaults to QueryParamMatchExact - if queryParam.Type != nil && *queryParam.Type == gatewayapiv1.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: utils.Map(oneOf, toAuthorinoUnstructuredPatternExpressionOrRef), - }, - } -} - -func authConfigBasicMutator(existingObj, desiredObj client.Object) (bool, error) { - existing, ok := existingObj.(*authorinoapi.AuthConfig) - if !ok { - return false, fmt.Errorf("%T is not an *authorinoapi.AuthConfig", existingObj) - } - desired, ok := desiredObj.(*authorinoapi.AuthConfig) - if !ok { - return false, fmt.Errorf("%T is not an *authorinoapi.AuthConfig", desiredObj) - } - - if reflect.DeepEqual(existing.Spec, desired.Spec) { - return false, nil - } - - existing.Spec = desired.Spec - - return true, nil -} diff --git a/controllers/authpolicy_authconfig_test.go b/controllers/authpolicy_authconfig_test.go deleted file mode 100644 index 4d940f7f3..000000000 --- a/controllers/authpolicy_authconfig_test.go +++ /dev/null @@ -1,743 +0,0 @@ -//go:build unit - -package controllers - -import ( - "reflect" - "testing" - - authorinoapi "github.com/kuadrant/authorino/api/v1beta2" - "k8s.io/utils/ptr" - gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" -) - -func TestAuthorinoConditionsFromHTTPRouteRule(t *testing.T) { - testCases := []struct { - name string - hostnames []gatewayapiv1.Hostname - rule gatewayapiv1.HTTPRouteRule - expected []authorinoapi.PatternExpressionOrRef - }{ - { - name: "No HTTPRouteMatch", - hostnames: []gatewayapiv1.Hostname{"toystore.kuadrant.io"}, - rule: gatewayapiv1.HTTPRouteRule{}, - expected: []authorinoapi.PatternExpressionOrRef{ - { - PatternExpression: authorinoapi.PatternExpression{ - Selector: "request.host", - Operator: "matches", - Value: `toystore\.kuadrant\.io`, - }, - }, - }, - }, - { - name: "Single HTTPRouteMatch", - hostnames: []gatewayapiv1.Hostname{"toystore.kuadrant.io"}, - rule: gatewayapiv1.HTTPRouteRule{ - Matches: []gatewayapiv1.HTTPRouteMatch{ - { - Path: &gatewayapiv1.HTTPPathMatch{ - Type: ptr.To(gatewayapiv1.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: []gatewayapiv1.Hostname{"toystore.kuadrant.io"}, - rule: gatewayapiv1.HTTPRouteRule{ - Matches: []gatewayapiv1.HTTPRouteMatch{ - { - Path: &gatewayapiv1.HTTPPathMatch{ - Type: ptr.To(gatewayapiv1.PathMatchType("PathPrefix")), - Value: ptr.To("/toy"), - }, - }, - { - Path: &gatewayapiv1.HTTPPathMatch{ - Type: ptr.To(gatewayapiv1.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: []gatewayapiv1.Hostname{"toystore.kuadrant.io", "gamestore.kuadrant.io"}, - rule: gatewayapiv1.HTTPRouteRule{ - Matches: []gatewayapiv1.HTTPRouteMatch{ - { - Path: &gatewayapiv1.HTTPPathMatch{ - Type: ptr.To(gatewayapiv1.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: []gatewayapiv1.Hostname{"*.kuadrant.io"}, - rule: gatewayapiv1.HTTPRouteRule{ - Matches: []gatewayapiv1.HTTPRouteMatch{ - { - Path: &gatewayapiv1.HTTPPathMatch{ - Type: ptr.To(gatewayapiv1.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: []gatewayapiv1.Hostname{"toystore.kuadrant.io", "*"}, - rule: gatewayapiv1.HTTPRouteRule{ - Matches: []gatewayapiv1.HTTPRouteMatch{ - { - Path: &gatewayapiv1.HTTPPathMatch{ - Type: ptr.To(gatewayapiv1.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: gatewayapiv1.HTTPRouteRule{ - Matches: []gatewayapiv1.HTTPRouteMatch{ - { - Method: ptr.To(gatewayapiv1.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: gatewayapiv1.HTTPRouteRule{ - Matches: []gatewayapiv1.HTTPRouteMatch{ - { - Path: &gatewayapiv1.HTTPPathMatch{ - Type: ptr.To(gatewayapiv1.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: gatewayapiv1.HTTPRouteRule{ - Matches: []gatewayapiv1.HTTPRouteMatch{ - { - Path: &gatewayapiv1.HTTPPathMatch{ - Type: ptr.To(gatewayapiv1.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: gatewayapiv1.HTTPRouteRule{ - Matches: []gatewayapiv1.HTTPRouteMatch{ - { - Path: &gatewayapiv1.HTTPPathMatch{ - Type: ptr.To(gatewayapiv1.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: gatewayapiv1.HTTPRouteRule{ - Matches: []gatewayapiv1.HTTPRouteMatch{ - { - Headers: []gatewayapiv1.HTTPHeaderMatch{ - { - Type: ptr.To(gatewayapiv1.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: gatewayapiv1.HTTPRouteRule{ - Matches: []gatewayapiv1.HTTPRouteMatch{ - { - Headers: []gatewayapiv1.HTTPHeaderMatch{ - { - Type: ptr.To(gatewayapiv1.HeaderMatchType("Exact")), - Name: "x-foo", - Value: "a-value", - }, - { - Type: ptr.To(gatewayapiv1.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: gatewayapiv1.HTTPRouteRule{ - Matches: []gatewayapiv1.HTTPRouteMatch{ - { - Headers: []gatewayapiv1.HTTPHeaderMatch{ - { - Type: ptr.To(gatewayapiv1.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: gatewayapiv1.HTTPRouteRule{ - Matches: []gatewayapiv1.HTTPRouteMatch{ - { - QueryParams: []gatewayapiv1.HTTPQueryParamMatch{ - { - Type: ptr.To(gatewayapiv1.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: gatewayapiv1.HTTPRouteRule{ - Matches: []gatewayapiv1.HTTPRouteMatch{ - { - QueryParams: []gatewayapiv1.HTTPQueryParamMatch{ - { - Type: ptr.To(gatewayapiv1.QueryParamMatchType("Exact")), - Name: "x-foo", - Value: "a-value", - }, - { - Type: ptr.To(gatewayapiv1.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: gatewayapiv1.HTTPRouteRule{ - Matches: []gatewayapiv1.HTTPRouteMatch{ - { - QueryParams: []gatewayapiv1.HTTPQueryParamMatch{ - { - Type: ptr.To(gatewayapiv1.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 \nbe\t%v, \ngot\t%v", i, tc.expected[i], result[i]) - } - } - }) - } -} diff --git a/controllers/authpolicy_controller.go b/controllers/authpolicy_controller.go deleted file mode 100644 index aad00a34f..000000000 --- a/controllers/authpolicy_controller.go +++ /dev/null @@ -1,278 +0,0 @@ -package controllers - -import ( - "context" - "encoding/json" - "fmt" - - "github.com/go-logr/logr" - authorinoapi "github.com/kuadrant/authorino/api/v1beta2" - apierrors "k8s.io/apimachinery/pkg/api/errors" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - "sigs.k8s.io/controller-runtime/pkg/handler" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" - - kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" - kuadrantgatewayapi "github.com/kuadrant/kuadrant-operator/pkg/library/gatewayapi" - "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" - "github.com/kuadrant/kuadrant-operator/pkg/library/mappers" - "github.com/kuadrant/kuadrant-operator/pkg/library/reconcilers" -) - -const authPolicyFinalizer = "authpolicy.kuadrant.io/finalizer" - -// AuthPolicyReconciler reconciles a AuthPolicy object -type AuthPolicyReconciler struct { - *reconcilers.BaseReconciler - TargetRefReconciler reconcilers.TargetRefReconciler - // AffectedPolicyMap tracks the affected policies to report their status. - AffectedPolicyMap *kuadrant.AffectedPolicyMap -} - -//+kubebuilder:rbac:groups=kuadrant.io,resources=authpolicies,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=kuadrant.io,resources=authpolicies/finalizers,verbs=update -//+kubebuilder:rbac:groups=kuadrant.io,resources=authpolicies/status,verbs=get;update;patch -//+kubebuilder:rbac:groups=authorino.kuadrant.io,resources=authconfigs,verbs=get;list;watch;create;update;patch;delete - -func (r *AuthPolicyReconciler) Reconcile(eventCtx context.Context, req ctrl.Request) (ctrl.Result, error) { - logger := r.Logger().WithValues("AuthPolicy", req.NamespacedName) - logger.Info("Reconciling AuthPolicy") - ctx := logr.NewContext(eventCtx, logger) - - // fetch the authpolicy - ap := &kuadrantv1beta3.AuthPolicy{} - if err := r.Client().Get(ctx, req.NamespacedName, ap); err != nil { - if apierrors.IsNotFound(err) { - logger.Info("no AuthPolicy found") - return ctrl.Result{}, nil - } - logger.Error(err, "failed to get AuthPolicy") - return ctrl.Result{}, err - } - - if logger.V(1).Enabled() { - jsonData, err := json.MarshalIndent(ap, "", " ") - if err != nil { - return ctrl.Result{}, err - } - logger.V(1).Info(string(jsonData)) - } - - markedForDeletion := ap.GetDeletionTimestamp() != nil - - // fetch the target network object - targetNetworkObject, err := reconcilers.FetchTargetRefObject(ctx, r.Client(), ap.GetTargetRef(), ap.Namespace, ap.TargetProgrammedGatewaysOnly()) - if err != nil { - if !markedForDeletion { - if apierrors.IsNotFound(err) { - logger.V(1).Info("Network object not found. Cleaning up") - delResErr := r.deleteResources(ctx, ap, nil) - if delResErr == nil { - delResErr = err - } - return r.reconcileStatus(ctx, ap, kuadrant.NewErrTargetNotFound(ap.Kind(), ap.GetTargetRef(), delResErr)) - } - return ctrl.Result{}, err - } - targetNetworkObject = nil // we need the object set to nil when there's an error, otherwise deleting the resources (when marked for deletion) will panic - } - - // handle authpolicy marked for deletion - if markedForDeletion { - if controllerutil.ContainsFinalizer(ap, authPolicyFinalizer) { - logger.V(1).Info("Handling removal of authpolicy object") - - if err := r.deleteResources(ctx, ap, targetNetworkObject); err != nil { - return ctrl.Result{}, err - } - - logger.Info("removing finalizer") - if err := r.RemoveFinalizer(ctx, ap, authPolicyFinalizer); err != nil { - return ctrl.Result{}, err - } - } - - return ctrl.Result{}, nil - } - - // add finalizer to the authpolicy - if !controllerutil.ContainsFinalizer(ap, authPolicyFinalizer) { - if err := r.AddFinalizer(ctx, ap, authPolicyFinalizer); client.IgnoreNotFound(err) != nil { - return ctrl.Result{Requeue: true}, err - } - } - - // reconcile the authpolicy spec - specErr := r.reconcileResources(ctx, ap, targetNetworkObject) - - // reconcile authpolicy status - statusResult, statusErr := r.reconcileStatus(ctx, ap, specErr) - - if specErr != nil { - return ctrl.Result{}, specErr - } - - if statusErr != nil { - return ctrl.Result{}, statusErr - } - - if statusResult.Requeue { - logger.V(1).Info("Reconciling status not finished. Requeueing.") - return statusResult, nil - } - - // trigger concurrent reconciliations of possibly affected gateway policies - switch route := targetNetworkObject.(type) { - case *gatewayapiv1.HTTPRoute: - if err := r.reconcileRouteParentGatewayPolicies(ctx, route); err != nil { - return ctrl.Result{}, err - } - } - - logger.Info("AuthPolicy reconciled successfully") - return ctrl.Result{}, nil -} - -// validate performs validation before proceeding with the reconcile loop, returning a common.ErrInvalid on any failing validation -func (r *AuthPolicyReconciler) validate(ap *kuadrantv1beta3.AuthPolicy, targetNetworkObject client.Object) error { - if err := kuadrant.ValidateHierarchicalRules(ap, targetNetworkObject); err != nil { - return kuadrant.NewErrInvalid(ap.Kind(), err) - } - - return nil -} - -func (r *AuthPolicyReconciler) reconcileResources(ctx context.Context, ap *kuadrantv1beta3.AuthPolicy, targetNetworkObject client.Object) error { - if err := r.validate(ap, targetNetworkObject); err != nil { - return err - } - - // reconcile based on gateway diffs - gatewayDiffObj, err := reconcilers.ComputeGatewayDiffs(ctx, r.Client(), ap, targetNetworkObject) - if err != nil { - return err - } - - if err := r.reconcileAuthConfigs(ctx, ap, targetNetworkObject); err != nil { - return fmt.Errorf("reconcile AuthConfig error %w", err) - } - - // if the AuthPolicy(ap) targets a Gateway then all policies attached to that Gateway need to be checked. - // this is due to not knowing if the Gateway AuthPolicy was updated to include or remove the overrides section. - switch obj := targetNetworkObject.(type) { - case *gatewayapiv1.Gateway: - gw := kuadrant.GatewayWrapper{Gateway: obj, Referrer: ap} - apKey := client.ObjectKeyFromObject(ap) - for _, policyKey := range gw.PolicyRefs() { - if policyKey == apKey { - continue - } - - ref := &kuadrantv1beta3.AuthPolicy{} - err = r.Client().Get(ctx, policyKey, ref) - if err != nil { - return err - } - - refNetworkObject, err := reconcilers.FetchTargetRefObject(ctx, r.Client(), ref.GetTargetRef(), ref.Namespace, ap.TargetProgrammedGatewaysOnly()) - if err != nil { - return err - } - - if err = r.reconcileAuthConfigs(ctx, ref, refNetworkObject); err != nil { - return err - } - } - } - - // set direct back ref - i.e. claim the target network object as taken asap - if err := r.reconcileNetworkResourceDirectBackReference(ctx, ap, targetNetworkObject); err != nil { - return fmt.Errorf("reconcile TargetBackReference error %w", err) - } - - // set annotation of policies affecting the gateway - should be the last step, only when all the reconciliation steps succeed - if err := r.TargetRefReconciler.ReconcileGatewayPolicyReferences(ctx, ap, gatewayDiffObj); err != nil { - return fmt.Errorf("ReconcileGatewayPolicyReferences error %w", err) - } - - return nil -} - -func (r *AuthPolicyReconciler) deleteResources(ctx context.Context, ap *kuadrantv1beta3.AuthPolicy, targetNetworkObject client.Object) error { - // delete based on gateway diffs - gatewayDiffObj, err := reconcilers.ComputeGatewayDiffs(ctx, r.Client(), ap, targetNetworkObject) - if err != nil { - return err - } - - // remove direct back ref - if targetNetworkObject != nil { - if err := r.deleteNetworkResourceDirectBackReference(ctx, targetNetworkObject, ap); err != nil { - return err - } - } - - // update annotation of policies affecting the gateway - return r.TargetRefReconciler.ReconcileGatewayPolicyReferences(ctx, ap, gatewayDiffObj) -} - -// Ensures only one RLP targets the network resource -func (r *AuthPolicyReconciler) reconcileNetworkResourceDirectBackReference(ctx context.Context, ap *kuadrantv1beta3.AuthPolicy, targetNetworkObject client.Object) error { - return r.TargetRefReconciler.ReconcileTargetBackReference(ctx, ap, targetNetworkObject, ap.DirectReferenceAnnotationName()) -} - -func (r *AuthPolicyReconciler) deleteNetworkResourceDirectBackReference(ctx context.Context, targetNetworkObject client.Object, ap *kuadrantv1beta3.AuthPolicy) error { - return r.TargetRefReconciler.DeleteTargetBackReference(ctx, targetNetworkObject, ap.DirectReferenceAnnotationName()) -} - -// 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 *gatewayapiv1.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 { - ok, err := kuadrantgatewayapi.IsGatewayAPIInstalled(mgr.GetRESTMapper()) - if err != nil { - return err - } - if !ok { - r.Logger().Info("AuthPolicy controller disabled. GatewayAPI was not found") - return nil - } - - httpRouteEventMapper := mappers.NewHTTPRouteEventMapper(mappers.WithLogger(r.Logger().WithName("httproute.mapper")), mappers.WithClient(mgr.GetClient())) - gatewayEventMapper := mappers.NewGatewayEventMapper( - kuadrantv1beta3.NewAuthPolicyType(), - mappers.WithLogger(r.Logger().WithName("gateway.mapper")), - mappers.WithClient(mgr.GetClient()), - ) - - return ctrl.NewControllerManagedBy(mgr). - For(&kuadrantv1beta3.AuthPolicy{}). - Owns(&authorinoapi.AuthConfig{}). - Watches( - &gatewayapiv1.HTTPRoute{}, - handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, object client.Object) []reconcile.Request { - return httpRouteEventMapper.MapToPolicy(ctx, object, kuadrantv1beta3.NewAuthPolicyType()) - }), - ). - Watches(&gatewayapiv1.Gateway{}, handler.EnqueueRequestsFromMapFunc(gatewayEventMapper.Map)). - Complete(r) -} diff --git a/controllers/authpolicy_envoysecuritypolicy_controller.go b/controllers/authpolicy_envoysecuritypolicy_controller.go deleted file mode 100644 index f7225ac51..000000000 --- a/controllers/authpolicy_envoysecuritypolicy_controller.go +++ /dev/null @@ -1,209 +0,0 @@ -package controllers - -import ( - "context" - "encoding/json" - "fmt" - - egv1alpha1 "github.com/envoyproxy/gateway/api/v1alpha1" - "github.com/go-logr/logr" - "github.com/samber/lo" - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/utils/ptr" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/handler" - gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" - gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" - - kuadrantv1beta1 "github.com/kuadrant/kuadrant-operator/api/v1beta1" - kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" - kuadrantenvoygateway "github.com/kuadrant/kuadrant-operator/pkg/envoygateway" - "github.com/kuadrant/kuadrant-operator/pkg/kuadranttools" - kuadrantgatewayapi "github.com/kuadrant/kuadrant-operator/pkg/library/gatewayapi" - "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" - "github.com/kuadrant/kuadrant-operator/pkg/library/mappers" - "github.com/kuadrant/kuadrant-operator/pkg/library/reconcilers" - "github.com/kuadrant/kuadrant-operator/pkg/library/utils" -) - -// AuthPolicyEnvoySecurityPolicyReconciler reconciles SecurityPolicy objects for auth -type AuthPolicyEnvoySecurityPolicyReconciler struct { - *reconcilers.BaseReconciler -} - -//+kubebuilder:rbac:groups=gateway.envoyproxy.io,resources=securitypolicies,verbs=get;list;watch;create;update;patch;delete - -func (r *AuthPolicyEnvoySecurityPolicyReconciler) Reconcile(eventCtx context.Context, req ctrl.Request) (ctrl.Result, error) { - logger := r.Logger().WithValues("Kuadrant", req.NamespacedName) - logger.Info("Reconciling auth SecurityPolicy") - ctx := logr.NewContext(eventCtx, logger) - - kObj := &kuadrantv1beta1.Kuadrant{} - if err := r.Client().Get(ctx, req.NamespacedName, kObj); err != nil { - if apierrors.IsNotFound(err) { - logger.Info("no kuadrant object found") - return ctrl.Result{}, nil - } - logger.Error(err, "failed to get kuadrant object") - return ctrl.Result{}, err - } - - if logger.V(1).Enabled() { - jsonData, err := json.MarshalIndent(kObj, "", " ") - if err != nil { - return ctrl.Result{}, err - } - logger.V(1).Info(string(jsonData)) - } - - topology, err := kuadranttools.TopologyForPolicies(ctx, r.Client(), kuadrantv1beta3.NewAuthPolicyType()) - if err != nil { - return ctrl.Result{}, err - } - - for _, policy := range topology.Policies() { - err := r.reconcileSecurityPolicy(ctx, policy, kObj.Namespace) - if err != nil { - return ctrl.Result{}, err - } - } - - return ctrl.Result{}, nil -} - -func (r *AuthPolicyEnvoySecurityPolicyReconciler) reconcileSecurityPolicy(ctx context.Context, policy kuadrantgatewayapi.PolicyNode, kuadrantNamespace string) error { - logger, _ := logr.FromContext(ctx) - logger = logger.WithName("reconcileSecurityPolicy") - - esp := envoySecurityPolicy(policy, kuadrantNamespace) - if err := r.SetOwnerReference(policy.Policy, esp); err != nil { - return err - } - - if err := r.ReconcileResource(ctx, &egv1alpha1.SecurityPolicy{}, esp, kuadrantenvoygateway.EnvoySecurityPolicyMutator); err != nil && !apierrors.IsAlreadyExists(err) { - logger.Error(err, "failed to reconcile envoy SecurityPolicy resource") - return err - } - - return nil -} - -func envoySecurityPolicy(policy kuadrantgatewayapi.PolicyNode, kuadrantNamespace string) *egv1alpha1.SecurityPolicy { - esp := &egv1alpha1.SecurityPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: EnvoySecurityPolicyName(policy.GetName()), - Namespace: policy.GetNamespace(), - Labels: map[string]string{ - kuadrant.KuadrantNamespaceAnnotation: kuadrantNamespace, - }, - }, - Spec: egv1alpha1.SecurityPolicySpec{ - PolicyTargetReferences: egv1alpha1.PolicyTargetReferences{ - TargetRefs: []gatewayapiv1alpha2.LocalPolicyTargetReferenceWithSectionName{}, - }, - ExtAuth: &egv1alpha1.ExtAuth{ - GRPC: &egv1alpha1.GRPCExtAuthService{ - BackendRefs: []egv1alpha1.BackendRef{ - { - BackendObjectReference: gatewayapiv1.BackendObjectReference{ - Name: kuadrant.AuthorinoServiceName, - Kind: ptr.To[gatewayapiv1.Kind]("Service"), - Namespace: ptr.To(gatewayapiv1.Namespace(kuadrantNamespace)), - Port: ptr.To(gatewayapiv1.PortNumber(50051)), - }, - }, - }, - }, - }, - }, - } - kuadrant.AnnotateObject(esp, kuadrantNamespace) - - // if targetref has been deleted, or - // if gateway target and not programmed, or - // route target which is not accepted by any parent; - // tag for deletion - targetRef := policy.TargetRef() - if (targetRef == nil || targetRef.GetGatewayNode() != nil && meta.IsStatusConditionFalse(targetRef.GetGatewayNode().Status.Conditions, string(gatewayapiv1.GatewayConditionProgrammed))) || - (targetRef.GetRouteNode() != nil && !lo.ContainsBy(targetRef.GetRouteNode().Status.Parents, func(p gatewayapiv1.RouteParentStatus) bool { - return meta.IsStatusConditionTrue(p.Conditions, string(gatewayapiv1.RouteConditionAccepted)) - })) { - utils.TagObjectToDelete(esp) - return esp - } - - targetNetworkObjectGvk := targetRef.GetObject().GetObjectKind().GroupVersionKind() - esp.Spec.PolicyTargetReferences.TargetRefs = append(esp.Spec.PolicyTargetReferences.TargetRefs, - gatewayapiv1alpha2.LocalPolicyTargetReferenceWithSectionName{ - LocalPolicyTargetReference: gatewayapiv1alpha2.LocalPolicyTargetReference{ - Group: gatewayapiv1.Group(targetNetworkObjectGvk.Group), - Kind: gatewayapiv1.Kind(targetNetworkObjectGvk.Kind), - Name: gatewayapiv1.ObjectName(targetRef.GetObject().GetName()), - }, - }) - - return esp -} - -func EnvoySecurityPolicyName(targetName string) string { - return fmt.Sprintf("for-%s", targetName) -} - -// SetupWithManager sets up the controller with the Manager. -func (r *AuthPolicyEnvoySecurityPolicyReconciler) SetupWithManager(mgr ctrl.Manager) error { - ok, err := kuadrantenvoygateway.IsEnvoyGatewaySecurityPolicyInstalled(mgr.GetRESTMapper()) - if err != nil { - return err - } - if !ok { - r.Logger().Info("EnvoyGateway SecurityPolicy controller disabled. EnvoyGateway API was not found") - return nil - } - - ok, err = kuadrantgatewayapi.IsGatewayAPIInstalled(mgr.GetRESTMapper()) - if err != nil { - return err - } - if !ok { - r.Logger().Info("EnvoyGateway SecurityPolicy controller disabled. GatewayAPI was not found") - return nil - } - - securityPolicyToKuadrantEventMapper := mappers.NewSecurityPolicyToKuadrantEventMapper( - mappers.WithLogger(r.Logger().WithName("securityPolicyToKuadrantEventMapper")), - mappers.WithClient(r.Client()), - ) - policyToKuadrantEventMapper := mappers.NewPolicyToKuadrantEventMapper( - mappers.WithLogger(r.Logger().WithName("policyToKuadrantEventMapper")), - mappers.WithClient(r.Client()), - ) - gatewayToKuadrantEventMapper := mappers.NewGatewayToKuadrantEventMapper( - mappers.WithLogger(r.Logger().WithName("gatewayToKuadrantEventMapper")), - mappers.WithClient(r.Client()), - ) - httpRouteToKuadrantEventMapper := mappers.NewHTTPRouteToKuadrantEventMapper( - mappers.WithLogger(r.Logger().WithName("httpRouteToKuadrantEventMapper")), - mappers.WithClient(r.Client()), - ) - return ctrl.NewControllerManagedBy(mgr). - For(&kuadrantv1beta1.Kuadrant{}). - Watches( - &egv1alpha1.SecurityPolicy{}, - handler.EnqueueRequestsFromMapFunc(securityPolicyToKuadrantEventMapper.Map), - ). - Watches( - &kuadrantv1beta3.AuthPolicy{}, - handler.EnqueueRequestsFromMapFunc(policyToKuadrantEventMapper.Map), - ). - Watches( - &gatewayapiv1.Gateway{}, - handler.EnqueueRequestsFromMapFunc(gatewayToKuadrantEventMapper.Map), - ). - Watches( - &gatewayapiv1.HTTPRoute{}, - handler.EnqueueRequestsFromMapFunc(httpRouteToKuadrantEventMapper.Map), - ). - Complete(r) -} diff --git a/controllers/authpolicy_istio_authorizationpolicy_controller.go b/controllers/authpolicy_istio_authorizationpolicy_controller.go deleted file mode 100644 index 5253cb3be..000000000 --- a/controllers/authpolicy_istio_authorizationpolicy_controller.go +++ /dev/null @@ -1,367 +0,0 @@ -package controllers - -import ( - "context" - "encoding/json" - "fmt" - - "github.com/go-logr/logr" - "github.com/google/uuid" - "github.com/samber/lo" - istiosecurity "istio.io/api/security/v1beta1" - istiov1beta1 "istio.io/client-go/pkg/apis/security/v1beta1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/utils/env" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/handler" - gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" - gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" - - kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" - "github.com/kuadrant/kuadrant-operator/pkg/common" - kuadrantistioutils "github.com/kuadrant/kuadrant-operator/pkg/istio" - "github.com/kuadrant/kuadrant-operator/pkg/kuadranttools" - kuadrantgatewayapi "github.com/kuadrant/kuadrant-operator/pkg/library/gatewayapi" - "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" - "github.com/kuadrant/kuadrant-operator/pkg/library/mappers" - "github.com/kuadrant/kuadrant-operator/pkg/library/reconcilers" - "github.com/kuadrant/kuadrant-operator/pkg/library/utils" -) - -var KuadrantExtAuthProviderName = env.GetString("AUTH_PROVIDER", "kuadrant-authorization") - -// AuthPolicyIstioAuthorizationPolicyReconciler reconciles IstioAuthorizationPolicy objects for auth -type AuthPolicyIstioAuthorizationPolicyReconciler struct { - *reconcilers.BaseReconciler -} - -//+kubebuilder:rbac:groups=security.istio.io,resources=authorizationpolicies,verbs=get;list;watch;create;update;patch;delete - -func (r *AuthPolicyIstioAuthorizationPolicyReconciler) Reconcile(eventCtx context.Context, req ctrl.Request) (ctrl.Result, error) { - logger := r.Logger().WithValues("Gateway", req.NamespacedName, "request id", uuid.NewString()) - logger.Info("Reconciling istio AuthorizationPolicy") - ctx := logr.NewContext(eventCtx, logger) - - gw := &gatewayapiv1.Gateway{} - if err := r.Client().Get(ctx, req.NamespacedName, gw); err != nil { - if apierrors.IsNotFound(err) { - logger.Info("no gateway found") - return ctrl.Result{}, nil - } - logger.Error(err, "failed to get gateway") - return ctrl.Result{}, err - } - - if logger.V(1).Enabled() { - jsonData, err := json.MarshalIndent(gw, "", " ") - if err != nil { - return ctrl.Result{}, err - } - logger.V(1).Info(string(jsonData)) - } - - if !kuadrant.IsKuadrantManaged(gw) { - return ctrl.Result{}, nil - } - - topology, err := kuadranttools.TopologyFromGateway(ctx, r.Client(), gw, kuadrantv1beta3.NewAuthPolicyType()) - if err != nil { - return ctrl.Result{}, err - } - topologyIndex := kuadrantgatewayapi.NewTopologyIndexes(topology) - policies := lo.FilterMap(topologyIndex.PoliciesFromGateway(gw), func(policy kuadrantgatewayapi.Policy, _ int) (*kuadrantv1beta3.AuthPolicy, bool) { - ap, ok := policy.(*kuadrantv1beta3.AuthPolicy) - if !ok { - return nil, false - } - return ap, true - }) - - for _, policy := range policies { - iap, err := r.istioAuthorizationPolicy(ctx, gw, policy, topologyIndex, topology) - if err != nil { - return ctrl.Result{}, err - } - - if policy.GetDeletionTimestamp() != nil { - utils.TagObjectToDelete(iap) - } - - if err := r.ReconcileResource(ctx, &istiov1beta1.AuthorizationPolicy{}, iap, kuadrantistioutils.AuthorizationPolicyMutator); err != nil && !apierrors.IsAlreadyExists(err) { - logger.Error(err, "failed to reconcile IstioAuthorizationPolicy resource") - return ctrl.Result{}, err - } - } - - return ctrl.Result{}, nil -} - -func (r *AuthPolicyIstioAuthorizationPolicyReconciler) istioAuthorizationPolicy(ctx context.Context, gateway *gatewayapiv1.Gateway, ap *kuadrantv1beta3.AuthPolicy, topologyIndex *kuadrantgatewayapi.TopologyIndexes, topology *kuadrantgatewayapi.Topology) (*istiov1beta1.AuthorizationPolicy, error) { - logger, _ := logr.FromContext(ctx) - logger = logger.WithName("istioAuthorizationPolicy") - - iap := &istiov1beta1.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, - TargetRef: kuadrantistioutils.PolicyTargetRefFromGateway(gateway), - ActionDetail: &istiosecurity.AuthorizationPolicy_Provider{ - Provider: &istiosecurity.AuthorizationPolicy_ExtensionProvider{ - Name: KuadrantExtAuthProviderName, - }, - }, - }, - } - - gwHostnames := kuadrantgatewayapi.GatewayHostnames(gateway) - if len(gwHostnames) == 0 { - gwHostnames = []gatewayapiv1.Hostname{"*"} - } - - var route *gatewayapiv1.HTTPRoute - var routeHostnames []gatewayapiv1.Hostname - targetNetworkObject := topologyIndex.GetPolicyTargetObject(ap) - - switch obj := targetNetworkObject.(type) { - case *gatewayapiv1.HTTPRoute: - route = obj - if len(route.Spec.Hostnames) > 0 { - routeHostnames = kuadrantgatewayapi.FilterValidSubdomains(gwHostnames, route.Spec.Hostnames) - } else { - routeHostnames = gwHostnames - } - case *gatewayapiv1.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([]gatewayapiv1.HTTPRouteRule, 0) - routes := topology.Routes() - for idx := range routes { - route := routes[idx].Route() - // 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") - utils.TagObjectToDelete(iap) - return iap, nil - } - route = &gatewayapiv1.HTTPRoute{ - Spec: gatewayapiv1.HTTPRouteSpec{ - Hostnames: gwHostnames, - Rules: rules, - }, - } - routeHostnames = gwHostnames - } - - rules := istioAuthorizationPolicyRulesFromHTTPRoute(route) - 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 := utils.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 - } - - if err := r.SetOwnerReference(gateway, iap); err != nil { - return nil, err - } - - return iap, nil -} - -func (r *AuthPolicyIstioAuthorizationPolicyReconciler) SetupWithManager(mgr ctrl.Manager) error { - ok, err := kuadrantistioutils.IsAuthorizationPolicyInstalled(mgr.GetRESTMapper()) - if err != nil { - return err - } - if !ok { - r.Logger().Info("Istio AuthorizationPolicy controller disabled. Istio was not found") - return nil - } - - ok, err = kuadrantgatewayapi.IsGatewayAPIInstalled(mgr.GetRESTMapper()) - if err != nil { - return err - } - if !ok { - r.Logger().Info("Istio AuthorizationPolicy controller disabled. GatewayAPI was not found") - return nil - } - - httpRouteToParentGatewaysEventMapper := mappers.NewHTTPRouteToParentGatewaysEventMapper( - mappers.WithLogger(r.Logger().WithName("httpRouteToParentGatewaysEventMapper")), - ) - - apToParentGatewaysEventMapper := mappers.NewPolicyToParentGatewaysEventMapper( - mappers.WithLogger(r.Logger().WithName("authPolicyToParentGatewaysEventMapper")), - mappers.WithClient(r.Client()), - ) - - return ctrl.NewControllerManagedBy(mgr). - For(&gatewayapiv1.Gateway{}). - Owns(&istiov1beta1.AuthorizationPolicy{}). - Watches( - &gatewayapiv1.HTTPRoute{}, - handler.EnqueueRequestsFromMapFunc(httpRouteToParentGatewaysEventMapper.Map), - ). - Watches( - &kuadrantv1beta3.AuthPolicy{}, - handler.EnqueueRequestsFromMapFunc(apToParentGatewaysEventMapper.Map), - ). - Complete(r) -} - -// IstioAuthorizationPolicyName generates the name of an AuthorizationPolicy. -func IstioAuthorizationPolicyName(gwName string, targetRef gatewayapiv1alpha2.LocalPolicyTargetReference) string { - switch targetRef.Kind { - case "Gateway": - return fmt.Sprintf("on-%s", gwName) // Without this, IAP will be named: on--using-; - case "HTTPRoute": - return fmt.Sprintf("on-%s-using-%s", gwName, targetRef.Name) - } - return "" -} - -func istioAuthorizationPolicyLabels(gwKey, apKey client.ObjectKey) map[string]string { - return map[string]string{ - common.AuthPolicyBackRefAnnotation: apKey.Name, - fmt.Sprintf("%s-namespace", common.AuthPolicyBackRefAnnotation): apKey.Namespace, - "gateway-namespace": gwKey.Namespace, - "gateway": gwKey.Name, - } -} - -// istioAuthorizationPolicyRulesFromHTTPRoute builds a list of Istio AuthorizationPolicy rules from an HTTPRoute. -// v1beta2 version of this function used RouteSelectors -// v1beta3 should use Section Names, once implemented -func istioAuthorizationPolicyRulesFromHTTPRoute(route *gatewayapiv1.HTTPRoute) []*istiosecurity.Rule { - istioRules := make([]*istiosecurity.Rule, 0) - for _, rule := range route.Spec.Rules { - istioRules = append(istioRules, istioAuthorizationPolicyRulesFromHTTPRouteRule(rule, []gatewayapiv1.Hostname{"*"})...) - } - - 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 gatewayapiv1.HTTPRouteRule, hostnames []gatewayapiv1.Hostname) (istioRules []*istiosecurity.Rule) { - hosts := []string{} - for _, hostname := range hostnames { - if hostname == "*" { - continue - } - hosts = append(hosts, string(hostname)) - } - - // 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: hosts, - }, - }, - }, - } - istioRules = append(istioRules, istioRule) - return - } - - // 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 gatewayapiv1.PathMatchExact: - operator = "" - case gatewayapiv1.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 == gatewayapiv1.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 -} diff --git a/controllers/authpolicy_istio_authorizationpolicy_test.go b/controllers/authpolicy_istio_authorizationpolicy_test.go deleted file mode 100644 index a64baf5d7..000000000 --- a/controllers/authpolicy_istio_authorizationpolicy_test.go +++ /dev/null @@ -1,345 +0,0 @@ -//go:build unit - -package controllers - -import ( - "reflect" - "testing" - - istiosecurity "istio.io/api/security/v1beta1" - "k8s.io/utils/ptr" - gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" -) - -func TestIstioAuthorizationPolicyRulesFromHTTPRouteRule(t *testing.T) { - testCases := []struct { - name string - hostnames []gatewayapiv1.Hostname - rule gatewayapiv1.HTTPRouteRule - expected []*istiosecurity.Rule - }{ - { - name: "No HTTPRouteMatch", - hostnames: []gatewayapiv1.Hostname{"toystore.kuadrant.io"}, - rule: gatewayapiv1.HTTPRouteRule{}, - expected: []*istiosecurity.Rule{ - { - To: []*istiosecurity.Rule_To{ - { - Operation: &istiosecurity.Operation{ - Hosts: []string{"toystore.kuadrant.io"}, - }, - }, - }, - }, - }, - }, - { - name: "Single HTTPRouteMatch", - hostnames: []gatewayapiv1.Hostname{"toystore.kuadrant.io"}, - rule: gatewayapiv1.HTTPRouteRule{ - Matches: []gatewayapiv1.HTTPRouteMatch{ - { - Path: &gatewayapiv1.HTTPPathMatch{ - Type: ptr.To(gatewayapiv1.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: []gatewayapiv1.Hostname{"toystore.kuadrant.io"}, - rule: gatewayapiv1.HTTPRouteRule{ - Matches: []gatewayapiv1.HTTPRouteMatch{ - { - Path: &gatewayapiv1.HTTPPathMatch{ - Type: ptr.To(gatewayapiv1.PathMatchType("PathPrefix")), - Value: ptr.To("/toy"), - }, - }, - { - Path: &gatewayapiv1.HTTPPathMatch{ - Type: ptr.To(gatewayapiv1.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: []gatewayapiv1.Hostname{"toystore.kuadrant.io", "gamestore.kuadrant.io"}, - rule: gatewayapiv1.HTTPRouteRule{ - Matches: []gatewayapiv1.HTTPRouteMatch{ - { - Path: &gatewayapiv1.HTTPPathMatch{ - Type: ptr.To(gatewayapiv1.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: []gatewayapiv1.Hostname{"toystore.kuadrant.io", "*"}, - rule: gatewayapiv1.HTTPRouteRule{ - Matches: []gatewayapiv1.HTTPRouteMatch{ - { - Path: &gatewayapiv1.HTTPPathMatch{ - Type: ptr.To(gatewayapiv1.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: gatewayapiv1.HTTPRouteRule{ - Matches: []gatewayapiv1.HTTPRouteMatch{ - { - Method: ptr.To(gatewayapiv1.HTTPMethod("GET")), - }, - }, - }, - expected: []*istiosecurity.Rule{ - { - To: []*istiosecurity.Rule_To{ - { - Operation: &istiosecurity.Operation{ - Methods: []string{"GET"}, - }, - }, - }, - }, - }, - }, - { - name: "PathMatchExact", - rule: gatewayapiv1.HTTPRouteRule{ - Matches: []gatewayapiv1.HTTPRouteMatch{ - { - Path: &gatewayapiv1.HTTPPathMatch{ - Type: ptr.To(gatewayapiv1.PathMatchType("Exact")), - Value: ptr.To("/toy"), - }, - }, - }, - }, - expected: []*istiosecurity.Rule{ - { - To: []*istiosecurity.Rule_To{ - { - Operation: &istiosecurity.Operation{ - Paths: []string{"/toy"}, - }, - }, - }, - }, - }, - }, - { - name: "PathMatchPrefix", - rule: gatewayapiv1.HTTPRouteRule{ - Matches: []gatewayapiv1.HTTPRouteMatch{ - { - Path: &gatewayapiv1.HTTPPathMatch{ - Type: ptr.To(gatewayapiv1.PathMatchType("PathPrefix")), - Value: ptr.To("/toy"), - }, - }, - }, - }, - expected: []*istiosecurity.Rule{ - { - To: []*istiosecurity.Rule_To{ - { - Operation: &istiosecurity.Operation{ - Paths: []string{"/toy*"}, - }, - }, - }, - }, - }, - }, - { - name: "PathMatchRegularExpression", - rule: gatewayapiv1.HTTPRouteRule{ - Matches: []gatewayapiv1.HTTPRouteMatch{ - { - Path: &gatewayapiv1.HTTPPathMatch{ - Type: ptr.To(gatewayapiv1.PathMatchType("RegularExpression")), - Value: ptr.To("/toy"), - }, - }, - }, - }, - expected: []*istiosecurity.Rule{ - { - To: []*istiosecurity.Rule_To{ - { - Operation: &istiosecurity.Operation{}, - }, - }, - }, - }, - }, - { - name: "Single header match", - rule: gatewayapiv1.HTTPRouteRule{ - Matches: []gatewayapiv1.HTTPRouteMatch{ - { - Headers: []gatewayapiv1.HTTPHeaderMatch{ - { - Type: ptr.To(gatewayapiv1.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: gatewayapiv1.HTTPRouteRule{ - Matches: []gatewayapiv1.HTTPRouteMatch{ - { - Headers: []gatewayapiv1.HTTPHeaderMatch{ - { - Type: ptr.To(gatewayapiv1.HeaderMatchType("Exact")), - Name: "x-foo", - Value: "a-value", - }, - { - Type: ptr.To(gatewayapiv1.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: gatewayapiv1.HTTPRouteRule{ - Matches: []gatewayapiv1.HTTPRouteMatch{ - { - Headers: []gatewayapiv1.HTTPHeaderMatch{ - { - Type: ptr.To(gatewayapiv1.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 deleted file mode 100644 index 7df00f53b..000000000 --- a/controllers/authpolicy_status.go +++ /dev/null @@ -1,179 +0,0 @@ -package controllers - -import ( - "context" - "errors" - "fmt" - "slices" - - "github.com/go-logr/logr" - authorinoapi "github.com/kuadrant/authorino/api/v1beta2" - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/utils/ptr" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" - gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" - - kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" - kuadrantgatewayapi "github.com/kuadrant/kuadrant-operator/pkg/library/gatewayapi" - "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" - "github.com/kuadrant/kuadrant-operator/pkg/library/utils" -) - -// reconcileStatus makes sure status block of AuthPolicy is up-to-date. -func (r *AuthPolicyReconciler) reconcileStatus(ctx context.Context, ap *kuadrantv1beta3.AuthPolicy, specErr error) (ctrl.Result, error) { - logger, _ := logr.FromContext(ctx) - logger.V(1).Info("Reconciling AuthPolicy status", "spec error", specErr) - - newStatus := r.calculateStatus(ctx, ap, specErr) - - equalStatus := ap.Status.Equals(newStatus, logger) - logger.V(1).Info("Status", "status is different", !equalStatus) - logger.V(1).Info("Status", "generation is different", ap.Generation != ap.Status.ObservedGeneration) - if equalStatus && ap.Generation == ap.Status.ObservedGeneration { - logger.V(1).Info("Status up-to-date. No changes required.") - return ctrl.Result{}, nil - } - - // Save the generation number we acted on, otherwise we might wrongfully indicate - // that we've seen a spec update when we retry. - // TODO: This can clobber an update if we allow multiple agents to write to the - // same status. - newStatus.ObservedGeneration = ap.Generation - - logger.V(1).Info("Updating Status", "sequence no:", fmt.Sprintf("sequence No: %v->%v", ap.Status.ObservedGeneration, newStatus.ObservedGeneration)) - - ap.Status = *newStatus - updateErr := r.Client().Status().Update(ctx, ap) - if updateErr != nil { - // Ignore conflicts, resource might just be outdated. - if apierrors.IsConflict(updateErr) { - logger.Info("Failed to update status: resource might just be outdated") - return ctrl.Result{Requeue: true}, nil - } - - return ctrl.Result{}, fmt.Errorf("failed to update status: %w", updateErr) - } - return ctrl.Result{}, nil -} - -func (r *AuthPolicyReconciler) calculateStatus(ctx context.Context, ap *kuadrantv1beta3.AuthPolicy, specErr error) *kuadrantv1beta3.AuthPolicyStatus { - newStatus := &kuadrantv1beta3.AuthPolicyStatus{ - Conditions: slices.Clone(ap.Status.Conditions), - ObservedGeneration: ap.Status.ObservedGeneration, - } - - acceptedCond := r.acceptedCondition(ap, specErr) - meta.SetStatusCondition(&newStatus.Conditions, *acceptedCond) - - // Do not set enforced condition if Accepted condition is false - if meta.IsStatusConditionFalse(newStatus.Conditions, string(gatewayapiv1alpha2.PolicyReasonAccepted)) { - meta.RemoveStatusCondition(&newStatus.Conditions, string(kuadrant.PolicyConditionEnforced)) - return newStatus - } - - enforcedCond := r.enforcedCondition(ctx, ap) - meta.SetStatusCondition(&newStatus.Conditions, *enforcedCond) - - return newStatus -} - -func (r *AuthPolicyReconciler) acceptedCondition(policy kuadrant.Policy, specErr error) *metav1.Condition { - return kuadrant.AcceptedCondition(policy, specErr) -} - -// enforcedCondition checks if the provided AuthPolicy is enforced, ensuring it is properly configured and applied based -// on the status of the associated AuthConfig and Gateway. -func (r *AuthPolicyReconciler) enforcedCondition(ctx context.Context, policy *kuadrantv1beta3.AuthPolicy) *metav1.Condition { - logger, _ := logr.FromContext(ctx) - - // Check if the policy is Affected - // Note: This logic assumes synchronous processing, where computing the desired AuthConfig, marking the AuthPolicy - // as Affected, and calculating the Enforced condition happen sequentially. - // Introducing a goroutine in this flow could break this assumption and lead to unexpected behavior. - if r.AffectedPolicyMap.IsPolicyAffected(policy) { - logger.V(1).Info("Gateway Policy is overridden") - return r.handlePolicyOverride(policy) - } - - // Check if the AuthConfig is ready - authConfigReady, err := r.isAuthConfigReady(ctx, policy) - if err != nil { - logger.Error(err, "Failed to check AuthConfig and Gateway") - return kuadrant.EnforcedCondition(policy, kuadrant.NewErrUnknown(policy.Kind(), err), false) - } - - if !authConfigReady { - logger.V(1).Info("AuthConfig is not ready") - return kuadrant.EnforcedCondition(policy, kuadrant.NewErrUnknown(policy.Kind(), errors.New("AuthScheme is not ready yet")), false) - } - - logger.V(1).Info("AuthPolicy is enforced") - return kuadrant.EnforcedCondition(policy, nil, true) -} - -// isAuthConfigReady checks if the AuthConfig is ready. -func (r *AuthPolicyReconciler) isAuthConfigReady(ctx context.Context, policy *kuadrantv1beta3.AuthPolicy) (bool, error) { - apKey := client.ObjectKeyFromObject(policy) - authConfigKey := client.ObjectKey{ - Namespace: policy.Namespace, - Name: AuthConfigName(apKey), - } - authConfig := &authorinoapi.AuthConfig{} - err := r.GetResource(ctx, authConfigKey, authConfig) - if err != nil { - if !apierrors.IsNotFound(err) { - return false, fmt.Errorf("failed to get AuthConfig: %w", err) - } - } - return authConfig.Status.Ready(), nil -} - -func (r *AuthPolicyReconciler) handlePolicyOverride(policy *kuadrantv1beta3.AuthPolicy) *metav1.Condition { - if !r.AffectedPolicyMap.IsPolicyOverridden(policy) { - return kuadrant.EnforcedCondition(policy, kuadrant.NewErrUnknown(policy.Kind(), errors.New("no free routes to enforce policy")), false) // Maybe this should be a standard condition rather than an unknown condition - } - - return kuadrant.EnforcedCondition(policy, kuadrant.NewErrOverridden(policy.Kind(), r.AffectedPolicyMap.PolicyAffectedBy(policy)), false) -} - -func (r *AuthPolicyReconciler) generateTopology(ctx context.Context) (*kuadrantgatewayapi.Topology, error) { - logger, _ := logr.FromContext(ctx) - - gwList := &gatewayapiv1.GatewayList{} - err := r.Client().List(ctx, gwList) - logger.V(1).Info("topology: list gateways", "#Gateways", len(gwList.Items), "err", err) - if err != nil { - return nil, err - } - - routeList := &gatewayapiv1.HTTPRouteList{} - err = r.Client().List(ctx, routeList) - logger.V(1).Info("topology: list httproutes", "#HTTPRoutes", len(routeList.Items), "err", err) - if err != nil { - return nil, err - } - - aplist := &kuadrantv1beta3.AuthPolicyList{} - err = r.Client().List(ctx, aplist) - logger.V(1).Info("topology: list rate limit policies", "#RLPS", len(aplist.Items), "err", err) - if err != nil { - return nil, err - } - - policies := utils.Map(aplist.Items, func(p kuadrantv1beta3.AuthPolicy) kuadrantgatewayapi.Policy { - return &p - }) - - return kuadrantgatewayapi.NewTopology( - kuadrantgatewayapi.WithAcceptedRoutesLinkedOnly(), - kuadrantgatewayapi.WithProgrammedGatewaysOnly(), - kuadrantgatewayapi.WithGateways(utils.Map(gwList.Items, ptr.To[gatewayapiv1.Gateway])), - kuadrantgatewayapi.WithRoutes(utils.Map(routeList.Items, ptr.To[gatewayapiv1.HTTPRoute])), - kuadrantgatewayapi.WithPolicies(policies), - kuadrantgatewayapi.WithLogger(logger), - ) -} diff --git a/controllers/authpolicy_status_test.go b/controllers/authpolicy_status_test.go deleted file mode 100644 index 783225891..000000000 --- a/controllers/authpolicy_status_test.go +++ /dev/null @@ -1,72 +0,0 @@ -//go:build unit - -package controllers - -import ( - "context" - "errors" - "reflect" - "testing" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" - - kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" - "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" -) - -func TestAuthPolicyReconciler_calculateStatus(t *testing.T) { - type args struct { - ctx context.Context - ap *kuadrantv1beta3.AuthPolicy - specErr error - } - tests := []struct { - name string - args args - want *kuadrantv1beta3.AuthPolicyStatus - }{ - { - name: "Enforced status block removed if policy not Accepted. (Regression test)", // https://github.com/Kuadrant/kuadrant-operator/issues/588 - args: args{ - ap: &kuadrantv1beta3.AuthPolicy{ - Status: kuadrantv1beta3.AuthPolicyStatus{ - Conditions: []metav1.Condition{ - { - Message: "not accepted", - Type: string(gatewayapiv1alpha2.PolicyConditionAccepted), - Status: metav1.ConditionFalse, - Reason: string(gatewayapiv1alpha2.PolicyReasonTargetNotFound), - }, - { - Message: "AuthPolicy has been successfully enforced", - Type: string(kuadrant.PolicyConditionEnforced), - Status: metav1.ConditionTrue, - Reason: string(kuadrant.PolicyConditionEnforced), - }, - }, - }, - }, - specErr: kuadrant.NewErrInvalid("AuthPolicy", errors.New("policy Error")), - }, - want: &kuadrantv1beta3.AuthPolicyStatus{ - Conditions: []metav1.Condition{ - { - Message: "AuthPolicy target is invalid: policy Error", - Type: string(gatewayapiv1alpha2.PolicyConditionAccepted), - Status: metav1.ConditionFalse, - Reason: string(gatewayapiv1alpha2.PolicyReasonInvalid), - }, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - r := &AuthPolicyReconciler{} - if got := r.calculateStatus(tt.args.ctx, tt.args.ap, tt.args.specErr); !reflect.DeepEqual(got, tt.want) { - t.Errorf("calculateStatus() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/controllers/data_plane_policies_workflow.go b/controllers/data_plane_policies_workflow.go new file mode 100644 index 000000000..8ad460dee --- /dev/null +++ b/controllers/data_plane_policies_workflow.go @@ -0,0 +1,106 @@ +package controllers + +import ( + "fmt" + + "github.com/kuadrant/policy-machinery/controller" + "github.com/kuadrant/policy-machinery/machinery" + "github.com/samber/lo" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic" + "k8s.io/utils/env" + + kuadrantv1beta1 "github.com/kuadrant/kuadrant-operator/api/v1beta1" + kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" + kuadrantenvoygateway "github.com/kuadrant/kuadrant-operator/pkg/envoygateway" + kuadrantistio "github.com/kuadrant/kuadrant-operator/pkg/istio" +) + +const ( + // make these configurable? + istioGatewayControllerName = "istio.io/gateway-controller" + envoyGatewayGatewayControllerName = "gateway.envoyproxy.io/gatewayclass-controller" +) + +var ( + WASMFilterImageURL = env.GetString("RELATED_IMAGE_WASMSHIM", "oci://quay.io/kuadrant/wasm-shim:latest") + + StateIstioExtensionsModified = "IstioExtensionsModified" + StateEnvoyGatewayExtensionsModified = "EnvoyGatewayExtensionsModified" + + // Event matchers to match events with potential impact on effective data plane policies (auth or rate limit) + dataPlaneEffectivePoliciesEventMatchers = []controller.ResourceEventMatcher{ + {Kind: &kuadrantv1beta1.KuadrantGroupKind}, + {Kind: &machinery.GatewayClassGroupKind}, + {Kind: &machinery.GatewayGroupKind}, + {Kind: &machinery.HTTPRouteGroupKind}, + {Kind: &kuadrantv1beta3.RateLimitPolicyGroupKind}, + {Kind: &kuadrantv1beta1.LimitadorGroupKind}, + {Kind: &kuadrantv1beta3.AuthPolicyGroupKind}, + {Kind: &kuadrantv1beta1.AuthConfigGroupKind}, + {Kind: &kuadrantistio.EnvoyFilterGroupKind}, + {Kind: &kuadrantistio.WasmPluginGroupKind}, + {Kind: &kuadrantenvoygateway.EnvoyPatchPolicyGroupKind}, + {Kind: &kuadrantenvoygateway.EnvoyExtensionPolicyGroupKind}, + } +) + +func NewDataPlanePoliciesWorkflow(client *dynamic.DynamicClient, isIstioInstalled, isEnvoyGatewayInstalled bool) *controller.Workflow { + dataPlanePoliciesValidation := &controller.Workflow{ + Tasks: []controller.ReconcileFunc{ + (&AuthPolicyValidator{}).Subscription().Reconcile, + (&RateLimitPolicyValidator{}).Subscription().Reconcile, + }, + } + + effectiveDataPlanePoliciesWorkflow := &controller.Workflow{ + Precondition: (&controller.Workflow{ + Tasks: []controller.ReconcileFunc{ + (&EffectiveAuthPolicyReconciler{client: client}).Subscription().Reconcile, + (&EffectiveRateLimitPolicyReconciler{client: client}).Subscription().Reconcile, + }, + }).Run, + Tasks: []controller.ReconcileFunc{ + (&AuthConfigsReconciler{client: client}).Subscription().Reconcile, + (&LimitadorLimitsReconciler{client: client}).Subscription().Reconcile, + }, + } + + if isIstioInstalled { + effectiveDataPlanePoliciesWorkflow.Tasks = append(effectiveDataPlanePoliciesWorkflow.Tasks, (&IstioAuthClusterReconciler{client: client}).Subscription().Reconcile) + effectiveDataPlanePoliciesWorkflow.Tasks = append(effectiveDataPlanePoliciesWorkflow.Tasks, (&IstioRateLimitClusterReconciler{client: client}).Subscription().Reconcile) + effectiveDataPlanePoliciesWorkflow.Tasks = append(effectiveDataPlanePoliciesWorkflow.Tasks, (&IstioExtensionReconciler{client: client}).Subscription().Reconcile) + } + + if isEnvoyGatewayInstalled { + effectiveDataPlanePoliciesWorkflow.Tasks = append(effectiveDataPlanePoliciesWorkflow.Tasks, (&EnvoyGatewayAuthClusterReconciler{client: client}).Subscription().Reconcile) + effectiveDataPlanePoliciesWorkflow.Tasks = append(effectiveDataPlanePoliciesWorkflow.Tasks, (&EnvoyGatewayRateLimitClusterReconciler{client: client}).Subscription().Reconcile) + effectiveDataPlanePoliciesWorkflow.Tasks = append(effectiveDataPlanePoliciesWorkflow.Tasks, (&EnvoyGatewayExtensionReconciler{client: client}).Subscription().Reconcile) + } + + dataPlanePoliciesStatus := &controller.Workflow{ + Tasks: []controller.ReconcileFunc{ + (&AuthPolicyStatusUpdater{client: client}).Subscription().Reconcile, + (&RateLimitPolicyStatusUpdater{client: client}).Subscription().Reconcile, + }, + } + + return &controller.Workflow{ + Precondition: dataPlanePoliciesValidation.Run, + Tasks: []controller.ReconcileFunc{effectiveDataPlanePoliciesWorkflow.Run}, + Postcondition: dataPlanePoliciesStatus.Run, + } +} + +func gatewayComponentsToSync(gateway *machinery.Gateway, componentGroupKind schema.GroupKind, modifiedGatewayLocators any, topology *machinery.Topology, requiredCondition func(machinery.Object) bool) []string { + missingConditionInTopologyFunc := func() bool { + obj, found := lo.Find(topology.Objects().Children(gateway), func(child machinery.Object) bool { + return child.GroupVersionKind().GroupKind() == componentGroupKind + }) + return !found || !requiredCondition(obj) + } + if (modifiedGatewayLocators != nil && lo.Contains(modifiedGatewayLocators.([]string), gateway.GetLocator())) || missingConditionInTopologyFunc() { + return []string{fmt.Sprintf("%s (%s/%s)", componentGroupKind.Kind, gateway.GetNamespace(), gateway.GetName())} + } + return nil +} diff --git a/controllers/effective_auth_policies_reconciler.go b/controllers/effective_auth_policies_reconciler.go new file mode 100644 index 000000000..d444373d2 --- /dev/null +++ b/controllers/effective_auth_policies_reconciler.go @@ -0,0 +1,93 @@ +package controllers + +import ( + "context" + "encoding/json" + "errors" + "sync" + + "github.com/kuadrant/policy-machinery/controller" + "github.com/kuadrant/policy-machinery/machinery" + "github.com/samber/lo" + "k8s.io/client-go/dynamic" + + kuadrantv1 "github.com/kuadrant/kuadrant-operator/api/v1" + kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" +) + +type EffectiveAuthPolicy struct { + Path []machinery.Targetable + Spec kuadrantv1beta3.AuthPolicy +} + +type EffectiveAuthPolicies map[string]EffectiveAuthPolicy + +type EffectiveAuthPolicyReconciler struct { + client *dynamic.DynamicClient +} + +// EffectiveAuthPolicyReconciler subscribe to the same events as rate limit because they are used together to compose gateway extension resources +func (r *EffectiveAuthPolicyReconciler) Subscription() controller.Subscription { + return controller.Subscription{ + ReconcileFunc: r.Reconcile, + Events: dataPlaneEffectivePoliciesEventMatchers, + } +} + +func (r *EffectiveAuthPolicyReconciler) Reconcile(ctx context.Context, _ []controller.ResourceEvent, topology *machinery.Topology, _ error, state *sync.Map) error { + logger := controller.LoggerFromContext(ctx).WithName("EffectiveAuthPolicyReconciler") + + kuadrant, err := GetKuadrantFromTopology(topology) + if err != nil { + if errors.Is(err, ErrMissingKuadrant) { + logger.V(1).Info(err.Error()) + return nil + } + return err + } + + effectivePolicies := r.calculateEffectivePolicies(ctx, topology, kuadrant, state) + + state.Store(StateEffectiveAuthPolicies, effectivePolicies) + + return nil +} + +func (r *EffectiveAuthPolicyReconciler) calculateEffectivePolicies(ctx context.Context, topology *machinery.Topology, kuadrant machinery.Object, state *sync.Map) EffectiveAuthPolicies { + logger := controller.LoggerFromContext(ctx).WithName("EffectiveAuthPolicyReconciler").WithName("calculateEffectivePolicies") + + targetables := topology.Targetables() + gatewayClasses := targetables.Children(kuadrant) // assumes only and all valid gateway classes are linked to kuadrant in the topology + httpRouteRules := targetables.Items(func(o machinery.Object) bool { + _, ok := o.(*machinery.HTTPRouteRule) + return ok + }) + + logger.V(1).Info("calculating effective auth policies", "httpRouteRules", len(httpRouteRules)) + + effectivePolicies := EffectiveAuthPolicies{} + + for _, gatewayClass := range gatewayClasses { + for _, httpRouteRule := range httpRouteRules { + paths := targetables.Paths(gatewayClass, httpRouteRule) // this may be expensive in clusters with many gateway classes - an alternative is to deep search the topology for httprouterules from each gatewayclass, keeping record of the paths + for i := range paths { + if effectivePolicy := kuadrantv1.EffectivePolicyForPath[*kuadrantv1beta3.AuthPolicy](paths[i], isAuthPolicyAcceptedAndNotDeletedFunc(state)); effectivePolicy != nil { + pathID := kuadrantv1.PathID(paths[i]) + effectivePolicies[pathID] = EffectiveAuthPolicy{ + Path: paths[i], + Spec: **effectivePolicy, + } + if logger.V(1).Enabled() { + jsonEffectivePolicy, _ := json.Marshal(effectivePolicy) + pathLocators := lo.Map(paths[i], machinery.MapTargetableToLocatorFunc) + logger.V(1).Info("effective policy", "kind", kuadrantv1beta3.AuthPolicyGroupKind.Kind, "pathID", pathID, "path", pathLocators, "effectivePolicy", string(jsonEffectivePolicy)) + } + } + } + } + } + + logger.V(1).Info("finished calculating effective auth policies", "effectivePolicies", len(effectivePolicies)) + + return effectivePolicies +} diff --git a/controllers/effective_ratelimitpolicies_reconciler.go b/controllers/effective_ratelimit_policies_reconciler.go similarity index 84% rename from controllers/effective_ratelimitpolicies_reconciler.go rename to controllers/effective_ratelimit_policies_reconciler.go index 641d0bbb0..0976d57bf 100644 --- a/controllers/effective_ratelimitpolicies_reconciler.go +++ b/controllers/effective_ratelimit_policies_reconciler.go @@ -22,19 +22,20 @@ type EffectiveRateLimitPolicy struct { type EffectiveRateLimitPolicies map[string]EffectiveRateLimitPolicy -type effectiveRateLimitPolicyReconciler struct { +type EffectiveRateLimitPolicyReconciler struct { client *dynamic.DynamicClient } -func (r *effectiveRateLimitPolicyReconciler) Subscription() controller.Subscription { +// EffectiveRateLimitPolicyReconciler subscribe to the same events as auth because they are used together to compose gateway extension resources +func (r *EffectiveRateLimitPolicyReconciler) Subscription() controller.Subscription { return controller.Subscription{ ReconcileFunc: r.Reconcile, - Events: rateLimitEventMatchers, + Events: dataPlaneEffectivePoliciesEventMatchers, } } -func (r *effectiveRateLimitPolicyReconciler) Reconcile(ctx context.Context, _ []controller.ResourceEvent, topology *machinery.Topology, _ error, state *sync.Map) error { - logger := controller.LoggerFromContext(ctx).WithName("effectiveRateLimitPolicyReconciler") +func (r *EffectiveRateLimitPolicyReconciler) Reconcile(ctx context.Context, _ []controller.ResourceEvent, topology *machinery.Topology, _ error, state *sync.Map) error { + logger := controller.LoggerFromContext(ctx).WithName("EffectiveRateLimitPolicyReconciler") kuadrant, err := GetKuadrantFromTopology(topology) if err != nil { @@ -52,8 +53,8 @@ func (r *effectiveRateLimitPolicyReconciler) Reconcile(ctx context.Context, _ [] return nil } -func (r *effectiveRateLimitPolicyReconciler) calculateEffectivePolicies(ctx context.Context, topology *machinery.Topology, kuadrant machinery.Object, state *sync.Map) EffectiveRateLimitPolicies { - logger := controller.LoggerFromContext(ctx).WithName("effectiveRateLimitPolicyReconciler").WithName("calculateEffectivePolicies") +func (r *EffectiveRateLimitPolicyReconciler) calculateEffectivePolicies(ctx context.Context, topology *machinery.Topology, kuadrant machinery.Object, state *sync.Map) EffectiveRateLimitPolicies { + logger := controller.LoggerFromContext(ctx).WithName("EffectiveRateLimitPolicyReconciler").WithName("calculateEffectivePolicies") targetables := topology.Targetables() gatewayClasses := targetables.Children(kuadrant) // assumes only and all valid gateway classes are linked to kuadrant in the topology diff --git a/controllers/envoy_gateway_auth_cluster_reconciler.go b/controllers/envoy_gateway_auth_cluster_reconciler.go new file mode 100644 index 000000000..363917a87 --- /dev/null +++ b/controllers/envoy_gateway_auth_cluster_reconciler.go @@ -0,0 +1,202 @@ +package controllers + +import ( + "context" + "errors" + "fmt" + "sync" + + envoygatewayv1alpha1 "github.com/envoyproxy/gateway/api/v1alpha1" + authorinooperatorv1beta1 "github.com/kuadrant/authorino-operator/api/v1beta1" + "github.com/kuadrant/policy-machinery/controller" + "github.com/kuadrant/policy-machinery/machinery" + "github.com/samber/lo" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + k8stypes "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/dynamic" + "k8s.io/utils/ptr" + gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + + kuadrantv1beta1 "github.com/kuadrant/kuadrant-operator/api/v1beta1" + kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" + "github.com/kuadrant/kuadrant-operator/pkg/common" + kuadrantenvoygateway "github.com/kuadrant/kuadrant-operator/pkg/envoygateway" +) + +// EnvoyGatewayAuthClusterReconciler reconciles Envoy Gateway EnvoyPatchPolicy custom resources for auth +type EnvoyGatewayAuthClusterReconciler struct { + client *dynamic.DynamicClient +} + +// EnvoyGatewayAuthClusterReconciler subscribes to events with potential impact on the Envoy Gateway EnvoyPatchPolicy custom resources for auth +func (r *EnvoyGatewayAuthClusterReconciler) Subscription() controller.Subscription { + return controller.Subscription{ + ReconcileFunc: r.Reconcile, + Events: []controller.ResourceEventMatcher{ + {Kind: &kuadrantv1beta1.KuadrantGroupKind}, + {Kind: &machinery.GatewayClassGroupKind}, + {Kind: &machinery.GatewayGroupKind}, + {Kind: &machinery.HTTPRouteGroupKind}, + {Kind: &kuadrantv1beta3.AuthPolicyGroupKind}, + {Kind: &kuadrantenvoygateway.EnvoyPatchPolicyGroupKind}, + }, + } +} + +func (r *EnvoyGatewayAuthClusterReconciler) Reconcile(ctx context.Context, _ []controller.ResourceEvent, topology *machinery.Topology, _ error, state *sync.Map) error { + logger := controller.LoggerFromContext(ctx).WithName("EnvoyGatewayAuthClusterReconciler") + + logger.V(1).Info("building envoy gateway auth clusters") + defer logger.V(1).Info("finished building envoy gateway auth clusters") + + kuadrant, err := GetKuadrantFromTopology(topology) + if err != nil { + if errors.Is(err, ErrMissingKuadrant) { + logger.V(1).Info(err.Error()) + return nil + } + return err + } + + authorinoObj, found := lo.Find(topology.Objects().Children(kuadrant), func(child machinery.Object) bool { + return child.GroupVersionKind().GroupKind() == kuadrantv1beta1.AuthorinoGroupKind + }) + if !found { + logger.V(1).Info(ErrMissingAuthorino.Error()) + return nil + } + authorino := authorinoObj.(*controller.RuntimeObject).Object.(*authorinooperatorv1beta1.Authorino) + + effectivePolicies, ok := state.Load(StateEffectiveAuthPolicies) + if !ok { + logger.Error(ErrMissingStateEffectiveAuthPolicies, "failed to get effective auth policies from state") + return nil + } + + gateways := lo.UniqBy(lo.FilterMap(lo.Values(effectivePolicies.(EffectiveAuthPolicies)), func(effectivePolicy EffectiveAuthPolicy, _ int) (*machinery.Gateway, bool) { + gatewayClass, gateway, _, _, _, _ := common.ObjectsInRequestPath(effectivePolicy.Path) + return gateway, gatewayClass.Spec.ControllerName == envoyGatewayGatewayControllerName + }), func(gateway *machinery.Gateway) string { + return gateway.GetLocator() + }) + + desiredEnvoyPatchPolicies := make(map[k8stypes.NamespacedName]struct{}) + var modifiedGateways []string + + // reconcile envoy gateway cluster for gateway + for _, gateway := range gateways { + gatewayKey := k8stypes.NamespacedName{Name: gateway.GetName(), Namespace: gateway.GetNamespace()} + + desiredEnvoyPatchPolicy, err := r.buildDesiredEnvoyPatchPolicy(authorino, gateway) + if err != nil { + logger.Error(err, "failed to build desired envoy patch policy") + continue + } + desiredEnvoyPatchPolicies[k8stypes.NamespacedName{Name: desiredEnvoyPatchPolicy.GetName(), Namespace: desiredEnvoyPatchPolicy.GetNamespace()}] = struct{}{} + + resource := r.client.Resource(kuadrantenvoygateway.EnvoyPatchPoliciesResource).Namespace(desiredEnvoyPatchPolicy.GetNamespace()) + + existingEnvoyPatchPolicyObj, found := lo.Find(topology.Objects().Children(gateway), func(child machinery.Object) bool { + return child.GroupVersionKind().GroupKind() == kuadrantenvoygateway.EnvoyPatchPolicyGroupKind && child.GetName() == desiredEnvoyPatchPolicy.GetName() && child.GetNamespace() == desiredEnvoyPatchPolicy.GetNamespace() && labels.Set(child.(*controller.RuntimeObject).GetLabels()).AsSelector().Matches(labels.Set(desiredEnvoyPatchPolicy.GetLabels())) + }) + + // create + if !found { + modifiedGateways = append(modifiedGateways, gateway.GetLocator()) // we only signal the gateway as modified when an envoypatchpolicy is created, because updates won't change the status + desiredEnvoyPatchPolicyUnstructured, err := controller.Destruct(desiredEnvoyPatchPolicy) + if err != nil { + logger.Error(err, "failed to destruct envoypatchpolicy object", "gateway", gatewayKey.String(), "envoypatchpolicy", desiredEnvoyPatchPolicy) + continue + } + if _, err = resource.Create(ctx, desiredEnvoyPatchPolicyUnstructured, metav1.CreateOptions{}); err != nil { + logger.Error(err, "failed to create envoypatchpolicy object", "gateway", gatewayKey.String(), "envoypatchpolicy", desiredEnvoyPatchPolicyUnstructured.Object) + // TODO: handle error + } + continue + } + + existingEnvoyPatchPolicy := existingEnvoyPatchPolicyObj.(*controller.RuntimeObject).Object.(*envoygatewayv1alpha1.EnvoyPatchPolicy) + + if kuadrantenvoygateway.EqualEnvoyPatchPolicies(existingEnvoyPatchPolicy, desiredEnvoyPatchPolicy) { + logger.V(1).Info("envoypatchpolicy object is up to date, nothing to do") + continue + } + + // update + existingEnvoyPatchPolicy.Spec = envoygatewayv1alpha1.EnvoyPatchPolicySpec{ + TargetRef: desiredEnvoyPatchPolicy.Spec.TargetRef, + Type: desiredEnvoyPatchPolicy.Spec.Type, + JSONPatches: desiredEnvoyPatchPolicy.Spec.JSONPatches, + Priority: desiredEnvoyPatchPolicy.Spec.Priority, + } + + existingEnvoyPatchPolicyUnstructured, err := controller.Destruct(existingEnvoyPatchPolicy) + if err != nil { + logger.Error(err, "failed to destruct envoypatchpolicy object", "gateway", gatewayKey.String(), "envoypatchpolicy", existingEnvoyPatchPolicy) + continue + } + if _, err = resource.Update(ctx, existingEnvoyPatchPolicyUnstructured, metav1.UpdateOptions{}); err != nil { + logger.Error(err, "failed to update envoypatchpolicy object", "gateway", gatewayKey.String(), "envoypatchpolicy", existingEnvoyPatchPolicyUnstructured.Object) + // TODO: handle error + } + } + + state.Store(StateEnvoyGatewayAuthClustersModified, modifiedGateways) + + // cleanup envoy gateway clusters for gateways that are not in the effective policies + staleEnvoyPatchPolicies := topology.Objects().Items(func(o machinery.Object) bool { + _, desired := desiredEnvoyPatchPolicies[k8stypes.NamespacedName{Name: o.GetName(), Namespace: o.GetNamespace()}] + return o.GroupVersionKind().GroupKind() == kuadrantenvoygateway.EnvoyPatchPolicyGroupKind && labels.Set(o.(*controller.RuntimeObject).GetLabels()).AsSelector().Matches(AuthObjectLabels()) && !desired + }) + + for _, envoyPatchPolicy := range staleEnvoyPatchPolicies { + if err := r.client.Resource(kuadrantenvoygateway.EnvoyPatchPoliciesResource).Namespace(envoyPatchPolicy.GetNamespace()).Delete(ctx, envoyPatchPolicy.GetName(), metav1.DeleteOptions{}); err != nil { + logger.Error(err, "failed to delete envoypatchpolicy object", "envoypatchpolicy", fmt.Sprintf("%s/%s", envoyPatchPolicy.GetNamespace(), envoyPatchPolicy.GetName())) + // TODO: handle error + } + } + + return nil +} + +func (r *EnvoyGatewayAuthClusterReconciler) buildDesiredEnvoyPatchPolicy(authorino *authorinooperatorv1beta1.Authorino, gateway *machinery.Gateway) (*envoygatewayv1alpha1.EnvoyPatchPolicy, error) { + envoyPatchPolicy := &envoygatewayv1alpha1.EnvoyPatchPolicy{ + TypeMeta: metav1.TypeMeta{ + Kind: kuadrantenvoygateway.EnvoyPatchPolicyGroupKind.Kind, + APIVersion: envoygatewayv1alpha1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: AuthClusterName(gateway.GetName()), + Namespace: gateway.GetNamespace(), + Labels: AuthObjectLabels(), + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: gateway.GroupVersionKind().GroupVersion().String(), + Kind: gateway.GroupVersionKind().Kind, + Name: gateway.Name, + UID: gateway.UID, + BlockOwnerDeletion: ptr.To(true), + Controller: ptr.To(true), + }, + }, + }, + Spec: envoygatewayv1alpha1.EnvoyPatchPolicySpec{ + TargetRef: gatewayapiv1alpha2.LocalPolicyTargetReference{ + Group: gatewayapiv1alpha2.Group(machinery.GatewayGroupKind.Group), + Kind: gatewayapiv1alpha2.Kind(machinery.GatewayGroupKind.Kind), + Name: gatewayapiv1alpha2.ObjectName(gateway.GetName()), + }, + Type: envoygatewayv1alpha1.JSONPatchEnvoyPatchType, + }, + } + + authorinoServiceInfo := authorinoServiceInfoFromAuthorino(authorino) + jsonPatches, err := kuadrantenvoygateway.BuildEnvoyPatchPolicyClusterPatch(common.KuadrantAuthClusterName, authorinoServiceInfo.Host, int(authorinoServiceInfo.Port), authClusterPatch) + if err != nil { + return nil, err + } + envoyPatchPolicy.Spec.JSONPatches = jsonPatches + + return envoyPatchPolicy, nil +} diff --git a/controllers/envoy_gateway_extension_reconciler.go b/controllers/envoy_gateway_extension_reconciler.go index 9e8ad2fdf..0b422ff35 100644 --- a/controllers/envoy_gateway_extension_reconciler.go +++ b/controllers/envoy_gateway_extension_reconciler.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "reflect" "sync" envoygatewayv1alpha1 "github.com/envoyproxy/gateway/api/v1alpha1" @@ -27,27 +26,29 @@ import ( "github.com/kuadrant/kuadrant-operator/pkg/wasm" ) -// envoyGatewayExtensionReconciler reconciles Envoy Gateway EnvoyExtensionPolicy custom resources -type envoyGatewayExtensionReconciler struct { +// EnvoyGatewayExtensionReconciler reconciles Envoy Gateway EnvoyExtensionPolicy custom resources +type EnvoyGatewayExtensionReconciler struct { client *dynamic.DynamicClient } -func (r *envoyGatewayExtensionReconciler) Subscription() controller.Subscription { +// EnvoyGatewayExtensionReconciler subscribes to events with potential impact on the Envoy Gateway EnvoyExtensionPolicy custom resources +func (r *EnvoyGatewayExtensionReconciler) Subscription() controller.Subscription { return controller.Subscription{ ReconcileFunc: r.Reconcile, - Events: []controller.ResourceEventMatcher{ // matches reconciliation events that change the rate limit definitions or status of rate limit policies + Events: []controller.ResourceEventMatcher{ {Kind: &kuadrantv1beta1.KuadrantGroupKind}, {Kind: &machinery.GatewayClassGroupKind}, {Kind: &machinery.GatewayGroupKind}, {Kind: &machinery.HTTPRouteGroupKind}, + {Kind: &kuadrantv1beta3.AuthPolicyGroupKind}, {Kind: &kuadrantv1beta3.RateLimitPolicyGroupKind}, {Kind: &kuadrantenvoygateway.EnvoyExtensionPolicyGroupKind}, }, } } -func (r *envoyGatewayExtensionReconciler) Reconcile(ctx context.Context, _ []controller.ResourceEvent, topology *machinery.Topology, _ error, state *sync.Map) error { - logger := controller.LoggerFromContext(ctx).WithName("envoyGatewayExtensionReconciler") +func (r *EnvoyGatewayExtensionReconciler) Reconcile(ctx context.Context, _ []controller.ResourceEvent, topology *machinery.Topology, _ error, state *sync.Map) error { + logger := controller.LoggerFromContext(ctx).WithName("EnvoyGatewayExtensionReconciler") logger.V(1).Info("building envoy gateway extension") defer logger.V(1).Info("finished building envoy gateway extension") @@ -55,7 +56,7 @@ func (r *envoyGatewayExtensionReconciler) Reconcile(ctx context.Context, _ []con // build wasm plugin configs for each gateway wasmConfigs, err := r.buildWasmConfigs(ctx, state) if err != nil { - if errors.Is(err, ErrMissingStateEffectiveRateLimitPolicies) { + if errors.Is(err, ErrMissingStateEffectiveAuthPolicies) || errors.Is(err, ErrMissingStateEffectiveRateLimitPolicies) { logger.V(1).Info(err.Error()) } else { return err @@ -137,28 +138,59 @@ func (r *envoyGatewayExtensionReconciler) Reconcile(ctx context.Context, _ []con } // buildWasmConfigs returns a map of envoy gateway gateway locators to an ordered list of corresponding wasm policies -func (r *envoyGatewayExtensionReconciler) buildWasmConfigs(ctx context.Context, state *sync.Map) (map[string]wasm.Config, error) { - logger := controller.LoggerFromContext(ctx).WithName("envoyGatewayExtensionReconciler").WithName("buildWasmConfigs") +func (r *EnvoyGatewayExtensionReconciler) buildWasmConfigs(ctx context.Context, state *sync.Map) (map[string]wasm.Config, error) { + logger := controller.LoggerFromContext(ctx).WithName("EnvoyGatewayExtensionReconciler").WithName("buildWasmConfigs") - effectivePolicies, ok := state.Load(StateEffectiveRateLimitPolicies) + effectiveAuthPolicies, ok := state.Load(StateEffectiveAuthPolicies) + if !ok { + return nil, ErrMissingStateEffectiveAuthPolicies + } + effectiveAuthPoliciesMap := effectiveAuthPolicies.(EffectiveAuthPolicies) + + effectiveRateLimitPolicies, ok := state.Load(StateEffectiveRateLimitPolicies) if !ok { return nil, ErrMissingStateEffectiveRateLimitPolicies } + effectiveRateLimitPoliciesMap := effectiveRateLimitPolicies.(EffectiveRateLimitPolicies) + + logger.V(1).Info("building wasm configs for envoy gateway extension", "effectiveRateLimitPolicies", len(effectiveAuthPoliciesMap), "effectiveRateLimitPolicies", len(effectiveRateLimitPoliciesMap)) - logger.V(1).Info("building wasm configs for envoy gateway extension", "effectivePolicies", len(effectivePolicies.(EffectiveRateLimitPolicies))) + paths := lo.UniqBy(append( + lo.Entries(lo.MapValues(effectiveAuthPoliciesMap, func(p EffectiveAuthPolicy, _ string) []machinery.Targetable { return p.Path })), + lo.Entries(lo.MapValues(effectiveRateLimitPoliciesMap, func(p EffectiveRateLimitPolicy, _ string) []machinery.Targetable { return p.Path }))..., + ), func(e lo.Entry[string, []machinery.Targetable]) string { return e.Key }) wasmActionSets := kuadrantgatewayapi.GrouppedHTTPRouteMatchConfigs{} // build the wasm policies for each topological path that contains an effective rate limit policy affecting an envoy gateway gateway - for pathID, effectivePolicy := range effectivePolicies.(EffectiveRateLimitPolicies) { - gatewayClass, gateway, _, _, _, _ := common.ObjectsInRequestPath(effectivePolicy.Path) + for i := range paths { + pathID := paths[i].Key + path := paths[i].Value + + gatewayClass, gateway, _, _, _, _ := common.ObjectsInRequestPath(path) // ignore if not an envoy gateway gateway if gatewayClass.Spec.ControllerName != envoyGatewayGatewayControllerName { continue } - wasmActionSetsForPath, err := wasm.BuildActionSetsForPath(pathID, effectivePolicy.Path, effectivePolicy.Spec.Rules(), rateLimitWasmActionBuilder(pathID, effectivePolicy, state)) + var actions []wasm.Action + + // auth + if effectivePolicy, ok := effectiveAuthPoliciesMap[pathID]; ok { + actions = append(actions, buildWasmActionsForAuth(pathID, effectivePolicy)...) + } + + // rate limit + if effectivePolicy, ok := effectiveRateLimitPoliciesMap[pathID]; ok { + actions = append(actions, buildWasmActionsForRateLimit(effectivePolicy, state)...) + } + + if len(actions) == 0 { + continue + } + + wasmActionSetsForPath, err := wasm.BuildActionSetsForPath(pathID, path, actions) if err != nil { logger.Error(err, "failed to build wasm policies for path", "pathID", pathID) continue @@ -250,20 +282,20 @@ func equalEnvoyExtensionPolicies(a, b *envoygatewayv1alpha1.EnvoyExtensionPolicy aWasms := a.Spec.Wasm bWasms := b.Spec.Wasm - return len(aWasms) == len(bWasms) && !lo.EveryBy(aWasms, func(aWasm envoygatewayv1alpha1.Wasm) bool { + return len(aWasms) == len(bWasms) && lo.EveryBy(aWasms, func(aWasm envoygatewayv1alpha1.Wasm) bool { return lo.SomeBy(bWasms, func(bWasm envoygatewayv1alpha1.Wasm) bool { if ptr.Deref(aWasm.Name, "") != ptr.Deref(bWasm.Name, "") || ptr.Deref(aWasm.RootID, "") != ptr.Deref(bWasm.RootID, "") || ptr.Deref(aWasm.FailOpen, false) != ptr.Deref(bWasm.FailOpen, false) || aWasm.Code.Type != bWasm.Code.Type || aWasm.Code.Image.URL != bWasm.Code.Image.URL { return false } - aWasmConfigJSON, err := wasm.ConfigFromJSON(aWasm.Config) + aConfig, err := wasm.ConfigFromJSON(aWasm.Config) if err != nil { return false } - bWasmConfigJSON, err := wasm.ConfigFromJSON(bWasm.Config) + bConfig, err := wasm.ConfigFromJSON(bWasm.Config) if err != nil { return false } - return reflect.DeepEqual(aWasmConfigJSON, bWasmConfigJSON) + return aConfig != nil && bConfig != nil && aConfig.EqualTo(bConfig) }) }) } diff --git a/controllers/envoy_gateway_rate_limit_cluster_reconciler.go b/controllers/envoy_gateway_ratelimit_cluster_reconciler.go similarity index 77% rename from controllers/envoy_gateway_rate_limit_cluster_reconciler.go rename to controllers/envoy_gateway_ratelimit_cluster_reconciler.go index b61eab34a..df8f36c87 100644 --- a/controllers/envoy_gateway_rate_limit_cluster_reconciler.go +++ b/controllers/envoy_gateway_ratelimit_cluster_reconciler.go @@ -2,7 +2,6 @@ package controllers import ( "context" - "encoding/json" "errors" "fmt" "sync" @@ -12,7 +11,6 @@ import ( "github.com/kuadrant/policy-machinery/controller" "github.com/kuadrant/policy-machinery/machinery" "github.com/samber/lo" - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" k8stypes "k8s.io/apimachinery/pkg/types" @@ -26,15 +24,16 @@ import ( kuadrantenvoygateway "github.com/kuadrant/kuadrant-operator/pkg/envoygateway" ) -// envoyGatewayRateLimitClusterReconciler reconciles Envoy Gateway EnvoyPatchPolicy custom resources -type envoyGatewayRateLimitClusterReconciler struct { +// EnvoyGatewayRateLimitClusterReconciler reconciles Envoy Gateway EnvoyPatchPolicy custom resources for rate limiting +type EnvoyGatewayRateLimitClusterReconciler struct { client *dynamic.DynamicClient } -func (r *envoyGatewayRateLimitClusterReconciler) Subscription() controller.Subscription { +// EnvoyGatewayRateLimitClusterReconciler subscribes to events with potential impact on the Envoy Gateway EnvoyPatchPolicy custom resources for rate limiting +func (r *EnvoyGatewayRateLimitClusterReconciler) Subscription() controller.Subscription { return controller.Subscription{ ReconcileFunc: r.Reconcile, - Events: []controller.ResourceEventMatcher{ // matches reconciliation events that change the rate limit definitions or status of rate limit policies + Events: []controller.ResourceEventMatcher{ {Kind: &kuadrantv1beta1.KuadrantGroupKind}, {Kind: &machinery.GatewayClassGroupKind}, {Kind: &machinery.GatewayGroupKind}, @@ -45,8 +44,8 @@ func (r *envoyGatewayRateLimitClusterReconciler) Subscription() controller.Subsc } } -func (r *envoyGatewayRateLimitClusterReconciler) Reconcile(ctx context.Context, _ []controller.ResourceEvent, topology *machinery.Topology, _ error, state *sync.Map) error { - logger := controller.LoggerFromContext(ctx).WithName("envoyGatewayRateLimitClusterReconciler") +func (r *EnvoyGatewayRateLimitClusterReconciler) Reconcile(ctx context.Context, _ []controller.ResourceEvent, topology *machinery.Topology, _ error, state *sync.Map) error { + logger := controller.LoggerFromContext(ctx).WithName("EnvoyGatewayRateLimitClusterReconciler") logger.V(1).Info("building envoy gateway rate limit clusters") defer logger.V(1).Info("finished building envoy gateway rate limit clusters") @@ -119,7 +118,7 @@ func (r *envoyGatewayRateLimitClusterReconciler) Reconcile(ctx context.Context, existingEnvoyPatchPolicy := existingEnvoyPatchPolicyObj.(*controller.RuntimeObject).Object.(*envoygatewayv1alpha1.EnvoyPatchPolicy) - if equalEnvoyPatchPolicies(existingEnvoyPatchPolicy, desiredEnvoyPatchPolicy) { + if kuadrantenvoygateway.EqualEnvoyPatchPolicies(existingEnvoyPatchPolicy, desiredEnvoyPatchPolicy) { logger.V(1).Info("envoypatchpolicy object is up to date, nothing to do") continue } @@ -161,7 +160,7 @@ func (r *envoyGatewayRateLimitClusterReconciler) Reconcile(ctx context.Context, return nil } -func (r *envoyGatewayRateLimitClusterReconciler) buildDesiredEnvoyPatchPolicy(limitador *limitadorv1alpha1.Limitador, gateway *machinery.Gateway) (*envoygatewayv1alpha1.EnvoyPatchPolicy, error) { +func (r *EnvoyGatewayRateLimitClusterReconciler) buildDesiredEnvoyPatchPolicy(limitador *limitadorv1alpha1.Limitador, gateway *machinery.Gateway) (*envoygatewayv1alpha1.EnvoyPatchPolicy, error) { envoyPatchPolicy := &envoygatewayv1alpha1.EnvoyPatchPolicy{ TypeMeta: metav1.TypeMeta{ Kind: kuadrantenvoygateway.EnvoyPatchPolicyGroupKind.Kind, @@ -192,7 +191,7 @@ func (r *envoyGatewayRateLimitClusterReconciler) buildDesiredEnvoyPatchPolicy(li }, } - jsonPatches, err := envoyGatewayEnvoyPatchPolicyClusterPatch(limitador.Status.Service.Host, int(limitador.Status.Service.Ports.GRPC)) + jsonPatches, err := kuadrantenvoygateway.BuildEnvoyPatchPolicyClusterPatch(common.KuadrantRateLimitClusterName, limitador.Status.Service.Host, int(limitador.Status.Service.Ports.GRPC), rateLimitClusterPatch) if err != nil { return nil, err } @@ -200,42 +199,3 @@ func (r *envoyGatewayRateLimitClusterReconciler) buildDesiredEnvoyPatchPolicy(li return envoyPatchPolicy, nil } - -// envoyGatewayEnvoyPatchPolicyClusterPatch returns a set envoy config patch that defines the rate limit cluster for the gateway. -// The rate limit cluster configures the endpoint of the external rate limit service. -func envoyGatewayEnvoyPatchPolicyClusterPatch(host string, port int) ([]envoygatewayv1alpha1.EnvoyJSONPatchConfig, error) { - patchRaw, _ := json.Marshal(rateLimitClusterPatch(host, port)) - patch := &apiextensionsv1.JSON{} - if err := patch.UnmarshalJSON(patchRaw); err != nil { - return nil, err - } - - return []envoygatewayv1alpha1.EnvoyJSONPatchConfig{ - { - Type: envoygatewayv1alpha1.ClusterEnvoyResourceType, - Name: common.KuadrantRateLimitClusterName, - Operation: envoygatewayv1alpha1.JSONPatchOperation{ - Op: envoygatewayv1alpha1.JSONPatchOperationType("add"), - Path: "", - Value: patch, - }, - }, - }, nil -} - -func equalEnvoyPatchPolicies(a, b *envoygatewayv1alpha1.EnvoyPatchPolicy) bool { - if a.Spec.Priority != b.Spec.Priority || a.Spec.TargetRef != b.Spec.TargetRef { - return false - } - - aJSONPatches := a.Spec.JSONPatches - bJSONPatches := b.Spec.JSONPatches - if len(aJSONPatches) != len(bJSONPatches) { - return false - } - return lo.EveryBy(aJSONPatches, func(aJSONPatch envoygatewayv1alpha1.EnvoyJSONPatchConfig) bool { - return lo.SomeBy(bJSONPatches, func(bJSONPatch envoygatewayv1alpha1.EnvoyJSONPatchConfig) bool { - return aJSONPatch.Type == bJSONPatch.Type && aJSONPatch.Name == bJSONPatch.Name && aJSONPatch.Operation == bJSONPatch.Operation - }) - }) -} diff --git a/controllers/envoysecuritypolicy_referencegrant_controller.go b/controllers/envoysecuritypolicy_referencegrant_controller.go deleted file mode 100644 index e0bcd3d01..000000000 --- a/controllers/envoysecuritypolicy_referencegrant_controller.go +++ /dev/null @@ -1,166 +0,0 @@ -package controllers - -import ( - "context" - "encoding/json" - - egv1alpha1 "github.com/envoyproxy/gateway/api/v1alpha1" - "github.com/go-logr/logr" - "github.com/samber/lo" - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/utils/ptr" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/handler" - gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" - gatewayapiv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" - - kuadrantv1beta1 "github.com/kuadrant/kuadrant-operator/api/v1beta1" - kuadrantenvoygateway "github.com/kuadrant/kuadrant-operator/pkg/envoygateway" - kuadrantgatewayapi "github.com/kuadrant/kuadrant-operator/pkg/library/gatewayapi" - "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" - "github.com/kuadrant/kuadrant-operator/pkg/library/mappers" - "github.com/kuadrant/kuadrant-operator/pkg/library/reconcilers" - "github.com/kuadrant/kuadrant-operator/pkg/library/utils" -) - -const ( - KuadrantReferenceGrantName = "kuadrant-authorization-rg" -) - -// EnvoySecurityPolicyReferenceGrantReconciler reconciles ReferenceGrant objects for auth -type EnvoySecurityPolicyReferenceGrantReconciler struct { - *reconcilers.BaseReconciler -} - -//+kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=referencegrants,verbs=get;list;watch;create;update;patch;delete - -func (r *EnvoySecurityPolicyReferenceGrantReconciler) Reconcile(eventCtx context.Context, req ctrl.Request) (ctrl.Result, error) { - logger := r.Logger().WithValues("Kuadrant", req.NamespacedName) - logger.Info("Reconciling SecurityPolicy ReferenceGrant") - ctx := logr.NewContext(eventCtx, logger) - - kObj := &kuadrantv1beta1.Kuadrant{} - if err := r.Client().Get(ctx, req.NamespacedName, kObj); err != nil { - if apierrors.IsNotFound(err) { - logger.Info("no kuadrant object found") - return ctrl.Result{}, nil - } - logger.Error(err, "failed to get kuadrant object") - return ctrl.Result{}, err - } - - if logger.V(1).Enabled() { - jsonData, err := json.MarshalIndent(kObj, "", " ") - if err != nil { - return ctrl.Result{}, err - } - logger.V(1).Info(string(jsonData)) - } - - rg, err := r.securityPolicyReferenceGrant(ctx, kObj.Namespace) - if err != nil { - return ctrl.Result{}, err - } - - if err := r.SetOwnerReference(kObj, rg); err != nil { - logger.Error(err, "failed to set owner reference on envoy SecurityPolicy ReferenceGrant resource") - return ctrl.Result{}, err - } - - if err := r.ReconcileResource(ctx, &gatewayapiv1beta1.ReferenceGrant{}, rg, kuadrantenvoygateway.SecurityPolicyReferenceGrantMutator); err != nil && !apierrors.IsAlreadyExists(err) { - logger.Error(err, "failed to reconcile envoy SecurityPolicy ReferenceGrant resource") - return ctrl.Result{}, err - } - - return ctrl.Result{}, nil -} - -func (r *EnvoySecurityPolicyReferenceGrantReconciler) securityPolicyReferenceGrant(ctx context.Context, kuadrantNamespace string) (*gatewayapiv1beta1.ReferenceGrant, error) { - logger, _ := logr.FromContext(ctx) - logger = logger.WithName("securityPolicyReferenceGrant") - - rg := &gatewayapiv1beta1.ReferenceGrant{ - ObjectMeta: metav1.ObjectMeta{ - Name: KuadrantReferenceGrantName, - Namespace: kuadrantNamespace, - }, - Spec: gatewayapiv1beta1.ReferenceGrantSpec{ - To: []gatewayapiv1beta1.ReferenceGrantTo{ - { - Group: "", - Kind: "Service", - Name: ptr.To[gatewayapiv1.ObjectName](kuadrant.AuthorinoServiceName), - }, - }, - }, - } - - espNamespaces := make(map[string]struct{}) - listOptions := &client.ListOptions{LabelSelector: labels.SelectorFromSet(map[string]string{kuadrant.KuadrantNamespaceAnnotation: kuadrantNamespace})} - espList := &egv1alpha1.SecurityPolicyList{} - if err := r.Client().List(ctx, espList, listOptions); err != nil { - return nil, err - } - - for _, esp := range espList.Items { - // only append namespaces that differ from the kuadrant namespace and are not marked for deletion - if esp.DeletionTimestamp == nil && esp.Namespace != kuadrantNamespace { - espNamespaces[esp.Namespace] = struct{}{} - } - } - - if len(espNamespaces) == 0 { - logger.V(1).Info("no security policies exist outside of the kuadrant namespace, skipping ReferenceGrant") - utils.TagObjectToDelete(rg) - return rg, nil - } - - refGrantFrom := lo.MapToSlice(espNamespaces, func(namespace string, _ struct{}) gatewayapiv1beta1.ReferenceGrantFrom { - return gatewayapiv1beta1.ReferenceGrantFrom{ - Group: egv1alpha1.GroupName, - Kind: egv1alpha1.KindSecurityPolicy, - Namespace: gatewayapiv1.Namespace(namespace), - } - }) - rg.Spec.From = refGrantFrom - - return rg, nil -} - -// SetupWithManager sets up the controller with the Manager. -func (r *EnvoySecurityPolicyReferenceGrantReconciler) SetupWithManager(mgr ctrl.Manager) error { - ok, err := kuadrantenvoygateway.IsEnvoyGatewaySecurityPolicyInstalled(mgr.GetRESTMapper()) - if err != nil { - return err - } - if !ok { - r.Logger().Info("Envoy SecurityPolicy ReferenceGrant controller disabled. EnvoyGateway API was not found") - return nil - } - - ok, err = kuadrantgatewayapi.IsGatewayAPIInstalled(mgr.GetRESTMapper()) - if err != nil { - return err - } - if !ok { - r.Logger().Info("Envoy SecurityPolicy ReferenceGrant controller disabled. GatewayAPI was not found") - return nil - } - - securityPolicyToKuadrantEventMapper := mappers.NewSecurityPolicyToKuadrantEventMapper( - mappers.WithLogger(r.Logger().WithName("securityPolicyToKuadrantEventMapper")), - mappers.WithClient(r.Client()), - ) - - return ctrl.NewControllerManagedBy(mgr). - For(&kuadrantv1beta1.Kuadrant{}). - Owns(&gatewayapiv1beta1.ReferenceGrant{}). - Watches( - &egv1alpha1.SecurityPolicy{}, - handler.EnqueueRequestsFromMapFunc(securityPolicyToKuadrantEventMapper.Map), - ). - Complete(r) -} diff --git a/controllers/istio_auth_cluster_reconciler.go b/controllers/istio_auth_cluster_reconciler.go new file mode 100644 index 000000000..53847a9dd --- /dev/null +++ b/controllers/istio_auth_cluster_reconciler.go @@ -0,0 +1,202 @@ +package controllers + +import ( + "context" + "errors" + "fmt" + "sync" + + authorinooperatorv1beta1 "github.com/kuadrant/authorino-operator/api/v1beta1" + "github.com/kuadrant/policy-machinery/controller" + "github.com/kuadrant/policy-machinery/machinery" + "github.com/samber/lo" + istioapinetworkingv1alpha3 "istio.io/api/networking/v1alpha3" + istiov1beta1 "istio.io/api/type/v1beta1" + istioclientgonetworkingv1alpha3 "istio.io/client-go/pkg/apis/networking/v1alpha3" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + k8stypes "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/dynamic" + "k8s.io/utils/ptr" + + kuadrantv1beta1 "github.com/kuadrant/kuadrant-operator/api/v1beta1" + kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" + "github.com/kuadrant/kuadrant-operator/pkg/common" + kuadrantistio "github.com/kuadrant/kuadrant-operator/pkg/istio" +) + +// IstioAuthClusterReconciler reconciles Istio EnvoyFilter custom resources for auth +type IstioAuthClusterReconciler struct { + client *dynamic.DynamicClient +} + +// IstioAuthClusterReconciler subscribes to events with potential impact on the Istio EnvoyFilter custom resources for auth +func (r *IstioAuthClusterReconciler) Subscription() controller.Subscription { + return controller.Subscription{ + ReconcileFunc: r.Reconcile, + Events: []controller.ResourceEventMatcher{ + {Kind: &kuadrantv1beta1.KuadrantGroupKind}, + {Kind: &machinery.GatewayClassGroupKind}, + {Kind: &machinery.GatewayGroupKind}, + {Kind: &machinery.HTTPRouteGroupKind}, + {Kind: &kuadrantv1beta3.AuthPolicyGroupKind}, + {Kind: &kuadrantistio.EnvoyFilterGroupKind}, + }, + } +} + +func (r *IstioAuthClusterReconciler) Reconcile(ctx context.Context, _ []controller.ResourceEvent, topology *machinery.Topology, _ error, state *sync.Map) error { + logger := controller.LoggerFromContext(ctx).WithName("IstioAuthClusterReconciler") + + logger.V(1).Info("building istio auth clusters") + defer logger.V(1).Info("finished building istio auth clusters") + + kuadrant, err := GetKuadrantFromTopology(topology) + if err != nil { + if errors.Is(err, ErrMissingKuadrant) { + logger.V(1).Info(err.Error()) + return nil + } + return err + } + + authorinoObj, found := lo.Find(topology.Objects().Children(kuadrant), func(child machinery.Object) bool { + return child.GroupVersionKind().GroupKind() == kuadrantv1beta1.AuthorinoGroupKind + }) + if !found { + logger.V(1).Info(ErrMissingAuthorino.Error()) + return nil + } + authorino := authorinoObj.(*controller.RuntimeObject).Object.(*authorinooperatorv1beta1.Authorino) + + effectivePolicies, ok := state.Load(StateEffectiveAuthPolicies) + if !ok { + logger.Error(ErrMissingStateEffectiveAuthPolicies, "failed to get effective auth policies from state") + return nil + } + + gateways := lo.UniqBy(lo.FilterMap(lo.Values(effectivePolicies.(EffectiveAuthPolicies)), func(effectivePolicy EffectiveAuthPolicy, _ int) (*machinery.Gateway, bool) { + gatewayClass, gateway, _, _, _, _ := common.ObjectsInRequestPath(effectivePolicy.Path) + return gateway, gatewayClass.Spec.ControllerName == istioGatewayControllerName + }), func(gateway *machinery.Gateway) string { + return gateway.GetLocator() + }) + + desiredEnvoyFilters := make(map[k8stypes.NamespacedName]struct{}) + var modifiedGateways []string + + // reconcile istio cluster for gateway + for _, gateway := range gateways { + gatewayKey := k8stypes.NamespacedName{Name: gateway.GetName(), Namespace: gateway.GetNamespace()} + + desiredEnvoyFilter, err := r.buildDesiredEnvoyFilter(authorino, gateway) + if err != nil { + logger.Error(err, "failed to build desired envoy filter") + continue + } + desiredEnvoyFilters[k8stypes.NamespacedName{Name: desiredEnvoyFilter.GetName(), Namespace: desiredEnvoyFilter.GetNamespace()}] = struct{}{} + + resource := r.client.Resource(kuadrantistio.EnvoyFiltersResource).Namespace(desiredEnvoyFilter.GetNamespace()) + + existingEnvoyFilterObj, found := lo.Find(topology.Objects().Children(gateway), func(child machinery.Object) bool { + return child.GroupVersionKind().GroupKind() == kuadrantistio.EnvoyFilterGroupKind && child.GetName() == desiredEnvoyFilter.GetName() && child.GetNamespace() == desiredEnvoyFilter.GetNamespace() && labels.Set(child.(*controller.RuntimeObject).GetLabels()).AsSelector().Matches(labels.Set(desiredEnvoyFilter.GetLabels())) + }) + + // create + if !found { + modifiedGateways = append(modifiedGateways, gateway.GetLocator()) // we only signal the gateway as modified when an envoyfilter is created, because updates won't change the status + desiredEnvoyFilterUnstructured, err := controller.Destruct(desiredEnvoyFilter) + if err != nil { + logger.Error(err, "failed to destruct envoyfilter object", "gateway", gatewayKey.String(), "envoyfilter", desiredEnvoyFilter) + continue + } + if _, err = resource.Create(ctx, desiredEnvoyFilterUnstructured, metav1.CreateOptions{}); err != nil { + logger.Error(err, "failed to create envoyfilter object", "gateway", gatewayKey.String(), "envoyfilter", desiredEnvoyFilterUnstructured.Object) + // TODO: handle error + } + continue + } + + existingEnvoyFilter := existingEnvoyFilterObj.(*controller.RuntimeObject).Object.(*istioclientgonetworkingv1alpha3.EnvoyFilter) + + if kuadrantistio.EqualEnvoyFilters(existingEnvoyFilter, desiredEnvoyFilter) { + logger.V(1).Info("envoyfilter object is up to date, nothing to do") + continue + } + + // update + existingEnvoyFilter.Spec = istioapinetworkingv1alpha3.EnvoyFilter{ + TargetRefs: desiredEnvoyFilter.Spec.TargetRefs, + ConfigPatches: desiredEnvoyFilter.Spec.ConfigPatches, + Priority: desiredEnvoyFilter.Spec.Priority, + } + + existingEnvoyFilterUnstructured, err := controller.Destruct(existingEnvoyFilter) + if err != nil { + logger.Error(err, "failed to destruct envoyfilter object", "gateway", gatewayKey.String(), "envoyfilter", existingEnvoyFilter) + continue + } + if _, err = resource.Update(ctx, existingEnvoyFilterUnstructured, metav1.UpdateOptions{}); err != nil { + logger.Error(err, "failed to update envoyfilter object", "gateway", gatewayKey.String(), "envoyfilter", existingEnvoyFilterUnstructured.Object) + // TODO: handle error + } + } + + state.Store(StateIstioAuthClustersModified, modifiedGateways) + + // cleanup istio clusters for gateways that are not in the effective policies + staleEnvoyFilters := topology.Objects().Items(func(o machinery.Object) bool { + _, desired := desiredEnvoyFilters[k8stypes.NamespacedName{Name: o.GetName(), Namespace: o.GetNamespace()}] + return o.GroupVersionKind().GroupKind() == kuadrantistio.EnvoyFilterGroupKind && labels.Set(o.(*controller.RuntimeObject).GetLabels()).AsSelector().Matches(AuthObjectLabels()) && !desired + }) + for _, envoyFilter := range staleEnvoyFilters { + if err := r.client.Resource(kuadrantistio.EnvoyFiltersResource).Namespace(envoyFilter.GetNamespace()).Delete(ctx, envoyFilter.GetName(), metav1.DeleteOptions{}); err != nil { + logger.Error(err, "failed to delete envoyfilter object", "envoyfilter", fmt.Sprintf("%s/%s", envoyFilter.GetNamespace(), envoyFilter.GetName())) + // TODO: handle error + } + } + + return nil +} + +func (r *IstioAuthClusterReconciler) buildDesiredEnvoyFilter(authorino *authorinooperatorv1beta1.Authorino, gateway *machinery.Gateway) (*istioclientgonetworkingv1alpha3.EnvoyFilter, error) { + envoyFilter := &istioclientgonetworkingv1alpha3.EnvoyFilter{ + TypeMeta: metav1.TypeMeta{ + Kind: kuadrantistio.EnvoyFilterGroupKind.Kind, + APIVersion: istioclientgonetworkingv1alpha3.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: AuthClusterName(gateway.GetName()), + Namespace: gateway.GetNamespace(), + Labels: AuthObjectLabels(), + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: gateway.GroupVersionKind().GroupVersion().String(), + Kind: gateway.GroupVersionKind().Kind, + Name: gateway.Name, + UID: gateway.UID, + BlockOwnerDeletion: ptr.To(true), + Controller: ptr.To(true), + }, + }, + }, + Spec: istioapinetworkingv1alpha3.EnvoyFilter{ + TargetRefs: []*istiov1beta1.PolicyTargetReference{ + { + Group: machinery.GatewayGroupKind.Group, + Kind: machinery.GatewayGroupKind.Kind, + Name: gateway.GetName(), + }, + }, + }, + } + + authorinoServiceInfo := authorinoServiceInfoFromAuthorino(authorino) + configPatches, err := kuadrantistio.BuildEnvoyFilterClusterPatch(authorinoServiceInfo.Host, int(authorinoServiceInfo.Port), authClusterPatch) + if err != nil { + return nil, err + } + envoyFilter.Spec.ConfigPatches = configPatches + + return envoyFilter, nil +} diff --git a/controllers/istio_extension_reconciler.go b/controllers/istio_extension_reconciler.go index 5ff22ac85..3ee02fbc7 100644 --- a/controllers/istio_extension_reconciler.go +++ b/controllers/istio_extension_reconciler.go @@ -27,27 +27,29 @@ import ( "github.com/kuadrant/kuadrant-operator/pkg/wasm" ) -// istioExtensionReconciler reconciles Istio WasmPlugin custom resources -type istioExtensionReconciler struct { +// IstioExtensionReconciler reconciles Istio WasmPlugin custom resources +type IstioExtensionReconciler struct { client *dynamic.DynamicClient } -func (r *istioExtensionReconciler) Subscription() controller.Subscription { +// IstioExtensionReconciler subscribes to events with potential impact on the Istio WasmPlugin custom resources +func (r *IstioExtensionReconciler) Subscription() controller.Subscription { return controller.Subscription{ ReconcileFunc: r.Reconcile, - Events: []controller.ResourceEventMatcher{ // matches reconciliation events that change the rate limit definitions or status of rate limit policies + Events: []controller.ResourceEventMatcher{ {Kind: &kuadrantv1beta1.KuadrantGroupKind}, {Kind: &machinery.GatewayClassGroupKind}, {Kind: &machinery.GatewayGroupKind}, {Kind: &machinery.HTTPRouteGroupKind}, {Kind: &kuadrantv1beta3.RateLimitPolicyGroupKind}, + {Kind: &kuadrantv1beta3.AuthPolicyGroupKind}, {Kind: &kuadrantistio.WasmPluginGroupKind}, }, } } -func (r *istioExtensionReconciler) Reconcile(ctx context.Context, _ []controller.ResourceEvent, topology *machinery.Topology, _ error, state *sync.Map) error { - logger := controller.LoggerFromContext(ctx).WithName("istioExtensionReconciler") +func (r *IstioExtensionReconciler) Reconcile(ctx context.Context, _ []controller.ResourceEvent, topology *machinery.Topology, _ error, state *sync.Map) error { + logger := controller.LoggerFromContext(ctx).WithName("IstioExtensionReconciler") logger.V(1).Info("building istio extension") defer logger.V(1).Info("finished building istio extension") @@ -55,7 +57,7 @@ func (r *istioExtensionReconciler) Reconcile(ctx context.Context, _ []controller // build wasm plugin configs for each gateway wasmConfigs, err := r.buildWasmConfigs(ctx, state) if err != nil { - if errors.Is(err, ErrMissingStateEffectiveRateLimitPolicies) { + if errors.Is(err, ErrMissingStateEffectiveAuthPolicies) || errors.Is(err, ErrMissingStateEffectiveRateLimitPolicies) { logger.V(1).Info(err.Error()) } else { return err @@ -139,28 +141,59 @@ func (r *istioExtensionReconciler) Reconcile(ctx context.Context, _ []controller } // buildWasmConfigs returns a map of istio gateway locators to an ordered list of corresponding wasm policies -func (r *istioExtensionReconciler) buildWasmConfigs(ctx context.Context, state *sync.Map) (map[string]wasm.Config, error) { - logger := controller.LoggerFromContext(ctx).WithName("istioExtensionReconciler").WithName("buildWasmConfigs") +func (r *IstioExtensionReconciler) buildWasmConfigs(ctx context.Context, state *sync.Map) (map[string]wasm.Config, error) { + logger := controller.LoggerFromContext(ctx).WithName("IstioExtensionReconciler").WithName("buildWasmConfigs") - effectivePolicies, ok := state.Load(StateEffectiveRateLimitPolicies) + effectiveAuthPolicies, ok := state.Load(StateEffectiveAuthPolicies) + if !ok { + return nil, ErrMissingStateEffectiveAuthPolicies + } + effectiveAuthPoliciesMap := effectiveAuthPolicies.(EffectiveAuthPolicies) + + effectiveRateLimitPolicies, ok := state.Load(StateEffectiveRateLimitPolicies) if !ok { return nil, ErrMissingStateEffectiveRateLimitPolicies } + effectiveRateLimitPoliciesMap := effectiveRateLimitPolicies.(EffectiveRateLimitPolicies) + + logger.V(1).Info("building wasm configs for istio extension", "effectiveRateLimitPolicies", len(effectiveAuthPoliciesMap), "effectiveRateLimitPolicies", len(effectiveRateLimitPoliciesMap)) - logger.V(1).Info("building wasm configs for istio extension", "effectivePolicies", len(effectivePolicies.(EffectiveRateLimitPolicies))) + paths := lo.UniqBy(append( + lo.Entries(lo.MapValues(effectiveAuthPoliciesMap, func(p EffectiveAuthPolicy, _ string) []machinery.Targetable { return p.Path })), + lo.Entries(lo.MapValues(effectiveRateLimitPoliciesMap, func(p EffectiveRateLimitPolicy, _ string) []machinery.Targetable { return p.Path }))..., + ), func(e lo.Entry[string, []machinery.Targetable]) string { return e.Key }) wasmActionSets := kuadrantgatewayapi.GrouppedHTTPRouteMatchConfigs{} // build the wasm policies for each topological path that contains an effective rate limit policy affecting an istio gateway - for pathID, effectivePolicy := range effectivePolicies.(EffectiveRateLimitPolicies) { - gatewayClass, gateway, _, _, _, _ := common.ObjectsInRequestPath(effectivePolicy.Path) + for i := range paths { + pathID := paths[i].Key + path := paths[i].Value + + gatewayClass, gateway, _, _, _, _ := common.ObjectsInRequestPath(path) // ignore if not an istio gateway if gatewayClass.Spec.ControllerName != istioGatewayControllerName { continue } - wasmActionSetsForPath, err := wasm.BuildActionSetsForPath(pathID, effectivePolicy.Path, effectivePolicy.Spec.Rules(), rateLimitWasmActionBuilder(pathID, effectivePolicy, state)) + var actions []wasm.Action + + // auth + if effectivePolicy, ok := effectiveAuthPoliciesMap[pathID]; ok { + actions = append(actions, buildWasmActionsForAuth(pathID, effectivePolicy)...) + } + + // rate limit + if effectivePolicy, ok := effectiveRateLimitPoliciesMap[pathID]; ok { + actions = append(actions, buildWasmActionsForRateLimit(effectivePolicy, state)...) + } + + if len(actions) == 0 { + continue + } + + wasmActionSetsForPath, err := wasm.BuildActionSetsForPath(pathID, path, actions) if err != nil { logger.Error(err, "failed to build wasm policies for path", "pathID", pathID) continue diff --git a/controllers/istio_rate_limit_cluster_reconciler.go b/controllers/istio_ratelimit_cluster_reconciler.go similarity index 70% rename from controllers/istio_rate_limit_cluster_reconciler.go rename to controllers/istio_ratelimit_cluster_reconciler.go index 10c20b792..37462748f 100644 --- a/controllers/istio_rate_limit_cluster_reconciler.go +++ b/controllers/istio_ratelimit_cluster_reconciler.go @@ -2,7 +2,6 @@ package controllers import ( "context" - "encoding/json" "errors" "fmt" "sync" @@ -26,15 +25,16 @@ import ( kuadrantistio "github.com/kuadrant/kuadrant-operator/pkg/istio" ) -// istioRateLimitClusterReconciler reconciles Istio EnvoyFilter custom resources -type istioRateLimitClusterReconciler struct { +// IstioRateLimitClusterReconciler reconciles Istio EnvoyFilter custom resources for rate limiting +type IstioRateLimitClusterReconciler struct { client *dynamic.DynamicClient } -func (r *istioRateLimitClusterReconciler) Subscription() controller.Subscription { +// IstioRateLimitClusterReconciler subscribes to events with potential impact on the Istio EnvoyFilter custom resources for rate limiting +func (r *IstioRateLimitClusterReconciler) Subscription() controller.Subscription { return controller.Subscription{ ReconcileFunc: r.Reconcile, - Events: []controller.ResourceEventMatcher{ // matches reconciliation events that change the rate limit definitions or status of rate limit policies + Events: []controller.ResourceEventMatcher{ {Kind: &kuadrantv1beta1.KuadrantGroupKind}, {Kind: &machinery.GatewayClassGroupKind}, {Kind: &machinery.GatewayGroupKind}, @@ -45,8 +45,8 @@ func (r *istioRateLimitClusterReconciler) Subscription() controller.Subscription } } -func (r *istioRateLimitClusterReconciler) Reconcile(ctx context.Context, _ []controller.ResourceEvent, topology *machinery.Topology, _ error, state *sync.Map) error { - logger := controller.LoggerFromContext(ctx).WithName("istioRateLimitClusterReconciler") +func (r *IstioRateLimitClusterReconciler) Reconcile(ctx context.Context, _ []controller.ResourceEvent, topology *machinery.Topology, _ error, state *sync.Map) error { + logger := controller.LoggerFromContext(ctx).WithName("IstioRateLimitClusterReconciler") logger.V(1).Info("building istio rate limit clusters") defer logger.V(1).Info("finished building istio rate limit clusters") @@ -119,7 +119,7 @@ func (r *istioRateLimitClusterReconciler) Reconcile(ctx context.Context, _ []con existingEnvoyFilter := existingEnvoyFilterObj.(*controller.RuntimeObject).Object.(*istioclientgonetworkingv1alpha3.EnvoyFilter) - if equalEnvoyFilters(existingEnvoyFilter, desiredEnvoyFilter) { + if kuadrantistio.EqualEnvoyFilters(existingEnvoyFilter, desiredEnvoyFilter) { logger.V(1).Info("envoyfilter object is up to date, nothing to do") continue } @@ -149,7 +149,6 @@ func (r *istioRateLimitClusterReconciler) Reconcile(ctx context.Context, _ []con _, desired := desiredEnvoyFilters[k8stypes.NamespacedName{Name: o.GetName(), Namespace: o.GetNamespace()}] return o.GroupVersionKind().GroupKind() == kuadrantistio.EnvoyFilterGroupKind && labels.Set(o.(*controller.RuntimeObject).GetLabels()).AsSelector().Matches(RateLimitObjectLabels()) && !desired }) - for _, envoyFilter := range staleEnvoyFilters { if err := r.client.Resource(kuadrantistio.EnvoyFiltersResource).Namespace(envoyFilter.GetNamespace()).Delete(ctx, envoyFilter.GetName(), metav1.DeleteOptions{}); err != nil { logger.Error(err, "failed to delete envoyfilter object", "envoyfilter", fmt.Sprintf("%s/%s", envoyFilter.GetNamespace(), envoyFilter.GetName())) @@ -160,7 +159,7 @@ func (r *istioRateLimitClusterReconciler) Reconcile(ctx context.Context, _ []con return nil } -func (r *istioRateLimitClusterReconciler) buildDesiredEnvoyFilter(limitador *limitadorv1alpha1.Limitador, gateway *machinery.Gateway) (*istioclientgonetworkingv1alpha3.EnvoyFilter, error) { +func (r *IstioRateLimitClusterReconciler) buildDesiredEnvoyFilter(limitador *limitadorv1alpha1.Limitador, gateway *machinery.Gateway) (*istioclientgonetworkingv1alpha3.EnvoyFilter, error) { envoyFilter := &istioclientgonetworkingv1alpha3.EnvoyFilter{ TypeMeta: metav1.TypeMeta{ Kind: kuadrantistio.EnvoyFilterGroupKind.Kind, @@ -192,7 +191,11 @@ func (r *istioRateLimitClusterReconciler) buildDesiredEnvoyFilter(limitador *lim }, } - configPatches, err := istioEnvoyFilterClusterPatch(limitador.Status.Service.Host, int(limitador.Status.Service.Ports.GRPC)) + limitadorService := limitador.Status.Service + if limitadorService == nil { + return nil, ErrMissingLimitadorServiceInfo + } + configPatches, err := kuadrantistio.BuildEnvoyFilterClusterPatch(limitador.Status.Service.Host, int(limitador.Status.Service.Ports.GRPC), rateLimitClusterPatch) if err != nil { return nil, err } @@ -200,76 +203,3 @@ func (r *istioRateLimitClusterReconciler) buildDesiredEnvoyFilter(limitador *lim return envoyFilter, nil } - -// istioEnvoyFilterClusterPatch returns an envoy config patch that defines the rate limit cluster for the gateway. -// The rate limit cluster configures the endpoint of the external rate limit service. -func istioEnvoyFilterClusterPatch(host string, port int) ([]*istioapinetworkingv1alpha3.EnvoyFilter_EnvoyConfigObjectPatch, error) { - patchRaw, _ := json.Marshal(map[string]any{"operation": "ADD", "value": rateLimitClusterPatch(host, port)}) - patch := &istioapinetworkingv1alpha3.EnvoyFilter_Patch{} - if err := patch.UnmarshalJSON(patchRaw); err != nil { - return nil, err - } - - return []*istioapinetworkingv1alpha3.EnvoyFilter_EnvoyConfigObjectPatch{ - { - ApplyTo: istioapinetworkingv1alpha3.EnvoyFilter_CLUSTER, - Match: &istioapinetworkingv1alpha3.EnvoyFilter_EnvoyConfigObjectMatch{ - ObjectTypes: &istioapinetworkingv1alpha3.EnvoyFilter_EnvoyConfigObjectMatch_Cluster{ - Cluster: &istioapinetworkingv1alpha3.EnvoyFilter_ClusterMatch{ - Service: host, - }, - }, - }, - Patch: patch, - }, - }, nil -} - -func equalEnvoyFilters(a, b *istioclientgonetworkingv1alpha3.EnvoyFilter) bool { - if a.Spec.Priority != b.Spec.Priority || !kuadrantistio.EqualTargetRefs(a.Spec.TargetRefs, b.Spec.TargetRefs) { - return false - } - - aConfigPatches := a.Spec.ConfigPatches - bConfigPatches := b.Spec.ConfigPatches - if len(aConfigPatches) != len(bConfigPatches) { - return false - } - return lo.EveryBy(aConfigPatches, func(aConfigPatch *istioapinetworkingv1alpha3.EnvoyFilter_EnvoyConfigObjectPatch) bool { - return lo.SomeBy(bConfigPatches, func(bConfigPatch *istioapinetworkingv1alpha3.EnvoyFilter_EnvoyConfigObjectPatch) bool { - if aConfigPatch == nil && bConfigPatch == nil { - return true - } - if (aConfigPatch == nil && bConfigPatch != nil) || (aConfigPatch != nil && bConfigPatch == nil) { - return false - } - - // apply_to - if aConfigPatch.ApplyTo != bConfigPatch.ApplyTo { - return false - } - - // cluster match - aCluster := aConfigPatch.Match.GetCluster() - bCluster := bConfigPatch.Match.GetCluster() - if aCluster == nil || bCluster == nil { - return false - } - if aCluster.Service != bCluster.Service || aCluster.PortNumber != bCluster.PortNumber || aCluster.Subset != bCluster.Subset { - return false - } - - // patch - aPatch := aConfigPatch.Patch - bPatch := bConfigPatch.Patch - - if aPatch.Operation != bPatch.Operation || aPatch.FilterClass != bPatch.FilterClass { - return false - } - - aPatchJSON, _ := aPatch.Value.MarshalJSON() - bPatchJSON, _ := aPatch.Value.MarshalJSON() - return string(aPatchJSON) == string(bPatchJSON) - }) - }) -} diff --git a/controllers/kuadrant_controller.go b/controllers/kuadrant_controller.go index a3ce7f462..9de534325 100644 --- a/controllers/kuadrant_controller.go +++ b/controllers/kuadrant_controller.go @@ -23,21 +23,13 @@ import ( "github.com/go-logr/logr" authorinov1beta1 "github.com/kuadrant/authorino-operator/api/v1beta1" limitadorv1alpha1 "github.com/kuadrant/limitador-operator/api/v1alpha1" - iopv1alpha1 "istio.io/istio/operator/pkg/apis/istio/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" - "k8s.io/utils/env" - istiov1alpha1 "maistra.io/istio-operator/api/v1alpha1" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - maistrav1 "github.com/kuadrant/kuadrant-operator/api/external/maistra/v1" - maistrav2 "github.com/kuadrant/kuadrant-operator/api/external/maistra/v2" kuadrantv1beta1 "github.com/kuadrant/kuadrant-operator/api/v1beta1" - "github.com/kuadrant/kuadrant-operator/pkg/istio" kuadrantgatewayapi "github.com/kuadrant/kuadrant-operator/pkg/library/gatewayapi" "github.com/kuadrant/kuadrant-operator/pkg/library/reconcilers" "github.com/kuadrant/kuadrant-operator/pkg/log" @@ -47,23 +39,6 @@ const ( kuadrantFinalizer = "kuadrant.io/finalizer" ) -const ( - // (Sail) The istio CR must be named default to process GW API resources - istioCRName = "default" -) - -func controlPlaneConfigMapName() string { - return env.GetString("ISTIOCONFIGMAP_NAME", "istio") -} - -func controlPlaneProviderNamespace() string { - return env.GetString("ISTIOOPERATOR_NAMESPACE", "istio-system") -} - -func controlPlaneProviderName() string { - return env.GetString("ISTIOOPERATOR_NAME", "istiocontrolplane") -} - // KuadrantReconciler reconciles a Kuadrant object type KuadrantReconciler struct { *reconcilers.BaseReconciler @@ -124,10 +99,6 @@ func (r *KuadrantReconciler) Reconcile(eventCtx context.Context, req ctrl.Reques if kObj.GetDeletionTimestamp() != nil && controllerutil.ContainsFinalizer(kObj, kuadrantFinalizer) { logger.V(1).Info("Handling removal of kuadrant object") - if err := r.unregisterExternalAuthorizer(ctx, kObj); err != nil { - return ctrl.Result{}, err - } - logger.Info("removing finalizer") controllerutil.RemoveFinalizer(kObj, kuadrantFinalizer) if err := r.Client().Update(ctx, kObj); client.IgnoreNotFound(err) != nil { @@ -149,16 +120,9 @@ func (r *KuadrantReconciler) Reconcile(eventCtx context.Context, req ctrl.Reques } } - specErr := r.reconcileSpec(ctx, kObj) - - statusResult, statusErr := r.reconcileStatus(ctx, kObj, specErr) - - if specErr != nil { - return ctrl.Result{}, specErr - } - - if statusErr != nil { - return ctrl.Result{}, statusErr + statusResult, err := r.reconcileStatus(ctx, kObj, nil) + if err != nil { + return ctrl.Result{}, err } if statusResult.Requeue { @@ -170,233 +134,6 @@ func (r *KuadrantReconciler) Reconcile(eventCtx context.Context, req ctrl.Reques return ctrl.Result{}, nil } -func (r *KuadrantReconciler) unregisterExternalAuthorizer(ctx context.Context, kObj *kuadrantv1beta1.Kuadrant) error { - isIstioInstalled, err := r.unregisterExternalAuthorizerIstio(ctx, kObj) - - if err == nil && !isIstioInstalled { - err = r.unregisterExternalAuthorizerOSSM(ctx, kObj) - } - - return err -} - -func (r *KuadrantReconciler) unregisterExternalAuthorizerIstio(ctx context.Context, kObj *kuadrantv1beta1.Kuadrant) (bool, error) { - logger, _ := logr.FromContext(ctx) - configsToUpdate, err := r.getIstioConfigObjects(ctx) - isIstioInstalled := configsToUpdate != nil - - if !isIstioInstalled || err != nil { - return isIstioInstalled, err - } - - kuadrantAuthorizer := istio.NewKuadrantAuthorizer(kObj.Namespace) - - for _, config := range configsToUpdate { - hasAuthorizer, err := istio.HasKuadrantAuthorizer(config, *kuadrantAuthorizer) - if err != nil { - return true, err - } - if hasAuthorizer { - if err = istio.UnregisterKuadrantAuthorizer(config, kuadrantAuthorizer); err != nil { - return true, err - } - - logger.Info("remove external authorizer from istio meshconfig") - if err = r.UpdateResource(ctx, config.GetConfigObject()); err != nil { - return true, err - } - } - } - return true, nil -} - -func (r *KuadrantReconciler) unregisterExternalAuthorizerOSSM(ctx context.Context, kObj *kuadrantv1beta1.Kuadrant) error { - logger, _ := logr.FromContext(ctx) - - smcp := &maistrav2.ServiceMeshControlPlane{} - - smcpKey := client.ObjectKey{Name: controlPlaneProviderName(), Namespace: controlPlaneProviderNamespace()} - if err := r.Client().Get(ctx, smcpKey, smcp); err != nil { - logger.V(1).Info("failed to get servicemeshcontrolplane object", "key", smcp, "err", err) - if apierrors.IsNotFound(err) || meta.IsNoMatchError(err) { - logger.Info("OSSM installation as GatewayAPI provider not found") - return nil - } - return err - } - - smcpWrapper := istio.NewOSSMControlPlaneWrapper(smcp) - kuadrantAuthorizer := istio.NewKuadrantAuthorizer(kObj.Namespace) - - hasAuthorizer, err := istio.HasKuadrantAuthorizer(smcpWrapper, *kuadrantAuthorizer) - if err != nil { - return err - } - if hasAuthorizer { - err = istio.UnregisterKuadrantAuthorizer(smcpWrapper, kuadrantAuthorizer) - if err != nil { - return err - } - logger.Info("removing external authorizer from OSSM meshconfig") - if err := r.UpdateResource(ctx, smcpWrapper.GetConfigObject()); err != nil { - return err - } - } - - return nil -} - -func (r *KuadrantReconciler) registerExternalAuthorizer(ctx context.Context, kObj *kuadrantv1beta1.Kuadrant) error { - isIstioInstalled, err := r.registerExternalAuthorizerIstio(ctx, kObj) - - if err == nil && !isIstioInstalled { - err = r.registerExternalAuthorizerOSSM(ctx, kObj) - } - - return err -} - -func (r *KuadrantReconciler) registerExternalAuthorizerIstio(ctx context.Context, kObj *kuadrantv1beta1.Kuadrant) (bool, error) { - logger, _ := logr.FromContext(ctx) - configsToUpdate, err := r.getIstioConfigObjects(ctx) - isIstioInstalled := configsToUpdate != nil - - if !isIstioInstalled || err != nil { - return isIstioInstalled, err - } - - kuadrantAuthorizer := istio.NewKuadrantAuthorizer(kObj.Namespace) - for _, config := range configsToUpdate { - hasKuadrantAuthorizer, err := istio.HasKuadrantAuthorizer(config, *kuadrantAuthorizer) - if err != nil { - return true, err - } - if !hasKuadrantAuthorizer { - err = istio.RegisterKuadrantAuthorizer(config, kuadrantAuthorizer) - if err != nil { - return true, err - } - logger.Info("adding external authorizer to istio meshconfig") - if err = r.UpdateResource(ctx, config.GetConfigObject()); err != nil { - return true, err - } - } - } - - return true, nil -} - -func (r *KuadrantReconciler) registerExternalAuthorizerOSSM(ctx context.Context, kObj *kuadrantv1beta1.Kuadrant) error { - logger, _ := logr.FromContext(ctx) - - smcp := &maistrav2.ServiceMeshControlPlane{} - - smcpKey := client.ObjectKey{Name: controlPlaneProviderName(), Namespace: controlPlaneProviderNamespace()} - if err := r.GetResource(ctx, smcpKey, smcp); err != nil { - logger.V(1).Info("failed to get servicemeshcontrolplane object", "key", smcp, "err", err) - if apierrors.IsNotFound(err) || meta.IsNoMatchError(err) { - logger.Info("OSSM installation as GatewayAPI provider not found") - return nil - } - } - - if err := r.registerServiceMeshMember(ctx, kObj); err != nil { - return err - } - - smcpWrapper := istio.NewOSSMControlPlaneWrapper(smcp) - kuadrantAuthorizer := istio.NewKuadrantAuthorizer(kObj.Namespace) - - hasAuthorizer, err := istio.HasKuadrantAuthorizer(smcpWrapper, *kuadrantAuthorizer) - if err != nil { - return err - } - if !hasAuthorizer { - err = istio.RegisterKuadrantAuthorizer(smcpWrapper, kuadrantAuthorizer) - if err != nil { - return err - } - logger.Info("adding external authorizer to OSSM meshconfig") - if err := r.UpdateResource(ctx, smcpWrapper.GetConfigObject()); err != nil { - return err - } - } - - return nil -} - -func (r *KuadrantReconciler) getIstioConfigObjects(ctx context.Context) ([]istio.ConfigWrapper, error) { - logger, _ := logr.FromContext(ctx) - var configsToUpdate []istio.ConfigWrapper - - iop := &iopv1alpha1.IstioOperator{} - istKey := client.ObjectKey{Name: controlPlaneProviderName(), Namespace: controlPlaneProviderNamespace()} - err := r.GetResource(ctx, istKey, iop) - // TODO(eguzki): 🔥 this spaghetti code 🔥 - if err == nil { - configsToUpdate = append(configsToUpdate, istio.NewOperatorWrapper(iop)) - } else if meta.IsNoMatchError(err) || apierrors.IsNotFound(err) { - // IstioOperator not existing or not CRD not found, so check for Istio CR instead - ist := &istiov1alpha1.Istio{} - istKey := client.ObjectKey{Name: istioCRName} - if err := r.GetResource(ctx, istKey, ist); err != nil { - logger.V(1).Info("failed to get istio object", "key", istKey, "err", err) - if meta.IsNoMatchError(err) || apierrors.IsNotFound(err) { - // return nil and nil if there's no istiooperator or istio CR - logger.Info("Istio installation as GatewayAPI provider not found") - return nil, nil - } - // return nil and err if there's an error other than not found (no istio CR) - return nil, err - } - configsToUpdate = append(configsToUpdate, istio.NewSailWrapper(ist)) - } else { - logger.V(1).Info("failed to get istiooperator object", "key", istKey, "err", err) - return nil, err - } - - istioConfigMap := &corev1.ConfigMap{} - if err := r.GetResource(ctx, client.ObjectKey{Name: controlPlaneConfigMapName(), Namespace: controlPlaneProviderNamespace()}, istioConfigMap); err != nil { - if !apierrors.IsNotFound(err) { - logger.V(1).Info("failed to get istio configMap", "key", istKey, "err", err) - return configsToUpdate, err - } - } else { - configsToUpdate = append(configsToUpdate, istio.NewConfigMapWrapper(istioConfigMap)) - } - return configsToUpdate, nil -} - -func (r *KuadrantReconciler) registerServiceMeshMember(ctx context.Context, kObj *kuadrantv1beta1.Kuadrant) error { - member := &maistrav1.ServiceMeshMember{ - TypeMeta: metav1.TypeMeta{ - Kind: "ServiceMeshMember", - APIVersion: maistrav1.SchemeGroupVersion.String(), - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "default", - Namespace: kObj.Namespace, - }, - Spec: maistrav1.ServiceMeshMemberSpec{ - ControlPlaneRef: maistrav1.ServiceMeshControlPlaneRef{ - Name: controlPlaneProviderName(), - Namespace: controlPlaneProviderNamespace(), - }, - }, - } - - err := r.SetOwnerReference(kObj, member) - if err != nil { - return err - } - - return r.ReconcileResource(ctx, &maistrav1.ServiceMeshMember{}, member, reconcilers.CreateOnlyMutator) -} - -func (r *KuadrantReconciler) reconcileSpec(ctx context.Context, kObj *kuadrantv1beta1.Kuadrant) error { - return r.registerExternalAuthorizer(ctx, kObj) -} - // SetupWithManager sets up the controller with the Manager. func (r *KuadrantReconciler) SetupWithManager(mgr ctrl.Manager) error { ok, err := kuadrantgatewayapi.IsGatewayAPIInstalled(mgr.GetRESTMapper()) diff --git a/controllers/limitador_limits_reconciler.go b/controllers/limitador_limits_reconciler.go index 1779fb56d..dd4548654 100644 --- a/controllers/limitador_limits_reconciler.go +++ b/controllers/limitador_limits_reconciler.go @@ -22,19 +22,27 @@ import ( "github.com/kuadrant/kuadrant-operator/pkg/ratelimit" ) -type limitadorLimitsReconciler struct { +type LimitadorLimitsReconciler struct { client *dynamic.DynamicClient } -func (r *limitadorLimitsReconciler) Subscription() controller.Subscription { +// LimitadorLimitsReconciler reconciles to events with impact to change the state of the Limitador custom resources regarding the definitions for the effective rate limit policies +func (r *LimitadorLimitsReconciler) Subscription() controller.Subscription { return controller.Subscription{ ReconcileFunc: r.Reconcile, - Events: rateLimitEventMatchers, + Events: []controller.ResourceEventMatcher{ + {Kind: &kuadrantv1beta1.KuadrantGroupKind}, + {Kind: &machinery.GatewayClassGroupKind}, + {Kind: &machinery.GatewayGroupKind}, + {Kind: &machinery.HTTPRouteGroupKind}, + {Kind: &kuadrantv1beta3.RateLimitPolicyGroupKind}, + {Kind: &kuadrantv1beta1.LimitadorGroupKind}, + }, } } -func (r *limitadorLimitsReconciler) Reconcile(ctx context.Context, _ []controller.ResourceEvent, topology *machinery.Topology, _ error, state *sync.Map) error { - logger := controller.LoggerFromContext(ctx).WithName("limitadorLimitsReconciler") +func (r *LimitadorLimitsReconciler) Reconcile(ctx context.Context, _ []controller.ResourceEvent, topology *machinery.Topology, _ error, state *sync.Map) error { + logger := controller.LoggerFromContext(ctx).WithName("LimitadorLimitsReconciler") limitador, err := GetLimitadorFromTopology(topology) if err != nil { @@ -77,31 +85,32 @@ func (r *limitadorLimitsReconciler) Reconcile(ctx context.Context, _ []controlle return nil } -func (r *limitadorLimitsReconciler) buildLimitadorLimits(ctx context.Context, state *sync.Map) ([]limitadorv1alpha1.RateLimit, error) { - logger := controller.LoggerFromContext(ctx).WithName("limitadorLimitsReconciler").WithName("buildLimitadorLimits") +func (r *LimitadorLimitsReconciler) buildLimitadorLimits(ctx context.Context, state *sync.Map) ([]limitadorv1alpha1.RateLimit, error) { + logger := controller.LoggerFromContext(ctx).WithName("LimitadorLimitsReconciler").WithName("buildLimitadorLimits") effectivePolicies, ok := state.Load(StateEffectiveRateLimitPolicies) if !ok { return nil, ErrMissingStateEffectiveRateLimitPolicies } + effectivePoliciesMap := effectivePolicies.(EffectiveRateLimitPolicies) - logger.V(1).Info("building limitador limits", "effectivePolicies", len(effectivePolicies.(EffectiveRateLimitPolicies))) + logger.V(1).Info("building limitador limits", "effectivePolicies", len(effectivePoliciesMap)) rateLimitIndex := ratelimit.NewIndex() - for pathID, effectivePolicy := range effectivePolicies.(EffectiveRateLimitPolicies) { + for pathID, effectivePolicy := range effectivePoliciesMap { _, _, _, httpRoute, _, _ := common.ObjectsInRequestPath(effectivePolicy.Path) limitsNamespace := LimitsNamespaceFromRoute(httpRoute.HTTPRoute) for limitKey, mergeableLimit := range effectivePolicy.Spec.Rules() { policy, found := lo.Find(kuadrantv1.PoliciesInPath(effectivePolicy.Path, isRateLimitPolicyAcceptedAndNotDeletedFunc(state)), func(p machinery.Policy) bool { - return p.GetLocator() == mergeableLimit.Source + return p.GetLocator() == mergeableLimit.GetSource() }) if !found { // should never happen - logger.Error(fmt.Errorf("origin policy %s not found in path %s", mergeableLimit.Source, pathID), "failed to build limitador limit definition") + logger.Error(fmt.Errorf("origin policy %s not found in path %s", mergeableLimit.GetSource(), pathID), "failed to build limitador limit definition") continue } limitIdentifier := LimitNameToLimitadorIdentifier(k8stypes.NamespacedName{Name: policy.GetName(), Namespace: policy.GetNamespace()}, limitKey) - limit := mergeableLimit.Spec.(kuadrantv1beta3.Limit) + limit := mergeableLimit.GetSpec().(*kuadrantv1beta3.Limit) rateLimits := lo.Map(limit.Rates, func(rate kuadrantv1beta3.Rate, _ int) limitadorv1alpha1.RateLimit { maxValue, seconds := rate.ToSeconds() return limitadorv1alpha1.RateLimit{ diff --git a/controllers/ratelimitpolicies_validator.go b/controllers/ratelimit_policies_validator.go similarity index 85% rename from controllers/ratelimitpolicies_validator.go rename to controllers/ratelimit_policies_validator.go index 76a89d3da..ccd9fdb27 100644 --- a/controllers/ratelimitpolicies_validator.go +++ b/controllers/ratelimit_policies_validator.go @@ -15,9 +15,10 @@ import ( kuadrant "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" ) -type rateLimitPolicyValidator struct{} +type RateLimitPolicyValidator struct{} -func (r *rateLimitPolicyValidator) Subscription() controller.Subscription { +// RateLimitPolicyValidator subscribes to events with potential to flip the validity of rate limit policies +func (r *RateLimitPolicyValidator) Subscription() controller.Subscription { return controller.Subscription{ ReconcileFunc: r.Validate, Events: []controller.ResourceEventMatcher{ @@ -29,8 +30,8 @@ func (r *rateLimitPolicyValidator) Subscription() controller.Subscription { } } -func (r *rateLimitPolicyValidator) Validate(ctx context.Context, _ []controller.ResourceEvent, topology *machinery.Topology, _ error, state *sync.Map) error { - logger := controller.LoggerFromContext(ctx).WithName("rateLimitPolicyValidator") +func (r *RateLimitPolicyValidator) Validate(ctx context.Context, _ []controller.ResourceEvent, topology *machinery.Topology, _ error, state *sync.Map) error { + logger := controller.LoggerFromContext(ctx).WithName("RateLimitPolicyValidator") policies := topology.Policies().Items(func(o machinery.Object) bool { return o.GroupVersionKind().GroupKind() == kuadrantv1beta3.RateLimitPolicyGroupKind diff --git a/controllers/ratelimitpolicy_status_updater.go b/controllers/ratelimit_policy_status_updater.go similarity index 80% rename from controllers/ratelimitpolicy_status_updater.go rename to controllers/ratelimit_policy_status_updater.go index 727d731c1..ecc7a0a22 100644 --- a/controllers/ratelimitpolicy_status_updater.go +++ b/controllers/ratelimit_policy_status_updater.go @@ -14,7 +14,6 @@ import ( "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime/schema" k8stypes "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/dynamic" "k8s.io/utils/ptr" @@ -31,19 +30,31 @@ import ( "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" ) -type rateLimitPolicyStatusUpdater struct { +type RateLimitPolicyStatusUpdater struct { client *dynamic.DynamicClient } -func (r *rateLimitPolicyStatusUpdater) Subscription() controller.Subscription { +// RateLimitPolicyStatusUpdater subscribe to events with potential impact on the status of RateLimitPolicy resources +func (r *RateLimitPolicyStatusUpdater) Subscription() controller.Subscription { return controller.Subscription{ ReconcileFunc: r.UpdateStatus, - Events: rateLimitEventMatchers, + Events: []controller.ResourceEventMatcher{ + {Kind: &kuadrantv1beta1.KuadrantGroupKind}, + {Kind: &machinery.GatewayClassGroupKind}, + {Kind: &machinery.GatewayGroupKind}, + {Kind: &machinery.HTTPRouteGroupKind}, + {Kind: &kuadrantv1beta3.RateLimitPolicyGroupKind}, + {Kind: &kuadrantv1beta1.LimitadorGroupKind}, + {Kind: &kuadrantistio.EnvoyFilterGroupKind}, + {Kind: &kuadrantistio.WasmPluginGroupKind}, + {Kind: &kuadrantenvoygateway.EnvoyPatchPolicyGroupKind}, + {Kind: &kuadrantenvoygateway.EnvoyExtensionPolicyGroupKind}, + }, } } -func (r *rateLimitPolicyStatusUpdater) UpdateStatus(ctx context.Context, _ []controller.ResourceEvent, topology *machinery.Topology, _ error, state *sync.Map) error { - logger := controller.LoggerFromContext(ctx).WithName("rateLimitPolicyStatusUpdater") +func (r *RateLimitPolicyStatusUpdater) UpdateStatus(ctx context.Context, _ []controller.ResourceEvent, topology *machinery.Topology, _ error, state *sync.Map) error { + logger := controller.LoggerFromContext(ctx).WithName("RateLimitPolicyStatusUpdater") policies := lo.FilterMap(topology.Policies().Items(), func(item machinery.Policy, index int) (*kuadrantv1beta3.RateLimitPolicy, bool) { p, ok := item.(*kuadrantv1beta3.RateLimitPolicy) @@ -52,8 +63,8 @@ func (r *rateLimitPolicyStatusUpdater) UpdateStatus(ctx context.Context, _ []con policyAcceptedFunc := rateLimitPolicyAcceptedStatusFunc(state) - logger.V(1).Info("updating rate limit policy statuses", "policies", len(policies)) - defer logger.V(1).Info("finished updating rate limit policy statuses") + logger.V(1).Info("updating ratelimitpolicy statuses", "policies", len(policies)) + defer logger.V(1).Info("finished updating ratelimitpolicy statuses") for _, policy := range policies { if policy.GetDeletionTimestamp() != nil { @@ -102,7 +113,7 @@ func (r *rateLimitPolicyStatusUpdater) UpdateStatus(ctx context.Context, _ []con return nil } -func (r *rateLimitPolicyStatusUpdater) enforcedCondition(policy *kuadrantv1beta3.RateLimitPolicy, topology *machinery.Topology, state *sync.Map) *metav1.Condition { +func (r *RateLimitPolicyStatusUpdater) enforcedCondition(policy *kuadrantv1beta3.RateLimitPolicy, topology *machinery.Topology, state *sync.Map) *metav1.Condition { policyKind := kuadrantv1beta3.RateLimitPolicyGroupKind.Kind effectivePolicies, ok := state.Load(StateEffectiveRateLimitPolicies) @@ -110,10 +121,15 @@ func (r *rateLimitPolicyStatusUpdater) enforcedCondition(policy *kuadrantv1beta3 return kuadrant.EnforcedCondition(policy, kuadrant.NewErrUnknown(policyKind, ErrMissingStateEffectiveRateLimitPolicies), false) } + type affectedGateway struct { + gateway *machinery.Gateway + gatewayClass *machinery.GatewayClass + } + // check the state of the rules of the policy in the effective policies policyRuleKeys := lo.Keys(policy.Rules()) - affectedPaths := map[string][][]machinery.Targetable{} // policyRuleKey → topological paths affected by the policy rule - overridingPolicies := map[string][]string{} // policyRuleKey → locators of policies overriding the policy rule + overridingPolicies := map[string][]string{} // policyRuleKey → locators of policies overriding the policy rule + affectedGateways := map[string]affectedGateway{} // Gateway locator → {GatewayClass, Gateway} for _, effectivePolicy := range effectivePolicies.(EffectiveRateLimitPolicies) { if len(kuadrantv1.PoliciesInPath(effectivePolicy.Path, func(p machinery.Policy) bool { return p.GetLocator() == policy.GetLocator() })) == 0 { continue @@ -124,25 +140,24 @@ func (r *rateLimitPolicyStatusUpdater) enforcedCondition(policy *kuadrantv1beta3 } effectivePolicyRules := effectivePolicy.Spec.Rules() for _, policyRuleKey := range policyRuleKeys { - if effectivePolicyRule, ok := effectivePolicyRules[policyRuleKey]; !ok || (ok && effectivePolicyRule.Source != policy.GetLocator()) { + if effectivePolicyRule, ok := effectivePolicyRules[policyRuleKey]; !ok || (ok && effectivePolicyRule.GetSource() != policy.GetLocator()) { // policy rule has been overridden by another policy var overriddenBy string if ok { // TODO(guicassolato): !ok → we cannot tell which policy is overriding the rule, this information is lost when the policy rule is dropped during an atomic override - overriddenBy = effectivePolicyRule.Source + overriddenBy = effectivePolicyRule.GetSource() } overridingPolicies[policyRuleKey] = append(overridingPolicies[policyRuleKey], overriddenBy) continue } - if affectedPaths[policyRuleKey] == nil { - affectedPaths[policyRuleKey] = [][]machinery.Targetable{} + // policy rule is in the effective policy, track the Gateway affected by the policy + affectedGateways[gateway.GetLocator()] = affectedGateway{ + gateway: gateway, + gatewayClass: gatewayClass, } - affectedPaths[policyRuleKey] = append(affectedPaths[policyRuleKey], effectivePolicy.Path) } } - // no rules of the policy found in the effective policies - if len(affectedPaths) == 0 { - // no rules of the policy have been overridden by any other policy - if len(overridingPolicies) == 0 { + if len(affectedGateways) == 0 { // no rules of the policy found in the effective policies + if len(overridingPolicies) == 0 { // no rules of the policy have been overridden by any other policy return kuadrant.EnforcedCondition(policy, kuadrant.NewErrNoRoutes(policyKind), false) } // all rules of the policy have been overridden by at least one other policy @@ -168,21 +183,7 @@ func (r *rateLimitPolicyStatusUpdater) enforcedCondition(policy *kuadrantv1beta3 } } - type affectedGateway struct { - gateway *machinery.Gateway - gatewayClass *machinery.GatewayClass - } - // check the status of the gateways' configuration resources - affectedGateways := lo.UniqBy(lo.Map(lo.Flatten(lo.Values(affectedPaths)), func(path []machinery.Targetable, _ int) affectedGateway { - gatewayClass, gateway, _, _, _, _ := common.ObjectsInRequestPath(path) - return affectedGateway{ - gateway: gateway, - gatewayClass: gatewayClass, - } - }), func(g affectedGateway) string { - return g.gateway.GetLocator() - }) for _, g := range affectedGateways { switch g.gatewayClass.Spec.ControllerName { case istioGatewayControllerName: @@ -221,16 +222,3 @@ func (r *rateLimitPolicyStatusUpdater) enforcedCondition(policy *kuadrantv1beta3 return kuadrant.EnforcedCondition(policy, nil, len(overridingPolicies) == 0) } - -func gatewayComponentsToSync(gateway *machinery.Gateway, componentGroupKind schema.GroupKind, modifiedGatewayLocators any, topology *machinery.Topology, requiredCondition func(machinery.Object) bool) []string { - missingConditionInTopologyFunc := func() bool { - obj, found := lo.Find(topology.Objects().Children(gateway), func(child machinery.Object) bool { - return child.GroupVersionKind().GroupKind() == componentGroupKind - }) - return !found || !requiredCondition(obj) - } - if (modifiedGatewayLocators != nil && lo.Contains(modifiedGatewayLocators.([]string), gateway.GetLocator())) || missingConditionInTopologyFunc() { - return []string{fmt.Sprintf("%s (%s/%s)", componentGroupKind.Kind, gateway.GetNamespace(), gateway.GetName())} - } - return nil -} diff --git a/controllers/ratelimit_workflow.go b/controllers/ratelimit_workflow_helpers.go similarity index 67% rename from controllers/ratelimit_workflow.go rename to controllers/ratelimit_workflow_helpers.go index de4ec0ccc..1eeb9a166 100644 --- a/controllers/ratelimit_workflow.go +++ b/controllers/ratelimit_workflow_helpers.go @@ -15,8 +15,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" k8stypes "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/dynamic" - "k8s.io/utils/env" gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" @@ -24,45 +22,21 @@ import ( kuadrantv1beta1 "github.com/kuadrant/kuadrant-operator/api/v1beta1" kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" "github.com/kuadrant/kuadrant-operator/pkg/common" - kuadrantenvoygateway "github.com/kuadrant/kuadrant-operator/pkg/envoygateway" - kuadrantistio "github.com/kuadrant/kuadrant-operator/pkg/istio" "github.com/kuadrant/kuadrant-operator/pkg/wasm" ) -const ( - rateLimitClusterLabelKey = "kuadrant.io/rate-limit-cluster" - - // make these configurable? - istioGatewayControllerName = "istio.io/gateway-controller" - envoyGatewayGatewayControllerName = "gateway.envoyproxy.io/gatewayclass-controller" -) +const rateLimitObjectLabelKey = "kuadrant.io/ratelimit" var ( - WASMFilterImageURL = env.GetString("RELATED_IMAGE_WASMSHIM", "oci://quay.io/kuadrant/wasm-shim:latest") - StateRateLimitPolicyValid = "RateLimitPolicyValid" StateEffectiveRateLimitPolicies = "EffectiveRateLimitPolicies" StateLimitadorLimitsModified = "LimitadorLimitsModified" StateIstioRateLimitClustersModified = "IstioRateLimitClustersModified" - StateIstioExtensionsModified = "IstioExtensionsModified" StateEnvoyGatewayRateLimitClustersModified = "EnvoyGatewayRateLimitClustersModified" - StateEnvoyGatewayExtensionsModified = "EnvoyGatewayExtensionsModified" ErrMissingLimitador = fmt.Errorf("missing limitador object in the topology") + ErrMissingLimitadorServiceInfo = fmt.Errorf("missing limitador service info in the limitador object") ErrMissingStateEffectiveRateLimitPolicies = fmt.Errorf("missing rate limit effective policies stored in the reconciliation state") - - rateLimitEventMatchers = []controller.ResourceEventMatcher{ // matches reconciliation events that change the rate limit definitions or status of rate limit policies - {Kind: &kuadrantv1beta1.KuadrantGroupKind}, - {Kind: &machinery.GatewayClassGroupKind}, - {Kind: &machinery.GatewayGroupKind}, - {Kind: &machinery.HTTPRouteGroupKind}, - {Kind: &kuadrantv1beta3.RateLimitPolicyGroupKind}, - {Kind: &kuadrantv1beta1.LimitadorGroupKind}, - {Kind: &kuadrantistio.EnvoyFilterGroupKind}, - {Kind: &kuadrantistio.WasmPluginGroupKind}, - {Kind: &kuadrantenvoygateway.EnvoyPatchPolicyGroupKind}, - {Kind: &kuadrantenvoygateway.EnvoyExtensionPolicyGroupKind}, - } ) //+kubebuilder:rbac:groups=kuadrant.io,resources=ratelimitpolicies,verbs=get;list;watch;create;update;patch;delete @@ -70,31 +44,6 @@ var ( //+kubebuilder:rbac:groups=kuadrant.io,resources=ratelimitpolicies/finalizers,verbs=update //+kubebuilder:rbac:groups=limitador.kuadrant.io,resources=limitadors,verbs=get;list;watch;create;update;patch;delete -func NewRateLimitWorkflow(client *dynamic.DynamicClient, isIstioInstalled, isEnvoyGatewayInstalled bool) *controller.Workflow { - effectiveRateLimitPoliciesWorkflow := &controller.Workflow{ - Precondition: (&effectiveRateLimitPolicyReconciler{client: client}).Subscription().Reconcile, - Tasks: []controller.ReconcileFunc{ - (&limitadorLimitsReconciler{client: client}).Subscription().Reconcile, - }, - } - - if isIstioInstalled { - effectiveRateLimitPoliciesWorkflow.Tasks = append(effectiveRateLimitPoliciesWorkflow.Tasks, (&istioRateLimitClusterReconciler{client: client}).Subscription().Reconcile) - effectiveRateLimitPoliciesWorkflow.Tasks = append(effectiveRateLimitPoliciesWorkflow.Tasks, (&istioExtensionReconciler{client: client}).Subscription().Reconcile) - } - - if isEnvoyGatewayInstalled { - effectiveRateLimitPoliciesWorkflow.Tasks = append(effectiveRateLimitPoliciesWorkflow.Tasks, (&envoyGatewayRateLimitClusterReconciler{client: client}).Subscription().Reconcile) - effectiveRateLimitPoliciesWorkflow.Tasks = append(effectiveRateLimitPoliciesWorkflow.Tasks, (&envoyGatewayExtensionReconciler{client: client}).Subscription().Reconcile) - } - - return &controller.Workflow{ - Precondition: (&rateLimitPolicyValidator{}).Subscription().Reconcile, - Tasks: []controller.ReconcileFunc{effectiveRateLimitPoliciesWorkflow.Run}, - Postcondition: (&rateLimitPolicyStatusUpdater{client: client}).Subscription().Reconcile, - } -} - func GetLimitadorFromTopology(topology *machinery.Topology) (*limitadorv1alpha1.Limitador, error) { kuadrant, err := GetKuadrantFromTopology(topology) if err != nil { @@ -137,7 +86,7 @@ func LimitNameToLimitadorIdentifier(rlpKey k8stypes.NamespacedName, uniqueLimitN func RateLimitObjectLabels() labels.Set { m := KuadrantManagedObjectLabels() - m[rateLimitClusterLabelKey] = "true" + m[rateLimitObjectLabelKey] = "true" return m } @@ -174,21 +123,25 @@ func rateLimitClusterPatch(host string, port int) map[string]any { } } -func rateLimitWasmActionBuilder(pathID string, effectivePolicy EffectiveRateLimitPolicy, state *sync.Map) wasm.ActionBuilderFunc { +func buildWasmActionsForRateLimit(effectivePolicy EffectiveRateLimitPolicy, state *sync.Map) []wasm.Action { policiesInPath := kuadrantv1.PoliciesInPath(effectivePolicy.Path, isRateLimitPolicyAcceptedAndNotDeletedFunc(state)) + _, _, _, httpRoute, _, _ := common.ObjectsInRequestPath(effectivePolicy.Path) limitsNamespace := LimitsNamespaceFromRoute(httpRoute.HTTPRoute) - return func(uniquePolicyRuleKey string, policyRule kuadrantv1.MergeableRule) (wasm.Action, error) { + + return lo.FilterMap(lo.Entries(effectivePolicy.Spec.Rules()), func(r lo.Entry[string, kuadrantv1.MergeableRule], _ int) (wasm.Action, bool) { + uniquePolicyRuleKey := r.Key + policyRule := r.Value source, found := lo.Find(policiesInPath, func(p machinery.Policy) bool { - return p.GetLocator() == policyRule.Source + return p.GetLocator() == policyRule.GetSource() }) if !found { // should never happen - return wasm.Action{}, fmt.Errorf("could not find source policy %s in path %s", policyRule.Source, pathID) + return wasm.Action{}, false } limitIdentifier := LimitNameToLimitadorIdentifier(k8stypes.NamespacedName{Name: source.GetName(), Namespace: source.GetNamespace()}, uniquePolicyRuleKey) - limit := policyRule.Spec.(kuadrantv1beta3.Limit) - return wasmActionFromLimit(limit, limitIdentifier, limitsNamespace), nil - } + limit := policyRule.GetSpec().(*kuadrantv1beta3.Limit) + return wasmActionFromLimit(limit, limitIdentifier, limitsNamespace), true + }) } // wasmActionFromLimit builds a wasm rate-limit action for a given limit. @@ -196,7 +149,7 @@ func rateLimitWasmActionBuilder(pathID string, effectivePolicy EffectiveRateLimi // // The only action of the rule is the ratelimit service, whose data includes the activation of the limit // and any counter qualifier of the limit. -func wasmActionFromLimit(limit kuadrantv1beta3.Limit, limitIdentifier, scope string) wasm.Action { +func wasmActionFromLimit(limit *kuadrantv1beta3.Limit, limitIdentifier, scope string) wasm.Action { action := wasm.Action{ ServiceName: wasm.RateLimitServiceName, Scope: scope, @@ -210,7 +163,7 @@ func wasmActionFromLimit(limit kuadrantv1beta3.Limit, limitIdentifier, scope str return action } -func wasmDataFromLimit(limitIdentifier string, limit kuadrantv1beta3.Limit) (data []wasm.DataType) { +func wasmDataFromLimit(limitIdentifier string, limit *kuadrantv1beta3.Limit) (data []wasm.DataType) { // static key representing the limit data = append(data, wasm.DataType{ diff --git a/controllers/ratelimit_workflow_test.go b/controllers/ratelimit_workflow_test.go index 759babe5f..00f1dc02b 100644 --- a/controllers/ratelimit_workflow_test.go +++ b/controllers/ratelimit_workflow_test.go @@ -70,14 +70,14 @@ func TestLimitNameToLimitadorIdentifier(t *testing.T) { func TestWasmActionFromLimit(t *testing.T) { testCases := []struct { name string - limit kuadrantv1beta3.Limit + limit *kuadrantv1beta3.Limit limitIdentifier string scope string expectedAction wasm.Action }{ { name: "limit without conditions nor counters", - limit: kuadrantv1beta3.Limit{}, + limit: &kuadrantv1beta3.Limit{}, limitIdentifier: "limit.myLimit__d681f6c3", scope: "my-ns/my-route", expectedAction: wasm.Action{ @@ -97,7 +97,7 @@ func TestWasmActionFromLimit(t *testing.T) { }, { name: "limit with counter qualifiers", - limit: kuadrantv1beta3.Limit{ + limit: &kuadrantv1beta3.Limit{ Counters: []kuadrantv1beta3.ContextSelector{"auth.identity.username"}, }, limitIdentifier: "limit.myLimit__d681f6c3", @@ -126,7 +126,7 @@ func TestWasmActionFromLimit(t *testing.T) { }, { name: "limit with counter qualifiers and when conditions", - limit: kuadrantv1beta3.Limit{ + limit: &kuadrantv1beta3.Limit{ Counters: []kuadrantv1beta3.ContextSelector{"auth.identity.username"}, When: []kuadrantv1beta3.WhenCondition{ { diff --git a/controllers/state_of_the_world.go b/controllers/state_of_the_world.go index dfa703a3e..4c1672b94 100644 --- a/controllers/state_of_the_world.go +++ b/controllers/state_of_the_world.go @@ -8,7 +8,8 @@ import ( certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" egv1alpha1 "github.com/envoyproxy/gateway/api/v1alpha1" "github.com/go-logr/logr" - authorinov1beta1 "github.com/kuadrant/authorino-operator/api/v1beta1" + authorinooperatorv1beta1 "github.com/kuadrant/authorino-operator/api/v1beta1" + authorinov1beta2 "github.com/kuadrant/authorino/api/v1beta2" kuadrantdnsv1alpha1 "github.com/kuadrant/dns-operator/api/v1alpha1" limitadorv1alpha1 "github.com/kuadrant/limitador-operator/api/v1alpha1" "github.com/kuadrant/policy-machinery/controller" @@ -17,7 +18,6 @@ import ( "github.com/samber/lo" istioclientgoextensionv1alpha1 "istio.io/client-go/pkg/apis/extensions/v1alpha1" istioclientnetworkingv1alpha3 "istio.io/client-go/pkg/apis/networking/v1alpha3" - istioclientgosecurityv1beta1 "istio.io/client-go/pkg/apis/security/v1beta1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" @@ -109,10 +109,16 @@ func NewPolicyMachineryController(manager ctrlruntime.Manager, client *dynamic.D metav1.NamespaceAll, )), controller.WithRunnable("authorino watcher", controller.Watch( - &authorinov1beta1.Authorino{}, + &authorinooperatorv1beta1.Authorino{}, kuadrantv1beta1.AuthorinosResource, metav1.NamespaceAll, )), + controller.WithRunnable("authconfig watcher", controller.Watch( + &authorinov1beta2.AuthConfig{}, + kuadrantv1beta1.AuthConfigsResource, + metav1.NamespaceAll, + controller.FilterResourcesByLabel[*authorinov1beta2.AuthConfig](fmt.Sprintf("%s=true", kuadrantManagedLabelKey)), + )), controller.WithPolicyKinds( kuadrantv1alpha1.DNSPolicyGroupKind, kuadrantv1alpha1.TLSPolicyGroupKind, @@ -124,11 +130,13 @@ func NewPolicyMachineryController(manager ctrlruntime.Manager, client *dynamic.D ConfigMapGroupKind, kuadrantv1beta1.LimitadorGroupKind, kuadrantv1beta1.AuthorinoGroupKind, + kuadrantv1beta1.AuthConfigGroupKind, ), controller.WithObjectLinks( kuadrantv1beta1.LinkKuadrantToGatewayClasses, kuadrantv1beta1.LinkKuadrantToLimitador, kuadrantv1beta1.LinkKuadrantToAuthorino, + kuadrantv1beta1.LinkHTTPRouteRuleToAuthConfig, ), } @@ -224,15 +232,9 @@ func (b *BootOptionsBuilder) getEnvoyGatewayOptions() []controller.ControllerOpt metav1.NamespaceAll, controller.FilterResourcesByLabel[*egv1alpha1.EnvoyExtensionPolicy](fmt.Sprintf("%s=true", kuadrantManagedLabelKey)), )), - controller.WithRunnable("envoysecuritypolicy watcher", controller.Watch( - &egv1alpha1.SecurityPolicy{}, - envoygateway.SecurityPoliciesResource, - metav1.NamespaceAll, - )), controller.WithObjectKinds( envoygateway.EnvoyPatchPolicyGroupKind, envoygateway.EnvoyExtensionPolicyGroupKind, - envoygateway.SecurityPolicyGroupKind, ), controller.WithObjectLinks( envoygateway.LinkGatewayToEnvoyPatchPolicy, @@ -265,15 +267,9 @@ func (b *BootOptionsBuilder) getIstioOptions() []controller.ControllerOption { metav1.NamespaceAll, controller.FilterResourcesByLabel[*istioclientgoextensionv1alpha1.WasmPlugin](fmt.Sprintf("%s=true", kuadrantManagedLabelKey)), )), - controller.WithRunnable("authorizationpolicy watcher", controller.Watch( - &istioclientgosecurityv1beta1.AuthorizationPolicy{}, - istio.AuthorizationPoliciesResource, - metav1.NamespaceAll, - )), controller.WithObjectKinds( istio.EnvoyFilterGroupKind, istio.WasmPluginGroupKind, - istio.AuthorizationPolicyGroupKind, ), controller.WithObjectLinks( istio.LinkGatewayToEnvoyFilter, @@ -343,8 +339,7 @@ func (b *BootOptionsBuilder) Reconciler() controller.ReconcileFunc { NewLimitadorReconciler(b.client).Subscription().Reconcile, NewDNSWorkflow(b.client, b.manager.GetScheme()).Run, NewTLSWorkflow(b.client, b.manager.GetScheme(), b.isCertManagerInstalled).Run, - NewAuthWorkflow().Run, - NewRateLimitWorkflow(b.client, b.isIstioInstalled, b.isEnvoyGatewayInstalled).Run, + NewDataPlanePoliciesWorkflow(b.client, b.isIstioInstalled, b.isEnvoyGatewayInstalled).Run, }, Postcondition: finalStepsWorkflow(b.client, b.isIstioInstalled, b.isGatewayAPIInstalled).Run, } diff --git a/controllers/test_common.go b/controllers/test_common.go index c4a9e3efb..60d5b5c42 100644 --- a/controllers/test_common.go +++ b/controllers/test_common.go @@ -55,7 +55,6 @@ import ( kuadrantv1beta1 "github.com/kuadrant/kuadrant-operator/api/v1beta1" kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" "github.com/kuadrant/kuadrant-operator/pkg/library/fieldindexers" - "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" "github.com/kuadrant/kuadrant-operator/pkg/library/reconcilers" ) @@ -73,20 +72,6 @@ func SetupKuadrantOperatorForTest(s *runtime.Scheme, cfg *rest.Config) { ) Expect(err).ToNot(HaveOccurred()) - authPolicyBaseReconciler := reconcilers.NewBaseReconciler( - mgr.GetClient(), - mgr.GetScheme(), - mgr.GetAPIReader(), - log.Log.WithName("authpolicy"), - ) - - err = (&AuthPolicyReconciler{ - BaseReconciler: authPolicyBaseReconciler, - TargetRefReconciler: reconcilers.TargetRefReconciler{Client: mgr.GetClient()}, - AffectedPolicyMap: kuadrant.NewAffectedPolicyMap(), - }).SetupWithManager(mgr) - Expect(err).NotTo(HaveOccurred()) - kuadrantBaseReconciler := reconcilers.NewBaseReconciler( mgr.GetClient(), mgr.GetScheme(), @@ -114,19 +99,6 @@ func SetupKuadrantOperatorForTest(s *runtime.Scheme, cfg *rest.Config) { Expect(err).NotTo(HaveOccurred()) - authPolicyIstioAuthorizationPolicyReconciler := reconcilers.NewBaseReconciler( - mgr.GetClient(), - mgr.GetScheme(), - mgr.GetAPIReader(), - log.Log.WithName("authpolicy").WithName("istioauthorizationpolicy"), - ) - - err = (&AuthPolicyIstioAuthorizationPolicyReconciler{ - BaseReconciler: authPolicyIstioAuthorizationPolicyReconciler, - }).SetupWithManager(mgr) - - Expect(err).NotTo(HaveOccurred()) - targetStatusBaseReconciler := reconcilers.NewBaseReconciler( mgr.GetClient(), mgr.GetScheme(), @@ -140,32 +112,6 @@ func SetupKuadrantOperatorForTest(s *runtime.Scheme, cfg *rest.Config) { Expect(err).NotTo(HaveOccurred()) - authPolicyEnvoySecurityPolicyReconciler := reconcilers.NewBaseReconciler( - mgr.GetClient(), - mgr.GetScheme(), - mgr.GetAPIReader(), - log.Log.WithName("authpolicy").WithName("securitypolicy"), - ) - - err = (&AuthPolicyEnvoySecurityPolicyReconciler{ - BaseReconciler: authPolicyEnvoySecurityPolicyReconciler, - }).SetupWithManager(mgr) - - Expect(err).NotTo(HaveOccurred()) - - envoySecurityPolicyReferenceGrantReconciler := reconcilers.NewBaseReconciler( - mgr.GetClient(), - mgr.GetScheme(), - mgr.GetAPIReader(), - log.Log.WithName("authpolicy").WithName("referencegrant"), - ) - - err = (&EnvoySecurityPolicyReferenceGrantReconciler{ - BaseReconciler: envoySecurityPolicyReferenceGrantReconciler, - }).SetupWithManager(mgr) - - Expect(err).NotTo(HaveOccurred()) - dClient, err := dynamic.NewForConfig(mgr.GetConfig()) Expect(err).NotTo(HaveOccurred()) 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 index bfefd94d5..807b0be62 100644 --- a/doc/user-guides/auth-for-app-devs-and-platform-engineers.md +++ b/doc/user-guides/auth-for-app-devs-and-platform-engineers.md @@ -2,41 +2,37 @@ This guide walks you through the process of setting up a local Kubernetes cluster with Kuadrant where you will protect [Gateway API](https://gateway-api.sigs.k8s.io/) endpoints by declaring Kuadrant AuthPolicy custom resources. -Two AuthPolicies will be declared: +Three 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 `kuadrant-ingressgateway` Gateway that enforces a trivial "deny-all" policy that locks down any other HTTPRoute attached to the Gateway. | +| Use case | AuthPolicies | +|--------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **App developer** | 2 AuthPolicies targeting a HTTPRoute that routes traffic to a sample "Toy Store" application → enforce API key authentication to all requests in this route; require 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 `kuadrant-ingressgateway` Gateway → enforces a trivial "deny-all" policy that locks down any other HTTPRoute attached to the Gateway. | Topology: ``` - ┌───────────────┐ - │ (AuthPolicy) │ - │ gw-auth │ - └───────┬───────┘ - │ - ▼ - ┌─────────────────────────┐ - │ (Gateway) │ - │ kuadrant-ingressgateway │ - ┌────►│ │◄───┐ - │ │ * │ │ - │ └─────────────────────────┘ │ - │ │ - ┌────────┴─────────┐ ┌────────┴─────────┐ - │ (HTTPRoute) │ │ (HTTPRoute) │ - │ toystore │ │ other │ - │ │ │ │ - │ api.toystore.com │ │ *.other-apps.com │ - └──────────────────┘ └──────────────────┘ - ▲ - │ - ┌───────┴───────┐ - │ (AuthPolicy) │ - │ toystore │ - └───────────────┘ + ┌─────────────────────────┐ + │ (Gateway) │ ┌───────────────┐ + │ kuadrant-ingressgateway │◄──│ (AuthPolicy) │ + │ │ │ gw-auth │ + │ * │ └───────────────┘ + └─────────────────────────┘ + ▲ ▲ + ┌────────┴─────────┐ ┌────────┴─────────┐ +┌────────────────┐ │ (HTTPRoute) │ │ (HTTPRoute) │ +│ (AuthPolicy) │──►│ toystore │ │ other │ +│ toystore-authn │ │ │ │ │ +└────────────────┘ │ api.toystore.com │ │ *.other-apps.com │ + └──────────────────┘ └──────────────────┘ + ▲ ▲ + ┌─────────┴───────┐ ┌──────┴──────────┐ + | (HTTPRouteRule) | | (HTTPRouteRule) | ┌─────────────────┐ + | rule-1 | | rule-2 |◄──│ (AuthPolicy) │ + | | | | │ toystore-admins │ + | - GET /cars* | | - /admins* | └─────────────────┘ + | - GET /dolls* | └─────────────────┘ + └─────────────────┘ ``` ## Requisites @@ -88,7 +84,7 @@ spec: hostnames: - api.toystore.com rules: - - matches: + - matches: # rule-1 - method: GET path: type: PathPrefix @@ -100,7 +96,7 @@ spec: backendRefs: - name: toystore port: 80 - - matches: + - matches: # rule-2 - path: type: PathPrefix value: "/admin" @@ -137,31 +133,47 @@ curl -H 'Host: api.toystore.com' http://$GATEWAY_URL/admin -i ### ③ Protect the Toy Store application (Persona: _App developer_) -Create the AuthPolicy to enforce the following auth rules: +Create AuthPolicies to enforce the following auth rules: - **Authentication:** - All users must present a valid API key - **Authorization:** - - `/admin*` routes require user mapped to the `admins` group (`kuadrant.io/groups=admins` annotation added to the Kubernetes API key Secret) + - `/admin*` paths (2nd rule of the HTTPRoute) require user mapped to the `admins` group (`kuadrant.io/groups=admins` annotation added to the Kubernetes API key Secret) ```sh kubectl apply -f - < -// service: -// name: kuadrant-authorization -func meshConfigFromStruct(structure *structpb.Struct) (*istiomeshv1alpha1.MeshConfig, error) { - if structure == nil { - return &istiomeshv1alpha1.MeshConfig{}, nil - } - - meshConfigJSON, err := structure.MarshalJSON() - if err != nil { - return nil, err - } - meshConfig := &istiomeshv1alpha1.MeshConfig{} - // istiomeshv1alpha1.MeshConfig doesn't implement JSON/Yaml marshalling, only protobuf - if err = protojson.Unmarshal(meshConfigJSON, meshConfig); err != nil { - return nil, err - } - - return meshConfig, nil -} - -// meshConfigToStruct Marshals the Istio MeshConfig into a struct -func meshConfigToStruct(config *istiomeshv1alpha1.MeshConfig) (*structpb.Struct, error) { - configJSON, err := protojson.Marshal(config) - if err != nil { - return nil, err - } - configStruct := &structpb.Struct{} - - if err = configStruct.UnmarshalJSON(configJSON); err != nil { - return nil, err - } - return configStruct, nil -} - -// meshConfigFromString returns the Istio MeshConfig from a ConfigMap -func meshConfigFromString(config string) (*istiomeshv1alpha1.MeshConfig, error) { - meshConfig := &istiomeshv1alpha1.MeshConfig{} - err := protomarshal.ApplyYAML(config, meshConfig) - if err != nil { - return nil, err - } - return meshConfig, nil -} - -// meshConfigToString returns the Istio MeshConfig as a string -func meshConfigToString(config *istiomeshv1alpha1.MeshConfig) (string, error) { - configString, err := protomarshal.ToYAML(config) - if err != nil { - return "", err - } - return configString, nil -} - -// ossmMeshConfigFromStruct returns a maistrav2.MeshConfig from struct -func ossmMeshConfigFromStruct(structure *structpb.Struct) (*maistrav2.MeshConfig, error) { - if structure == nil { - return &maistrav2.MeshConfig{}, nil - } - - meshConfigJSON, err := structure.MarshalJSON() - if err != nil { - return nil, err - } - - meshConfig := &maistrav2.MeshConfig{} - if err = json.Unmarshal(meshConfigJSON, meshConfig); err != nil { - return nil, err - } - - return meshConfig, nil -} - -func ossmMeshConfigToStruct(config *maistrav2.MeshConfig) (*structpb.Struct, error) { - configJSON, err := json.Marshal(config) - if err != nil { - return nil, err - } - return jsonByteToStruct(configJSON) -} - -func jsonByteToStruct(configJSON []byte) (*structpb.Struct, error) { - configStruct := &structpb.Struct{} - if err := configStruct.UnmarshalJSON(configJSON); err != nil { - return nil, err - } - return configStruct, nil -} diff --git a/pkg/istio/mesh_config_test.go b/pkg/istio/mesh_config_test.go deleted file mode 100644 index ac7cee634..000000000 --- a/pkg/istio/mesh_config_test.go +++ /dev/null @@ -1,321 +0,0 @@ -//go:build unit - -package istio - -import ( - "fmt" - "testing" - - "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" - "google.golang.org/protobuf/types/known/structpb" - "gotest.tools/assert" - istiomeshv1alpha1 "istio.io/api/mesh/v1alpha1" - istioapiv1alpha1 "istio.io/api/operator/v1alpha1" - iopv1alpha1 "istio.io/istio/operator/pkg/apis/istio/v1alpha1" - corev1 "k8s.io/api/core/v1" - istiov1alpha1 "maistra.io/istio-operator/api/v1alpha1" - "maistra.io/istio-operator/pkg/helm" - "sigs.k8s.io/controller-runtime/pkg/client" - - maistrav2 "github.com/kuadrant/kuadrant-operator/api/external/maistra/v2" -) - -type stubbedConfigWrapper struct { - istioMeshConfig *istiomeshv1alpha1.MeshConfig -} - -func (c *stubbedConfigWrapper) SetMeshConfig(config *istiomeshv1alpha1.MeshConfig) error { - c.istioMeshConfig = config - return nil -} - -func (c *stubbedConfigWrapper) GetMeshConfig() (*istiomeshv1alpha1.MeshConfig, error) { - return c.istioMeshConfig, nil -} - -func (c *stubbedConfigWrapper) GetConfigObject() client.Object { - return nil -} - -func TestKuadrantAuthorizer_GetExtensionProvider(t *testing.T) { - authorizer := NewKuadrantAuthorizer("default") - provider := authorizer.GetExtensionProvider() - - assert.Equal(t, provider.Name, ExtAuthorizerName) - assert.Equal(t, provider.GetEnvoyExtAuthzGrpc().Service, fmt.Sprintf("%s.default.svc.cluster.local", kuadrant.AuthorinoServiceName)) -} - -func TestHasKuadrantAuthorizer(t *testing.T) { - authorizer := NewKuadrantAuthorizer("default") - configWrapper := &stubbedConfigWrapper{getStubbedMeshConfig()} - - hasAuthorizer, err := HasKuadrantAuthorizer(configWrapper, *authorizer) - - assert.NilError(t, err) - assert.Equal(t, hasAuthorizer, false) - - configWrapper.istioMeshConfig.ExtensionProviders = append(configWrapper.istioMeshConfig.ExtensionProviders, authorizer.GetExtensionProvider()) - hasAuthorizer, err = HasKuadrantAuthorizer(configWrapper, *authorizer) - assert.NilError(t, err) - assert.Equal(t, hasAuthorizer, true) -} - -func TestRegisterKuadrantAuthorizer(t *testing.T) { - authorizer := NewKuadrantAuthorizer("default") - configWrapper := &stubbedConfigWrapper{getStubbedMeshConfig()} - - err := RegisterKuadrantAuthorizer(configWrapper, authorizer) - assert.NilError(t, err) - - meshConfig, _ := configWrapper.GetMeshConfig() - assert.Equal(t, meshConfig.ExtensionProviders[1].Name, "kuadrant-authorization") -} - -func TestUnregisterKuadrantAuthorizer(t *testing.T) { - authorizer := NewKuadrantAuthorizer("default") - configWrapper := &stubbedConfigWrapper{getStubbedMeshConfig()} - - err := RegisterKuadrantAuthorizer(configWrapper, authorizer) - assert.NilError(t, err) - assert.Equal(t, len(configWrapper.istioMeshConfig.ExtensionProviders), 2) - - err = UnregisterKuadrantAuthorizer(configWrapper, authorizer) - assert.NilError(t, err) - assert.Equal(t, len(configWrapper.istioMeshConfig.ExtensionProviders), 1) - - meshConfig, _ := configWrapper.GetMeshConfig() - assert.Equal(t, meshConfig.GetExtensionProviders()[0].Name, "custom-authorizer") -} - -func getStubbedMeshConfig() *istiomeshv1alpha1.MeshConfig { - providers := make([]*istiomeshv1alpha1.MeshConfig_ExtensionProvider, 0) - provider := &istiomeshv1alpha1.MeshConfig_ExtensionProvider{ - Name: "custom-authorizer", - Provider: &istiomeshv1alpha1.MeshConfig_ExtensionProvider_EnvoyExtAuthzGrpc{ - EnvoyExtAuthzGrpc: &istiomeshv1alpha1.MeshConfig_ExtensionProvider_EnvoyExternalAuthorizationGrpcProvider{ - Port: 50051, - Service: "custom-authorizer.default.svc.cluster.local", - }, - }, - } - providers = append(providers, provider) - return &istiomeshv1alpha1.MeshConfig{ - ExtensionProviders: providers, - } -} - -func getStubbedMeshConfigStruct() *structpb.Struct { - return &structpb.Struct{ - Fields: map[string]*structpb.Value{ - "extensionProviders": { - Kind: &structpb.Value_ListValue{ - ListValue: &structpb.ListValue{ - Values: []*structpb.Value{ - { - Kind: &structpb.Value_StructValue{ - StructValue: &structpb.Struct{ - Fields: map[string]*structpb.Value{ - "name": { - Kind: &structpb.Value_StringValue{ - StringValue: "custom-authorizer", - }, - }, - "envoyExtAuthzGrpc": { - Kind: &structpb.Value_StructValue{ - StructValue: &structpb.Struct{ - Fields: map[string]*structpb.Value{ - "port": { - Kind: &structpb.Value_NumberValue{ - NumberValue: 50051, - }, - }, - "service": { - Kind: &structpb.Value_StringValue{ - StringValue: "custom-authorizer.default.svc.cluster.local", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - } -} - -func TestOperatorWrapper_GetConfigObject(t *testing.T) { - config := &iopv1alpha1.IstioOperator{} - wrapper := NewOperatorWrapper(config) - - assert.Equal(t, wrapper.GetConfigObject(), config) -} - -func TestOperatorWrapper_GetMeshConfig(t *testing.T) { - structConfig := getStubbedMeshConfigStruct() - - config := &iopv1alpha1.IstioOperator{ - Spec: &istioapiv1alpha1.IstioOperatorSpec{ - MeshConfig: structConfig, - }, - } - wrapper := NewOperatorWrapper(config) - - meshConfig, err := wrapper.GetMeshConfig() - assert.NilError(t, err) - assert.Equal(t, meshConfig.ExtensionProviders[0].Name, "custom-authorizer") - assert.Equal(t, meshConfig.ExtensionProviders[0].GetEnvoyExtAuthzGrpc().GetPort(), uint32(50051)) -} - -func TestOperatorWrapper_SetMeshConfig(t *testing.T) { - config := &iopv1alpha1.IstioOperator{ - Spec: &istioapiv1alpha1.IstioOperatorSpec{}, - } - wrapper := NewOperatorWrapper(config) - - stubbedMeshConfig := getStubbedMeshConfig() - err := wrapper.SetMeshConfig(stubbedMeshConfig) - assert.NilError(t, err) - - meshConfig, _ := wrapper.GetMeshConfig() - - assert.Equal(t, meshConfig.ExtensionProviders[0].Name, stubbedMeshConfig.ExtensionProviders[0].Name) - assert.Equal(t, meshConfig.ExtensionProviders[0].GetEnvoyExtAuthzGrpc().GetPort(), uint32(50051)) -} - -func TestConfigMapWrapper_GetConfigObject(t *testing.T) { - configMap := &corev1.ConfigMap{} - wrapper := NewConfigMapWrapper(configMap) - - assert.Equal(t, wrapper.GetConfigObject(), configMap) -} - -func TestConfigMapWrapper_GetMeshConfig(t *testing.T) { - configMap := &corev1.ConfigMap{ - Data: map[string]string{ - "mesh": ` -extensionProviders: -- name: "custom-authorizer" - envoyExtAuthzGrpc: - service: "custom-authorizer.default.svc.cluster.local" - port: "50051" -`, - }, - } - wrapper := NewConfigMapWrapper(configMap) - - meshConfig, _ := wrapper.GetMeshConfig() - assert.Equal(t, meshConfig.ExtensionProviders[0].Name, "custom-authorizer") - assert.Equal(t, meshConfig.ExtensionProviders[0].GetEnvoyExtAuthzGrpc().GetPort(), uint32(50051)) -} - -func TestConfigMapWrapper_SetMeshConfig(t *testing.T) { - configMap := &corev1.ConfigMap{ - Data: map[string]string{ - "mesh": "", - }, - } - wrapper := NewConfigMapWrapper(configMap) - - stubbedMeshConfig := getStubbedMeshConfig() - err := wrapper.SetMeshConfig(stubbedMeshConfig) - assert.NilError(t, err) - - meshConfig, _ := wrapper.GetMeshConfig() - - assert.Equal(t, meshConfig.ExtensionProviders[0].Name, "custom-authorizer") - assert.Equal(t, meshConfig.ExtensionProviders[0].GetEnvoyExtAuthzGrpc().GetPort(), uint32(50051)) -} - -func TestOSSMControlPlaneWrapper_GetConfigObject(t *testing.T) { - ossmControlPlane := &maistrav2.ServiceMeshControlPlane{} - wrapper := NewOSSMControlPlaneWrapper(ossmControlPlane) - assert.Equal(t, wrapper.GetConfigObject(), ossmControlPlane) -} - -func TestOSSMControlPlaneWrapper_GetMeshConfig(t *testing.T) { - ossmControlPlane := &maistrav2.ServiceMeshControlPlane{} - ossmMeshConfig, err := ossmMeshConfigFromStruct(getStubbedMeshConfigStruct()) - ossmControlPlane.Spec.MeshConfig = ossmMeshConfig - assert.NilError(t, err) - - wrapper := NewOSSMControlPlaneWrapper(ossmControlPlane) - meshConfig, _ := wrapper.GetMeshConfig() - - assert.Equal(t, meshConfig.ExtensionProviders[0].Name, "custom-authorizer") - assert.Equal(t, meshConfig.ExtensionProviders[0].GetEnvoyExtAuthzGrpc().GetPort(), uint32(50051)) - - // additional test branches for ossmMeshConfigFromStruct - ossmMeshConfig, err = ossmMeshConfigFromStruct(nil) - assert.NilError(t, err) - assert.DeepEqual(t, ossmMeshConfig, &maistrav2.MeshConfig{}) - - invalidStruct := &structpb.Struct{ - Fields: map[string]*structpb.Value{ - "invalid": {}, - }, - } - - ossmMeshConfig, err = ossmMeshConfigFromStruct(invalidStruct) - assert.Check(t, err != nil) - assert.Check(t, ossmMeshConfig == nil) -} - -func TestOSSMControlPlaneWrapper_SetMeshConfig(t *testing.T) { - ossmControlPlane := &maistrav2.ServiceMeshControlPlane{} - wrapper := NewOSSMControlPlaneWrapper(ossmControlPlane) - - stubbedMeshConfig := getStubbedMeshConfig() - err := wrapper.SetMeshConfig(stubbedMeshConfig) - assert.NilError(t, err) - - meshConfig, _ := wrapper.GetMeshConfig() - - assert.Equal(t, meshConfig.ExtensionProviders[0].Name, "custom-authorizer") - assert.Equal(t, meshConfig.ExtensionProviders[0].GetEnvoyExtAuthzGrpc().GetPort(), uint32(50051)) -} - -func TestSailWrapper_GetConfigObject(t *testing.T) { - ist := &istiov1alpha1.Istio{} - wrapper := NewSailWrapper(ist) - - assert.Equal(t, wrapper.GetConfigObject(), ist) -} - -func TestSailWrapper_GetMeshConfig(t *testing.T) { - structConfig := getStubbedMeshConfigStruct() - values := helm.HelmValues{} - if err := values.Set("meshConfig", structConfig.AsMap()); err != nil { - assert.NilError(t, err) - } - config := &istiov1alpha1.Istio{} - if err := config.Spec.SetValues(values); err != nil { - assert.NilError(t, err) - } - wrapper := NewSailWrapper(config) - - meshConfig, err := wrapper.GetMeshConfig() - assert.NilError(t, err) - assert.Equal(t, meshConfig.ExtensionProviders[0].Name, "custom-authorizer") - assert.Equal(t, meshConfig.ExtensionProviders[0].GetEnvoyExtAuthzGrpc().GetPort(), uint32(50051)) -} - -func TestSailWrapper_SetMeshConfig(t *testing.T) { - config := &istiov1alpha1.Istio{} - wrapper := NewSailWrapper(config) - - stubbedMeshConfig := getStubbedMeshConfig() - err := wrapper.SetMeshConfig(stubbedMeshConfig) - assert.NilError(t, err) - - meshConfig, _ := wrapper.GetMeshConfig() - - assert.Equal(t, meshConfig.ExtensionProviders[0].Name, stubbedMeshConfig.ExtensionProviders[0].Name) - assert.Equal(t, meshConfig.ExtensionProviders[0].GetEnvoyExtAuthzGrpc().GetPort(), uint32(50051)) -} diff --git a/pkg/istio/utils.go b/pkg/istio/utils.go index 6a3a01762..c31ee30ed 100644 --- a/pkg/istio/utils.go +++ b/pkg/istio/utils.go @@ -1,10 +1,13 @@ package istio import ( + "encoding/json" + "github.com/kuadrant/policy-machinery/controller" "github.com/kuadrant/policy-machinery/machinery" "github.com/samber/lo" istioapimetav1alpha1 "istio.io/api/meta/v1alpha1" + istioapinetworkingv1alpha3 "istio.io/api/networking/v1alpha3" istioapiv1beta1 "istio.io/api/type/v1beta1" istioclientgoextensionv1alpha1 "istio.io/client-go/pkg/apis/extensions/v1alpha1" istioclientgonetworkingv1alpha3 "istio.io/client-go/pkg/apis/networking/v1alpha3" @@ -18,13 +21,11 @@ import ( ) var ( - EnvoyFiltersResource = istioclientgonetworkingv1alpha3.SchemeGroupVersion.WithResource("envoyfilters") - WasmPluginsResource = istioclientgoextensionv1alpha1.SchemeGroupVersion.WithResource("wasmplugins") - AuthorizationPoliciesResource = istioclientgosecurityv1beta1.SchemeGroupVersion.WithResource("authorizationpolicies") + EnvoyFiltersResource = istioclientgonetworkingv1alpha3.SchemeGroupVersion.WithResource("envoyfilters") + WasmPluginsResource = istioclientgoextensionv1alpha1.SchemeGroupVersion.WithResource("wasmplugins") - EnvoyFilterGroupKind = schema.GroupKind{Group: istioclientgonetworkingv1alpha3.GroupName, Kind: "EnvoyFilter"} - WasmPluginGroupKind = schema.GroupKind{Group: istioclientgoextensionv1alpha1.GroupName, Kind: "WasmPlugin"} - AuthorizationPolicyGroupKind = schema.GroupKind{Group: istioclientgosecurityv1beta1.GroupName, Kind: "AuthorizationPolicy"} + EnvoyFilterGroupKind = schema.GroupKind{Group: istioclientgonetworkingv1alpha3.GroupName, Kind: "EnvoyFilter"} + WasmPluginGroupKind = schema.GroupKind{Group: istioclientgoextensionv1alpha1.GroupName, Kind: "WasmPlugin"} ) func PolicyTargetRefFromGateway(gateway *gatewayapiv1.Gateway) *istioapiv1beta1.PolicyTargetReference { @@ -43,6 +44,78 @@ func EqualTargetRefs(a, b []*istioapiv1beta1.PolicyTargetReference) bool { }) } +// BuildEnvoyFilterClusterPatch returns an envoy config patch that adds a cluster to the gateway. +func BuildEnvoyFilterClusterPatch(host string, port int, clusterPatchBuilder func(string, int) map[string]any) ([]*istioapinetworkingv1alpha3.EnvoyFilter_EnvoyConfigObjectPatch, error) { + patchRaw, _ := json.Marshal(map[string]any{"operation": "ADD", "value": clusterPatchBuilder(host, port)}) + patch := &istioapinetworkingv1alpha3.EnvoyFilter_Patch{} + if err := patch.UnmarshalJSON(patchRaw); err != nil { + return nil, err + } + + return []*istioapinetworkingv1alpha3.EnvoyFilter_EnvoyConfigObjectPatch{ + { + ApplyTo: istioapinetworkingv1alpha3.EnvoyFilter_CLUSTER, + Match: &istioapinetworkingv1alpha3.EnvoyFilter_EnvoyConfigObjectMatch{ + ObjectTypes: &istioapinetworkingv1alpha3.EnvoyFilter_EnvoyConfigObjectMatch_Cluster{ + Cluster: &istioapinetworkingv1alpha3.EnvoyFilter_ClusterMatch{ + Service: host, + }, + }, + }, + Patch: patch, + }, + }, nil +} + +func EqualEnvoyFilters(a, b *istioclientgonetworkingv1alpha3.EnvoyFilter) bool { + if a.Spec.Priority != b.Spec.Priority || !EqualTargetRefs(a.Spec.TargetRefs, b.Spec.TargetRefs) { + return false + } + + aConfigPatches := a.Spec.ConfigPatches + bConfigPatches := b.Spec.ConfigPatches + if len(aConfigPatches) != len(bConfigPatches) { + return false + } + return lo.EveryBy(aConfigPatches, func(aConfigPatch *istioapinetworkingv1alpha3.EnvoyFilter_EnvoyConfigObjectPatch) bool { + return lo.SomeBy(bConfigPatches, func(bConfigPatch *istioapinetworkingv1alpha3.EnvoyFilter_EnvoyConfigObjectPatch) bool { + if aConfigPatch == nil && bConfigPatch == nil { + return true + } + if (aConfigPatch == nil && bConfigPatch != nil) || (aConfigPatch != nil && bConfigPatch == nil) { + return false + } + + // apply_to + if aConfigPatch.ApplyTo != bConfigPatch.ApplyTo { + return false + } + + // cluster match + aCluster := aConfigPatch.Match.GetCluster() + bCluster := bConfigPatch.Match.GetCluster() + if aCluster == nil || bCluster == nil { + return false + } + if aCluster.Service != bCluster.Service || aCluster.PortNumber != bCluster.PortNumber || aCluster.Subset != bCluster.Subset { + return false + } + + // patch + aPatch := aConfigPatch.Patch + bPatch := bConfigPatch.Patch + + if aPatch.Operation != bPatch.Operation || aPatch.FilterClass != bPatch.FilterClass { + return false + } + + aPatchJSON, _ := aPatch.Value.MarshalJSON() + bPatchJSON, _ := aPatch.Value.MarshalJSON() + return string(aPatchJSON) == string(bPatchJSON) + }) + }) +} + func ConditionToProperConditionFunc(istioCondition *istioapimetav1alpha1.IstioCondition, _ int) metav1.Condition { return metav1.Condition{ Type: istioCondition.GetType(), diff --git a/pkg/library/gatewayapi/utils.go b/pkg/library/gatewayapi/utils.go index c2190dcd9..fba397d90 100644 --- a/pkg/library/gatewayapi/utils.go +++ b/pkg/library/gatewayapi/utils.go @@ -212,7 +212,7 @@ func GetGatewayParentKeys(route *gatewayapiv1.HTTPRoute) []client.ObjectKey { } func EqualLocalPolicyTargetReferencesWithSectionName(a, b []gatewayapiv1alpha2.LocalPolicyTargetReferenceWithSectionName) bool { - return len(a) == len(b) && !lo.EveryBy(a, func(aTargetRef gatewayapiv1alpha2.LocalPolicyTargetReferenceWithSectionName) bool { + return len(a) == len(b) && lo.EveryBy(a, func(aTargetRef gatewayapiv1alpha2.LocalPolicyTargetReferenceWithSectionName) bool { return lo.SomeBy(b, func(bTargetRef gatewayapiv1alpha2.LocalPolicyTargetReferenceWithSectionName) bool { return aTargetRef.Group == bTargetRef.Group && aTargetRef.Kind == bTargetRef.Kind && aTargetRef.Name == bTargetRef.Name && ptr.Deref(aTargetRef.SectionName, gatewayapiv1alpha2.SectionName("")) == ptr.Deref(bTargetRef.SectionName, gatewayapiv1alpha2.SectionName("")) }) diff --git a/pkg/library/kuadrant/apimachinery_status_conditions.go b/pkg/library/kuadrant/apimachinery_status_conditions.go index 4f605b14f..6a9256fa0 100644 --- a/pkg/library/kuadrant/apimachinery_status_conditions.go +++ b/pkg/library/kuadrant/apimachinery_status_conditions.go @@ -6,11 +6,8 @@ import ( "fmt" "slices" "sort" - "sync" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" ) @@ -23,63 +20,6 @@ const ( PolicyReasonMissingDependency gatewayapiv1alpha2.PolicyConditionReason = "MissingDependency" ) -func NewAffectedPolicyMap() *AffectedPolicyMap { - return &AffectedPolicyMap{ - policies: make(map[types.UID][]client.ObjectKey), - } -} - -type AffectedPolicyMap struct { - policies map[types.UID][]client.ObjectKey - mu sync.RWMutex -} - -// SetAffectedPolicy sets the provided Policy as Affected in the tracking map. -func (o *AffectedPolicyMap) SetAffectedPolicy(p Policy, affectedBy []client.ObjectKey) { - o.mu.Lock() - defer o.mu.Unlock() - - if o.policies == nil { - o.policies = make(map[types.UID][]client.ObjectKey) - } - o.policies[p.GetUID()] = affectedBy -} - -// RemoveAffectedPolicy removes the provided Policy from the tracking map of Affected policies. -func (o *AffectedPolicyMap) RemoveAffectedPolicy(p Policy) { - o.mu.Lock() - defer o.mu.Unlock() - - delete(o.policies, p.GetUID()) -} - -// IsPolicyAffected checks if the provided Policy is affected based on the tracking map maintained. -func (o *AffectedPolicyMap) IsPolicyAffected(p Policy) bool { - o.mu.Lock() - defer o.mu.Unlock() - - return o.policies[p.GetUID()] != nil -} - -// IsPolicyOverridden checks if the provided Policy is affected based on the tracking map maintained. -// It is overridden if there is policies affecting it -func (o *AffectedPolicyMap) IsPolicyOverridden(p Policy) bool { - pAffected := o.IsPolicyAffected(p) - - o.mu.Lock() - defer o.mu.Unlock() - - return pAffected && len(o.policies[p.GetUID()]) > 0 -} - -// PolicyAffectedBy returns the clients keys that a policy is Affected by -func (o *AffectedPolicyMap) PolicyAffectedBy(p Policy) []client.ObjectKey { - o.mu.Lock() - defer o.mu.Unlock() - - return o.policies[p.GetUID()] -} - // ConditionMarshal marshals the set of conditions as a JSON array, sorted by condition type. func ConditionMarshal(conditions []metav1.Condition) ([]byte, error) { condCopy := slices.Clone(conditions) diff --git a/pkg/library/kuadrant/errors.go b/pkg/library/kuadrant/errors.go index 5b0a2935b..3d272ca6d 100644 --- a/pkg/library/kuadrant/errors.go +++ b/pkg/library/kuadrant/errors.go @@ -167,6 +167,9 @@ type ErrOverridden struct { } func (e ErrOverridden) Error() string { + if len(e.OverridingPolicies) == 0 { + return fmt.Sprintf("%s is overridden", e.Kind) + } return fmt.Sprintf("%s is overridden by %s", e.Kind, e.OverridingPolicies) } diff --git a/pkg/library/kuadrant/kuadrant.go b/pkg/library/kuadrant/kuadrant.go index e50a6d033..a607f5009 100644 --- a/pkg/library/kuadrant/kuadrant.go +++ b/pkg/library/kuadrant/kuadrant.go @@ -20,7 +20,6 @@ const ( KuadrantNamespaceAnnotation = "kuadrant.io/namespace" TopologyLabel = "kuadrant.io/topology" ControllerName = "kuadrant.io/policy-controller" - AuthorinoServiceName = "authorino-authorino-authorization" ) type Policy interface { diff --git a/pkg/library/mappers/httproute.go b/pkg/library/mappers/httproute.go deleted file mode 100644 index 01088531b..000000000 --- a/pkg/library/mappers/httproute.go +++ /dev/null @@ -1,92 +0,0 @@ -package mappers - -import ( - "context" - "fmt" - - "k8s.io/utils/ptr" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" - - "github.com/kuadrant/kuadrant-operator/pkg/library/fieldindexers" - kuadrantgatewayapi "github.com/kuadrant/kuadrant-operator/pkg/library/gatewayapi" - "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" - "github.com/kuadrant/kuadrant-operator/pkg/library/utils" -) - -func NewHTTPRouteEventMapper(o ...MapperOption) *HTTPRouteEventMapper { - return &HTTPRouteEventMapper{opts: Apply(o...)} -} - -type HTTPRouteEventMapper struct { - opts MapperOptions -} - -func (m *HTTPRouteEventMapper) MapToPolicy(ctx context.Context, obj client.Object, policyType kuadrantgatewayapi.PolicyType) []reconcile.Request { - logger := m.opts.Logger.WithValues("httproute", client.ObjectKeyFromObject(obj)) - requests := make([]reconcile.Request, 0) - httpRoute, ok := obj.(*gatewayapiv1.HTTPRoute) - if !ok { - logger.Info("cannot map httproute event to kuadrant policy", "error", fmt.Sprintf("%T is not a *gatewayapiv1beta1.HTTPRoute", obj)) - return []reconcile.Request{} - } - - gatewayKeys := kuadrantgatewayapi.GetRouteAcceptedGatewayParentKeys(httpRoute) - - for _, gatewayKey := range gatewayKeys { - gateway := &gatewayapiv1.Gateway{} - err := m.opts.Client.Get(ctx, gatewayKey, gateway) - if err != nil { - logger.Info("cannot get gateway", "error", err) - continue - } - - routeList := &gatewayapiv1.HTTPRouteList{} - fields := client.MatchingFields{fieldindexers.HTTPRouteGatewayParentField: client.ObjectKeyFromObject(gateway).String()} - if err = m.opts.Client.List(ctx, routeList, fields); err != nil { - logger.Info("cannot list httproutes", "error", err) - continue - } - - policies, err := policyType.GetList(ctx, m.opts.Client, client.InNamespace(obj.GetNamespace())) - if err != nil { - logger.Error(err, "unable to list policies") - continue - } - if len(policies) == 0 { - logger.Info("no kuadrant policy possibly affected by the gateway related event") - continue - } - topology, err := kuadrantgatewayapi.NewTopology( - kuadrantgatewayapi.WithGateways([]*gatewayapiv1.Gateway{gateway}), - kuadrantgatewayapi.WithRoutes(utils.Map(routeList.Items, ptr.To[gatewayapiv1.HTTPRoute])), - kuadrantgatewayapi.WithPolicies(policies), - kuadrantgatewayapi.WithLogger(logger), - ) - if err != nil { - logger.Info("unable to build topology for gateway", "error", err) - continue - } - index := kuadrantgatewayapi.NewTopologyIndexes(topology) - data := utils.Map(index.PoliciesFromGateway(gateway), func(p kuadrantgatewayapi.Policy) reconcile.Request { - policyKey := client.ObjectKeyFromObject(p) - logger.V(1).Info("kuadrant policy possibly affected by the gateway related event found") - return reconcile.Request{NamespacedName: policyKey} - }) - requests = append(requests, data...) - } - - if len(requests) != 0 { - return requests - } - - policyKey, err := kuadrant.DirectReferencesFromObject(httpRoute, policyType.DirectReferenceAnnotationName()) - if err != nil { - logger.Info("could not create direct reference from object", "error", err) - return requests - } - requests = append(requests, reconcile.Request{NamespacedName: policyKey}) - - return requests -} diff --git a/pkg/library/mappers/httproute_test.go b/pkg/library/mappers/httproute_test.go deleted file mode 100644 index 58782bb9a..000000000 --- a/pkg/library/mappers/httproute_test.go +++ /dev/null @@ -1,137 +0,0 @@ -//go:build unit - -package mappers - -import ( - "context" - "testing" - - "gotest.tools/assert" - appsv1 "k8s.io/api/apps/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" - "k8s.io/utils/ptr" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/client/fake" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" - gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" - - kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" - "github.com/kuadrant/kuadrant-operator/pkg/library/fieldindexers" - kuadrantgatewayapi "github.com/kuadrant/kuadrant-operator/pkg/library/gatewayapi" - "github.com/kuadrant/kuadrant-operator/pkg/library/utils" - "github.com/kuadrant/kuadrant-operator/pkg/log" -) - -func TestNewHTTPRouteEventMapper(t *testing.T) { - testScheme := runtime.NewScheme() - - err := appsv1.AddToScheme(testScheme) - if err != nil { - t.Fatal(err) - } - err = gatewayapiv1.AddToScheme(testScheme) - if err != nil { - t.Fatal(err) - } - err = kuadrantv1beta3.AddToScheme(testScheme) - if err != nil { - t.Fatal(err) - } - - spec := kuadrantv1beta3.AuthPolicySpec{ - TargetRef: gatewayapiv1alpha2.LocalPolicyTargetReference{ - Group: "gateway.networking.k8s.io", - Kind: "HTTPRoute", - Name: "test-route", - }, - } - routeList := &gatewayapiv1.HTTPRouteList{Items: make([]gatewayapiv1.HTTPRoute, 0)} - authPolicyList := &kuadrantv1beta3.AuthPolicyList{Items: []kuadrantv1beta3.AuthPolicy{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "policy-1", - Namespace: "app-ns", - }, - Spec: spec, - }, - }} - gateway := &gatewayapiv1.Gateway{ - ObjectMeta: metav1.ObjectMeta{Name: "test-gw", Namespace: "app-ns"}, - Status: gatewayapiv1.GatewayStatus{ - Conditions: []metav1.Condition{ - { - Type: "Programmed", - Status: "True", - }, - }, - }, - } - objs := []runtime.Object{routeList, authPolicyList, gateway} - cl := fake.NewClientBuilder().WithScheme(testScheme).WithRuntimeObjects(objs...).WithIndex(&gatewayapiv1.HTTPRoute{}, fieldindexers.HTTPRouteGatewayParentField, func(rawObj client.Object) []string { - return nil - }).Build() - em := NewHTTPRouteEventMapper(WithLogger(log.NewLogger()), WithClient(cl)) - - t.Run("not http route related event", func(subT *testing.T) { - requests := em.MapToPolicy(context.Background(), &gatewayapiv1.Gateway{}, kuadrantv1beta3.NewAuthPolicyType()) - assert.DeepEqual(subT, []reconcile.Request{}, requests) - }) - - t.Run("http route related event - no requests", func(subT *testing.T) { - requests := em.MapToPolicy(context.Background(), &gatewayapiv1.HTTPRoute{}, kuadrantv1beta3.NewAuthPolicyType()) - assert.DeepEqual(subT, []reconcile.Request{}, requests) - }) - - t.Run("http related event - requests", func(subT *testing.T) { - httpRoute := &gatewayapiv1.HTTPRoute{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-route", - Namespace: "app-ns", - Annotations: map[string]string{"kuadrant.io/testpolicies": `[{"Namespace":"app-ns","Name":"policy-1"},{"Namespace":"app-ns","Name":"policy-2"}]`}, - }, - Spec: gatewayapiv1.HTTPRouteSpec{ - CommonRouteSpec: gatewayapiv1.CommonRouteSpec{ - ParentRefs: []gatewayapiv1.ParentReference{{Namespace: ptr.To(gatewayapiv1.Namespace("app-ns")), Name: "test-gw"}}, - }, - }, - - Status: gatewayapiv1.HTTPRouteStatus{ - RouteStatus: gatewayapiv1.RouteStatus{ - Parents: []gatewayapiv1.RouteParentStatus{ - { - ParentRef: gatewayapiv1.ParentReference{ - Name: "test-gw", - Namespace: ptr.To(gatewayapiv1.Namespace("app-ns")), - }, - Conditions: []metav1.Condition{ - { - Type: "Accepted", - Status: metav1.ConditionTrue, - }, - }, - }, - }, - }, - }, - } - - objs = []runtime.Object{routeList, authPolicyList, gateway, httpRoute} - cl = fake.NewClientBuilder().WithScheme(testScheme).WithRuntimeObjects(objs...).WithIndex(&gatewayapiv1.HTTPRoute{}, fieldindexers.HTTPRouteGatewayParentField, func(rawObj client.Object) []string { - route, assertionOk := rawObj.(*gatewayapiv1.HTTPRoute) - if !assertionOk { - return nil - } - - return utils.Map(kuadrantgatewayapi.GetRouteAcceptedGatewayParentKeys(route), func(key client.ObjectKey) string { - return key.String() - }) - }).Build() - em = NewHTTPRouteEventMapper(WithLogger(log.NewLogger()), WithClient(cl)) - requests := em.MapToPolicy(context.Background(), httpRoute, kuadrantv1beta3.NewAuthPolicyType()) - expected := []reconcile.Request{{NamespacedName: types.NamespacedName{Namespace: "app-ns", Name: "policy-1"}}} - assert.DeepEqual(subT, expected, requests) - }) -} diff --git a/pkg/wasm/types_test.go b/pkg/wasm/types_test.go index 5a796e876..ae6fe51f1 100644 --- a/pkg/wasm/types_test.go +++ b/pkg/wasm/types_test.go @@ -4,94 +4,183 @@ package wasm import ( "encoding/json" - "fmt" "testing" "github.com/google/go-cmp/cmp" "sigs.k8s.io/yaml" - - kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" - "github.com/kuadrant/kuadrant-operator/pkg/common" ) -func TestConfig(t *testing.T) { +func TestConfigToJSON(t *testing.T) { + config := testBasicConfig + j, err := config.ToJSON() + if err != nil { + t.Fatal(err) + } + + convertedConfig, _ := ConfigFromJSON(j) + + if !cmp.Equal(convertedConfig, testBasicConfig) { + diff := cmp.Diff(convertedConfig, testBasicConfig) + t.Fatalf("unexpected converted wasm config (-want +got):\n%s", diff) + } +} + +func TestConfigToStruct(t *testing.T) { + config := testBasicConfig + s, err := config.ToStruct() + if err != nil { + t.Fatal(err) + } + + convertedConfig, _ := ConfigFromStruct(s) + + if !cmp.Equal(testBasicConfig, convertedConfig) { + diff := cmp.Diff(testBasicConfig, convertedConfig) + t.Fatalf("unexpected converted wasm config (-want +got):\n%s", diff) + } +} + +func TestConfigEqual(t *testing.T) { testCases := []struct { - name string - expectedConfig *Config - yaml string + name string + config1 *Config + config2 *Config + expected bool }{ { - name: "basic example", - expectedConfig: testBasicConfigExample(), - yaml: ` -services: - ratelimit-service: - type: ratelimit - endpoint: kuadrant-rate-limiting-service - failureMode: allow -actionSets: -- name: rlp-ns-A/rlp-name-A - routeRuleConditions: - hostnames: - - '*.toystore.com' - - example.com - matches: - - selector: request.path - operator: startswith - value: /cars - actions: - - service: ratelimit-service - scope: rlp-ns-A/rlp-name-A - conditions: - - selector: source.ip - operator: neq - value: 127.0.0.1 - data: - - static: - key: rlp-ns-A/rlp-name-A - value: "1" - - selector: - selector: auth.metadata.username -`, + name: "equal configs", + config1: &Config{ + Services: map[string]Service{ + "ratelimit-service": { + Type: "ratelimit", + Endpoint: "kuadrant-ratelimit-service", + FailureMode: "allow", + }, + }, + ActionSets: []ActionSet{ + { + Name: "5755da0b3c275ba6b8f553890eb32b04768a703b60ab9a5d7f4e0948e23ef0ab", + RouteRuleConditions: RouteRuleConditions{ + Hostnames: []string{"other.example.com"}, + Matches: []Predicate{ + { + Selector: "request.url_path", + Operator: "startswith", + Value: "/", + }, + }, + }, + Actions: []Action{ + { + ServiceName: "ratelimit-service", + Scope: "default/other", + Conditions: []Predicate{ + { + Selector: "source.address", + Operator: "neq", + Value: "127.0.0.1", + }, + }, + Data: []DataType{ + { + Value: &Static{ + Static: StaticSpec{ + Key: "limit.global__f63bec56", + Value: "1", + }, + }, + }, + }, + }, + }, + }, + }, + }, + config2: &Config{ // same config as config1 with fields orted alphabetically + ActionSets: []ActionSet{ + { + Actions: []Action{ + { + Conditions: []Predicate{ + { + Operator: "neq", + Selector: "source.address", + Value: "127.0.0.1", + }, + }, + Data: []DataType{ + { + Value: &Static{ + Static: StaticSpec{ + Key: "limit.global__f63bec56", + Value: "1", + }, + }, + }, + }, + ServiceName: "ratelimit-service", + Scope: "default/other", + }, + }, + Name: "5755da0b3c275ba6b8f553890eb32b04768a703b60ab9a5d7f4e0948e23ef0ab", + RouteRuleConditions: RouteRuleConditions{ + Hostnames: []string{"other.example.com"}, + Matches: []Predicate{ + { + Operator: "startswith", + Selector: "request.url_path", + Value: "/", + }, + }, + }, + }, + }, + Services: map[string]Service{ + "ratelimit-service": { + Type: "ratelimit", + Endpoint: "kuadrant-ratelimit-service", + FailureMode: "allow", + }, + }, + }, + expected: true, + }, + { + name: "different configs", + config1: testBasicConfig, + config2: &Config{}, + expected: false, }, } - for _, tc := range testCases { t.Run(tc.name, func(subT *testing.T) { - var conf Config - if err := yaml.Unmarshal([]byte(tc.yaml), &conf); err != nil { - subT.Fatal(err) - } - - if !cmp.Equal(tc.expectedConfig, &conf) { - diff := cmp.Diff(tc.expectedConfig, &conf) - subT.Fatalf("unexpected config (-want +got):\n%s", diff) + if tc.config1.EqualTo(tc.config2) != tc.expected { + subT.Fatalf("unexpected config equality result") } }) } } -func TestConfigMarshallUnmarshalling(t *testing.T) { - conf := testBasicConfigExample() - serializedConfig, err := json.Marshal(conf) +func TestMarshallUnmarshalConfig(t *testing.T) { + config := testBasicConfig + + marshalledConfig, err := json.Marshal(config) if err != nil { t.Fatal(err) } - fmt.Println(string(serializedConfig)) - - var unMarshalledConf Config - if err := json.Unmarshal(serializedConfig, &unMarshalledConf); err != nil { + var unmarshalledConfig Config + if err := json.Unmarshal(marshalledConfig, &unmarshalledConfig); err != nil { t.Fatal(err) } - if !cmp.Equal(conf, &unMarshalledConf) { - diff := cmp.Diff(conf, &unMarshalledConf) - t.Fatalf("unexpected wasm rules (-want +got):\n%s", diff) + if !cmp.Equal(config, &unmarshalledConfig) { + diff := cmp.Diff(config, &unmarshalledConfig) + t.Fatalf("unexpected wasm config (-want +got):\n%s", diff) } } -func TestValidActionConfig(t *testing.T) { +func TestValidAction(t *testing.T) { testCases := []struct { name string yaml string @@ -125,7 +214,7 @@ scope: some-scope } } -func TestInValidActionConfig(t *testing.T) { +func TestInvalidAction(t *testing.T) { testCases := []struct { name string yaml string @@ -163,63 +252,3 @@ data: }) } } - -func testBasicConfigExample() *Config { - return &Config{ - Services: map[string]Service{ - RateLimitServiceName: { - Type: RateLimitServiceType, - Endpoint: common.KuadrantRateLimitClusterName, - FailureMode: FailureModeAllow, - }, - }, - ActionSets: []ActionSet{ - { - Name: "rlp-ns-A/rlp-name-A", - RouteRuleConditions: RouteRuleConditions{ - Hostnames: []string{ - "*.toystore.com", - "example.com", - }, - Matches: []Predicate{ - { - Selector: "request.path", - Operator: PatternOperator(kuadrantv1beta3.StartsWithOperator), - Value: "/cars", - }, - }, - }, - Actions: []Action{ - { - ServiceName: RateLimitServiceName, - Scope: "rlp-ns-A/rlp-name-A", - Conditions: []Predicate{ - { - Selector: "source.ip", - Operator: PatternOperator(kuadrantv1beta3.NotEqualOperator), - Value: "127.0.0.1", - }, - }, - Data: []DataType{ - { - Value: &Static{ - Static: StaticSpec{ - Key: "rlp-ns-A/rlp-name-A", - Value: "1", - }, - }, - }, - { - Value: &Selector{ - Selector: SelectorSpec{ - Selector: "auth.metadata.username", - }, - }, - }, - }, - }, - }, - }, - }, - } -} diff --git a/pkg/wasm/utils.go b/pkg/wasm/utils.go index 462f7bdd5..32a66a365 100644 --- a/pkg/wasm/utils.go +++ b/pkg/wasm/utils.go @@ -9,11 +9,10 @@ import ( "github.com/kuadrant/policy-machinery/machinery" "github.com/samber/lo" - _struct "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/structpb" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" - kuadrantv1 "github.com/kuadrant/kuadrant-operator/api/v1" kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" "github.com/kuadrant/kuadrant-operator/pkg/common" kuadrantgatewayapi "github.com/kuadrant/kuadrant-operator/pkg/library/gatewayapi" @@ -31,34 +30,27 @@ func ExtensionName(gatewayName string) string { func BuildConfigForActionSet(actionSets []ActionSet) Config { return Config{ Services: map[string]Service{ + AuthServiceName: { + Type: AuthServiceType, + Endpoint: common.KuadrantAuthClusterName, + FailureMode: FailureModeDeny, + }, RateLimitServiceName: { Type: RateLimitServiceType, Endpoint: common.KuadrantRateLimitClusterName, FailureMode: FailureModeAllow, }, - // TODO: add auth extension }, ActionSets: actionSets, } } -type ActionBuilderFunc func(uniquePolicyRuleKey string, policyRule kuadrantv1.MergeableRule) (Action, error) - -func BuildActionSetsForPath(pathID string, path []machinery.Targetable, policyRules map[string]kuadrantv1.MergeableRule, actionBuilder ActionBuilderFunc) ([]kuadrantgatewayapi.HTTPRouteMatchConfig, error) { +func BuildActionSetsForPath(pathID string, path []machinery.Targetable, actions []Action) ([]kuadrantgatewayapi.HTTPRouteMatchConfig, error) { _, _, listener, httpRoute, httpRouteRule, err := common.ObjectsInRequestPath(path) if err != nil { return nil, err } - actions := lo.FilterMap(lo.Entries(policyRules), func(r lo.Entry[string, kuadrantv1.MergeableRule], _ int) (Action, bool) { - action, err := actionBuilder(r.Key, r.Value) - if err != nil { - errors.Join(err) - return Action{}, false - } - return action, true - }) - return lo.FlatMap(kuadrantgatewayapi.HostnamesFromListenerAndHTTPRoute(listener.Listener, httpRoute.HTTPRoute), func(hostname gatewayapiv1.Hostname, _ int) []kuadrantgatewayapi.HTTPRouteMatchConfig { return lo.Map(httpRouteRule.Matches, func(httpRouteMatch gatewayapiv1.HTTPRouteMatch, j int) kuadrantgatewayapi.HTTPRouteMatchConfig { actionSet := ActionSet{ @@ -90,7 +82,7 @@ func ActionSetNameForPath(pathID string, httpRouteMatchIndex int, hostname strin return hex.EncodeToString(hash[:]) } -func ConfigFromStruct(structure *_struct.Struct) (*Config, error) { +func ConfigFromStruct(structure *structpb.Struct) (*Config, error) { if structure == nil { return nil, errors.New("cannot desestructure config from nil") } diff --git a/pkg/wasm/utils_test.go b/pkg/wasm/utils_test.go new file mode 100644 index 000000000..4c0f52744 --- /dev/null +++ b/pkg/wasm/utils_test.go @@ -0,0 +1,268 @@ +//go:build unit + +package wasm + +import ( + "errors" + "testing" + + "github.com/google/go-cmp/cmp" + "google.golang.org/protobuf/types/known/structpb" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "sigs.k8s.io/yaml" +) + +var ( + testBasicConfig = &Config{ + Services: map[string]Service{ + "auth-service": { + Type: "auth", + Endpoint: "kuadrant-auth-service", + FailureMode: "deny", + }, + "ratelimit-service": { + Type: "ratelimit", + Endpoint: "kuadrant-ratelimit-service", + FailureMode: "allow", + }, + }, + ActionSets: []ActionSet{ + { + Name: "5755da0b3c275ba6b8f553890eb32b04768a703b60ab9a5d7f4e0948e23ef0ab", + RouteRuleConditions: RouteRuleConditions{ + Hostnames: []string{"other.example.com"}, + Matches: []Predicate{ + { + Selector: "request.url_path", + Operator: "startswith", + Value: "/", + }, + }, + }, + Actions: []Action{ + { + ServiceName: "ratelimit-service", + Scope: "default/other", + Conditions: []Predicate{ + { + Selector: "source.address", + Operator: "neq", + Value: "127.0.0.1", + }, + }, + Data: []DataType{ + { + Value: &Static{ + Static: StaticSpec{ + Key: "limit.global__f63bec56", + Value: "1", + }, + }, + }, + }, + }, + }, + }, + { + Name: "21cb3adc608c09a360d62a03fd1afd7cc6f8720999a51d7916927fff26a34ef8", + RouteRuleConditions: RouteRuleConditions{ + Hostnames: []string{"*"}, + Matches: []Predicate{ + { + Selector: "request.method", + Operator: "eq", + Value: "GET", + }, + { + Selector: "request.url_path", + Operator: "startswith", + Value: "/", + }, + }, + }, + Actions: []Action{ + { + ServiceName: "auth-service", + Scope: "e2db39952dd3bc72e152330a2eb15abbd9675c7ac6b54a1a292f07f25f09f138", + }, + { + ServiceName: "ratelimit-service", + Scope: "default/toystore", + Data: []DataType{ + { + Value: &Static{ + Static: StaticSpec{ + Key: "limit.specific__69ea4d2d", + Value: "1", + }, + }, + }, + }, + }, + { + ServiceName: "ratelimit-service", + Scope: "default/toystore", + Conditions: []Predicate{ + { + Selector: "source.address", + Operator: "neq", + Value: "127.0.0.1", + }, + }, + Data: []DataType{ + { + Value: &Static{ + Static: StaticSpec{ + Key: "limit.global__f63bec56", + Value: "1", + }, + }, + }, + }, + }, + }, + }, + }, + } + testBasicConfigJSON = `{"services":{"auth-service":{"endpoint":"kuadrant-auth-service","type":"auth","failureMode":"deny"},"ratelimit-service":{"endpoint":"kuadrant-ratelimit-service","type":"ratelimit","failureMode":"allow"}},"actionSets":[{"name":"5755da0b3c275ba6b8f553890eb32b04768a703b60ab9a5d7f4e0948e23ef0ab","routeRuleConditions":{"hostnames":["other.example.com"],"matches":[{"operator":"startswith","selector":"request.url_path","value":"/"}]},"actions":[{"service":"ratelimit-service","scope":"default/other","conditions":[{"operator":"neq","selector":"source.address","value":"127.0.0.1"}],"data":[{"static":{"key":"limit.global__f63bec56","value":"1"}}]}]},{"name":"21cb3adc608c09a360d62a03fd1afd7cc6f8720999a51d7916927fff26a34ef8","routeRuleConditions":{"hostnames":["*"],"matches":[{"operator":"eq","selector":"request.method","value":"GET"},{"operator":"startswith","selector":"request.url_path","value":"/"}]},"actions":[{"service":"auth-service","scope":"e2db39952dd3bc72e152330a2eb15abbd9675c7ac6b54a1a292f07f25f09f138"},{"service":"ratelimit-service","scope":"default/toystore","data":[{"static":{"key":"limit.specific__69ea4d2d","value":"1"}}]},{"service":"ratelimit-service","scope":"default/toystore","conditions":[{"operator":"neq","selector":"source.address","value":"127.0.0.1"}],"data":[{"static":{"key":"limit.global__f63bec56","value":"1"}}]}]}]}` + testBasicConfigYAML = ` +services: + auth-service: + type: auth + endpoint: kuadrant-auth-service + failureMode: deny + ratelimit-service: + type: ratelimit + endpoint: kuadrant-ratelimit-service + failureMode: allow +actionSets: + - name: 5755da0b3c275ba6b8f553890eb32b04768a703b60ab9a5d7f4e0948e23ef0ab + routeRuleConditions: + hostnames: + - other.example.com + matches: + - operator: startswith + selector: request.url_path + value: / + actions: + - service: ratelimit-service + scope: default/other + conditions: + - operator: neq + selector: source.address + value: 127.0.0.1 + data: + - static: + key: limit.global__f63bec56 + value: "1" + - name: 21cb3adc608c09a360d62a03fd1afd7cc6f8720999a51d7916927fff26a34ef8 + routeRuleConditions: + hostnames: + - "*" + matches: + - operator: eq + selector: request.method + value: GET + - operator: startswith + selector: request.url_path + value: / + actions: + - service: auth-service + scope: e2db39952dd3bc72e152330a2eb15abbd9675c7ac6b54a1a292f07f25f09f138 + - service: ratelimit-service + scope: default/toystore + data: + - static: + key: limit.specific__69ea4d2d + value: "1" + - service: ratelimit-service + scope: default/toystore + conditions: + - operator: neq + selector: source.address + value: 127.0.0.1 + data: + - static: + key: limit.global__f63bec56 + value: "1" +` +) + +func TestConfigFromJSON(t *testing.T) { + testCases := []struct { + name string + json *apiextensionsv1.JSON + expectedConfig *Config + expectedError error + }{ + { + name: "nil config", + json: nil, + expectedError: errors.New("cannot desestructure config from nil"), + }, + { + name: "valid config", + json: &apiextensionsv1.JSON{Raw: []byte(testBasicConfigJSON)}, + expectedConfig: testBasicConfig, + }, + { + name: "invalid config", + json: &apiextensionsv1.JSON{Raw: []byte(`{invalid}`)}, + expectedError: errors.New("invalid character 'i' looking for beginning of object key string"), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(subT *testing.T) { + config, err := ConfigFromJSON(tc.json) + if (err == nil && tc.expectedError != nil) || (err != nil && tc.expectedError == nil) || (err != nil && tc.expectedError != nil && err.Error() != tc.expectedError.Error()) { + t.Fatalf("unexpected error to be: %+v, got: %+v", tc.expectedError, err) + } + if !cmp.Equal(tc.expectedConfig, config) { + diff := cmp.Diff(tc.expectedConfig, config) + subT.Fatalf("unexpected config (-want +got):\n%s", diff) + } + }) + } +} + +func TestConfigFromStruct(t *testing.T) { + testCases := []struct { + name string + yaml *string + expectedConfig *Config + expectedError error + }{ + { + name: "nil config", + yaml: nil, + expectedError: errors.New("cannot desestructure config from nil"), + }, + { + name: "valid config", + yaml: &testBasicConfigYAML, + expectedConfig: testBasicConfig, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(subT *testing.T) { + var structure *structpb.Struct + if y := tc.yaml; y != nil { + m := map[string]any{} + if err := yaml.Unmarshal([]byte(*tc.yaml), &m); err != nil { + subT.Fatal(err) + } + structure, _ = structpb.NewStruct(m) + } + config, err := ConfigFromStruct(structure) + if (err == nil && tc.expectedError != nil) || (err != nil && tc.expectedError == nil) || (err != nil && tc.expectedError != nil && err.Error() != tc.expectedError.Error()) { + t.Fatalf("unexpected error to be: %+v, got: %+v", tc.expectedError, err) + } + if !cmp.Equal(tc.expectedConfig, config) { + diff := cmp.Diff(tc.expectedConfig, config) + subT.Fatalf("unexpected config (-want +got):\n%s", diff) + } + }) + } +} diff --git a/tests/common/authpolicy/authpolicy_controller_test.go b/tests/common/authpolicy/authpolicy_controller_test.go index dfac1bf66..31a8f317d 100644 --- a/tests/common/authpolicy/authpolicy_controller_test.go +++ b/tests/common/authpolicy/authpolicy_controller_test.go @@ -12,8 +12,8 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - authorinov1beta1 "github.com/kuadrant/authorino-operator/api/v1beta1" - authorinoapi "github.com/kuadrant/authorino/api/v1beta2" + authorinov1beta2 "github.com/kuadrant/authorino/api/v1beta2" + "github.com/kuadrant/policy-machinery/machinery" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -26,142 +26,56 @@ import ( gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + kuadrantv1 "github.com/kuadrant/kuadrant-operator/api/v1" kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" "github.com/kuadrant/kuadrant-operator/controllers" "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" "github.com/kuadrant/kuadrant-operator/tests" ) -var _ = Describe("AuthPolicy controller (Serial)", Serial, func() { +var _ = Describe("AuthPolicy controller", func() { const ( testTimeOut = SpecTimeout(2 * time.Minute) afterEachTimeOut = NodeTimeout(3 * time.Minute) ) var ( testNamespace string + gateway *gatewayapiv1.Gateway + gatewayClass *gatewayapiv1.GatewayClass gwHost = fmt.Sprintf("*.toystore-%s.com", rand.String(6)) ) - BeforeEach(func(ctx SpecContext) { - testNamespace = tests.CreateNamespace(ctx, testClient()) - - gateway := tests.BuildBasicGateway(TestGatewayName, testNamespace, func(gateway *gatewayapiv1.Gateway) { - gateway.Spec.Listeners[0].Hostname = ptr.To(gatewayapiv1.Hostname(gwHost)) - }) - err := k8sClient.Create(ctx, gateway) - Expect(err).ToNot(HaveOccurred()) - - Eventually(tests.GatewayIsReady(ctx, testClient(), gateway)).WithContext(ctx).Should(BeTrue()) - }) - - AfterEach(func(ctx SpecContext) { - tests.DeleteNamespace(ctx, testClient(), testNamespace) - }, afterEachTimeOut) - - Context("AuthPolicy enforced condition reasons", func() { - assertAcceptedCondTrueAndEnforcedCond := func(ctx context.Context, policy *kuadrantv1beta3.AuthPolicy, conditionStatus metav1.ConditionStatus, reason, message string) func() bool { - return func() bool { - existingPolicy := &kuadrantv1beta3.AuthPolicy{} - err := k8sClient.Get(ctx, client.ObjectKeyFromObject(policy), existingPolicy) - if err != nil { - return false - } - acceptedCond := meta.FindStatusCondition(existingPolicy.Status.Conditions, string(gatewayapiv1alpha2.PolicyConditionAccepted)) - if acceptedCond == nil { - return false - } - - acceptedCondMatch := acceptedCond.Status == metav1.ConditionTrue && acceptedCond.Reason == string(gatewayapiv1alpha2.PolicyReasonAccepted) - - enforcedCond := meta.FindStatusCondition(existingPolicy.Status.Conditions, string(kuadrant.PolicyReasonEnforced)) - if enforcedCond == nil { - return false - } - enforcedCondMatch := enforcedCond.Status == conditionStatus && enforcedCond.Reason == reason && enforcedCond.Message == message - - return acceptedCondMatch && enforcedCondMatch - } - } - - policyFactory := func(mutateFns ...func(policy *kuadrantv1beta3.AuthPolicy)) *kuadrantv1beta3.AuthPolicy { - policy := &kuadrantv1beta3.AuthPolicy{ - TypeMeta: metav1.TypeMeta{ - Kind: "AuthPolicy", - APIVersion: kuadrantv1beta3.GroupVersion.String(), - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "toystore", - Namespace: testNamespace, - }, - Spec: kuadrantv1beta3.AuthPolicySpec{ - TargetRef: gatewayapiv1alpha2.LocalPolicyTargetReference{ - Group: gatewayapiv1.GroupName, - Kind: "HTTPRoute", - Name: TestHTTPRouteName, - }, - Defaults: &kuadrantv1beta3.AuthPolicyCommonSpec{ - AuthScheme: tests.BuildBasicAuthScheme(), - }, - }, - } - for _, mutateFn := range mutateFns { - mutateFn(policy) - } - return policy - } + authConfigKeyForPath := func(httpRoute *gatewayapiv1.HTTPRoute, httpRouteRuleIndex int) types.NamespacedName { + mGateway := &machinery.Gateway{Gateway: gateway} + mHTTPRoute := &machinery.HTTPRoute{HTTPRoute: httpRoute} + authConfigName := controllers.AuthConfigNameForPath(kuadrantv1.PathID([]machinery.Targetable{ + &machinery.GatewayClass{GatewayClass: gatewayClass}, + mGateway, + &machinery.Listener{Listener: &gateway.Spec.Listeners[0], Gateway: mGateway}, + mHTTPRoute, + &machinery.HTTPRouteRule{HTTPRoute: mHTTPRoute, HTTPRouteRule: &httpRoute.Spec.Rules[httpRouteRuleIndex], Name: "rule-1"}, + })) + return types.NamespacedName{Name: authConfigName, Namespace: kuadrantInstallationNS} + } - randomHostFromGWHost := func() string { - return strings.Replace(gwHost, "*", rand.String(3), 1) + fetchReadyAuthConfig := func(ctx context.Context, httpRoute *gatewayapiv1.HTTPRoute, httpRouteRuleIndex int, authConfig *authorinov1beta2.AuthConfig) func() bool { + authConfigKey := authConfigKeyForPath(httpRoute, httpRouteRuleIndex) + return func() bool { + err := k8sClient.Get(ctx, authConfigKey, authConfig) + logf.Log.V(1).Info("Fetching Authorino's AuthConfig", "key", authConfigKey.String(), "error", err) + return err == nil && authConfig.Status.Ready() } - - BeforeEach(func(ctx SpecContext) { - route := tests.BuildBasicHttpRoute(TestHTTPRouteName, TestGatewayName, testNamespace, []string{randomHostFromGWHost()}) - err := k8sClient.Create(ctx, route) - Expect(err).ToNot(HaveOccurred()) - Eventually(tests.RouteIsAccepted(ctx, testClient(), client.ObjectKeyFromObject(route))).WithContext(ctx).Should(BeTrue()) - }) - - It("Unknown reason", func(ctx SpecContext) { - // Remove kuadrant to simulate AuthPolicy enforcement error - defer tests.ApplyKuadrantCR(ctx, testClient(), kuadrantInstallationNS) - tests.DeleteKuadrantCR(ctx, testClient(), kuadrantInstallationNS) - - Eventually(func(g Gomega) { - authorinos := &authorinov1beta1.AuthorinoList{} - err := testClient().List(ctx, authorinos, &client.ListOptions{Namespace: kuadrantInstallationNS}) - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(len(authorinos.Items)).To(Equal(0)) - }).Should(Succeed()) - - policy := policyFactory() - - err := k8sClient.Create(ctx, policy) - logf.Log.V(1).Info("Creating AuthPolicy", "key", client.ObjectKeyFromObject(policy).String(), "error", err) - Expect(err).ToNot(HaveOccurred()) - - Eventually(assertAcceptedCondTrueAndEnforcedCond(ctx, policy, metav1.ConditionFalse, string(kuadrant.PolicyReasonUnknown), - "AuthPolicy has encountered some issues: AuthScheme is not ready yet")).WithContext(ctx).Should(BeTrue()) - }, testTimeOut) - }) -}) - -var _ = Describe("AuthPolicy controller", func() { - const ( - testTimeOut = SpecTimeout(2 * time.Minute) - afterEachTimeOut = NodeTimeout(3 * time.Minute) - ) - var ( - testNamespace string - gwHost = fmt.Sprintf("*.toystore-%s.com", rand.String(6)) - ) + } BeforeEach(func(ctx SpecContext) { testNamespace = tests.CreateNamespace(ctx, testClient()) - - gateway := tests.BuildBasicGateway(TestGatewayName, testNamespace, func(gateway *gatewayapiv1.Gateway) { + gatewayClass = &gatewayapiv1.GatewayClass{} + err := testClient().Get(ctx, types.NamespacedName{Name: tests.GatewayClassName}, gatewayClass) + Expect(err).ToNot(HaveOccurred()) + gateway = tests.BuildBasicGateway(TestGatewayName, testNamespace, func(gateway *gatewayapiv1.Gateway) { gateway.Spec.Listeners[0].Hostname = ptr.To(gatewayapiv1.Hostname(gwHost)) }) - err := k8sClient.Create(ctx, gateway) + err = k8sClient.Create(ctx, gateway) Expect(err).ToNot(HaveOccurred()) Eventually(tests.GatewayIsReady(ctx, testClient(), gateway)).WithContext(ctx).Should(BeTrue()) @@ -182,13 +96,17 @@ var _ = Describe("AuthPolicy controller", func() { Namespace: testNamespace, }, Spec: kuadrantv1beta3.AuthPolicySpec{ - TargetRef: gatewayapiv1alpha2.LocalPolicyTargetReference{ - Group: gatewayapiv1.GroupName, - Kind: "HTTPRoute", - Name: TestHTTPRouteName, + TargetRef: gatewayapiv1alpha2.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gatewayapiv1alpha2.LocalPolicyTargetReference{ + Group: gatewayapiv1.GroupName, + Kind: "HTTPRoute", + Name: TestHTTPRouteName, + }, }, - Defaults: &kuadrantv1beta3.AuthPolicyCommonSpec{ - AuthScheme: tests.BuildBasicAuthScheme(), + Defaults: &kuadrantv1beta3.MergeableAuthPolicySpec{ + AuthPolicySpecProper: kuadrantv1beta3.AuthPolicySpecProper{ + AuthScheme: tests.BuildBasicAuthScheme(), + }, }, }, } @@ -202,72 +120,25 @@ var _ = Describe("AuthPolicy controller", func() { } Context("Basic HTTPRoute", func() { - routeHost := randomHostFromGWHost() + var ( + httpRoute *gatewayapiv1.HTTPRoute + routeHost = randomHostFromGWHost() + ) BeforeEach(func(ctx SpecContext) { - route := tests.BuildBasicHttpRoute(TestHTTPRouteName, TestGatewayName, testNamespace, []string{routeHost}) - err := k8sClient.Create(ctx, route) + httpRoute = tests.BuildBasicHttpRoute(TestHTTPRouteName, TestGatewayName, testNamespace, []string{routeHost}) + err := k8sClient.Create(ctx, httpRoute) Expect(err).ToNot(HaveOccurred()) - Eventually(tests.RouteIsAccepted(ctx, testClient(), client.ObjectKeyFromObject(route))).WithContext(ctx).Should(BeTrue()) + Eventually(tests.RouteIsAccepted(ctx, testClient(), client.ObjectKeyFromObject(httpRoute))).WithContext(ctx).Should(BeTrue()) }) - It("Attaches policy to the Gateway (without hostname defined in listener)", func(ctx SpecContext) { - // Create GW with no hostname defined in listener - gwName := "no-defined-hostname" - gateway := tests.BuildBasicGateway(gwName, testNamespace) - err := k8sClient.Create(ctx, gateway) - Expect(err).ToNot(HaveOccurred()) - Eventually(tests.GatewayIsReady(ctx, k8sClient, gateway)).WithContext(ctx).Should(BeTrue()) - - // Create route with this GW as parent - route := tests.BuildBasicHttpRoute("other-route", gwName, testNamespace, []string{routeHost}) - err = k8sClient.Create(ctx, route) - Expect(err).ToNot(HaveOccurred()) - Eventually(tests.RouteIsAccepted(ctx, k8sClient, client.ObjectKeyFromObject(route))).WithContext(ctx).Should(BeTrue()) - + It("Attaches policy to the Gateway", func(ctx SpecContext) { policy := policyFactory(func(policy *kuadrantv1beta3.AuthPolicy) { policy.Name = "gw-auth" policy.Spec.TargetRef.Group = gatewayapiv1.GroupName policy.Spec.TargetRef.Kind = "Gateway" - policy.Spec.TargetRef.Name = gatewayapiv1.ObjectName(gwName) - policy.Spec.CommonSpec().AuthScheme.Authentication["apiKey"].ApiKey.Selector.MatchLabels["admin"] = "yes" - }) - - err = k8sClient.Create(ctx, policy) - logf.Log.V(1).Info("Creating AuthPolicy", "key", client.ObjectKeyFromObject(policy).String(), "error", err) - Expect(err).ToNot(HaveOccurred()) - - // check policy status - Eventually(tests.IsAuthPolicyAcceptedAndEnforced(ctx, testClient(), policy)).WithContext(ctx).Should(BeTrue()) - - // check authorino authconfig - authConfigKey := types.NamespacedName{Name: controllers.AuthConfigName(client.ObjectKeyFromObject(policy)), Namespace: testNamespace} - authConfig := &authorinoapi.AuthConfig{} - Eventually(func() bool { - err := k8sClient.Get(ctx, authConfigKey, authConfig) - logf.Log.V(1).Info("Fetching Authorino's AuthConfig", "key", authConfigKey.String(), "error", err) - return err == nil && authConfig.Status.Ready() - }).WithContext(ctx).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.*")) - }, testTimeOut) - - It("Attaches policy to a Gateway with hostname in listeners", func(ctx SpecContext) { - policy := policyFactory(func(policy *kuadrantv1beta3.AuthPolicy) { - policy.Name = "gw-auth" - policy.Spec.TargetRef.Group = gatewayapiv1.GroupName - policy.Spec.TargetRef.Kind = "Gateway" - policy.Spec.TargetRef.Name = TestGatewayName + policy.Spec.TargetRef.Name = gatewayapiv1.ObjectName(TestGatewayName) + policy.Spec.Proper().AuthScheme.Authentication["apiKey"].ApiKey.Selector.MatchLabels["admin"] = "yes" }) err := k8sClient.Create(ctx, policy) @@ -277,16 +148,23 @@ var _ = Describe("AuthPolicy controller", func() { // check policy status Eventually(tests.IsAuthPolicyAcceptedAndEnforced(ctx, testClient(), policy)).WithContext(ctx).Should(BeTrue()) - // check authorino authconfig hosts - authConfigKey := types.NamespacedName{Name: controllers.AuthConfigName(client.ObjectKeyFromObject(policy)), Namespace: testNamespace} - authConfig := &authorinoapi.AuthConfig{} - Eventually(func() bool { - err := k8sClient.Get(ctx, authConfigKey, authConfig) - logf.Log.V(1).Info("Fetching Authorino's AuthConfig", "key", authConfigKey.String(), "error", err) - return err == nil && authConfig.Status.Ready() - }).WithContext(ctx).Should(BeTrue()) - - Expect(authConfig.Spec.Hosts).To(ConsistOf(gwHost)) + // check authorino authconfig + authConfig := &authorinov1beta2.AuthConfig{} + Eventually(fetchReadyAuthConfig(ctx, httpRoute, 0, authConfig)).WithContext(ctx).Should(BeTrue()) + Expect(authConfig.Spec.Authentication).To(HaveLen(1)) + Expect(authConfig.Spec.Authentication).To(HaveKeyWithValue("apiKey", policy.Spec.Proper().AuthScheme.Authentication["apiKey"].AuthenticationSpec)) + + // create other route + otherHTTPRoute := tests.BuildBasicHttpRoute("other-route", TestGatewayName, testNamespace, []string{routeHost}) + err = k8sClient.Create(ctx, otherHTTPRoute) + Expect(err).ToNot(HaveOccurred()) + Eventually(tests.RouteIsAccepted(ctx, k8sClient, client.ObjectKeyFromObject(otherHTTPRoute))).WithContext(ctx).Should(BeTrue()) + + // check authorino other authconfig + otherAuthConfig := &authorinov1beta2.AuthConfig{} + Eventually(fetchReadyAuthConfig(ctx, otherHTTPRoute, 0, otherAuthConfig)).WithContext(ctx).Should(BeTrue()) + Expect(otherAuthConfig.Spec.Authentication).To(HaveLen(1)) + Expect(otherAuthConfig.Spec.Authentication).To(HaveKeyWithValue("apiKey", policy.Spec.Proper().AuthScheme.Authentication["apiKey"].AuthenticationSpec)) }, testTimeOut) It("Attaches policy to the HTTPRoute", func(ctx SpecContext) { @@ -300,25 +178,10 @@ var _ = Describe("AuthPolicy controller", func() { Eventually(tests.IsAuthPolicyAcceptedAndEnforced(ctx, testClient(), policy)).WithContext(ctx).Should(BeTrue()) // check authorino authconfig - authConfigKey := types.NamespacedName{Name: controllers.AuthConfigName(client.ObjectKeyFromObject(policy)), Namespace: testNamespace} - authConfig := &authorinoapi.AuthConfig{} - Eventually(func() bool { - err := k8sClient.Get(ctx, authConfigKey, authConfig) - logf.Log.V(1).Info("Fetching Authorino's AuthConfig", "key", authConfigKey.String(), "error", err) - return err == nil && authConfig.Status.Ready() - }).WithContext(ctx).Should(BeTrue()) - logf.Log.V(1).Info("authConfig.Spec", "hosts", authConfig.Spec.Hosts, "conditions", authConfig.Spec.Conditions) - Expect(authConfig.Spec.Hosts).To(Equal([]string{routeHost})) - 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.*")) + authConfig := &authorinov1beta2.AuthConfig{} + Eventually(fetchReadyAuthConfig(ctx, httpRoute, 0, authConfig)).WithContext(ctx).Should(BeTrue()) + Expect(authConfig.Spec.Authentication).To(HaveLen(1)) + Expect(authConfig.Spec.Authentication).To(HaveKeyWithValue("apiKey", policy.Spec.Proper().AuthScheme.Authentication["apiKey"].AuthenticationSpec)) }, testTimeOut) It("Attaches policy to the Gateway while having other policies attached to some HTTPRoutes", func(ctx SpecContext) { @@ -333,15 +196,6 @@ var _ = Describe("AuthPolicy controller", func() { // create second (policyless) httproute otherRoute := tests.BuildBasicHttpRoute("policyless-route", TestGatewayName, testNamespace, []string{randomHostFromGWHost()}) - otherRoute.Spec.Rules = []gatewayapiv1.HTTPRouteRule{ - { - Matches: []gatewayapiv1.HTTPRouteMatch{ - { - Method: ptr.To(gatewayapiv1.HTTPMethod("POST")), - }, - }, - }, - } err = k8sClient.Create(ctx, otherRoute) Expect(err).ToNot(HaveOccurred()) Eventually(tests.RouteIsAccepted(ctx, testClient(), client.ObjectKeyFromObject(otherRoute))).WithContext(ctx).Should(BeTrue()) @@ -352,6 +206,7 @@ var _ = Describe("AuthPolicy controller", func() { policy.Spec.TargetRef.Group = gatewayapiv1.GroupName policy.Spec.TargetRef.Kind = "Gateway" policy.Spec.TargetRef.Name = TestGatewayName + policy.Spec.Proper().AuthScheme.Authentication["apiKey"].ApiKey.Selector.MatchLabels["gateway"] = "yes" }) err = k8sClient.Create(ctx, gwPolicy) @@ -362,25 +217,15 @@ var _ = Describe("AuthPolicy controller", func() { Eventually(tests.IsAuthPolicyAcceptedAndEnforced(ctx, testClient(), gwPolicy)).WithContext(ctx).Should(BeTrue()) // check authorino authconfig - authConfigKey := types.NamespacedName{Name: controllers.AuthConfigName(client.ObjectKeyFromObject(gwPolicy)), Namespace: testNamespace} - authConfig := &authorinoapi.AuthConfig{} - Eventually(func() bool { - err := k8sClient.Get(ctx, authConfigKey, authConfig) - logf.Log.V(1).Info("Fetching Authorino's AuthConfig", "key", authConfigKey.String(), "error", err) - return err == nil && authConfig.Status.Ready() - }).WithContext(ctx).Should(BeTrue()) - logf.Log.V(1).Info("authConfig.Spec", "hosts", authConfig.Spec.Hosts, "conditions", authConfig.Spec.Conditions) - Expect(authConfig.Spec.Hosts).To(Equal([]string{gwHost})) - 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("/.*")) + authConfig := &authorinov1beta2.AuthConfig{} + Eventually(fetchReadyAuthConfig(ctx, httpRoute, 0, authConfig)).WithContext(ctx).Should(BeTrue()) + Expect(authConfig.Spec.Authentication).To(HaveLen(1)) + Expect(authConfig.Spec.Authentication).To(HaveKeyWithValue("apiKey", routePolicy.Spec.Proper().AuthScheme.Authentication["apiKey"].AuthenticationSpec)) + + otherAuthConfig := &authorinov1beta2.AuthConfig{} + Eventually(fetchReadyAuthConfig(ctx, otherRoute, 0, otherAuthConfig)).WithContext(ctx).Should(BeTrue()) + Expect(otherAuthConfig.Spec.Authentication).To(HaveLen(1)) + Expect(otherAuthConfig.Spec.Authentication).To(HaveKeyWithValue("apiKey", gwPolicy.Spec.Proper().AuthScheme.Authentication["apiKey"].AuthenticationSpec)) }, testTimeOut) It("Deletes resources when the policy is deleted", func(ctx SpecContext) { @@ -398,110 +243,122 @@ var _ = Describe("AuthPolicy controller", func() { Expect(err).ToNot(HaveOccurred()) // check authorino authconfig - authConfigKey := types.NamespacedName{Name: controllers.AuthConfigName(client.ObjectKey{Name: "toystore", Namespace: testNamespace}), Namespace: testNamespace} + authConfigKey := authConfigKeyForPath(httpRoute, 0) Eventually(func() bool { - err := k8sClient.Get(ctx, authConfigKey, &authorinoapi.AuthConfig{}) + err := k8sClient.Get(ctx, authConfigKey, &authorinov1beta2.AuthConfig{}) return apierrors.IsNotFound(err) }).WithContext(ctx).Should(BeTrue()) }, testTimeOut) It("Maps to all fields of the AuthConfig", func(ctx SpecContext) { policy := policyFactory(func(policy *kuadrantv1beta3.AuthPolicy) { - policy.Spec.CommonSpec().NamedPatterns = map[string]authorinoapi.PatternExpressions{ - "internal-source": []authorinoapi.PatternExpression{ - { - Selector: "source.ip", - Operator: authorinoapi.PatternExpressionOperator("matches"), - Value: `192\.168\..*`, + policy.Spec.Proper().NamedPatterns = map[string]kuadrantv1beta3.MergeablePatternExpressions{ + "internal-source": { + PatternExpressions: []authorinov1beta2.PatternExpression{ + { + Selector: "source.ip", + Operator: authorinov1beta2.PatternExpressionOperator("matches"), + Value: `192\.168\..*`, + }, }, }, - "authz-and-rl-required": []authorinoapi.PatternExpression{ - { - Selector: "source.ip", - Operator: authorinoapi.PatternExpressionOperator("neq"), - Value: "192.168.0.10", + "authz-and-rl-required": { + PatternExpressions: []authorinov1beta2.PatternExpression{ + { + Selector: "source.ip", + Operator: authorinov1beta2.PatternExpressionOperator("neq"), + Value: "192.168.0.10", + }, }, }, } - policy.Spec.CommonSpec().Conditions = []authorinoapi.PatternExpressionOrRef{ + policy.Spec.Proper().Conditions = []kuadrantv1beta3.MergeablePatternExpressionOrRef{ { - PatternRef: authorinoapi.PatternRef{ - Name: "internal-source", + PatternExpressionOrRef: authorinov1beta2.PatternExpressionOrRef{ + PatternRef: authorinov1beta2.PatternRef{ + Name: "internal-source", + }, }, }, } - policy.Spec.CommonSpec().AuthScheme = &kuadrantv1beta3.AuthSchemeSpec{ - Authentication: map[string]authorinoapi.AuthenticationSpec{ + policy.Spec.Proper().AuthScheme = &kuadrantv1beta3.AuthSchemeSpec{ + Authentication: map[string]kuadrantv1beta3.MergeableAuthenticationSpec{ "jwt": { - CommonEvaluatorSpec: authorinoapi.CommonEvaluatorSpec{ - Conditions: []authorinoapi.PatternExpressionOrRef{ - { - PatternExpression: authorinoapi.PatternExpression{ - Selector: `filter_metadata.envoy\.filters\.http\.jwt_authn|verified_jwt`, - Operator: "neq", - Value: "", + AuthenticationSpec: authorinov1beta2.AuthenticationSpec{ + CommonEvaluatorSpec: authorinov1beta2.CommonEvaluatorSpec{ + Conditions: []authorinov1beta2.PatternExpressionOrRef{ + { + PatternExpression: authorinov1beta2.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`, + AuthenticationMethodSpec: authorinov1beta2.AuthenticationMethodSpec{ + Plain: &authorinov1beta2.PlainIdentitySpec{ + Selector: `filter_metadata.envoy\.filters\.http\.jwt_authn|verified_jwt`, + }, }, }, }, }, - Metadata: map[string]authorinoapi.MetadataSpec{ + Metadata: map[string]kuadrantv1beta3.MergeableMetadataSpec{ "user-groups": { - CommonEvaluatorSpec: authorinoapi.CommonEvaluatorSpec{ - Conditions: []authorinoapi.PatternExpressionOrRef{ - { - PatternExpression: authorinoapi.PatternExpression{ - Selector: "auth.identity.admin", - Operator: authorinoapi.PatternExpressionOperator("neq"), - Value: "true", + MetadataSpec: authorinov1beta2.MetadataSpec{ + CommonEvaluatorSpec: authorinov1beta2.CommonEvaluatorSpec{ + Conditions: []authorinov1beta2.PatternExpressionOrRef{ + { + PatternExpression: authorinov1beta2.PatternExpression{ + Selector: "auth.identity.admin", + Operator: authorinov1beta2.PatternExpressionOperator("neq"), + Value: "true", + }, }, }, }, - }, - MetadataMethodSpec: authorinoapi.MetadataMethodSpec{ - Http: &authorinoapi.HttpEndpointSpec{ - Url: "http://user-groups/username={auth.identity.username}", + MetadataMethodSpec: authorinov1beta2.MetadataMethodSpec{ + Http: &authorinov1beta2.HttpEndpointSpec{ + Url: "http://user-groups/username={auth.identity.username}", + }, }, }, }, }, - Authorization: map[string]authorinoapi.AuthorizationSpec{ + Authorization: map[string]kuadrantv1beta3.MergeableAuthorizationSpec{ "admin-or-privileged": { - CommonEvaluatorSpec: authorinoapi.CommonEvaluatorSpec{ - Conditions: []authorinoapi.PatternExpressionOrRef{ - { - PatternRef: authorinoapi.PatternRef{ - Name: "authz-and-rl-required", + AuthorizationSpec: authorinov1beta2.AuthorizationSpec{ + CommonEvaluatorSpec: authorinov1beta2.CommonEvaluatorSpec{ + Conditions: []authorinov1beta2.PatternExpressionOrRef{ + { + PatternRef: authorinov1beta2.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", + AuthorizationMethodSpec: authorinov1beta2.AuthorizationMethodSpec{ + PatternMatching: &authorinov1beta2.PatternMatchingAuthorizationSpec{ + Patterns: []authorinov1beta2.PatternExpressionOrRef{ + { + Any: []authorinov1beta2.UnstructuredPatternExpressionOrRef{ + { + PatternExpressionOrRef: authorinov1beta2.PatternExpressionOrRef{ + PatternExpression: authorinov1beta2.PatternExpression{ + Selector: "auth.identity.admin", + Operator: authorinov1beta2.PatternExpressionOperator("eq"), + Value: "true", + }, }, }, - }, - { - PatternExpressionOrRef: authorinoapi.PatternExpressionOrRef{ - PatternExpression: authorinoapi.PatternExpression{ - Selector: "auth.metadata.user-groups", - Operator: authorinoapi.PatternExpressionOperator("incl"), - Value: "privileged", + { + PatternExpressionOrRef: authorinov1beta2.PatternExpressionOrRef{ + PatternExpression: authorinov1beta2.PatternExpression{ + Selector: "auth.metadata.user-groups", + Operator: authorinov1beta2.PatternExpressionOperator("incl"), + Value: "privileged", + }, }, }, }, @@ -512,59 +369,67 @@ var _ = Describe("AuthPolicy controller", func() { }, }, }, - Response: &kuadrantv1beta3.ResponseSpec{ - Unauthenticated: &authorinoapi.DenyWithSpec{ - Message: &authorinoapi.ValueOrSelector{ - Value: k8sruntime.RawExtension{Raw: []byte(`"Missing verified JWT injected by the gateway"`)}, + Response: &kuadrantv1beta3.MergeableResponseSpec{ + Unauthenticated: &kuadrantv1beta3.MergeableDenyWithSpec{ + DenyWithSpec: authorinov1beta2.DenyWithSpec{ + Message: &authorinov1beta2.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"`)}, + Unauthorized: &kuadrantv1beta3.MergeableDenyWithSpec{ + DenyWithSpec: authorinov1beta2.DenyWithSpec{ + Message: &authorinov1beta2.ValueOrSelector{ + Value: k8sruntime.RawExtension{Raw: []byte(`"User must be admin or member of privileged group"`)}, + }, }, }, - Success: kuadrantv1beta3.WrappedSuccessResponseSpec{ - Headers: map[string]kuadrantv1beta3.HeaderSuccessResponseSpec{ + Success: kuadrantv1beta3.MergeableWrappedSuccessResponseSpec{ + Headers: map[string]kuadrantv1beta3.MergeableHeaderSuccessResponseSpec{ "x-username": { - 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", + HeaderSuccessResponseSpec: authorinov1beta2.HeaderSuccessResponseSpec{ + SuccessResponseSpec: authorinov1beta2.SuccessResponseSpec{ + CommonEvaluatorSpec: authorinov1beta2.CommonEvaluatorSpec{ + Conditions: []authorinov1beta2.PatternExpressionOrRef{ + { + PatternExpression: authorinov1beta2.PatternExpression{ + Selector: "request.headers.x-propagate-username.@case:lower", + Operator: authorinov1beta2.PatternExpressionOperator("matches"), + Value: "1|yes|true", + }, }, }, }, - }, - AuthResponseMethodSpec: authorinoapi.AuthResponseMethodSpec{ - Plain: &authorinoapi.PlainAuthResponseSpec{ - Selector: "auth.identity.username", + AuthResponseMethodSpec: authorinov1beta2.AuthResponseMethodSpec{ + Plain: &authorinov1beta2.PlainAuthResponseSpec{ + Selector: "auth.identity.username", + }, }, }, }, }, }, - DynamicMetadata: map[string]authorinoapi.SuccessResponseSpec{ + DynamicMetadata: map[string]kuadrantv1beta3.MergeableSuccessResponseSpec{ "x-auth-data": { - CommonEvaluatorSpec: authorinoapi.CommonEvaluatorSpec{ - Conditions: []authorinoapi.PatternExpressionOrRef{ - { - PatternRef: authorinoapi.PatternRef{ - Name: "authz-and-rl-required", + SuccessResponseSpec: authorinov1beta2.SuccessResponseSpec{ + CommonEvaluatorSpec: authorinov1beta2.CommonEvaluatorSpec{ + Conditions: []authorinov1beta2.PatternExpressionOrRef{ + { + PatternRef: authorinov1beta2.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", + AuthResponseMethodSpec: authorinov1beta2.AuthResponseMethodSpec{ + Json: &authorinov1beta2.JsonAuthResponseSpec{ + Properties: authorinov1beta2.NamedValuesOrSelectors{ + "username": { + Selector: "auth.identity.username", + }, + "groups": { + Selector: "auth.metadata.user-groups", + }, }, }, }, @@ -573,31 +438,33 @@ var _ = Describe("AuthPolicy controller", func() { }, }, }, - Callbacks: map[string]authorinoapi.CallbackSpec{ + Callbacks: map[string]kuadrantv1beta3.MergeableCallbackSpec{ "unauthorized-attempt": { - CommonEvaluatorSpec: authorinoapi.CommonEvaluatorSpec{ - Conditions: []authorinoapi.PatternExpressionOrRef{ - { - PatternRef: authorinoapi.PatternRef{ - Name: "authz-and-rl-required", + CallbackSpec: authorinov1beta2.CallbackSpec{ + CommonEvaluatorSpec: authorinov1beta2.CommonEvaluatorSpec{ + Conditions: []authorinov1beta2.PatternExpressionOrRef{ + { + PatternRef: authorinov1beta2.PatternRef{ + Name: "authz-and-rl-required", + }, }, - }, - { - PatternExpression: authorinoapi.PatternExpression{ - Selector: "auth.authorization.admin-or-privileged", - Operator: authorinoapi.PatternExpressionOperator("neq"), - Value: "true", + { + PatternExpression: authorinov1beta2.PatternExpression{ + Selector: "auth.authorization.admin-or-privileged", + Operator: authorinov1beta2.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}\}`, + CallbackMethodSpec: authorinov1beta2.CallbackMethodSpec{ + Http: &authorinov1beta2.HttpEndpointSpec{ + Url: "http://events/unauthorized", + Method: ptr.To(authorinov1beta2.HttpMethod("POST")), + ContentType: authorinov1beta2.HttpContentType("application/json"), + Body: &authorinov1beta2.ValueOrSelector{ + Selector: `\{"identity":{auth.identity},"request-id":{request.id}\}`, + }, }, }, }, @@ -614,20 +481,15 @@ var _ = Describe("AuthPolicy controller", func() { Eventually(tests.IsAuthPolicyAcceptedAndEnforced(ctx, testClient(), policy)).WithContext(ctx).Should(BeTrue()) // check authorino authconfig - authConfigKey := types.NamespacedName{Name: controllers.AuthConfigName(client.ObjectKeyFromObject(policy)), Namespace: testNamespace} - authConfig := &authorinoapi.AuthConfig{} - Eventually(func() bool { - err := k8sClient.Get(ctx, authConfigKey, authConfig) - logf.Log.V(1).Info("Fetching Authorino's AuthConfig", "key", authConfigKey.String(), "error", err) - return err == nil && authConfig.Status.Ready() - }).WithContext(ctx).Should(BeTrue()) + authConfig := &authorinov1beta2.AuthConfig{} + Eventually(fetchReadyAuthConfig(ctx, httpRoute, 0, authConfig)).WithContext(ctx).Should(BeTrue()) authConfigSpecAsJSON, _ := json.Marshal(authConfig.Spec) - Expect(string(authConfigSpecAsJSON)).To(Equal(fmt.Sprintf(`{"hosts":["%s"],"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":{},"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":{}}}},"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":{}}}}}`, routeHost))) + Expect(string(authConfigSpecAsJSON)).To(Equal(fmt.Sprintf(`{"hosts":["%s"],"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"}],"authentication":{"jwt":{"when":[{"selector":"filter_metadata.envoy\\.filters\\.http\\.jwt_authn|verified_jwt","operator":"neq"}],"credentials":{},"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":{}}}},"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":{}}}}}`, authConfig.GetName()))) }, testTimeOut) It("Succeeds when AuthScheme is not defined", func(ctx SpecContext) { policy := policyFactory(func(policy *kuadrantv1beta3.AuthPolicy) { - policy.Spec.CommonSpec().AuthScheme = nil + policy.Spec.Proper().AuthScheme = nil }) err := k8sClient.Create(ctx, policy) @@ -640,15 +502,16 @@ var _ = Describe("AuthPolicy controller", func() { Context("Complex HTTPRoute with multiple rules and hostnames", func() { var ( - host1 = randomHostFromGWHost() - host2 = randomHostFromGWHost() + httpRoute *gatewayapiv1.HTTPRoute + host1 = randomHostFromGWHost() + host2 = randomHostFromGWHost() ) BeforeEach(func(ctx SpecContext) { - route := tests.BuildMultipleRulesHttpRoute(TestHTTPRouteName, TestGatewayName, testNamespace, []string{host1, host2}) - err := k8sClient.Create(ctx, route) + httpRoute = tests.BuildMultipleRulesHttpRoute(TestHTTPRouteName, TestGatewayName, testNamespace, []string{host1, host2}) + err := k8sClient.Create(ctx, httpRoute) Expect(err).ToNot(HaveOccurred()) - Eventually(tests.RouteIsAccepted(ctx, testClient(), client.ObjectKeyFromObject(route))).WithContext(ctx).Should(BeTrue()) + Eventually(tests.RouteIsAccepted(ctx, testClient(), client.ObjectKeyFromObject(httpRoute))).WithContext(ctx).Should(BeTrue()) }) It("Attaches simple policy to the HTTPRoute", func(ctx SpecContext) { @@ -660,41 +523,12 @@ var _ = Describe("AuthPolicy controller", func() { // check policy status Eventually(tests.IsAuthPolicyAcceptedAndEnforced(ctx, testClient(), policy)).WithContext(ctx).Should(BeTrue()) - // check authorino authconfig - authConfigKey := types.NamespacedName{Name: controllers.AuthConfigName(client.ObjectKeyFromObject(policy)), Namespace: testNamespace} - authConfig := &authorinoapi.AuthConfig{} - Eventually(func() bool { - err := k8sClient.Get(ctx, authConfigKey, authConfig) - logf.Log.V(1).Info("Fetching Authorino's AuthConfig", "key", authConfigKey.String(), "error", err) - return err == nil && authConfig.Status.Ready() - }).WithContext(ctx).Should(BeTrue()) - logf.Log.V(1).Info("authConfig.Spec", "hosts", authConfig.Spec.Hosts, "conditions", authConfig.Spec.Conditions) - Expect(authConfig.Spec.Hosts).To(Equal([]string{host1, host2})) - 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.*")) + // check authorino authconfigs + authConfigPOST_DELETE_admin := &authorinov1beta2.AuthConfig{} + Eventually(fetchReadyAuthConfig(ctx, httpRoute, 0, authConfigPOST_DELETE_admin)).WithContext(ctx).Should(BeTrue()) + + authConfigGET_private := &authorinov1beta2.AuthConfig{} + Eventually(fetchReadyAuthConfig(ctx, httpRoute, 1, authConfigGET_private)).WithContext(ctx).Should(BeTrue()) }, testTimeOut) }) @@ -733,35 +567,11 @@ var _ = Describe("AuthPolicy controller", func() { Eventually(assertAcceptedCondFalseAndEnforcedCondNil(ctx, policy, string(gatewayapiv1alpha2.PolicyReasonTargetNotFound), fmt.Sprintf("AuthPolicy target %s was not found", TestHTTPRouteName))).WithContext(ctx).Should(BeTrue()) }, testTimeOut) - - It("Conflict reason", func(ctx SpecContext) { - route := tests.BuildBasicHttpRoute(TestHTTPRouteName, TestGatewayName, testNamespace, []string{randomHostFromGWHost()}) - err := k8sClient.Create(ctx, route) - Expect(err).ToNot(HaveOccurred()) - Eventually(tests.RouteIsAccepted(ctx, testClient(), client.ObjectKeyFromObject(route))).WithContext(ctx).Should(BeTrue()) - - policy := policyFactory() - err = k8sClient.Create(ctx, policy) - logf.Log.V(1).Info("Creating AuthPolicy", "key", client.ObjectKeyFromObject(policy).String(), "error", err) - Expect(err).ToNot(HaveOccurred()) - - Eventually(tests.IsAuthPolicyAccepted(ctx, testClient(), policy)).WithContext(ctx).Should(BeTrue()) - - policy2 := policyFactory(func(policy *kuadrantv1beta3.AuthPolicy) { - policy.Name = "conflicting-ap" - }) - err = k8sClient.Create(ctx, policy2) - logf.Log.V(1).Info("Creating AuthPolicy", "key", client.ObjectKeyFromObject(policy2).String(), "error", err) - Expect(err).ToNot(HaveOccurred()) - - // check policy status - Eventually(assertAcceptedCondFalseAndEnforcedCondNil(ctx, policy2, string(gatewayapiv1alpha2.PolicyReasonConflicted), - fmt.Sprintf("AuthPolicy is conflicted by %[1]v/toystore: the gateway.networking.k8s.io/v1, Kind=HTTPRoute target %[1]v/toystore-route is already referenced by policy %[1]v/toystore", testNamespace), - )).WithContext(ctx).Should(BeTrue()) - }, testTimeOut) }) Context("AuthPolicy enforced condition reasons", func() { + var httpRoute *gatewayapiv1.HTTPRoute + assertAcceptedCondTrueAndEnforcedCond := func(ctx context.Context, policy *kuadrantv1beta3.AuthPolicy, conditionStatus metav1.ConditionStatus, reason, message string) func() bool { return func() bool { existingPolicy := &kuadrantv1beta3.AuthPolicy{} @@ -787,10 +597,10 @@ var _ = Describe("AuthPolicy controller", func() { } BeforeEach(func(ctx SpecContext) { - route := tests.BuildBasicHttpRoute(TestHTTPRouteName, TestGatewayName, testNamespace, []string{randomHostFromGWHost()}) - err := k8sClient.Create(ctx, route) + httpRoute = tests.BuildBasicHttpRoute(TestHTTPRouteName, TestGatewayName, testNamespace, []string{randomHostFromGWHost()}) + err := k8sClient.Create(ctx, httpRoute) Expect(err).ToNot(HaveOccurred()) - Eventually(tests.RouteIsAccepted(ctx, testClient(), client.ObjectKeyFromObject(route))).WithContext(ctx).Should(BeTrue()) + Eventually(tests.RouteIsAccepted(ctx, testClient(), client.ObjectKeyFromObject(httpRoute))).WithContext(ctx).Should(BeTrue()) }) It("Enforced reason", func(ctx SpecContext) { @@ -806,38 +616,43 @@ var _ = Describe("AuthPolicy controller", func() { It("Overridden reason - Attaches policy to the Gateway while having other policies attached to all HTTPRoutes", func(ctx SpecContext) { routePolicy := policyFactory() + routePolicyKey := client.ObjectKeyFromObject(routePolicy) err := k8sClient.Create(ctx, routePolicy) - logf.Log.V(1).Info("Creating AuthPolicy", "key", client.ObjectKeyFromObject(routePolicy).String(), "error", err) + logf.Log.V(1).Info("Creating AuthPolicy", "key", routePolicyKey.String(), "error", err) Expect(err).ToNot(HaveOccurred()) // check route policy status Eventually(tests.IsAuthPolicyAcceptedAndEnforced(ctx, testClient(), routePolicy)).WithContext(ctx).Should(BeTrue()) + // check authorino authconfig + authConfig := &authorinov1beta2.AuthConfig{} + Eventually(fetchReadyAuthConfig(ctx, httpRoute, 0, authConfig)).WithContext(ctx).Should(BeTrue()) + Expect(authConfig.Spec.Authentication).To(HaveLen(1)) + Expect(authConfig.Spec.Authentication).To(HaveKeyWithValue("apiKey", routePolicy.Spec.Proper().AuthScheme.Authentication["apiKey"].AuthenticationSpec)) + // attach policy to the gatewaay gwPolicy := policyFactory(func(policy *kuadrantv1beta3.AuthPolicy) { policy.Name = "gw-auth" policy.Spec.TargetRef.Group = gatewayapiv1.GroupName policy.Spec.TargetRef.Kind = "Gateway" policy.Spec.TargetRef.Name = TestGatewayName + policy.Spec.Proper().AuthScheme.Authentication["apiKey"].ApiKey.Selector.MatchLabels["gateway"] = "yes" }) - + gatewayPolicyKey := client.ObjectKeyFromObject(gwPolicy) err = k8sClient.Create(ctx, gwPolicy) - logf.Log.V(1).Info("Creating AuthPolicy", "key", client.ObjectKeyFromObject(gwPolicy).String(), "error", err) + logf.Log.V(1).Info("Creating AuthPolicy", "key", gatewayPolicyKey.String(), "error", err) Expect(err).ToNot(HaveOccurred()) // check policy status Eventually(tests.IsAuthPolicyAccepted(ctx, testClient(), gwPolicy)).WithContext(ctx).Should(BeTrue()) - Eventually( - assertAcceptedCondTrueAndEnforcedCond(ctx, gwPolicy, metav1.ConditionFalse, string(kuadrant.PolicyReasonOverridden), - fmt.Sprintf("AuthPolicy is overridden by [%s/%s]", testNamespace, routePolicy.Name))).WithContext(ctx).Should(BeTrue()) + Eventually(assertAcceptedCondTrueAndEnforcedCond(ctx, gwPolicy, metav1.ConditionFalse, string(kuadrant.PolicyReasonOverridden), fmt.Sprintf("AuthPolicy is overridden by [%s]", routePolicyKey.String()))).WithContext(ctx).Should(BeTrue()) // check authorino authconfig - authConfigKey := types.NamespacedName{Name: controllers.AuthConfigName(client.ObjectKeyFromObject(gwPolicy)), Namespace: testNamespace} - Eventually(func() bool { - err := k8sClient.Get(ctx, authConfigKey, &authorinoapi.AuthConfig{}) - return apierrors.IsNotFound(err) - }).WithContext(ctx).Should(BeTrue()) + authConfig = &authorinov1beta2.AuthConfig{} + Eventually(fetchReadyAuthConfig(ctx, httpRoute, 0, authConfig)).WithContext(ctx).Should(BeTrue()) + Expect(authConfig.Spec.Authentication).To(HaveLen(1)) + Expect(authConfig.Spec.Authentication).To(HaveKeyWithValue("apiKey", routePolicy.Spec.Proper().AuthScheme.Authentication["apiKey"].AuthenticationSpec)) // GW Policy should go back to being enforced when a HTTPRoute with no AP attached becomes available route2 := tests.BuildBasicHttpRoute("route2", TestGatewayName, testNamespace, []string{randomHostFromGWHost()}) @@ -863,33 +678,35 @@ var _ = Describe("AuthPolicy controller", func() { policy.Spec.TargetRef.Group = gatewayapiv1.GroupName policy.Spec.TargetRef.Kind = "Gateway" policy.Spec.TargetRef.Name = TestGatewayName - policy.Spec.Overrides = &kuadrantv1beta3.AuthPolicyCommonSpec{} + policy.Spec.Overrides = &kuadrantv1beta3.MergeableAuthPolicySpec{} policy.Spec.Defaults = nil policy.Spec.Overrides.AuthScheme = tests.BuildBasicAuthScheme() policy.Spec.Overrides.AuthScheme.Authentication["apiKey"].ApiKey.Selector.MatchLabels["admin"] = "yes" }) - + gatewayPolicyKey := client.ObjectKeyFromObject(gatewayPolicy) err := k8sClient.Create(ctx, gatewayPolicy) - logf.Log.V(1).Info("Creating AuthPolicy", "key", client.ObjectKeyFromObject(gatewayPolicy).String(), "error", err) + logf.Log.V(1).Info("Creating AuthPolicy", "key", gatewayPolicyKey.String(), "error", err) Expect(err).ToNot(HaveOccurred()) // check policy status Eventually(tests.IsAuthPolicyAcceptedAndEnforced(ctx, testClient(), gatewayPolicy)).WithContext(ctx).Should(BeTrue()) routePolicy := policyFactory() + routePolicyKey := client.ObjectKeyFromObject(routePolicy) err = k8sClient.Create(ctx, routePolicy) - logf.Log.V(1).Info("Creating AuthPolicy", "key", client.ObjectKeyFromObject(routePolicy).String(), "error", err) + logf.Log.V(1).Info("Creating AuthPolicy", "key", routePolicyKey.String(), "error", err) Expect(err).ToNot(HaveOccurred()) // check policy status Eventually(tests.IsAuthPolicyAcceptedAndNotEnforced(ctx, testClient(), routePolicy)).WithContext(ctx).Should(BeTrue()) - Eventually(tests.IsAuthPolicyEnforcedCondition(ctx, testClient(), client.ObjectKeyFromObject(routePolicy), kuadrant.PolicyReasonOverridden, fmt.Sprintf("AuthPolicy is overridden by [%s]", client.ObjectKeyFromObject(gatewayPolicy)))).WithContext(ctx).Should(BeTrue()) + Eventually(tests.IsAuthPolicyEnforcedCondition(ctx, testClient(), routePolicyKey, kuadrant.PolicyReasonOverridden, fmt.Sprintf("AuthPolicy is overridden by [%s]", gatewayPolicyKey.String()))).WithContext(ctx).Should(BeTrue()) }, testTimeOut) It("Route AuthPolicy exists and Gateway AuthPolicy with overrides is added.", func(ctx SpecContext) { routePolicy := policyFactory() + routePolicyKey := client.ObjectKeyFromObject(routePolicy) err := k8sClient.Create(ctx, routePolicy) - logf.Log.V(1).Info("Creating AuthPolicy", "key", client.ObjectKeyFromObject(routePolicy).String(), "error", err) + logf.Log.V(1).Info("Creating AuthPolicy", "key", routePolicyKey.String(), "error", err) Expect(err).ToNot(HaveOccurred()) // check policy status @@ -900,26 +717,27 @@ var _ = Describe("AuthPolicy controller", func() { policy.Spec.TargetRef.Group = gatewayapiv1.GroupName policy.Spec.TargetRef.Kind = "Gateway" policy.Spec.TargetRef.Name = TestGatewayName - policy.Spec.Overrides = &kuadrantv1beta3.AuthPolicyCommonSpec{} + policy.Spec.Overrides = &kuadrantv1beta3.MergeableAuthPolicySpec{} policy.Spec.Defaults = nil policy.Spec.Overrides.AuthScheme = tests.BuildBasicAuthScheme() policy.Spec.Overrides.AuthScheme.Authentication["apiKey"].ApiKey.Selector.MatchLabels["admin"] = "yes" }) - + gatewayPolicyKey := client.ObjectKeyFromObject(gatewayPolicy) err = k8sClient.Create(ctx, gatewayPolicy) - logf.Log.V(1).Info("Creating AuthPolicy", "key", client.ObjectKeyFromObject(gatewayPolicy).String(), "error", err) + logf.Log.V(1).Info("Creating AuthPolicy", "key", gatewayPolicyKey.String(), "error", err) Expect(err).ToNot(HaveOccurred()) // check policy status Eventually(tests.IsAuthPolicyAcceptedAndEnforced(ctx, testClient(), gatewayPolicy)).WithContext(ctx).Should(BeTrue()) Eventually(tests.IsAuthPolicyEnforced(ctx, testClient(), routePolicy)).WithContext(ctx).Should(BeFalse()) - Eventually(tests.IsAuthPolicyEnforcedCondition(ctx, testClient(), client.ObjectKeyFromObject(routePolicy), kuadrant.PolicyReasonOverridden, fmt.Sprintf("AuthPolicy is overridden by [%s]", client.ObjectKeyFromObject(gatewayPolicy)))).WithContext(ctx).Should(BeTrue()) + Eventually(tests.IsAuthPolicyEnforcedCondition(ctx, testClient(), routePolicyKey, kuadrant.PolicyReasonOverridden, fmt.Sprintf("AuthPolicy is overridden by [%s]", gatewayPolicyKey.String()))).WithContext(ctx).Should(BeTrue()) }, testTimeOut) It("Route AuthPolicy exists and Gateway AuthPolicy with overrides is removed.", func(ctx SpecContext) { routePolicy := policyFactory() + routePolicyKey := client.ObjectKeyFromObject(routePolicy) err := k8sClient.Create(ctx, routePolicy) - logf.Log.V(1).Info("Creating AuthPolicy", "key", client.ObjectKeyFromObject(routePolicy).String(), "error", err) + logf.Log.V(1).Info("Creating AuthPolicy", "key", routePolicyKey.String(), "error", err) Expect(err).ToNot(HaveOccurred()) // check policy status @@ -930,23 +748,23 @@ var _ = Describe("AuthPolicy controller", func() { policy.Spec.TargetRef.Group = gatewayapiv1.GroupName policy.Spec.TargetRef.Kind = "Gateway" policy.Spec.TargetRef.Name = TestGatewayName - policy.Spec.Overrides = &kuadrantv1beta3.AuthPolicyCommonSpec{} + policy.Spec.Overrides = &kuadrantv1beta3.MergeableAuthPolicySpec{} policy.Spec.Defaults = nil policy.Spec.Overrides.AuthScheme = tests.BuildBasicAuthScheme() policy.Spec.Overrides.AuthScheme.Authentication["apiKey"].ApiKey.Selector.MatchLabels["admin"] = "yes" }) - + gatewayPolicyKey := client.ObjectKeyFromObject(gatewayPolicy) err = k8sClient.Create(ctx, gatewayPolicy) - logf.Log.V(1).Info("Creating AuthPolicy", "key", client.ObjectKeyFromObject(gatewayPolicy).String(), "error", err) + logf.Log.V(1).Info("Creating AuthPolicy", "key", gatewayPolicyKey.String(), "error", err) Expect(err).ToNot(HaveOccurred()) // check policy status Eventually(tests.IsAuthPolicyAcceptedAndEnforced(ctx, testClient(), gatewayPolicy)).WithContext(ctx).Should(BeTrue()) Eventually(tests.IsAuthPolicyEnforced(ctx, testClient(), routePolicy)).WithContext(ctx).Should(BeFalse()) - Eventually(tests.IsAuthPolicyEnforcedCondition(ctx, testClient(), client.ObjectKeyFromObject(routePolicy), kuadrant.PolicyReasonOverridden, fmt.Sprintf("AuthPolicy is overridden by [%s]", client.ObjectKeyFromObject(gatewayPolicy)))).WithContext(ctx).Should(BeTrue()) + Eventually(tests.IsAuthPolicyEnforcedCondition(ctx, testClient(), routePolicyKey, kuadrant.PolicyReasonOverridden, fmt.Sprintf("AuthPolicy is overridden by [%s]", gatewayPolicyKey.String()))).WithContext(ctx).Should(BeTrue()) err = k8sClient.Delete(ctx, gatewayPolicy) - logf.Log.V(1).Info("Deleting AuthPolicy", "key", client.ObjectKeyFromObject(gatewayPolicy).String(), "error", err) + logf.Log.V(1).Info("Deleting AuthPolicy", "key", gatewayPolicyKey.String(), "error", err) Expect(err).ToNot(HaveOccurred()) // check policy status @@ -955,8 +773,9 @@ var _ = Describe("AuthPolicy controller", func() { It("Route and Gateway AuthPolicies exist. Gateway AuthPolicy updated to include overrides.", func(ctx SpecContext) { routePolicy := policyFactory() + routePolicyKey := client.ObjectKeyFromObject(routePolicy) err := k8sClient.Create(ctx, routePolicy) - logf.Log.V(1).Info("Creating AuthPolicy", "key", client.ObjectKeyFromObject(routePolicy).String(), "error", err) + logf.Log.V(1).Info("Creating AuthPolicy", "key", routePolicyKey.String(), "error", err) Expect(err).ToNot(HaveOccurred()) // check policy status @@ -967,24 +786,24 @@ var _ = Describe("AuthPolicy controller", func() { policy.Spec.TargetRef.Group = gatewayapiv1.GroupName policy.Spec.TargetRef.Kind = "Gateway" policy.Spec.TargetRef.Name = TestGatewayName - policy.Spec.CommonSpec().AuthScheme.Authentication["apiKey"].ApiKey.Selector.MatchLabels["admin"] = "yes" + policy.Spec.Proper().AuthScheme.Authentication["apiKey"].ApiKey.Selector.MatchLabels["admin"] = "yes" }) - + gatewayPolicyKey := client.ObjectKeyFromObject(gatewayPolicy) err = k8sClient.Create(ctx, gatewayPolicy) - logf.Log.V(1).Info("Creating AuthPolicy", "key", client.ObjectKeyFromObject(gatewayPolicy).String(), "error", err) + logf.Log.V(1).Info("Creating AuthPolicy", "key", gatewayPolicyKey.String(), "error", err) Expect(err).ToNot(HaveOccurred()) // check policy status Eventually(tests.IsAuthPolicyAcceptedAndNotEnforced(ctx, testClient(), gatewayPolicy)).WithContext(ctx).Should(BeTrue()) - Eventually(tests.IsAuthPolicyEnforcedCondition(ctx, testClient(), client.ObjectKeyFromObject(gatewayPolicy), kuadrant.PolicyReasonOverridden, fmt.Sprintf("AuthPolicy is overridden by [%s]", client.ObjectKeyFromObject(routePolicy)))).WithContext(ctx).Should(BeTrue()) + Eventually(tests.IsAuthPolicyEnforcedCondition(ctx, testClient(), gatewayPolicyKey, kuadrant.PolicyReasonOverridden, fmt.Sprintf("AuthPolicy is overridden by [%s]", routePolicyKey.String()))).WithContext(ctx).Should(BeTrue()) Eventually(tests.IsAuthPolicyEnforced(ctx, testClient(), routePolicy)).WithContext(ctx).Should(BeTrue()) Eventually(func() bool { - err = k8sClient.Get(ctx, client.ObjectKeyFromObject(gatewayPolicy), gatewayPolicy) + err = k8sClient.Get(ctx, gatewayPolicyKey, gatewayPolicy) if err != nil { return false } - gatewayPolicy.Spec.Overrides = &kuadrantv1beta3.AuthPolicyCommonSpec{} + gatewayPolicy.Spec.Overrides = &kuadrantv1beta3.MergeableAuthPolicySpec{} gatewayPolicy.Spec.Defaults = nil gatewayPolicy.Spec.Overrides.AuthScheme = tests.BuildBasicAuthScheme() gatewayPolicy.Spec.Overrides.AuthScheme.Authentication["apiKey"].ApiKey.Selector.MatchLabels["admin"] = "yes" @@ -996,13 +815,14 @@ var _ = Describe("AuthPolicy controller", func() { // check policy status Eventually(tests.IsAuthPolicyAcceptedAndEnforced(ctx, testClient(), gatewayPolicy)).WithContext(ctx).Should(BeTrue()) Eventually(tests.IsAuthPolicyEnforced(ctx, testClient(), routePolicy)).WithContext(ctx).Should(BeFalse()) - Eventually(tests.IsAuthPolicyEnforcedCondition(ctx, testClient(), client.ObjectKeyFromObject(routePolicy), kuadrant.PolicyReasonOverridden, fmt.Sprintf("AuthPolicy is overridden by [%s]", client.ObjectKeyFromObject(gatewayPolicy)))).WithContext(ctx).Should(BeTrue()) + Eventually(tests.IsAuthPolicyEnforcedCondition(ctx, testClient(), routePolicyKey, kuadrant.PolicyReasonOverridden, fmt.Sprintf("AuthPolicy is overridden by [%s]", gatewayPolicyKey.String()))).WithContext(ctx).Should(BeTrue()) }, testTimeOut) It("Route and Gateway AuthPolicies exist. Gateway AuthPolicy updated to remove overrides.", func(ctx SpecContext) { routePolicy := policyFactory() + routePolicyKey := client.ObjectKeyFromObject(routePolicy) err := k8sClient.Create(ctx, routePolicy) - logf.Log.V(1).Info("Creating AuthPolicy", "key", client.ObjectKeyFromObject(routePolicy).String(), "error", err) + logf.Log.V(1).Info("Creating AuthPolicy", "key", routePolicyKey.String(), "error", err) Expect(err).ToNot(HaveOccurred()) // check policy status @@ -1013,51 +833,39 @@ var _ = Describe("AuthPolicy controller", func() { policy.Spec.TargetRef.Group = gatewayapiv1.GroupName policy.Spec.TargetRef.Kind = "Gateway" policy.Spec.TargetRef.Name = TestGatewayName - policy.Spec.Overrides = &kuadrantv1beta3.AuthPolicyCommonSpec{} + policy.Spec.Overrides = &kuadrantv1beta3.MergeableAuthPolicySpec{} policy.Spec.Defaults = nil policy.Spec.Overrides.AuthScheme = tests.BuildBasicAuthScheme() policy.Spec.Overrides.AuthScheme.Authentication["apiKey"].ApiKey.Selector.MatchLabels["admin"] = "yes" }) - + gatewayPolicyKey := client.ObjectKeyFromObject(gatewayPolicy) err = k8sClient.Create(ctx, gatewayPolicy) - logf.Log.V(1).Info("Creating AuthPolicy", "key", client.ObjectKeyFromObject(gatewayPolicy).String(), "error", err) + logf.Log.V(1).Info("Creating AuthPolicy", "key", gatewayPolicyKey.String(), "error", err) Expect(err).ToNot(HaveOccurred()) // check policy status Eventually(tests.IsAuthPolicyAcceptedAndEnforced(ctx, testClient(), gatewayPolicy)).WithContext(ctx).Should(BeTrue()) Eventually(tests.IsAuthPolicyEnforced(ctx, testClient(), routePolicy)).WithContext(ctx).Should(BeFalse()) - Eventually(tests.IsAuthPolicyEnforcedCondition(ctx, testClient(), client.ObjectKeyFromObject(routePolicy), kuadrant.PolicyReasonOverridden, fmt.Sprintf("AuthPolicy is overridden by [%s]", client.ObjectKeyFromObject(gatewayPolicy)))).WithContext(ctx).Should(BeTrue()) + Eventually(tests.IsAuthPolicyEnforcedCondition(ctx, testClient(), routePolicyKey, kuadrant.PolicyReasonOverridden, fmt.Sprintf("AuthPolicy is overridden by [%s]", gatewayPolicyKey.String()))).WithContext(ctx).Should(BeTrue()) Eventually(func() bool { - err = k8sClient.Get(ctx, client.ObjectKeyFromObject(gatewayPolicy), gatewayPolicy) + err = k8sClient.Get(ctx, gatewayPolicyKey, gatewayPolicy) if err != nil { return false } gatewayPolicy.Spec.Overrides = nil - gatewayPolicy.Spec.CommonSpec().AuthScheme = tests.BuildBasicAuthScheme() - gatewayPolicy.Spec.CommonSpec().AuthScheme.Authentication["apiKey"].ApiKey.Selector.MatchLabels["admin"] = "yes" + gatewayPolicy.Spec.Proper().AuthScheme = tests.BuildBasicAuthScheme() + gatewayPolicy.Spec.Proper().AuthScheme.Authentication["apiKey"].ApiKey.Selector.MatchLabels["admin"] = "yes" err = k8sClient.Update(ctx, gatewayPolicy) - logf.Log.V(1).Info("Updating AuthPolicy", "key", client.ObjectKeyFromObject(gatewayPolicy).String(), "error", err) + logf.Log.V(1).Info("Updating AuthPolicy", "key", gatewayPolicyKey.String(), "error", err) return err == nil }).WithContext(ctx).Should(BeTrue()) // check policy status Eventually(tests.IsAuthPolicyAcceptedAndNotEnforced(ctx, testClient(), gatewayPolicy)).WithContext(ctx).Should(BeTrue()) - Eventually(tests.IsAuthPolicyEnforcedCondition(ctx, testClient(), client.ObjectKeyFromObject(gatewayPolicy), kuadrant.PolicyReasonOverridden, fmt.Sprintf("AuthPolicy is overridden by [%s]", client.ObjectKeyFromObject(routePolicy)))).WithContext(ctx).Should(BeTrue()) + Eventually(tests.IsAuthPolicyEnforcedCondition(ctx, testClient(), gatewayPolicyKey, kuadrant.PolicyReasonOverridden, fmt.Sprintf("AuthPolicy is overridden by [%s]", routePolicyKey.String()))).WithContext(ctx).Should(BeTrue()) Eventually(tests.IsAuthPolicyEnforced(ctx, testClient(), routePolicy)).WithContext(ctx).Should(BeTrue()) }, testTimeOut) - - It("Blocks creation of AuthPolicies with overrides targeting HTTPRoutes", func(ctx SpecContext) { - routePolicy := policyFactory(func(policy *kuadrantv1beta3.AuthPolicy) { - policy.Spec.Overrides = &kuadrantv1beta3.AuthPolicyCommonSpec{} - policy.Spec.Defaults = nil - policy.Spec.Overrides.AuthScheme = tests.BuildBasicAuthScheme() - }) - err := k8sClient.Create(ctx, routePolicy) - logf.Log.V(1).Info("Creating AuthPolicy", "key", client.ObjectKeyFromObject(routePolicy).String(), "error", err) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("Overrides are not allowed for policies targeting a HTTPRoute resource")) - }, testTimeOut) }) }) @@ -1083,10 +891,12 @@ var _ = Describe("AuthPolicy CEL Validations", func() { Namespace: testNamespace, }, Spec: kuadrantv1beta3.AuthPolicySpec{ - TargetRef: gatewayapiv1alpha2.LocalPolicyTargetReference{ - Group: gatewayapiv1.GroupName, - Kind: "HTTPRoute", - Name: "my-target", + TargetRef: gatewayapiv1alpha2.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gatewayapiv1alpha2.LocalPolicyTargetReference{ + Group: gatewayapiv1.GroupName, + Kind: "HTTPRoute", + Name: "my-target", + }, }, }, } @@ -1142,8 +952,10 @@ var _ = Describe("AuthPolicy CEL Validations", func() { It("Valid when only explicit defaults are used", func(ctx SpecContext) { policy := policyFactory(func(policy *kuadrantv1beta3.AuthPolicy) { - policy.Spec.Defaults = &kuadrantv1beta3.AuthPolicyCommonSpec{ - AuthScheme: tests.BuildBasicAuthScheme(), + policy.Spec.Defaults = &kuadrantv1beta3.MergeableAuthPolicySpec{ + AuthPolicySpecProper: kuadrantv1beta3.AuthPolicySpecProper{ + AuthScheme: tests.BuildBasicAuthScheme(), + }, } }) Expect(k8sClient.Create(ctx, policy)).To(Succeed()) @@ -1151,7 +963,7 @@ var _ = Describe("AuthPolicy CEL Validations", func() { It("Invalid when both implicit and explicit defaults are used - authScheme", func(ctx SpecContext) { policy := policyFactory(func(policy *kuadrantv1beta3.AuthPolicy) { - policy.Spec.Defaults = &kuadrantv1beta3.AuthPolicyCommonSpec{} + policy.Spec.Defaults = &kuadrantv1beta3.MergeableAuthPolicySpec{} policy.Spec.AuthScheme = tests.BuildBasicAuthScheme() }) err := k8sClient.Create(ctx, policy) @@ -1161,13 +973,15 @@ var _ = Describe("AuthPolicy CEL Validations", func() { It("Invalid when both implicit and explicit defaults are used - namedPatterns", func(ctx SpecContext) { policy := policyFactory(func(policy *kuadrantv1beta3.AuthPolicy) { - policy.Spec.Defaults = &kuadrantv1beta3.AuthPolicyCommonSpec{} - policy.Spec.NamedPatterns = map[string]authorinoapi.PatternExpressions{ - "internal-source": []authorinoapi.PatternExpression{ - { - Selector: "source.ip", - Operator: authorinoapi.PatternExpressionOperator("matches"), - Value: `192\.168\..*`, + policy.Spec.Defaults = &kuadrantv1beta3.MergeableAuthPolicySpec{} + policy.Spec.NamedPatterns = map[string]kuadrantv1beta3.MergeablePatternExpressions{ + "internal-source": { + PatternExpressions: []authorinov1beta2.PatternExpression{ + { + Selector: "source.ip", + Operator: authorinov1beta2.PatternExpressionOperator("matches"), + Value: `192\.168\..*`, + }, }, }, } @@ -1179,11 +993,13 @@ var _ = Describe("AuthPolicy CEL Validations", func() { It("Invalid when both implicit and explicit defaults are used - conditions", func(ctx SpecContext) { policy := policyFactory(func(policy *kuadrantv1beta3.AuthPolicy) { - policy.Spec.Defaults = &kuadrantv1beta3.AuthPolicyCommonSpec{} - policy.Spec.Conditions = []authorinoapi.PatternExpressionOrRef{ + policy.Spec.Defaults = &kuadrantv1beta3.MergeableAuthPolicySpec{} + policy.Spec.Conditions = []kuadrantv1beta3.MergeablePatternExpressionOrRef{ { - PatternRef: authorinoapi.PatternRef{ - Name: "internal-source", + PatternExpressionOrRef: authorinov1beta2.PatternExpressionOrRef{ + PatternRef: authorinov1beta2.PatternRef{ + Name: "internal-source", + }, }, }, } diff --git a/tests/common/targetstatus/target_status_controller_test.go b/tests/common/targetstatus/target_status_controller_test.go index 0b0f41bb0..d8fca4208 100644 --- a/tests/common/targetstatus/target_status_controller_test.go +++ b/tests/common/targetstatus/target_status_controller_test.go @@ -132,17 +132,23 @@ var _ = Describe("Target status reconciler", func() { Namespace: testNamespace, }, Spec: kuadrantv1beta3.AuthPolicySpec{ - TargetRef: gatewayapiv1alpha2.LocalPolicyTargetReference{ - Group: gatewayapiv1.GroupName, - Kind: "HTTPRoute", - Name: TestHTTPRouteName, + TargetRef: gatewayapiv1alpha2.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gatewayapiv1alpha2.LocalPolicyTargetReference{ + Group: gatewayapiv1.GroupName, + Kind: "HTTPRoute", + Name: TestHTTPRouteName, + }, }, - Defaults: &kuadrantv1beta3.AuthPolicyCommonSpec{ - AuthScheme: &kuadrantv1beta3.AuthSchemeSpec{ - Authentication: map[string]authorinoapi.AuthenticationSpec{ - "anonymous": { - AuthenticationMethodSpec: authorinoapi.AuthenticationMethodSpec{ - AnonymousAccess: &authorinoapi.AnonymousAccessSpec{}, + Defaults: &kuadrantv1beta3.MergeableAuthPolicySpec{ + AuthPolicySpecProper: kuadrantv1beta3.AuthPolicySpecProper{ + AuthScheme: &kuadrantv1beta3.AuthSchemeSpec{ + Authentication: map[string]kuadrantv1beta3.MergeableAuthenticationSpec{ + "anonymous": { + AuthenticationSpec: authorinoapi.AuthenticationSpec{ + AuthenticationMethodSpec: authorinoapi.AuthenticationMethodSpec{ + AnonymousAccess: &authorinoapi.AnonymousAccessSpec{}, + }, + }, }, }, }, @@ -163,7 +169,7 @@ var _ = Describe("Target status reconciler", func() { if !tests.IsAuthPolicyAccepted(ctx, testClient(), policy)() { return false } - return targetsAffected(ctx, client.ObjectKeyFromObject(policy), policyAffectedCondition, policy.Spec.TargetRef, routeNames...) + return targetsAffected(ctx, client.ObjectKeyFromObject(policy), policyAffectedCondition, policy.GetTargetRef(), routeNames...) } } @@ -214,10 +220,12 @@ var _ = Describe("Target status reconciler", func() { It("adds PolicyAffected status condition to the targeted gateway and routes", func(ctx SpecContext) { policy := policyFactory(func(policy *kuadrantv1beta3.AuthPolicy) { policy.Name = "gateway-auth" - policy.Spec.TargetRef = gatewayapiv1alpha2.LocalPolicyTargetReference{ - Group: gatewayapiv1.GroupName, - Kind: "Gateway", - Name: TestGatewayName, + policy.Spec.TargetRef = gatewayapiv1alpha2.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gatewayapiv1alpha2.LocalPolicyTargetReference{ + Group: gatewayapiv1.GroupName, + Kind: "Gateway", + Name: TestGatewayName, + }, } }) Expect(k8sClient.Create(ctx, policy)).To(Succeed()) @@ -227,10 +235,12 @@ var _ = Describe("Target status reconciler", func() { It("removes PolicyAffected status condition from the targeted gateway and routes when the policy is deleted", func(ctx SpecContext) { policy := policyFactory(func(policy *kuadrantv1beta3.AuthPolicy) { policy.Name = "gateway-auth" - policy.Spec.TargetRef = gatewayapiv1alpha2.LocalPolicyTargetReference{ - Group: gatewayapiv1.GroupName, - Kind: "Gateway", - Name: TestGatewayName, + policy.Spec.TargetRef = gatewayapiv1alpha2.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gatewayapiv1alpha2.LocalPolicyTargetReference{ + Group: gatewayapiv1.GroupName, + Kind: "Gateway", + Name: TestGatewayName, + }, } }) Expect(k8sClient.Create(ctx, policy)).To(Succeed()) @@ -266,10 +276,12 @@ var _ = Describe("Target status reconciler", func() { gatewayPolicy := policyFactory(func(policy *kuadrantv1beta3.AuthPolicy) { policy.Name = "gateway-auth" - policy.Spec.TargetRef = gatewayapiv1alpha2.LocalPolicyTargetReference{ - Group: gatewayapiv1.GroupName, - Kind: "Gateway", - Name: TestGatewayName, + policy.Spec.TargetRef = gatewayapiv1alpha2.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gatewayapiv1alpha2.LocalPolicyTargetReference{ + Group: gatewayapiv1.GroupName, + Kind: "Gateway", + Name: TestGatewayName, + }, } }) Expect(k8sClient.Create(ctx, gatewayPolicy)).To(Succeed()) diff --git a/tests/commons.go b/tests/commons.go index d9fd8f26f..aa6495ee8 100644 --- a/tests/commons.go +++ b/tests/commons.go @@ -655,20 +655,22 @@ func KuadrantIsReady(ctx context.Context, cl client.Client, key client.ObjectKey func BuildBasicAuthScheme() *kuadrantv1beta3.AuthSchemeSpec { return &kuadrantv1beta3.AuthSchemeSpec{ - Authentication: map[string]authorinoapi.AuthenticationSpec{ + Authentication: map[string]kuadrantv1beta3.MergeableAuthenticationSpec{ "apiKey": { - AuthenticationMethodSpec: authorinoapi.AuthenticationMethodSpec{ - ApiKey: &authorinoapi.ApiKeyAuthenticationSpec{ - Selector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - "app": "toystore", + AuthenticationSpec: authorinoapi.AuthenticationSpec{ + AuthenticationMethodSpec: authorinoapi.AuthenticationMethodSpec{ + ApiKey: &authorinoapi.ApiKeyAuthenticationSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "toystore", + }, }, }, }, - }, - Credentials: authorinoapi.Credentials{ - AuthorizationHeader: &authorinoapi.Prefixed{ - Prefix: "APIKEY", + Credentials: authorinoapi.Credentials{ + AuthorizationHeader: &authorinoapi.Prefixed{ + Prefix: "APIKEY", + }, }, }, }, diff --git a/tests/envoygateway/authpolicy_envoysecuritypolicy_controller_test.go b/tests/envoygateway/authpolicy_envoysecuritypolicy_controller_test.go deleted file mode 100644 index c08c1b2c2..000000000 --- a/tests/envoygateway/authpolicy_envoysecuritypolicy_controller_test.go +++ /dev/null @@ -1,276 +0,0 @@ -//go:build integration - -package envoygateway_test - -import ( - "fmt" - "strings" - "time" - - egv1alpha1 "github.com/envoyproxy/gateway/api/v1alpha1" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - . "github.com/onsi/gomega/gstruct" - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/rand" - "sigs.k8s.io/controller-runtime/pkg/client" - logf "sigs.k8s.io/controller-runtime/pkg/log" - gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" - gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" - - kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" - "github.com/kuadrant/kuadrant-operator/controllers" - "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" - "github.com/kuadrant/kuadrant-operator/tests" -) - -var _ = Describe("Auth Envoy SecurityPolicy controller", func() { - const ( - testTimeOut = SpecTimeout(2 * time.Minute) - afterEachTimeOut = NodeTimeout(3 * time.Minute) - ) - var ( - testNamespace string - gwHost = fmt.Sprintf("*.toystore-%s.com", rand.String(4)) - gateway *gatewayapiv1.Gateway - ) - - BeforeEach(func(ctx SpecContext) { - testNamespace = tests.CreateNamespace(ctx, testClient()) - gateway = tests.NewGatewayBuilder(TestGatewayName, tests.GatewayClassName, testNamespace). - WithHTTPListener("test-listener", gwHost). - Gateway - err := k8sClient.Create(ctx, gateway) - Expect(err).ToNot(HaveOccurred()) - - Eventually(tests.GatewayIsReady(ctx, testClient(), gateway)).WithContext(ctx).Should(BeTrue()) - }) - - AfterEach(func(ctx SpecContext) { - tests.DeleteNamespace(ctx, testClient(), testNamespace) - }, afterEachTimeOut) - - policyFactory := func(mutateFns ...func(policy *kuadrantv1beta3.AuthPolicy)) *kuadrantv1beta3.AuthPolicy { - policy := &kuadrantv1beta3.AuthPolicy{ - TypeMeta: metav1.TypeMeta{ - Kind: "AuthPolicy", - APIVersion: kuadrantv1beta3.GroupVersion.String(), - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "toystore", - Namespace: testNamespace, - }, - Spec: kuadrantv1beta3.AuthPolicySpec{ - TargetRef: gatewayapiv1alpha2.LocalPolicyTargetReference{ - Group: gatewayapiv1.GroupName, - Kind: "HTTPRoute", - Name: TestHTTPRouteName, - }, - Defaults: &kuadrantv1beta3.AuthPolicyCommonSpec{ - AuthScheme: tests.BuildBasicAuthScheme(), - }, - }, - } - for _, mutateFn := range mutateFns { - mutateFn(policy) - } - return policy - } - - randomHostFromGWHost := func() string { - return strings.Replace(gwHost, "*", rand.String(4), 1) - } - - Context("Auth Policy attached to the gateway", func() { - - var ( - gwPolicy *kuadrantv1beta3.AuthPolicy - ) - - BeforeEach(func(ctx SpecContext) { - gwRoute := tests.BuildBasicHttpRoute(TestHTTPRouteName, TestGatewayName, testNamespace, []string{randomHostFromGWHost()}) - err := k8sClient.Create(ctx, gwRoute) - Expect(err).ToNot(HaveOccurred()) - Eventually(tests.RouteIsAccepted(ctx, testClient(), client.ObjectKeyFromObject(gwRoute))).WithContext(ctx).Should(BeTrue()) - - gwPolicy = policyFactory(func(policy *kuadrantv1beta3.AuthPolicy) { - policy.Name = "gw-auth" - policy.Spec.TargetRef.Group = gatewayapiv1.GroupName - policy.Spec.TargetRef.Kind = "Gateway" - policy.Spec.TargetRef.Name = TestGatewayName - policy.Spec.CommonSpec().AuthScheme.Authentication["apiKey"].ApiKey.Selector.MatchLabels["admin"] = "yes" - }) - - err = k8sClient.Create(ctx, gwPolicy) - logf.Log.V(1).Info("Creating AuthPolicy", "key", client.ObjectKeyFromObject(gwPolicy).String(), "error", err) - Expect(err).ToNot(HaveOccurred()) - - // check policy status - Eventually(tests.IsAuthPolicyAcceptedAndEnforced(ctx, testClient(), gwPolicy)).WithContext(ctx).Should(BeTrue()) - }) - - It("Creates security policy", func(ctx SpecContext) { - spKey := types.NamespacedName{Name: controllers.EnvoySecurityPolicyName(gwPolicy.GetName()), Namespace: testNamespace} - sp := &egv1alpha1.SecurityPolicy{} - Eventually(func() bool { - err := k8sClient.Get(ctx, spKey, sp) - logf.Log.V(1).Info("Fetching envoy SecurityPolicy", "key", spKey.String(), "error", err) - return err == nil - }).WithContext(ctx).Should(BeTrue()) - - //has correct configuration - Expect(*sp).To( - MatchFields(IgnoreExtras, Fields{ - "Spec": MatchFields(IgnoreExtras, Fields{ - "PolicyTargetReferences": MatchFields(IgnoreExtras, Fields{ - "TargetRefs": ConsistOf(MatchFields(IgnoreExtras, Fields{ - "LocalPolicyTargetReference": MatchFields(IgnoreExtras, Fields{ - "Group": Equal(gatewayapiv1.Group(gatewayapiv1.GroupName)), - "Kind": Equal(gatewayapiv1.Kind("Gateway")), - "Name": Equal(gatewayapiv1.ObjectName(TestGatewayName)), - }), - })), - }), - "ExtAuth": PointTo(MatchFields(IgnoreExtras, Fields{ - "GRPC": PointTo(MatchFields(IgnoreExtras, Fields{ - "BackendRefs": ConsistOf(MatchFields(IgnoreExtras, Fields{ - "BackendObjectReference": MatchFields(IgnoreExtras, Fields{ - "Group": PointTo(Equal(gatewayapiv1.Group(""))), - "Kind": PointTo(Equal(gatewayapiv1.Kind("Service"))), - "Name": Equal(gatewayapiv1.ObjectName(kuadrant.AuthorinoServiceName)), - "Namespace": PointTo(Equal(gatewayapiv1.Namespace(kuadrantInstallationNS))), - "Port": PointTo(Equal(gatewayapiv1.PortNumber(50051))), - }), - })), - })), - })), - }), - })) - }, testTimeOut) - - It("Deletes security policy when auth policy is deleted", func(ctx SpecContext) { - err := k8sClient.Delete(ctx, gwPolicy) - logf.Log.V(1).Info("Deleting AuthPolicy", "key", client.ObjectKeyFromObject(gwPolicy).String(), "error", err) - Expect(err).ToNot(HaveOccurred()) - - spKey := types.NamespacedName{Name: controllers.EnvoySecurityPolicyName(TestGatewayName), Namespace: testNamespace} - sp := &egv1alpha1.SecurityPolicy{} - Eventually(func() bool { - err := k8sClient.Get(ctx, spKey, sp) - logf.Log.V(1).Info("Fetching envoy SecurityPolicy", "key", spKey.String(), "error", err) - return apierrors.IsNotFound(err) - }).WithContext(ctx).Should(BeTrue()) - }, testTimeOut) - - It("Deletes security policy if gateway is deleted", func(ctx SpecContext) { - err := k8sClient.Delete(ctx, gateway) - logf.Log.V(1).Info("Deleting Gateway", "key", client.ObjectKeyFromObject(gateway).String(), "error", err) - Expect(err).ToNot(HaveOccurred()) - - spKey := types.NamespacedName{Name: controllers.EnvoySecurityPolicyName(TestGatewayName), Namespace: testNamespace} - sp := &egv1alpha1.SecurityPolicy{} - Eventually(func() bool { - err := k8sClient.Get(ctx, spKey, sp) - logf.Log.V(1).Info("Fetching envoy SecurityPolicy", "key", spKey.String(), "error", err) - return apierrors.IsNotFound(err) - }).WithContext(ctx).Should(BeTrue()) - }, testTimeOut) - }) - - Context("Auth Policy attached to the route", func() { - - var ( - routePolicy *kuadrantv1beta3.AuthPolicy - gwRoute *gatewayapiv1.HTTPRoute - ) - - BeforeEach(func(ctx SpecContext) { - gwRoute = tests.BuildBasicHttpRoute(TestHTTPRouteName, TestGatewayName, testNamespace, []string{randomHostFromGWHost()}) - err := k8sClient.Create(ctx, gwRoute) - Expect(err).ToNot(HaveOccurred()) - Eventually(tests.RouteIsAccepted(ctx, testClient(), client.ObjectKeyFromObject(gwRoute))).WithContext(ctx).Should(BeTrue()) - - routePolicy = policyFactory(func(policy *kuadrantv1beta3.AuthPolicy) { - policy.Spec.TargetRef.Group = gatewayapiv1.GroupName - policy.Spec.TargetRef.Kind = "HTTPRoute" - policy.Spec.TargetRef.Name = TestHTTPRouteName - }) - - err = k8sClient.Create(ctx, routePolicy) - logf.Log.V(1).Info("Creating AuthPolicy", "key", client.ObjectKeyFromObject(routePolicy).String(), "error", err) - Expect(err).ToNot(HaveOccurred()) - - // check policy status - Eventually(tests.IsAuthPolicyAcceptedAndEnforced(ctx, testClient(), routePolicy)).WithContext(ctx).Should(BeTrue()) - }) - - It("Creates security policy", func(ctx SpecContext) { - spKey := types.NamespacedName{Name: controllers.EnvoySecurityPolicyName(routePolicy.GetName()), Namespace: testNamespace} - sp := &egv1alpha1.SecurityPolicy{} - Eventually(func() bool { - err := k8sClient.Get(ctx, spKey, sp) - logf.Log.V(1).Info("Fetching envoy SecurityPolicy", "key", spKey.String(), "error", err) - return err == nil - }).WithContext(ctx).Should(BeTrue()) - - //has correct configuration - Expect(*sp).To( - MatchFields(IgnoreExtras, Fields{ - "Spec": MatchFields(IgnoreExtras, Fields{ - "PolicyTargetReferences": MatchFields(IgnoreExtras, Fields{ - "TargetRefs": ConsistOf(MatchFields(IgnoreExtras, Fields{ - "LocalPolicyTargetReference": MatchFields(IgnoreExtras, Fields{ - "Group": Equal(gatewayapiv1.Group(gatewayapiv1.GroupName)), - "Kind": Equal(gatewayapiv1.Kind("HTTPRoute")), - "Name": Equal(gatewayapiv1.ObjectName(TestHTTPRouteName)), - }), - })), - }), - "ExtAuth": PointTo(MatchFields(IgnoreExtras, Fields{ - "GRPC": PointTo(MatchFields(IgnoreExtras, Fields{ - "BackendRefs": ConsistOf(MatchFields(IgnoreExtras, Fields{ - "BackendObjectReference": MatchFields(IgnoreExtras, Fields{ - "Group": PointTo(Equal(gatewayapiv1.Group(""))), - "Kind": PointTo(Equal(gatewayapiv1.Kind("Service"))), - "Name": Equal(gatewayapiv1.ObjectName(kuadrant.AuthorinoServiceName)), - "Namespace": PointTo(Equal(gatewayapiv1.Namespace(kuadrantInstallationNS))), - "Port": PointTo(Equal(gatewayapiv1.PortNumber(50051))), - }), - })), - })), - })), - }), - })) - }, testTimeOut) - - It("Security policy deleted when auth policy is deleted", func(ctx SpecContext) { - err := k8sClient.Delete(ctx, routePolicy) - logf.Log.V(1).Info("Deleting AuthPolicy", "key", client.ObjectKeyFromObject(routePolicy).String(), "error", err) - Expect(err).ToNot(HaveOccurred()) - - spKey := types.NamespacedName{Name: controllers.EnvoySecurityPolicyName(TestHTTPRouteName), Namespace: testNamespace} - sp := &egv1alpha1.SecurityPolicy{} - Eventually(func() bool { - err := k8sClient.Get(ctx, spKey, sp) - logf.Log.V(1).Info("Fetching envoy SecurityPolicy", "key", spKey.String(), "error", err) - return apierrors.IsNotFound(err) - }).WithContext(ctx).Should(BeTrue()) - }, testTimeOut) - - It("Deletes security policy if route is deleted", func(ctx SpecContext) { - err := k8sClient.Delete(ctx, gwRoute) - logf.Log.V(1).Info("Deleting AuthPolicy", "key", client.ObjectKeyFromObject(routePolicy).String(), "error", err) - Expect(err).ToNot(HaveOccurred()) - - spKey := types.NamespacedName{Name: controllers.EnvoySecurityPolicyName(TestHTTPRouteName), Namespace: testNamespace} - sp := &egv1alpha1.SecurityPolicy{} - Eventually(func() bool { - err := k8sClient.Get(ctx, spKey, sp) - logf.Log.V(1).Info("Fetching envoy SecurityPolicy", "key", spKey.String(), "error", err) - return apierrors.IsNotFound(err) - }).WithContext(ctx).Should(BeTrue()) - }, testTimeOut) - }) -}) diff --git a/tests/envoygateway/envoysecuritypolicy_referencegrant_controller_test.go b/tests/envoygateway/envoysecuritypolicy_referencegrant_controller_test.go deleted file mode 100644 index 20cb50737..000000000 --- a/tests/envoygateway/envoysecuritypolicy_referencegrant_controller_test.go +++ /dev/null @@ -1,271 +0,0 @@ -//go:build integration - -package envoygateway_test - -import ( - "time" - - egv1alpha1 "github.com/envoyproxy/gateway/api/v1alpha1" - "github.com/kuadrant/kuadrant-operator/controllers" - "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" - "github.com/kuadrant/kuadrant-operator/tests" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - . "github.com/onsi/gomega/gstruct" - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" - logf "sigs.k8s.io/controller-runtime/pkg/log" - gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" - gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" - gatewayapiv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" - - kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" -) - -var _ = Describe("Envoy SecurityPolicy ReferenceGrant controller", func() { - const ( - testTimeOut = SpecTimeout(2 * time.Minute) - afterEachTimeOut = NodeTimeout(3 * time.Minute) - ) - var ( - routePolicyOne *kuadrantv1beta3.AuthPolicy - gateway *gatewayapiv1.Gateway - route *gatewayapiv1.HTTPRoute - ) - - initGatewayRoutePolicy := func(ctx SpecContext, testNamespace string, policy *kuadrantv1beta3.AuthPolicy) { - gateway = tests.BuildBasicGateway(TestGatewayName, testNamespace) - err := k8sClient.Create(ctx, gateway) - Expect(err).ToNot(HaveOccurred()) - Eventually(tests.GatewayIsReady(ctx, testClient(), gateway)).WithContext(ctx).Should(BeTrue()) - - route = tests.BuildBasicHttpRoute(TestHTTPRouteName, TestGatewayName, testNamespace, []string{"*.example.com"}) - err = k8sClient.Create(ctx, route) - Expect(err).ToNot(HaveOccurred()) - Eventually(tests.RouteIsAccepted(ctx, testClient(), client.ObjectKeyFromObject(route))).WithContext(ctx).Should(BeTrue()) - - err = k8sClient.Create(ctx, policy) - logf.Log.V(1).Info("Creating AuthPolicy", "key", client.ObjectKeyFromObject(policy).String(), "error", err) - Expect(err).ToNot(HaveOccurred()) - } - - policyFactory := func(testNamespace string, mutateFns ...func(policy *kuadrantv1beta3.AuthPolicy)) *kuadrantv1beta3.AuthPolicy { - policy := &kuadrantv1beta3.AuthPolicy{ - TypeMeta: metav1.TypeMeta{ - Kind: "AuthPolicy", - APIVersion: kuadrantv1beta3.GroupVersion.String(), - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "toystore", - Namespace: testNamespace, - }, - Spec: kuadrantv1beta3.AuthPolicySpec{ - TargetRef: gatewayapiv1alpha2.LocalPolicyTargetReference{ - Group: gatewayapiv1.GroupName, - Kind: "HTTPRoute", - Name: TestHTTPRouteName, - }, - Defaults: &kuadrantv1beta3.AuthPolicyCommonSpec{ - AuthScheme: tests.BuildBasicAuthScheme(), - }, - }, - } - for _, mutateFn := range mutateFns { - mutateFn(policy) - } - return policy - } - - Context("Single auth policy namespace", func() { - - var ( - testNamespaceOne string - ) - - BeforeEach(func(ctx SpecContext) { - testNamespaceOne = tests.CreateNamespace(ctx, testClient()) - routePolicyOne = policyFactory(testNamespaceOne) - initGatewayRoutePolicy(ctx, testNamespaceOne, routePolicyOne) - }) - - AfterEach(func(ctx SpecContext) { - tests.DeleteNamespace(ctx, testClient(), testNamespaceOne) - }, afterEachTimeOut) - - It("Creates reference grant", func(ctx SpecContext) { - rgKey := types.NamespacedName{Name: controllers.KuadrantReferenceGrantName, Namespace: kuadrantInstallationNS} - - Eventually(func() *gatewayapiv1beta1.ReferenceGrant { - rg := &gatewayapiv1beta1.ReferenceGrant{} - err := k8sClient.Get(ctx, rgKey, rg) - logf.Log.V(1).Info("Fetching ReferenceGrant", "key", rgKey.String(), "error", err) - if err != nil { - return nil - } - return rg - }).WithContext(ctx).Should(PointTo(MatchFields(IgnoreExtras, Fields{ - "ObjectMeta": MatchFields(IgnoreExtras, Fields{ - "Name": Equal(controllers.KuadrantReferenceGrantName), - "Namespace": Equal(kuadrantInstallationNS), - }), - "Spec": MatchFields(IgnoreExtras, Fields{ - "To": ConsistOf(MatchFields(IgnoreExtras, Fields{ - "Group": Equal(gatewayapiv1.Group("")), - "Kind": Equal(gatewayapiv1.Kind("Service")), - "Name": PointTo(Equal(gatewayapiv1.ObjectName(kuadrant.AuthorinoServiceName))), - })), - "From": ContainElement(MatchFields(IgnoreExtras, Fields{ - "Group": Equal(gatewayapiv1.Group(egv1alpha1.GroupName)), - "Kind": Equal(gatewayapiv1.Kind(egv1alpha1.KindSecurityPolicy)), - "Namespace": Equal(gatewayapiv1.Namespace(testNamespaceOne)), - })), - }), - }))) - }, testTimeOut) - - It("Deleting auth policy removes reference grant", func(ctx SpecContext) { - err := k8sClient.Delete(ctx, routePolicyOne) - logf.Log.V(1).Info("Deleting AuthPolicy", "key", client.ObjectKeyFromObject(routePolicyOne).String(), "error", err) - Expect(err).ToNot(HaveOccurred()) - - rgKey := types.NamespacedName{Name: controllers.KuadrantReferenceGrantName, Namespace: kuadrantInstallationNS} - rg := &gatewayapiv1beta1.ReferenceGrant{} - Eventually(func() bool { - err := k8sClient.Get(ctx, rgKey, rg) - logf.Log.V(1).Info("Fetching ReferenceGrant", "key", rgKey.String(), "error", err) - return apierrors.IsNotFound(err) - }).WithContext(ctx).Should(BeTrue()) - }, testTimeOut) - }) - - Context("Single auth policy in kuadrant installation namespace", func() { - - BeforeEach(func(ctx SpecContext) { - routePolicyOne = policyFactory(kuadrantInstallationNS) - initGatewayRoutePolicy(ctx, kuadrantInstallationNS, routePolicyOne) - }) - - AfterEach(func(ctx SpecContext) { - err := k8sClient.Delete(ctx, routePolicyOne) - logf.Log.V(1).Info("Deleting AuthPolicy", "key", client.ObjectKeyFromObject(routePolicyOne).String(), "error", err) - Expect(err).ToNot(HaveOccurred()) - - err = k8sClient.Delete(ctx, route) - logf.Log.V(1).Info("Deleting HTTPRoute", "key", client.ObjectKeyFromObject(route).String(), "error", err) - Expect(err).ToNot(HaveOccurred()) - - err = k8sClient.Delete(ctx, gateway) - logf.Log.V(1).Info("Deleting Gateway", "key", client.ObjectKeyFromObject(route).String(), "error", err) - Expect(err).ToNot(HaveOccurred()) - - }, afterEachTimeOut) - - It("Does not create reference grant", func(ctx SpecContext) { - rgKey := types.NamespacedName{Name: controllers.KuadrantReferenceGrantName, Namespace: kuadrantInstallationNS} - rg := &gatewayapiv1beta1.ReferenceGrant{} - Eventually(func() bool { - err := k8sClient.Get(ctx, rgKey, rg) - logf.Log.V(1).Info("Fetching ReferenceGrant", "key", rgKey.String(), "error", err) - return apierrors.IsNotFound(err) - }).WithContext(ctx).Should(BeTrue()) - }) - - }) - - Context("Multiple auth policy namespaces", func() { - - var ( - testNamespaceOne string - testNamespaceTwo string - routePolicyTwo *kuadrantv1beta3.AuthPolicy - ) - - BeforeEach(func(ctx SpecContext) { - testNamespaceOne = tests.CreateNamespace(ctx, testClient()) - routePolicyOne = policyFactory(testNamespaceOne) - initGatewayRoutePolicy(ctx, testNamespaceOne, routePolicyOne) - testNamespaceTwo = tests.CreateNamespace(ctx, testClient()) - routePolicyTwo = policyFactory(testNamespaceTwo) - initGatewayRoutePolicy(ctx, testNamespaceTwo, routePolicyTwo) - }) - - AfterEach(func(ctx SpecContext) { - tests.DeleteNamespace(ctx, testClient(), testNamespaceOne) - tests.DeleteNamespace(ctx, testClient(), testNamespaceTwo) - }, afterEachTimeOut) - - It("Creates reference grant", func(ctx SpecContext) { - rgKey := types.NamespacedName{Name: controllers.KuadrantReferenceGrantName, Namespace: kuadrantInstallationNS} - - Eventually(func() *gatewayapiv1beta1.ReferenceGrant { - rg := &gatewayapiv1beta1.ReferenceGrant{} - err := k8sClient.Get(ctx, rgKey, rg) - logf.Log.V(1).Info("Fetching ReferenceGrant", "key", rgKey.String(), "error", err) - if err != nil { - return nil - } - return rg - }).WithContext(ctx).Should(PointTo(MatchFields(IgnoreExtras, Fields{ - "ObjectMeta": MatchFields(IgnoreExtras, Fields{ - "Name": Equal(controllers.KuadrantReferenceGrantName), - "Namespace": Equal(kuadrantInstallationNS), - }), - "Spec": MatchFields(IgnoreExtras, Fields{ - "To": ConsistOf(MatchFields(IgnoreExtras, Fields{ - "Group": Equal(gatewayapiv1.Group("")), - "Kind": Equal(gatewayapiv1.Kind("Service")), - "Name": PointTo(Equal(gatewayapiv1.ObjectName(kuadrant.AuthorinoServiceName))), - })), - "From": ContainElements( - MatchFields(IgnoreExtras, Fields{ - "Group": Equal(gatewayapiv1.Group(egv1alpha1.GroupName)), - "Kind": Equal(gatewayapiv1.Kind(egv1alpha1.KindSecurityPolicy)), - "Namespace": Equal(gatewayapiv1.Namespace(testNamespaceOne)), - }), - MatchFields(IgnoreExtras, Fields{ - "Group": Equal(gatewayapiv1.Group(egv1alpha1.GroupName)), - "Kind": Equal(gatewayapiv1.Kind(egv1alpha1.KindSecurityPolicy)), - "Namespace": Equal(gatewayapiv1.Namespace(testNamespaceTwo)), - })), - }), - }))) - }, testTimeOut) - - It("Deleting policy updates reference grant", func(ctx SpecContext) { - err := k8sClient.Delete(ctx, routePolicyTwo) - logf.Log.V(1).Info("Deleting AuthPolicy", "key", client.ObjectKeyFromObject(routePolicyTwo).String(), "error", err) - Expect(err).ToNot(HaveOccurred()) - - rgKey := types.NamespacedName{Name: controllers.KuadrantReferenceGrantName, Namespace: kuadrantInstallationNS} - - Eventually(func() *gatewayapiv1beta1.ReferenceGrant { - rg := &gatewayapiv1beta1.ReferenceGrant{} - err := k8sClient.Get(ctx, rgKey, rg) - logf.Log.V(1).Info("Fetching ReferenceGrant", "key", rgKey.String(), "error", err) - if err != nil { - return nil - } - return rg - }).WithContext(ctx).Should(PointTo(MatchFields(IgnoreExtras, Fields{ - "ObjectMeta": MatchFields(IgnoreExtras, Fields{ - "Name": Equal(controllers.KuadrantReferenceGrantName), - "Namespace": Equal(kuadrantInstallationNS), - }), - "Spec": MatchFields(IgnoreExtras, Fields{ - "To": ConsistOf(MatchFields(IgnoreExtras, Fields{ - "Group": Equal(gatewayapiv1.Group("")), - "Kind": Equal(gatewayapiv1.Kind("Service")), - "Name": PointTo(Equal(gatewayapiv1.ObjectName(kuadrant.AuthorinoServiceName))), - })), - "From": ContainElement(MatchFields(IgnoreExtras, Fields{ - "Group": Equal(gatewayapiv1.Group(egv1alpha1.GroupName)), - "Kind": Equal(gatewayapiv1.Kind(egv1alpha1.KindSecurityPolicy)), - "Namespace": Equal(gatewayapiv1.Namespace(testNamespaceOne)), - })), - }), - }))) - }, testTimeOut) - }) -}) diff --git a/tests/envoygateway/extension_reconciler_test.go b/tests/envoygateway/extension_reconciler_test.go index 04bfc1323..5ed82c081 100644 --- a/tests/envoygateway/extension_reconciler_test.go +++ b/tests/envoygateway/extension_reconciler_test.go @@ -168,6 +168,11 @@ var _ = Describe("wasm controller", func() { Expect(err).ToNot(HaveOccurred()) Expect(existingWASMConfig).To(Equal(&wasm.Config{ Services: map[string]wasm.Service{ + wasm.AuthServiceName: { + Type: wasm.AuthServiceType, + Endpoint: common.KuadrantAuthClusterName, + FailureMode: wasm.FailureModeDeny, + }, wasm.RateLimitServiceName: { Type: wasm.RateLimitServiceType, Endpoint: common.KuadrantRateLimitClusterName, @@ -335,6 +340,11 @@ var _ = Describe("wasm controller", func() { Expect(err).ToNot(HaveOccurred()) Expect(existingWASMConfig).To(Equal(&wasm.Config{ Services: map[string]wasm.Service{ + wasm.AuthServiceName: { + Type: wasm.AuthServiceType, + Endpoint: common.KuadrantAuthClusterName, + FailureMode: wasm.FailureModeDeny, + }, wasm.RateLimitServiceName: { Type: wasm.RateLimitServiceType, Endpoint: common.KuadrantRateLimitClusterName, diff --git a/tests/istio/authpolicy_controller_authorizationpolicy_test.go b/tests/istio/authpolicy_controller_authorizationpolicy_test.go deleted file mode 100644 index 546de197f..000000000 --- a/tests/istio/authpolicy_controller_authorizationpolicy_test.go +++ /dev/null @@ -1,328 +0,0 @@ -//go:build integration - -package istio_test - -import ( - "fmt" - "strings" - "time" - - . "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" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/rand" - "k8s.io/utils/ptr" - "sigs.k8s.io/controller-runtime/pkg/client" - logf "sigs.k8s.io/controller-runtime/pkg/log" - gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" - gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" - - kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" - "github.com/kuadrant/kuadrant-operator/controllers" - "github.com/kuadrant/kuadrant-operator/tests" -) - -var _ = Describe("AuthPolicy controller managing authorization policy", func() { - const ( - testTimeOut = SpecTimeout(2 * time.Minute) - afterEachTimeOut = NodeTimeout(3 * time.Minute) - ) - var ( - testNamespace string - gwHost = fmt.Sprintf("*.toystore-%s.com", rand.String(4)) - ) - - BeforeEach(func(ctx SpecContext) { - testNamespace = tests.CreateNamespace(ctx, testClient()) - - gateway := tests.BuildBasicGateway(TestGatewayName, testNamespace, func(gateway *gatewayapiv1.Gateway) { - gateway.Spec.Listeners[0].Hostname = ptr.To(gatewayapiv1.Hostname(gwHost)) - }) - err := k8sClient.Create(ctx, gateway) - Expect(err).ToNot(HaveOccurred()) - - Eventually(tests.GatewayIsReady(ctx, testClient(), gateway)).WithContext(ctx).Should(BeTrue()) - }) - - AfterEach(func(ctx SpecContext) { - tests.DeleteNamespace(ctx, testClient(), testNamespace) - }, afterEachTimeOut) - - policyFactory := func(mutateFns ...func(policy *kuadrantv1beta3.AuthPolicy)) *kuadrantv1beta3.AuthPolicy { - policy := &kuadrantv1beta3.AuthPolicy{ - TypeMeta: metav1.TypeMeta{ - Kind: "AuthPolicy", - APIVersion: kuadrantv1beta3.GroupVersion.String(), - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "toystore", - Namespace: testNamespace, - }, - Spec: kuadrantv1beta3.AuthPolicySpec{ - TargetRef: gatewayapiv1alpha2.LocalPolicyTargetReference{ - Group: gatewayapiv1.GroupName, - Kind: "HTTPRoute", - Name: TestHTTPRouteName, - }, - Defaults: &kuadrantv1beta3.AuthPolicyCommonSpec{ - AuthScheme: tests.BuildBasicAuthScheme(), - }, - }, - } - for _, mutateFn := range mutateFns { - mutateFn(policy) - } - return policy - } - - randomHostFromGWHost := func() string { - return strings.Replace(gwHost, "*", rand.String(4), 1) - } - - Context("policy attached to the gateway", func() { - - var ( - gwPolicy *kuadrantv1beta3.AuthPolicy - ) - - BeforeEach(func(ctx SpecContext) { - route := tests.BuildBasicHttpRoute(TestHTTPRouteName, TestGatewayName, testNamespace, []string{randomHostFromGWHost()}) - err := k8sClient.Create(ctx, route) - Expect(err).ToNot(HaveOccurred()) - Eventually(tests.RouteIsAccepted(ctx, testClient(), client.ObjectKeyFromObject(route))).WithContext(ctx).Should(BeTrue()) - - gwPolicy = policyFactory(func(policy *kuadrantv1beta3.AuthPolicy) { - policy.Name = "gw-auth" - policy.Spec.TargetRef.Group = gatewayapiv1.GroupName - policy.Spec.TargetRef.Kind = "Gateway" - policy.Spec.TargetRef.Name = TestGatewayName - policy.Spec.CommonSpec().AuthScheme.Authentication["apiKey"].ApiKey.Selector.MatchLabels["admin"] = "yes" - }) - - err = k8sClient.Create(ctx, gwPolicy) - logf.Log.V(1).Info("Creating AuthPolicy", "key", client.ObjectKeyFromObject(gwPolicy).String(), "error", err) - Expect(err).ToNot(HaveOccurred()) - - // check policy status - Eventually(tests.IsAuthPolicyAcceptedAndEnforced(ctx, testClient(), gwPolicy)).WithContext(ctx).Should(BeTrue()) - }) - - It("authpolicy has rules added", func(ctx SpecContext) { - // check istio authorizationpolicy - iapKey := types.NamespacedName{Name: controllers.IstioAuthorizationPolicyName(TestGatewayName, gwPolicy.Spec.TargetRef), Namespace: testNamespace} - iap := &secv1beta1resources.AuthorizationPolicy{} - Eventually(func() bool { - err := k8sClient.Get(ctx, iapKey, iap) - logf.Log.V(1).Info("Fetching Istio's AuthorizationPolicy", "key", iapKey.String(), "error", err) - return err == nil - }).WithContext(ctx).Should(BeTrue()) - - // has the correct target ref - Expect(iap.Spec.TargetRef).To(Not(BeNil())) - Expect(iap.Spec.TargetRef.Group).To(Equal("gateway.networking.k8s.io")) - Expect(iap.Spec.TargetRef.Kind).To(Equal("Gateway")) - Expect(iap.Spec.TargetRef.Name).To(Equal(TestGatewayName)) - 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{gwHost})) - 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*"})) - }, testTimeOut) - }) - - Context("policy attached to the route", func() { - var ( - routePolicy *kuadrantv1beta3.AuthPolicy - routeHost = randomHostFromGWHost() - ) - - BeforeEach(func(ctx SpecContext) { - route := tests.BuildBasicHttpRoute(TestHTTPRouteName, TestGatewayName, testNamespace, []string{routeHost}) - err := k8sClient.Create(ctx, route) - Expect(err).ToNot(HaveOccurred()) - Eventually(tests.RouteIsAccepted(ctx, testClient(), client.ObjectKeyFromObject(route))).WithContext(ctx).Should(BeTrue()) - - routePolicy = policyFactory(func(policy *kuadrantv1beta3.AuthPolicy) { - policy.Spec.TargetRef.Group = gatewayapiv1.GroupName - policy.Spec.TargetRef.Kind = "HTTPRoute" - policy.Spec.TargetRef.Name = TestHTTPRouteName - }) - - err = k8sClient.Create(ctx, routePolicy) - logf.Log.V(1).Info("Creating AuthPolicy", "key", client.ObjectKeyFromObject(routePolicy).String(), "error", err) - Expect(err).ToNot(HaveOccurred()) - - // check policy status - Eventually(tests.IsAuthPolicyAcceptedAndEnforced(ctx, testClient(), routePolicy)).WithContext(ctx).Should(BeTrue()) - }) - - It("authorization policy has rules added", func(ctx SpecContext) { - // check istio authorizationpolicy - iapKey := types.NamespacedName{Name: controllers.IstioAuthorizationPolicyName(TestGatewayName, routePolicy.Spec.TargetRef), Namespace: testNamespace} - iap := &secv1beta1resources.AuthorizationPolicy{} - Eventually(func() bool { - err := k8sClient.Get(ctx, iapKey, iap) - logf.Log.V(1).Info("Fetching Istio's AuthorizationPolicy", "key", iapKey.String(), "error", err) - return err == nil - }).WithContext(ctx).Should(BeTrue()) - - // has the correct target ref - Expect(iap.Spec.TargetRef).To(Not(BeNil())) - Expect(iap.Spec.TargetRef.Group).To(Equal("gateway.networking.k8s.io")) - Expect(iap.Spec.TargetRef.Kind).To(Equal("Gateway")) - Expect(iap.Spec.TargetRef.Name).To(Equal(TestGatewayName)) - 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{routeHost})) - 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*"})) - }, testTimeOut) - - It("Deletes authorizationpolicy when the policy is deleted", func(ctx SpecContext) { - // delete policy - err := k8sClient.Delete(ctx, routePolicy) - logf.Log.V(1).Info("Deleting AuthPolicy", "key", client.ObjectKeyFromObject(routePolicy).String(), "error", err) - Expect(err).ToNot(HaveOccurred()) - - // check istio authorizationpolicy - iapKey := types.NamespacedName{Name: controllers.IstioAuthorizationPolicyName(TestGatewayName, routePolicy.Spec.TargetRef), Namespace: testNamespace} - Eventually(func() bool { - err := k8sClient.Get(ctx, iapKey, &secv1beta1resources.AuthorizationPolicy{}) - logf.Log.V(1).Info("Fetching Istio's AuthorizationPolicy", "key", iapKey.String(), "error", err) - return apierrors.IsNotFound(err) - }).WithContext(ctx).Should(BeTrue()) - }, testTimeOut) - }) - - Context("Attaches policy to the Gateway while having other policies attached to some HTTPRoutes", func() { - // Gw A - // Route A -> Gw A - // Route B -> Gw A - // RLP 1 -> Gw A - // RLP 2 -> Route A - var ( - gwPolicy *kuadrantv1beta3.AuthPolicy - routeHost = randomHostFromGWHost() - ) - BeforeEach(func(ctx SpecContext) { - route := tests.BuildBasicHttpRoute(TestHTTPRouteName, TestGatewayName, testNamespace, []string{routeHost}) - err := k8sClient.Create(ctx, route) - Expect(err).ToNot(HaveOccurred()) - Eventually(tests.RouteIsAccepted(ctx, testClient(), client.ObjectKeyFromObject(route))).WithContext(ctx).Should(BeTrue()) - - gwPolicy = policyFactory(func(policy *kuadrantv1beta3.AuthPolicy) { - policy.Name = "gw-auth" - policy.Spec.TargetRef.Group = gatewayapiv1.GroupName - policy.Spec.TargetRef.Kind = "Gateway" - policy.Spec.TargetRef.Name = TestGatewayName - policy.Spec.CommonSpec().AuthScheme.Authentication["apiKey"].ApiKey.Selector.MatchLabels["admin"] = "yes" - }) - - err = k8sClient.Create(ctx, gwPolicy) - logf.Log.V(1).Info("Creating AuthPolicy", "key", client.ObjectKeyFromObject(gwPolicy).String(), "error", err) - Expect(err).ToNot(HaveOccurred()) - - // check policy status - Eventually(tests.IsAuthPolicyAcceptedAndEnforced(ctx, testClient(), gwPolicy)).WithContext(ctx).Should(BeTrue()) - - routePolicy := policyFactory(func(policy *kuadrantv1beta3.AuthPolicy) { - policy.Spec.TargetRef.Group = gatewayapiv1.GroupName - policy.Spec.TargetRef.Kind = "HTTPRoute" - policy.Spec.TargetRef.Name = TestHTTPRouteName - }) - - err = k8sClient.Create(ctx, routePolicy) - logf.Log.V(1).Info("Creating AuthPolicy", "key", client.ObjectKeyFromObject(routePolicy).String(), "error", err) - Expect(err).ToNot(HaveOccurred()) - - // check policy status - Eventually(tests.IsAuthPolicyAcceptedAndEnforced(ctx, testClient(), routePolicy)).WithContext(ctx).Should(BeTrue()) - - // create second (policyless) httproute - otherRoute := tests.BuildBasicHttpRoute("policyless-route", TestGatewayName, testNamespace, []string{randomHostFromGWHost()}) - otherRoute.Spec.Rules = []gatewayapiv1.HTTPRouteRule{ - { - Matches: []gatewayapiv1.HTTPRouteMatch{ - { - Method: ptr.To(gatewayapiv1.HTTPMethod("POST")), - }, - }, - }, - } - err = k8sClient.Create(ctx, otherRoute) - Expect(err).ToNot(HaveOccurred()) - Eventually(tests.RouteIsAccepted(ctx, testClient(), client.ObjectKeyFromObject(otherRoute))).WithContext(ctx).Should(BeTrue()) - }) - - It("check istio authorizationpolicy", func(ctx SpecContext) { - iapKey := types.NamespacedName{Name: controllers.IstioAuthorizationPolicyName(TestGatewayName, gwPolicy.Spec.TargetRef), Namespace: testNamespace} - iap := &secv1beta1resources.AuthorizationPolicy{} - Eventually(func() bool { - err := k8sClient.Get(ctx, iapKey, iap) - logf.Log.V(1).Info("Fetching Istio's AuthorizationPolicy", "key", iapKey.String(), "error", err) - return err == nil - }).WithContext(ctx).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{gwHost})) - 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{"/*"})) - }, testTimeOut) - }) - - Context("Complex HTTPRoute with multiple rules and hostnames", func() { - - var ( - routeHost1 = randomHostFromGWHost() - routeHost2 = randomHostFromGWHost() - ) - - BeforeEach(func(ctx SpecContext) { - route := tests.BuildMultipleRulesHttpRoute(TestHTTPRouteName, TestGatewayName, testNamespace, []string{routeHost1, routeHost2}) - err := k8sClient.Create(ctx, route) - Expect(err).ToNot(HaveOccurred()) - Eventually(tests.RouteIsAccepted(ctx, testClient(), client.ObjectKeyFromObject(route))).WithContext(ctx).Should(BeTrue()) - }) - - It("Attaches simple policy to the HTTPRoute", func(ctx SpecContext) { - policy := policyFactory() - - err := k8sClient.Create(ctx, policy) - Expect(err).ToNot(HaveOccurred()) - - // check policy status - Eventually(tests.IsAuthPolicyAcceptedAndEnforced(ctx, testClient(), policy)).WithContext(ctx).Should(BeTrue()) - - // check istio authorizationpolicy - iapKey := types.NamespacedName{Name: controllers.IstioAuthorizationPolicyName(TestGatewayName, policy.Spec.TargetRef), Namespace: testNamespace} - iap := &secv1beta1resources.AuthorizationPolicy{} - Eventually(func() bool { - err := k8sClient.Get(ctx, iapKey, iap) - logf.Log.V(1).Info("Fetching Istio's AuthorizationPolicy", "key", iapKey.String(), "error", err) - return err == nil - }).WithContext(ctx).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{routeHost1, routeHost2})) - 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{routeHost1, routeHost2})) - 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{routeHost1, routeHost2})) - 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*"})) - }, testTimeOut) - }) -}) diff --git a/tests/istio/extension_reconciler_test.go b/tests/istio/extension_reconciler_test.go index 46cbe8a8c..5e54aceb9 100644 --- a/tests/istio/extension_reconciler_test.go +++ b/tests/istio/extension_reconciler_test.go @@ -147,6 +147,11 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { Expect(err).ToNot(HaveOccurred()) Expect(existingWASMConfig).To(Equal(&wasm.Config{ Services: map[string]wasm.Service{ + wasm.AuthServiceName: { + Type: wasm.AuthServiceType, + Endpoint: common.KuadrantAuthClusterName, + FailureMode: wasm.FailureModeDeny, + }, wasm.RateLimitServiceName: { Type: wasm.RateLimitServiceType, Endpoint: common.KuadrantRateLimitClusterName, @@ -737,6 +742,11 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { Expect(err).ToNot(HaveOccurred()) Expect(existingWASMConfig).To(Equal(&wasm.Config{ Services: map[string]wasm.Service{ + wasm.AuthServiceName: { + Type: wasm.AuthServiceType, + Endpoint: common.KuadrantAuthClusterName, + FailureMode: wasm.FailureModeDeny, + }, wasm.RateLimitServiceName: { Endpoint: common.KuadrantRateLimitClusterName, FailureMode: wasm.FailureModeAllow, @@ -961,6 +971,11 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { expectedPlugin := &wasm.Config{ Services: map[string]wasm.Service{ + wasm.AuthServiceName: { + Endpoint: common.KuadrantAuthClusterName, + FailureMode: wasm.FailureModeDeny, + Type: wasm.AuthServiceType, + }, wasm.RateLimitServiceName: { Endpoint: common.KuadrantRateLimitClusterName, FailureMode: wasm.FailureModeAllow, @@ -1174,6 +1189,11 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { expectedPlugin := &wasm.Config{ Services: map[string]wasm.Service{ + wasm.AuthServiceName: { + Type: wasm.AuthServiceType, + Endpoint: common.KuadrantAuthClusterName, + FailureMode: wasm.FailureModeDeny, + }, wasm.RateLimitServiceName: { Endpoint: common.KuadrantRateLimitClusterName, FailureMode: wasm.FailureModeAllow, @@ -1305,6 +1325,11 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { expectedPlugin := &wasm.Config{ Services: map[string]wasm.Service{ + wasm.AuthServiceName: { + Type: wasm.AuthServiceType, + Endpoint: common.KuadrantAuthClusterName, + FailureMode: wasm.FailureModeDeny, + }, wasm.RateLimitServiceName: { Endpoint: common.KuadrantRateLimitClusterName, FailureMode: wasm.FailureModeAllow, @@ -1508,6 +1533,11 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { expectedPlugin := &wasm.Config{ Services: map[string]wasm.Service{ + wasm.AuthServiceName: { + Type: wasm.AuthServiceType, + Endpoint: common.KuadrantAuthClusterName, + FailureMode: wasm.FailureModeDeny, + }, wasm.RateLimitServiceName: { Endpoint: common.KuadrantRateLimitClusterName, FailureMode: wasm.FailureModeAllow, @@ -1602,6 +1632,11 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { expectedPlugin := &wasm.Config{ Services: map[string]wasm.Service{ + wasm.AuthServiceName: { + Type: wasm.AuthServiceType, + Endpoint: common.KuadrantAuthClusterName, + FailureMode: wasm.FailureModeDeny, + }, wasm.RateLimitServiceName: { Endpoint: common.KuadrantRateLimitClusterName, FailureMode: wasm.FailureModeAllow, @@ -1780,6 +1815,11 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { expectedPlugin := &wasm.Config{ Services: map[string]wasm.Service{ + wasm.AuthServiceName: { + Type: wasm.AuthServiceType, + Endpoint: common.KuadrantAuthClusterName, + FailureMode: wasm.FailureModeDeny, + }, wasm.RateLimitServiceName: { Endpoint: common.KuadrantRateLimitClusterName, FailureMode: wasm.FailureModeAllow, @@ -1892,6 +1932,11 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { expectedPlugin := &wasm.Config{ Services: map[string]wasm.Service{ + wasm.AuthServiceName: { + Type: wasm.AuthServiceType, + Endpoint: common.KuadrantAuthClusterName, + FailureMode: wasm.FailureModeDeny, + }, wasm.RateLimitServiceName: { Endpoint: common.KuadrantRateLimitClusterName, FailureMode: wasm.FailureModeAllow, @@ -2106,6 +2151,11 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { expectedPlugin := &wasm.Config{ Services: map[string]wasm.Service{ + wasm.AuthServiceName: { + Type: wasm.AuthServiceType, + Endpoint: common.KuadrantAuthClusterName, + FailureMode: wasm.FailureModeDeny, + }, wasm.RateLimitServiceName: { Endpoint: common.KuadrantRateLimitClusterName, FailureMode: wasm.FailureModeAllow, @@ -2215,6 +2265,11 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { expectedPlugin := &wasm.Config{ Services: map[string]wasm.Service{ + wasm.AuthServiceName: { + Type: wasm.AuthServiceType, + Endpoint: common.KuadrantAuthClusterName, + FailureMode: wasm.FailureModeDeny, + }, wasm.RateLimitServiceName: { Endpoint: common.KuadrantRateLimitClusterName, FailureMode: wasm.FailureModeAllow, @@ -2391,6 +2446,11 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { Expect(err).ToNot(HaveOccurred()) Expect(existingWASMConfig).To(Equal(&wasm.Config{ Services: map[string]wasm.Service{ + wasm.AuthServiceName: { + Type: wasm.AuthServiceType, + Endpoint: common.KuadrantAuthClusterName, + FailureMode: wasm.FailureModeDeny, + }, wasm.RateLimitServiceName: { Endpoint: common.KuadrantRateLimitClusterName, FailureMode: wasm.FailureModeAllow, @@ -2470,6 +2530,11 @@ var _ = Describe("Rate Limiting WasmPlugin controller", func() { return &wasm.Config{ Services: map[string]wasm.Service{ + wasm.AuthServiceName: { + Type: wasm.AuthServiceType, + Endpoint: common.KuadrantAuthClusterName, + FailureMode: wasm.FailureModeDeny, + }, wasm.RateLimitServiceName: { Endpoint: common.KuadrantRateLimitClusterName, FailureMode: wasm.FailureModeAllow,