From 7c716c24ab0c9d9296792c7db9a124d6ff3c5985 Mon Sep 17 00:00:00 2001 From: Joshua Reese Date: Tue, 23 Sep 2025 13:03:27 -0500 Subject: [PATCH 1/3] Validation for Backends, BackendTrafficPolicies, HTTPRouteFilters, and SecurityPolicies. The next set of changes will add validation options to BackendTrafficPolicies, and wire validation options in from the service configuration. In the future, many of these limits may be influenced by quotas. --- internal/validation/backend_validation.go | 30 +- .../validation/backend_validation_test.go | 54 +++ .../backendtrafficpolicy_validation.go | 49 ++- .../backendtrafficpolicy_validation_test.go | 259 ++++++++++++ .../validation/httproutefilter_validation.go | 21 +- .../httproutefilter_validation_test.go | 47 +++ .../validation/securitypolicy_validation.go | 327 ++++++++++++++- .../securitypolicy_validation_test.go | 382 ++++++++++++++++++ .../v1alpha1/httproutefilter_webhook.go | 12 +- .../v1alpha1/securitypolicy_webhook.go | 16 +- 10 files changed, 1164 insertions(+), 33 deletions(-) create mode 100644 internal/validation/backend_validation_test.go create mode 100644 internal/validation/httproutefilter_validation_test.go create mode 100644 internal/validation/securitypolicy_validation_test.go diff --git a/internal/validation/backend_validation.go b/internal/validation/backend_validation.go index 68befc9c..711a6fe4 100644 --- a/internal/validation/backend_validation.go +++ b/internal/validation/backend_validation.go @@ -3,8 +3,36 @@ package validation import ( envoygatewayv1alpha1 "github.com/envoyproxy/gateway/api/v1alpha1" "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/utils/ptr" ) func ValidateBackend(backend *envoygatewayv1alpha1.Backend) field.ErrorList { - return nil + allErrs := field.ErrorList{} + + specPath := field.NewPath("spec") + + if ptr.Deref(backend.Spec.Type, envoygatewayv1alpha1.BackendTypeEndpoints) != envoygatewayv1alpha1.BackendTypeEndpoints { + supportedTypes := []envoygatewayv1alpha1.BackendType{envoygatewayv1alpha1.BackendTypeEndpoints} + allErrs = append(allErrs, field.NotSupported(specPath.Child("type"), backend.Spec.Type, supportedTypes)) + } + + allErrs = append(allErrs, validateBackendEndpoints(backend.Spec.Endpoints, specPath.Child("endpoints"))...) + + // appProtocols, fallback, and TLS settings are considered safe to leverage as is. + + return allErrs +} + +func validateBackendEndpoints(endpoints []envoygatewayv1alpha1.BackendEndpoint, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + for i, endpoint := range endpoints { + endpointPath := fldPath.Index(i) + + if endpoint.Unix != nil { + allErrs = append(allErrs, field.Forbidden(endpointPath.Child("unix"), "unix endpoints are not permitted")) + } + } + + return allErrs } diff --git a/internal/validation/backend_validation_test.go b/internal/validation/backend_validation_test.go new file mode 100644 index 00000000..1277103d --- /dev/null +++ b/internal/validation/backend_validation_test.go @@ -0,0 +1,54 @@ +package validation + +import ( + "testing" + + envoygatewayv1alpha1 "github.com/envoyproxy/gateway/api/v1alpha1" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/utils/ptr" +) + +func TestValidateBackend(t *testing.T) { + scenarios := map[string]struct { + backend *envoygatewayv1alpha1.Backend + expectedErrors field.ErrorList + }{ + "invalid backend type": { + backend: &envoygatewayv1alpha1.Backend{ + Spec: envoygatewayv1alpha1.BackendSpec{ + Type: ptr.To(envoygatewayv1alpha1.BackendTypeDynamicResolver), + }, + }, + expectedErrors: field.ErrorList{ + field.NotSupported(field.NewPath("spec", "type"), "", []string{}), + }, + }, + "invalid backend endpoints - unix": { + backend: &envoygatewayv1alpha1.Backend{ + Spec: envoygatewayv1alpha1.BackendSpec{ + Type: ptr.To(envoygatewayv1alpha1.BackendTypeEndpoints), + Endpoints: []envoygatewayv1alpha1.BackendEndpoint{ + { + Unix: ptr.To(envoygatewayv1alpha1.UnixSocket{}), + }, + }, + }, + }, + expectedErrors: field.ErrorList{ + field.Forbidden(field.NewPath("spec", "endpoints").Index(0).Child("unix"), ""), + }, + }, + } + + for name, scenario := range scenarios { + t.Run(name, func(t *testing.T) { + errs := ValidateBackend(scenario.backend) + delta := cmp.Diff(scenario.expectedErrors, errs, cmpopts.IgnoreFields(field.Error{}, "BadValue", "Detail")) + if delta != "" { + t.Errorf("Testcase %s - expected errors '%v', got '%v', diff: '%v'", name, scenario.expectedErrors, errs, delta) + } + }) + } +} diff --git a/internal/validation/backendtrafficpolicy_validation.go b/internal/validation/backendtrafficpolicy_validation.go index c3357fcc..02e375d8 100644 --- a/internal/validation/backendtrafficpolicy_validation.go +++ b/internal/validation/backendtrafficpolicy_validation.go @@ -2,6 +2,7 @@ package validation import ( "fmt" + "strconv" "time" envoygatewayv1alpha1 "github.com/envoyproxy/gateway/api/v1alpha1" @@ -22,25 +23,33 @@ func ValidateBackendTrafficPolicy(backendTrafficPolicy *envoygatewayv1alpha1.Bac allErrs = append(allErrs, field.Forbidden(specPath.Child("targetRef"), "deprecated field, use spec.targetRefs or spec.targetSelectors instead")) } - allErrs = append(allErrs, validateBackendTrafficPolicyLoadBalancer(backendTrafficPolicy.Spec.LoadBalancer, specPath.Child("loadBalancer"))...) + allErrs = append(allErrs, validateGatewayClusterSettings(backendTrafficPolicy.Spec.ClusterSettings, specPath)...) + allErrs = append(allErrs, validateBackendTrafficPolicyRateLimit(backendTrafficPolicy.Spec.RateLimit, specPath.Child("rateLimit"))...) + allErrs = append(allErrs, validateBackendTrafficPolicyFaultInjection(backendTrafficPolicy.Spec.FaultInjection, specPath.Child("faultInjection"))...) + + return allErrs +} - if backendTrafficPolicy.Spec.Retry != nil { - allErrs = append(allErrs, field.Forbidden(specPath.Child("retry"), "retry settings are not permitted")) +func validateGatewayClusterSettings(clusterSettings envoygatewayv1alpha1.ClusterSettings, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + allErrs = append(allErrs, validateGatewayBackendLoadBalancer(clusterSettings.LoadBalancer, fldPath.Child("loadBalancer"))...) + + if clusterSettings.Retry != nil { + allErrs = append(allErrs, field.Forbidden(fldPath.Child("retry"), "retry settings are not permitted")) } - allErrs = append(allErrs, validateBackendTrafficPolicyTCPKeepalive(backendTrafficPolicy.Spec.TCPKeepalive, specPath.Child("tcpKeepalive"))...) - allErrs = append(allErrs, validateBackendTrafficPolicyHealthCheck(backendTrafficPolicy.Spec.HealthCheck, specPath.Child("healthCheck"))...) - allErrs = append(allErrs, validateBackendTrafficPolicyTimeout(backendTrafficPolicy.Spec.Timeout, specPath.Child("timeout"))...) - allErrs = append(allErrs, validateBackendTrafficPolicyConnection(backendTrafficPolicy.Spec.Connection, specPath.Child("connection"))...) - allErrs = append(allErrs, validateBackendTrafficPolicyDNS(backendTrafficPolicy.Spec.DNS, specPath.Child("dns"))...) - allErrs = append(allErrs, validateBackendTrafficPolicyHTTP2(backendTrafficPolicy.Spec.HTTP2, specPath.Child("http2"))...) - allErrs = append(allErrs, validateBackendTrafficPolicyRateLimit(backendTrafficPolicy.Spec.RateLimit, specPath.Child("rateLimit"))...) - allErrs = append(allErrs, validateBackendTrafficPolicyFaultInjection(backendTrafficPolicy.Spec.FaultInjection, specPath.Child("faultInjection"))...) + allErrs = append(allErrs, validateGatewayClusterSettingsTCPKeepalive(clusterSettings.TCPKeepalive, fldPath.Child("tcpKeepalive"))...) + allErrs = append(allErrs, validateGatewayClusterSettingsHealthCheck(clusterSettings.HealthCheck, fldPath.Child("healthCheck"))...) + allErrs = append(allErrs, validateGatewayClusterSettingsTimeout(clusterSettings.Timeout, fldPath.Child("timeout"))...) + allErrs = append(allErrs, validateGatewayClusterSettingsConnection(clusterSettings.Connection, fldPath.Child("connection"))...) + allErrs = append(allErrs, validateGatewayClusterSettingsDNS(clusterSettings.DNS, fldPath.Child("dns"))...) + allErrs = append(allErrs, validateGatewayClusterSettingsHTTP2(clusterSettings.HTTP2, fldPath.Child("http2"))...) return allErrs } -func validateBackendTrafficPolicyLoadBalancer(loadBalancer *envoygatewayv1alpha1.LoadBalancer, fldPath *field.Path) field.ErrorList { +func validateGatewayBackendLoadBalancer(loadBalancer *envoygatewayv1alpha1.LoadBalancer, fldPath *field.Path) field.ErrorList { if loadBalancer == nil { return nil } @@ -54,7 +63,7 @@ func validateBackendTrafficPolicyLoadBalancer(loadBalancer *envoygatewayv1alpha1 return allErrs } -func validateBackendTrafficPolicyTCPKeepalive(tcpKeepalive *envoygatewayv1alpha1.TCPKeepalive, fldPath *field.Path) field.ErrorList { +func validateGatewayClusterSettingsTCPKeepalive(tcpKeepalive *envoygatewayv1alpha1.TCPKeepalive, fldPath *field.Path) field.ErrorList { if tcpKeepalive == nil { return nil } @@ -62,7 +71,7 @@ func validateBackendTrafficPolicyTCPKeepalive(tcpKeepalive *envoygatewayv1alpha1 allErrs := field.ErrorList{} if tcpKeepalive.Probes != nil && *tcpKeepalive.Probes < 9 { - allErrs = append(allErrs, field.Invalid(fldPath.Child("probe"), *tcpKeepalive.Probes, "must be greater than or equal to 9")) + allErrs = append(allErrs, field.Invalid(fldPath.Child("probes"), strconv.FormatUint(uint64(*tcpKeepalive.Probes), 10), "must be greater than or equal to 9")) } if v := tcpKeepalive.IdleTime; v != nil { @@ -78,7 +87,7 @@ func validateBackendTrafficPolicyTCPKeepalive(tcpKeepalive *envoygatewayv1alpha1 return allErrs } -func validateBackendTrafficPolicyHealthCheck(healthCheck *envoygatewayv1alpha1.HealthCheck, fldPath *field.Path) field.ErrorList { +func validateGatewayClusterSettingsHealthCheck(healthCheck *envoygatewayv1alpha1.HealthCheck, fldPath *field.Path) field.ErrorList { if healthCheck == nil { return nil } @@ -92,7 +101,7 @@ func validateBackendTrafficPolicyHealthCheck(healthCheck *envoygatewayv1alpha1.H return allErrs } -func validateBackendTrafficPolicyTimeout(timeout *envoygatewayv1alpha1.Timeout, fldPath *field.Path) field.ErrorList { +func validateGatewayClusterSettingsTimeout(timeout *envoygatewayv1alpha1.Timeout, fldPath *field.Path) field.ErrorList { if timeout == nil { return nil } @@ -102,7 +111,7 @@ func validateBackendTrafficPolicyTimeout(timeout *envoygatewayv1alpha1.Timeout, if timeout.TCP != nil { if v := timeout.TCP.ConnectTimeout; v != nil { connectTimeoutFieldPath := fldPath.Child("tcp").Child("connectTimeout") - allErrs = append(allErrs, validateGatewayDuration(connectTimeoutFieldPath, v, nil, ptr.To(1*time.Hour))...) + allErrs = append(allErrs, validateGatewayDuration(connectTimeoutFieldPath, v, nil, ptr.To(10*time.Second))...) } } @@ -129,7 +138,7 @@ func validateBackendTrafficPolicyTimeout(timeout *envoygatewayv1alpha1.Timeout, return allErrs } -func validateBackendTrafficPolicyConnection(connection *envoygatewayv1alpha1.BackendConnection, fldPath *field.Path) field.ErrorList { +func validateGatewayClusterSettingsConnection(connection *envoygatewayv1alpha1.BackendConnection, fldPath *field.Path) field.ErrorList { if connection == nil { return nil } @@ -144,7 +153,7 @@ func validateBackendTrafficPolicyConnection(connection *envoygatewayv1alpha1.Bac return nil } -func validateBackendTrafficPolicyDNS(dns *envoygatewayv1alpha1.DNS, fldPath *field.Path) field.ErrorList { +func validateGatewayClusterSettingsDNS(dns *envoygatewayv1alpha1.DNS, fldPath *field.Path) field.ErrorList { if dns == nil { return nil } @@ -163,7 +172,7 @@ func validateBackendTrafficPolicyDNS(dns *envoygatewayv1alpha1.DNS, fldPath *fie return allErrs } -func validateBackendTrafficPolicyHTTP2(http2 *envoygatewayv1alpha1.HTTP2Settings, fldPath *field.Path) field.ErrorList { +func validateGatewayClusterSettingsHTTP2(http2 *envoygatewayv1alpha1.HTTP2Settings, fldPath *field.Path) field.ErrorList { if http2 == nil { return nil } diff --git a/internal/validation/backendtrafficpolicy_validation_test.go b/internal/validation/backendtrafficpolicy_validation_test.go index 22ab3c5f..52d3d9c8 100644 --- a/internal/validation/backendtrafficpolicy_validation_test.go +++ b/internal/validation/backendtrafficpolicy_validation_test.go @@ -6,6 +6,7 @@ import ( envoygatewayv1alpha1 "github.com/envoyproxy/gateway/api/v1alpha1" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "k8s.io/apimachinery/pkg/api/resource" "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/utils/ptr" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" @@ -31,6 +32,264 @@ func TestValidateBackendTrafficPolicy(t *testing.T) { field.Forbidden(field.NewPath("spec", "targetRef"), "Invalid"), }, }, + "invalid load balancer endpoint override": { + backendTrafficPolicy: &envoygatewayv1alpha1.BackendTrafficPolicy{ + Spec: envoygatewayv1alpha1.BackendTrafficPolicySpec{ + ClusterSettings: envoygatewayv1alpha1.ClusterSettings{ + LoadBalancer: &envoygatewayv1alpha1.LoadBalancer{ + EndpointOverride: &envoygatewayv1alpha1.EndpointOverride{ + ExtractFrom: []envoygatewayv1alpha1.EndpointOverrideExtractFrom{}, + }, + }, + }, + }, + }, + expectedErrors: field.ErrorList{ + field.Forbidden(field.NewPath("spec", "loadBalancer", "endpointOverride"), ""), + }, + }, + "retry not permitted": { + backendTrafficPolicy: &envoygatewayv1alpha1.BackendTrafficPolicy{ + Spec: envoygatewayv1alpha1.BackendTrafficPolicySpec{ + ClusterSettings: envoygatewayv1alpha1.ClusterSettings{ + Retry: &envoygatewayv1alpha1.Retry{ + NumRetries: ptr.To(int32(3)), + }, + }, + }, + }, + expectedErrors: field.ErrorList{ + field.Forbidden(field.NewPath("spec", "retry"), ""), + }, + }, + "tcp keepalive probes too low": { + backendTrafficPolicy: &envoygatewayv1alpha1.BackendTrafficPolicy{ + Spec: envoygatewayv1alpha1.BackendTrafficPolicySpec{ + ClusterSettings: envoygatewayv1alpha1.ClusterSettings{ + TCPKeepalive: &envoygatewayv1alpha1.TCPKeepalive{ + Probes: ptr.To(uint32(1)), + }, + }, + }, + }, + expectedErrors: field.ErrorList{ + field.Invalid(field.NewPath("spec", "tcpKeepalive", "probes"), int32(0), ""), + }, + }, + "tcp idleTime too low": { + backendTrafficPolicy: &envoygatewayv1alpha1.BackendTrafficPolicy{ + Spec: envoygatewayv1alpha1.BackendTrafficPolicySpec{ + ClusterSettings: envoygatewayv1alpha1.ClusterSettings{ + TCPKeepalive: &envoygatewayv1alpha1.TCPKeepalive{ + IdleTime: ptr.To(gatewayv1.Duration("5s")), + }, + }, + }, + }, + expectedErrors: field.ErrorList{ + field.Invalid(field.NewPath("spec", "tcpKeepalive", "idleTime"), "4m0s", ""), + }, + }, + "tcp interval too low": { + backendTrafficPolicy: &envoygatewayv1alpha1.BackendTrafficPolicy{ + Spec: envoygatewayv1alpha1.BackendTrafficPolicySpec{ + ClusterSettings: envoygatewayv1alpha1.ClusterSettings{ + TCPKeepalive: &envoygatewayv1alpha1.TCPKeepalive{ + Interval: ptr.To(gatewayv1.Duration("5s")), + }, + }, + }, + }, + expectedErrors: field.ErrorList{ + field.Invalid(field.NewPath("spec", "tcpKeepalive", "interval"), "4m0s", ""), + }, + }, + "active healthcheck not permitted": { + backendTrafficPolicy: &envoygatewayv1alpha1.BackendTrafficPolicy{ + Spec: envoygatewayv1alpha1.BackendTrafficPolicySpec{ + ClusterSettings: envoygatewayv1alpha1.ClusterSettings{ + HealthCheck: &envoygatewayv1alpha1.HealthCheck{ + Active: &envoygatewayv1alpha1.ActiveHealthCheck{}, + }, + }, + }, + }, + expectedErrors: field.ErrorList{ + field.Forbidden(field.NewPath("spec", "healthCheck", "active"), ""), + }, + }, + "tcp connect timeout too high": { + backendTrafficPolicy: &envoygatewayv1alpha1.BackendTrafficPolicy{ + Spec: envoygatewayv1alpha1.BackendTrafficPolicySpec{ + ClusterSettings: envoygatewayv1alpha1.ClusterSettings{ + Timeout: &envoygatewayv1alpha1.Timeout{ + TCP: &envoygatewayv1alpha1.TCPTimeout{ + ConnectTimeout: ptr.To(gatewayv1.Duration("24h")), + }, + }, + }, + }, + }, + expectedErrors: field.ErrorList{ + field.Invalid(field.NewPath("spec", "timeout", "tcp", "connectTimeout"), "", ""), + }, + }, + "http connection idle timeout too high": { + backendTrafficPolicy: &envoygatewayv1alpha1.BackendTrafficPolicy{ + Spec: envoygatewayv1alpha1.BackendTrafficPolicySpec{ + ClusterSettings: envoygatewayv1alpha1.ClusterSettings{ + Timeout: &envoygatewayv1alpha1.Timeout{ + HTTP: &envoygatewayv1alpha1.HTTPTimeout{ + ConnectionIdleTimeout: ptr.To(gatewayv1.Duration("24h")), + }, + }, + }, + }, + }, + expectedErrors: field.ErrorList{ + field.Invalid(field.NewPath("spec", "timeout", "http", "connectionIdleTimeout"), "", ""), + }, + }, + "http max connection duration too high": { + backendTrafficPolicy: &envoygatewayv1alpha1.BackendTrafficPolicy{ + Spec: envoygatewayv1alpha1.BackendTrafficPolicySpec{ + ClusterSettings: envoygatewayv1alpha1.ClusterSettings{ + Timeout: &envoygatewayv1alpha1.Timeout{ + HTTP: &envoygatewayv1alpha1.HTTPTimeout{ + MaxConnectionDuration: ptr.To(gatewayv1.Duration("24h")), + }, + }, + }, + }, + }, + expectedErrors: field.ErrorList{ + field.Invalid(field.NewPath("spec", "timeout", "http", "maxConnectionDuration"), "", ""), + }, + }, + "http request timeout too high": { + backendTrafficPolicy: &envoygatewayv1alpha1.BackendTrafficPolicy{ + Spec: envoygatewayv1alpha1.BackendTrafficPolicySpec{ + ClusterSettings: envoygatewayv1alpha1.ClusterSettings{ + Timeout: &envoygatewayv1alpha1.Timeout{ + HTTP: &envoygatewayv1alpha1.HTTPTimeout{ + RequestTimeout: ptr.To(gatewayv1.Duration("24h")), + }, + }, + }, + }, + }, + expectedErrors: field.ErrorList{ + field.Invalid(field.NewPath("spec", "timeout", "http", "requestTimeout"), "", ""), + }, + }, + "connection buffer limit too high": { + backendTrafficPolicy: &envoygatewayv1alpha1.BackendTrafficPolicy{ + Spec: envoygatewayv1alpha1.BackendTrafficPolicySpec{ + ClusterSettings: envoygatewayv1alpha1.ClusterSettings{ + Connection: &envoygatewayv1alpha1.BackendConnection{ + BufferLimit: ptr.To(resource.MustParse("2Gi")), + }, + }, + }, + }, + expectedErrors: field.ErrorList{ + field.Invalid(field.NewPath("spec", "connection", "bufferLimit"), "", ""), + }, + }, + "dns refresh rate too low": { + backendTrafficPolicy: &envoygatewayv1alpha1.BackendTrafficPolicy{ + Spec: envoygatewayv1alpha1.BackendTrafficPolicySpec{ + ClusterSettings: envoygatewayv1alpha1.ClusterSettings{ + DNS: &envoygatewayv1alpha1.DNS{ + DNSRefreshRate: ptr.To(gatewayv1.Duration("500ms")), + }, + }, + }, + }, + expectedErrors: field.ErrorList{ + field.Invalid(field.NewPath("spec", "dns", "dnsRefreshRate"), "", ""), + }, + }, + "dns respect ttl false": { + backendTrafficPolicy: &envoygatewayv1alpha1.BackendTrafficPolicy{ + Spec: envoygatewayv1alpha1.BackendTrafficPolicySpec{ + ClusterSettings: envoygatewayv1alpha1.ClusterSettings{ + DNS: &envoygatewayv1alpha1.DNS{ + RespectDNSTTL: ptr.To(false), + }, + }, + }, + }, + expectedErrors: field.ErrorList{ + field.Forbidden(field.NewPath("spec", "dns", "respectDnsTtl"), "must respect DNS TTL"), + }, + }, + "http2 initial stream window size too large": { + backendTrafficPolicy: &envoygatewayv1alpha1.BackendTrafficPolicy{ + Spec: envoygatewayv1alpha1.BackendTrafficPolicySpec{ + ClusterSettings: envoygatewayv1alpha1.ClusterSettings{ + HTTP2: &envoygatewayv1alpha1.HTTP2Settings{ + InitialStreamWindowSize: ptr.To(resource.MustParse("2Gi")), + }, + }, + }, + }, + expectedErrors: field.ErrorList{ + field.Invalid(field.NewPath("spec", "http2", "initialStreamWindowSize"), 0, ""), + }, + }, + "http2 initial connection window size too large": { + backendTrafficPolicy: &envoygatewayv1alpha1.BackendTrafficPolicy{ + Spec: envoygatewayv1alpha1.BackendTrafficPolicySpec{ + ClusterSettings: envoygatewayv1alpha1.ClusterSettings{ + HTTP2: &envoygatewayv1alpha1.HTTP2Settings{ + InitialConnectionWindowSize: ptr.To(resource.MustParse("2Gi")), + }, + }, + }, + }, + expectedErrors: field.ErrorList{ + field.Invalid(field.NewPath("spec", "http2", "initialConnectionWindowSize"), 0, ""), + }, + }, + "http2 max concurrent streams too large": { + backendTrafficPolicy: &envoygatewayv1alpha1.BackendTrafficPolicy{ + Spec: envoygatewayv1alpha1.BackendTrafficPolicySpec{ + ClusterSettings: envoygatewayv1alpha1.ClusterSettings{ + HTTP2: &envoygatewayv1alpha1.HTTP2Settings{ + MaxConcurrentStreams: ptr.To(uint32(2000000)), + }, + }, + }, + }, + expectedErrors: field.ErrorList{ + field.Invalid(field.NewPath("spec", "http2", "maxConcurrentStreams"), 0, ""), + }, + }, + "rate limit type invalid": { + backendTrafficPolicy: &envoygatewayv1alpha1.BackendTrafficPolicy{ + Spec: envoygatewayv1alpha1.BackendTrafficPolicySpec{ + RateLimit: &envoygatewayv1alpha1.RateLimitSpec{ + Type: envoygatewayv1alpha1.GlobalRateLimitType, + Global: &envoygatewayv1alpha1.GlobalRateLimit{}, + }, + }, + }, + expectedErrors: field.ErrorList{ + field.NotSupported(field.NewPath("spec", "rateLimit", "type"), "", []string{}), + field.Forbidden(field.NewPath("spec", "rateLimit", "global"), ""), + }, + }, + "fault injection not permitted": { + backendTrafficPolicy: &envoygatewayv1alpha1.BackendTrafficPolicy{ + Spec: envoygatewayv1alpha1.BackendTrafficPolicySpec{ + FaultInjection: &envoygatewayv1alpha1.FaultInjection{}, + }, + }, + expectedErrors: field.ErrorList{ + field.Forbidden(field.NewPath("spec", "faultInjection"), "fault injection is not permitted"), + }, + }, } for name, scenario := range scenarios { diff --git a/internal/validation/httproutefilter_validation.go b/internal/validation/httproutefilter_validation.go index 55acc648..bff42e9b 100644 --- a/internal/validation/httproutefilter_validation.go +++ b/internal/validation/httproutefilter_validation.go @@ -5,6 +5,23 @@ import ( "k8s.io/apimachinery/pkg/util/validation/field" ) -func ValidateHTTPRouteFilter(httpRouteFilter *envoygatewayv1alpha1.HTTPRouteFilter) field.ErrorList { - return nil +type HTTPRouteFilterValidationOptions struct { + // MaxInlineBodySize is the maximum allowed size for an inline body in a direct response filter. + MaxInlineBodySize int +} + +func ValidateHTTPRouteFilter(httpRouteFilter *envoygatewayv1alpha1.HTTPRouteFilter, opts HTTPRouteFilterValidationOptions) field.ErrorList { + allErrs := field.ErrorList{} + + specPath := field.NewPath("spec") + + if directResponse := httpRouteFilter.Spec.DirectResponse; directResponse != nil && + directResponse.Body != nil && + directResponse.Body.Inline != nil { + if len(*directResponse.Body.Inline) > opts.MaxInlineBodySize { + allErrs = append(allErrs, field.TooLong(specPath.Child("directResponse").Child("body").Child("inline"), len(*directResponse.Body.Inline), opts.MaxInlineBodySize)) + } + } + + return allErrs } diff --git a/internal/validation/httproutefilter_validation_test.go b/internal/validation/httproutefilter_validation_test.go new file mode 100644 index 00000000..9c1c7616 --- /dev/null +++ b/internal/validation/httproutefilter_validation_test.go @@ -0,0 +1,47 @@ +package validation + +import ( + "testing" + + envoygatewayv1alpha1 "github.com/envoyproxy/gateway/api/v1alpha1" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/utils/ptr" +) + +func TestValidateHTTPRouteFilter(t *testing.T) { + scenarios := map[string]struct { + httpRouteFilter *envoygatewayv1alpha1.HTTPRouteFilter + opts HTTPRouteFilterValidationOptions + expectedErrors field.ErrorList + }{ + "direct response - inline body too large": { + httpRouteFilter: &envoygatewayv1alpha1.HTTPRouteFilter{ + Spec: envoygatewayv1alpha1.HTTPRouteFilterSpec{ + DirectResponse: &envoygatewayv1alpha1.HTTPDirectResponseFilter{ + Body: &envoygatewayv1alpha1.CustomResponseBody{ + Inline: ptr.To("too large"), + }, + }, + }, + }, + opts: HTTPRouteFilterValidationOptions{ + MaxInlineBodySize: 1, + }, + expectedErrors: field.ErrorList{ + field.TooLong(field.NewPath("spec", "directResponse", "body", "inline"), 0, 0), + }, + }, + } + + for name, scenario := range scenarios { + t.Run(name, func(t *testing.T) { + errs := ValidateHTTPRouteFilter(scenario.httpRouteFilter, scenario.opts) + delta := cmp.Diff(scenario.expectedErrors, errs, cmpopts.IgnoreFields(field.Error{}, "BadValue", "Detail")) + if delta != "" { + t.Errorf("Testcase %s - expected errors '%v', got '%v', diff: '%v'", name, scenario.expectedErrors, errs, delta) + } + }) + } +} diff --git a/internal/validation/securitypolicy_validation.go b/internal/validation/securitypolicy_validation.go index fc7492c9..8fc65205 100644 --- a/internal/validation/securitypolicy_validation.go +++ b/internal/validation/securitypolicy_validation.go @@ -1,10 +1,333 @@ package validation import ( + "time" + envoygatewayv1alpha1 "github.com/envoyproxy/gateway/api/v1alpha1" "k8s.io/apimachinery/pkg/util/validation/field" + gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" ) -func ValidateSecurityPolicy(securityPolicy *envoygatewayv1alpha1.SecurityPolicy) field.ErrorList { - return nil +type SecurityPolicyValidationOptions struct { + APIKeyAuth APIKeyAuthValidationOptions + CORS CORSValidationOptions + JWTProvider JWTProviderValidationOptions + OIDC OIDCValidationOptions + Authorization AuthorizationValidationOptions +} + +type APIKeyAuthValidationOptions struct { + MaxCredentialRefs int + MaxExtractFrom int + MaxExtractFromFieldLength int + MaxForwardClientIDHeaderLength int +} + +type CORSValidationOptions struct { + MaxFieldLength int +} + +type JWTProviderValidationOptions struct { + MaxClaimToHeaders int + MaxExtractorLength int +} + +type OIDCValidationOptions struct { + MaxScopes int + MaxResources int + MinRefreshTokenTTL time.Duration +} + +type AuthorizationValidationOptions struct { + MaxRules int + MaxClientCIDRs int +} + +func ValidateSecurityPolicy(securityPolicy *envoygatewayv1alpha1.SecurityPolicy, opts SecurityPolicyValidationOptions) field.ErrorList { + allErrs := field.ErrorList{} + + specPath := field.NewPath("spec") + + // nolint:staticcheck + if securityPolicy.Spec.TargetRef != nil { + allErrs = append(allErrs, field.Forbidden(specPath.Child("targetRef"), "deprecated field, use spec.targetRefs or spec.targetSelectors instead")) + } + + allErrs = append(allErrs, validateSecurityPolicyAPIKeyAuth(securityPolicy.Spec.APIKeyAuth, specPath.Child("apiKeyAuth"), opts)...) + allErrs = append(allErrs, validateSecurityPolicyCORS(securityPolicy.Spec.CORS, specPath.Child("cors"), opts)...) + allErrs = append(allErrs, validateSecurityPolicyBasicAuth(securityPolicy.Spec.BasicAuth, specPath.Child("basicAuth"))...) + allErrs = append(allErrs, validateSecurityPolicyJWT(securityPolicy.Spec.JWT, specPath.Child("jwt"), opts)...) + allErrs = append(allErrs, validateSecurityPolicyOIDC(securityPolicy.Spec.OIDC, specPath.Child("oidc"), opts)...) + + if securityPolicy.Spec.ExtAuth != nil { + allErrs = append(allErrs, field.Forbidden(specPath.Child("extAuth"), "extAuth is not permitted")) + } + + allErrs = append(allErrs, validateSecurityPolicyAuthorization(securityPolicy.Spec.Authorization, specPath.Child("authorization"), opts)...) + + return allErrs +} + +func validateSecurityPolicyAPIKeyAuth(apiKeyAuth *envoygatewayv1alpha1.APIKeyAuth, fldPath *field.Path, opts SecurityPolicyValidationOptions) field.ErrorList { + if apiKeyAuth == nil { + return nil + } + + allErrs := field.ErrorList{} + + allErrs = append(allErrs, validateSecurityPolicyCredentialRefs(apiKeyAuth.CredentialRefs, fldPath.Child("credentialRefs"), opts)...) + + if len(apiKeyAuth.ExtractFrom) > opts.APIKeyAuth.MaxExtractFrom { + allErrs = append(allErrs, field.TooMany(fldPath.Child("extractFrom"), len(apiKeyAuth.ExtractFrom), opts.APIKeyAuth.MaxExtractFrom)) + } + + for i := range apiKeyAuth.ExtractFrom { + extractFromPath := fldPath.Child("extractFrom").Index(i) + allErrs = append(allErrs, validateSecurityPolicyExtractFrom(apiKeyAuth.ExtractFrom[i], extractFromPath, opts)...) + } + + if apiKeyAuth.ForwardClientIDHeader != nil && len(*apiKeyAuth.ForwardClientIDHeader) > opts.APIKeyAuth.MaxForwardClientIDHeaderLength { + allErrs = append(allErrs, field.TooLong(fldPath.Child("forwardClientIDHeader"), len(*apiKeyAuth.ForwardClientIDHeader), opts.APIKeyAuth.MaxForwardClientIDHeaderLength)) + } + + return allErrs +} + +func validateSecurityPolicyCORS(cors *envoygatewayv1alpha1.CORS, fldPath *field.Path, opts SecurityPolicyValidationOptions) field.ErrorList { + if cors == nil { + return nil + } + + allErrs := field.ErrorList{} + + if len(cors.AllowOrigins) > opts.CORS.MaxFieldLength { + allErrs = append(allErrs, field.TooMany(fldPath.Child("allowOrigins"), len(cors.AllowOrigins), opts.CORS.MaxFieldLength)) + } + + if len(cors.AllowMethods) > opts.CORS.MaxFieldLength { + allErrs = append(allErrs, field.TooMany(fldPath.Child("allowMethods"), len(cors.AllowMethods), opts.CORS.MaxFieldLength)) + } + + if len(cors.AllowHeaders) > opts.CORS.MaxFieldLength { + allErrs = append(allErrs, field.TooMany(fldPath.Child("allowHeaders"), len(cors.AllowHeaders), opts.CORS.MaxFieldLength)) + } + + if len(cors.ExposeHeaders) > opts.CORS.MaxFieldLength { + allErrs = append(allErrs, field.TooMany(fldPath.Child("exposeHeaders"), len(cors.ExposeHeaders), opts.CORS.MaxFieldLength)) + } + + return allErrs +} + +func validateSecurityPolicyBasicAuth(basicAuth *envoygatewayv1alpha1.BasicAuth, fldPath *field.Path) field.ErrorList { + if basicAuth == nil { + return nil + } + + allErrs := field.ErrorList{} + + allErrs = append(allErrs, validateGatewaySecretObjectReference(&basicAuth.Users, fldPath.Child("users"))...) + + return allErrs +} + +func validateSecurityPolicyJWT(jwt *envoygatewayv1alpha1.JWT, fldPath *field.Path, opts SecurityPolicyValidationOptions) field.ErrorList { + if jwt == nil { + return nil + } + + allErrs := field.ErrorList{} + + // JWT structs have a variety of length limit checks already. + + for i, provider := range jwt.Providers { + providerPath := fldPath.Child("providers").Index(i) + allErrs = append(allErrs, validateGatewayJWTProvider(provider, providerPath, opts)...) + } + + return allErrs +} + +func validateSecurityPolicyOIDC(oidc *envoygatewayv1alpha1.OIDC, fldPath *field.Path, opts SecurityPolicyValidationOptions) field.ErrorList { + if oidc == nil { + return nil + } + + allErrs := field.ErrorList{} + allErrs = append(allErrs, validateSecurityPolicyOIDCProvider(oidc.Provider, fldPath.Child("provider"))...) + allErrs = append(allErrs, validateGatewaySecretObjectReference(oidc.ClientIDRef, fldPath.Child("clientIDRef"))...) + allErrs = append(allErrs, validateGatewaySecretObjectReference(&oidc.ClientSecret, fldPath.Child("clientSecret"))...) + + if len(oidc.Scopes) > opts.OIDC.MaxScopes { + allErrs = append(allErrs, field.TooMany(fldPath.Child("scopes"), len(oidc.Scopes), opts.OIDC.MaxScopes)) + } + + if len(oidc.Resources) > opts.OIDC.MaxResources { + allErrs = append(allErrs, field.TooMany(fldPath.Child("resources"), len(oidc.Resources), opts.OIDC.MaxResources)) + } + + if oidc.DefaultRefreshTokenTTL != nil { + allErrs = append(allErrs, validateGatewayDuration(fldPath.Child("defaultRefreshTokenTTL"), oidc.DefaultRefreshTokenTTL, &opts.OIDC.MinRefreshTokenTTL, nil)...) + } + + return allErrs +} + +func validateSecurityPolicyAuthorization(authorization *envoygatewayv1alpha1.Authorization, fldPath *field.Path, opts SecurityPolicyValidationOptions) field.ErrorList { + if authorization == nil { + return nil + } + + allErrs := field.ErrorList{} + + if len(authorization.Rules) > opts.Authorization.MaxRules { + allErrs = append(allErrs, field.TooMany(fldPath.Child("rules"), len(authorization.Rules), opts.Authorization.MaxRules)) + } + + for i, rule := range authorization.Rules { + rulePath := fldPath.Child("rules").Index(i) + + allErrs = append(allErrs, validateAuthorizationPrincipal(rule.Principal, rulePath.Child("principal"), opts)...) + + } + + return allErrs +} + +func validateAuthorizationPrincipal(principal envoygatewayv1alpha1.Principal, fldPath *field.Path, opts SecurityPolicyValidationOptions) field.ErrorList { + + allErrs := field.ErrorList{} + + if len(principal.ClientCIDRs) > opts.Authorization.MaxClientCIDRs { + allErrs = append(allErrs, field.TooMany(fldPath.Child("clientCIDRs"), len(principal.ClientCIDRs), opts.Authorization.MaxClientCIDRs)) + } + + return allErrs +} + +func validateSecurityPolicyOIDCProvider(provider envoygatewayv1alpha1.OIDCProvider, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + allErrs = append(allErrs, validateGatewayBackendCluster(provider.BackendCluster, fldPath)...) + + return allErrs +} + +func validateGatewayJWTProvider(provider envoygatewayv1alpha1.JWTProvider, fldPath *field.Path, opts SecurityPolicyValidationOptions) field.ErrorList { + allErrs := field.ErrorList{} + + allErrs = append(allErrs, validateRemoteJWKS(provider.RemoteJWKS, fldPath.Child("remoteJWKS"))...) + + if len(provider.ClaimToHeaders) > opts.JWTProvider.MaxClaimToHeaders { + allErrs = append(allErrs, field.TooMany(fldPath.Child("claimToHeaders"), len(provider.ClaimToHeaders), opts.JWTProvider.MaxClaimToHeaders)) + } + + if provider.ExtractFrom != nil { + if len(provider.ExtractFrom.Headers) > opts.JWTProvider.MaxExtractorLength { + allErrs = append(allErrs, field.TooMany(fldPath.Child("extractFrom").Child("headers"), len(provider.ExtractFrom.Headers), opts.JWTProvider.MaxExtractorLength)) + } + + if len(provider.ExtractFrom.Cookies) > opts.JWTProvider.MaxExtractorLength { + allErrs = append(allErrs, field.TooMany(fldPath.Child("extractFrom").Child("cookies"), len(provider.ExtractFrom.Cookies), opts.JWTProvider.MaxExtractorLength)) + } + + if len(provider.ExtractFrom.Params) > opts.JWTProvider.MaxExtractorLength { + allErrs = append(allErrs, field.TooMany(fldPath.Child("extractFrom").Child("params"), len(provider.ExtractFrom.Params), opts.JWTProvider.MaxExtractorLength)) + } + } + + return allErrs +} + +func validateRemoteJWKS(remoteJWKS *envoygatewayv1alpha1.RemoteJWKS, fldPath *field.Path) field.ErrorList { + if remoteJWKS == nil { + return nil + } + + allErrs := field.ErrorList{} + + allErrs = append(allErrs, validateGatewayBackendCluster(remoteJWKS.BackendCluster, fldPath)...) + + return allErrs +} + +func validateGatewayBackendCluster(backendCluster envoygatewayv1alpha1.BackendCluster, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + // nolint:staticcheck + if backendCluster.BackendRef != nil { + allErrs = append(allErrs, field.Forbidden(fldPath.Child("backendRef"), "backendRef is not permitted")) + } + + for i, backendRef := range backendCluster.BackendRefs { + allErrs = append(allErrs, validateBackendClusterBackendObjectReference(backendRef.BackendObjectReference, fldPath.Child("backendRefs").Index(i))...) + } + + if backendCluster.BackendSettings != nil { + allErrs = append(allErrs, validateGatewayClusterSettings(*backendCluster.BackendSettings, fldPath.Child("backendSettings"))...) + } + + return allErrs +} + +func validateBackendClusterBackendObjectReference(backendObjectRef gatewayv1.BackendObjectReference, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + if backendObjectRef.Namespace != nil { + allErrs = append(allErrs, field.Forbidden(fldPath.Child("namespace"), "must not be set")) + } + + return allErrs +} + +func validateSecurityPolicyExtractFrom(extractFrom *envoygatewayv1alpha1.ExtractFrom, fldPath *field.Path, opts SecurityPolicyValidationOptions) field.ErrorList { + if extractFrom == nil { + return nil + } + + allErrs := field.ErrorList{} + + if len(extractFrom.Headers) > opts.APIKeyAuth.MaxExtractFromFieldLength { + allErrs = append(allErrs, field.TooMany(fldPath.Child("headers"), len(extractFrom.Headers), opts.APIKeyAuth.MaxExtractFromFieldLength)) + } + + if len(extractFrom.Params) > opts.APIKeyAuth.MaxExtractFromFieldLength { + allErrs = append(allErrs, field.TooMany(fldPath.Child("params"), len(extractFrom.Params), opts.APIKeyAuth.MaxExtractFromFieldLength)) + } + + if len(extractFrom.Cookies) > opts.APIKeyAuth.MaxExtractFromFieldLength { + allErrs = append(allErrs, field.TooMany(fldPath.Child("cookies"), len(extractFrom.Cookies), opts.APIKeyAuth.MaxExtractFromFieldLength)) + } + + return allErrs +} + +func validateSecurityPolicyCredentialRefs(refs []gatewayv1.SecretObjectReference, fldPath *field.Path, opts SecurityPolicyValidationOptions) field.ErrorList { + allErrs := field.ErrorList{} + + if len(refs) > opts.APIKeyAuth.MaxCredentialRefs { + allErrs = append(allErrs, field.TooMany(fldPath, len(refs), opts.APIKeyAuth.MaxCredentialRefs)) + } + + for i, ref := range refs { + refPath := fldPath.Index(i) + allErrs = append(allErrs, validateGatewaySecretObjectReference(&ref, refPath)...) + } + + return allErrs +} + +func validateGatewaySecretObjectReference(secretRef *gatewayv1.SecretObjectReference, fldPath *field.Path) field.ErrorList { + if secretRef == nil { + return nil + } + + allErrs := field.ErrorList{} + + // Namespace must not be set + if secretRef.Namespace != nil { + allErrs = append(allErrs, field.Forbidden(fldPath.Child("namespace"), "must not be set")) + } + + return allErrs } diff --git a/internal/validation/securitypolicy_validation_test.go b/internal/validation/securitypolicy_validation_test.go new file mode 100644 index 00000000..a529880b --- /dev/null +++ b/internal/validation/securitypolicy_validation_test.go @@ -0,0 +1,382 @@ +package validation + +import ( + "testing" + "time" + + envoygatewayv1alpha1 "github.com/envoyproxy/gateway/api/v1alpha1" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/utils/ptr" + gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" + gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" +) + +func TestValidateSecurityPolicy(t *testing.T) { + scenarios := map[string]struct { + securityPolicy *envoygatewayv1alpha1.SecurityPolicy + opts SecurityPolicyValidationOptions + expectedErrors field.ErrorList + }{ + "spec.targetRef forbidden": { + securityPolicy: &envoygatewayv1alpha1.SecurityPolicy{ + Spec: envoygatewayv1alpha1.SecurityPolicySpec{ + PolicyTargetReferences: envoygatewayv1alpha1.PolicyTargetReferences{ + TargetRef: &gatewayv1alpha2.LocalPolicyTargetReferenceWithSectionName{ + SectionName: ptr.To(gatewayv1.SectionName("test")), + }, + }, + }, + }, + expectedErrors: field.ErrorList{ + field.Forbidden(field.NewPath("spec", "targetRef"), "Invalid"), + }, + }, + "apiKeyAuth.credentialRefs namespace set": { + securityPolicy: &envoygatewayv1alpha1.SecurityPolicy{ + Spec: envoygatewayv1alpha1.SecurityPolicySpec{ + APIKeyAuth: &envoygatewayv1alpha1.APIKeyAuth{ + CredentialRefs: []gatewayv1.SecretObjectReference{ + { + Name: "test-secret", + Namespace: ptr.To(gatewayv1.Namespace("other-namespace")), + }, + { + Name: "test-secret", + }, + }, + }, + }, + }, + opts: SecurityPolicyValidationOptions{ + APIKeyAuth: APIKeyAuthValidationOptions{ + MaxCredentialRefs: 1, + }, + }, + expectedErrors: field.ErrorList{ + field.TooMany(field.NewPath("spec", "apiKeyAuth", "credentialRefs"), 2, 1), + field.Forbidden(field.NewPath("spec", "apiKeyAuth", "credentialRefs").Index(0).Child("namespace"), "must not be set"), + }, + }, + "apiKeyAuth.extractFrom too many": { + securityPolicy: &envoygatewayv1alpha1.SecurityPolicy{ + Spec: envoygatewayv1alpha1.SecurityPolicySpec{ + APIKeyAuth: &envoygatewayv1alpha1.APIKeyAuth{ + ExtractFrom: []*envoygatewayv1alpha1.ExtractFrom{ + {}, + {}, + }, + }, + }, + }, + opts: SecurityPolicyValidationOptions{ + APIKeyAuth: APIKeyAuthValidationOptions{ + MaxExtractFrom: 1, + }, + }, + expectedErrors: field.ErrorList{ + field.TooMany(field.NewPath("spec", "apiKeyAuth", "extractFrom"), 2, 1), + }, + }, + "apiKeyAuth.extractFrom fields too long": { + securityPolicy: &envoygatewayv1alpha1.SecurityPolicy{ + Spec: envoygatewayv1alpha1.SecurityPolicySpec{ + APIKeyAuth: &envoygatewayv1alpha1.APIKeyAuth{ + ExtractFrom: []*envoygatewayv1alpha1.ExtractFrom{ + { + Headers: []string{"header1", "header2", "header3"}, + Params: []string{"param1", "param2", "param3"}, + Cookies: []string{"cookie1", "cookie2", "cookie3"}, + }, + }, + }, + }, + }, + opts: SecurityPolicyValidationOptions{ + APIKeyAuth: APIKeyAuthValidationOptions{ + MaxExtractFrom: 1, + MaxExtractFromFieldLength: 1, + }, + }, + expectedErrors: field.ErrorList{ + field.TooMany(field.NewPath("spec", "apiKeyAuth", "extractFrom").Index(0).Child("headers"), 0, 0), + field.TooMany(field.NewPath("spec", "apiKeyAuth", "extractFrom").Index(0).Child("params"), 0, 0), + field.TooMany(field.NewPath("spec", "apiKeyAuth", "extractFrom").Index(0).Child("cookies"), 0, 0), + }, + }, + "apiKeyAuth.forwardClientIDHeader too long": { + securityPolicy: &envoygatewayv1alpha1.SecurityPolicy{ + Spec: envoygatewayv1alpha1.SecurityPolicySpec{ + APIKeyAuth: &envoygatewayv1alpha1.APIKeyAuth{ + ForwardClientIDHeader: ptr.To("this-header-name-is-way-too-long"), + }, + }, + }, + opts: SecurityPolicyValidationOptions{ + APIKeyAuth: APIKeyAuthValidationOptions{ + MaxForwardClientIDHeaderLength: 1, + }, + }, + expectedErrors: field.ErrorList{ + field.TooLong(field.NewPath("spec", "apiKeyAuth", "forwardClientIDHeader"), 0, 0), + }, + }, + "CORS fields too long": { + securityPolicy: &envoygatewayv1alpha1.SecurityPolicy{ + Spec: envoygatewayv1alpha1.SecurityPolicySpec{ + CORS: &envoygatewayv1alpha1.CORS{ + AllowOrigins: []envoygatewayv1alpha1.Origin{"origin1", "origin2", "origin3"}, + AllowMethods: []string{"GET", "POST", "PUT", "DELETE"}, + AllowHeaders: []string{"header1", "header2", "header3"}, + ExposeHeaders: []string{"expose1", "expose2", "expose3"}, + }, + }, + }, + opts: SecurityPolicyValidationOptions{ + CORS: CORSValidationOptions{ + MaxFieldLength: 1, + }, + }, + expectedErrors: field.ErrorList{ + field.TooMany(field.NewPath("spec", "cors", "allowOrigins"), 3, 2), + field.TooMany(field.NewPath("spec", "cors", "allowMethods"), 4, 2), + field.TooMany(field.NewPath("spec", "cors", "allowHeaders"), 3, 2), + field.TooMany(field.NewPath("spec", "cors", "exposeHeaders"), 3, 2), + }, + }, + "basicAuth.users namespace set": { + securityPolicy: &envoygatewayv1alpha1.SecurityPolicy{ + Spec: envoygatewayv1alpha1.SecurityPolicySpec{ + BasicAuth: &envoygatewayv1alpha1.BasicAuth{ + Users: gatewayv1.SecretObjectReference{ + Name: "test-secret", + Namespace: ptr.To(gatewayv1.Namespace("other-namespace")), + }, + }, + }, + }, + expectedErrors: field.ErrorList{ + field.Forbidden(field.NewPath("spec", "basicAuth", "users", "namespace"), "must not be set"), + }, + }, + "invalid remote jwks": { + securityPolicy: &envoygatewayv1alpha1.SecurityPolicy{ + Spec: envoygatewayv1alpha1.SecurityPolicySpec{ + JWT: &envoygatewayv1alpha1.JWT{ + Providers: []envoygatewayv1alpha1.JWTProvider{ + { + Name: "test-provider", + RemoteJWKS: &envoygatewayv1alpha1.RemoteJWKS{ + BackendCluster: envoygatewayv1alpha1.BackendCluster{ + BackendRef: &gatewayv1.BackendObjectReference{}, + }, + }, + }, + { + Name: "invalid namespace", + RemoteJWKS: &envoygatewayv1alpha1.RemoteJWKS{ + BackendCluster: envoygatewayv1alpha1.BackendCluster{ + BackendRefs: []envoygatewayv1alpha1.BackendRef{ + { + BackendObjectReference: gatewayv1.BackendObjectReference{ + Name: "test-backend", + Namespace: ptr.To(gatewayv1.Namespace("other-namespace")), + }, + }, + }, + }, + }, + }, + { + // No comprehensive cluster setting validation here, as it's covered + // by other tests. We just want to ensure that code path is executed. + Name: "invalid cluster settings", + RemoteJWKS: &envoygatewayv1alpha1.RemoteJWKS{ + BackendCluster: envoygatewayv1alpha1.BackendCluster{ + BackendRefs: []envoygatewayv1alpha1.BackendRef{ + { + BackendObjectReference: gatewayv1.BackendObjectReference{ + Name: "test-backend", + }, + }, + }, + BackendSettings: &envoygatewayv1alpha1.ClusterSettings{ + Retry: &envoygatewayv1alpha1.Retry{}, + }, + }, + }, + }, + }, + }, + }, + }, + expectedErrors: field.ErrorList{ + field.Forbidden(field.NewPath("spec", "jwt", "providers").Index(0).Child("remoteJWKS", "backendRef"), ""), + field.Forbidden(field.NewPath("spec", "jwt", "providers").Index(1).Child("remoteJWKS", "backendRefs").Index(0).Child("namespace"), ""), + field.Forbidden(field.NewPath("spec", "jwt", "providers").Index(2).Child("remoteJWKS", "backendSettings", "retry"), ""), + }, + }, + "invalid JWT provider field lengths": { + securityPolicy: &envoygatewayv1alpha1.SecurityPolicy{ + Spec: envoygatewayv1alpha1.SecurityPolicySpec{ + JWT: &envoygatewayv1alpha1.JWT{ + Providers: []envoygatewayv1alpha1.JWTProvider{ + { + Name: "test", + ClaimToHeaders: []envoygatewayv1alpha1.ClaimToHeader{ + { + Claim: "claim1", + Header: "header1", + }, + { + Claim: "claim2", + Header: "header2", + }, + }, + ExtractFrom: &envoygatewayv1alpha1.JWTExtractor{ + Headers: []envoygatewayv1alpha1.JWTHeaderExtractor{ + {}, {}, {}, + }, + Cookies: []string{"cookie1", "cookie2", "cookie3"}, + Params: []string{"param1", "param2", "param3"}, + }, + }, + }, + }, + }, + }, + opts: SecurityPolicyValidationOptions{ + JWTProvider: JWTProviderValidationOptions{ + MaxClaimToHeaders: 1, + MaxExtractorLength: 2, + }, + }, + expectedErrors: field.ErrorList{ + field.TooMany(field.NewPath("spec", "jwt", "providers").Index(0).Child("claimToHeaders"), 2, 1), + field.TooMany(field.NewPath("spec", "jwt", "providers").Index(0).Child("extractFrom").Child("headers"), 3, 2), + field.TooMany(field.NewPath("spec", "jwt", "providers").Index(0).Child("extractFrom").Child("cookies"), 3, 2), + field.TooMany(field.NewPath("spec", "jwt", "providers").Index(0).Child("extractFrom").Child("params"), 3, 2), + }, + }, + "invalid OIDC provider backend cluster settings": { + securityPolicy: &envoygatewayv1alpha1.SecurityPolicy{ + Spec: envoygatewayv1alpha1.SecurityPolicySpec{ + OIDC: &envoygatewayv1alpha1.OIDC{ + Provider: envoygatewayv1alpha1.OIDCProvider{ + // No comprehensive cluster setting validation here, as it's covered + // by other tests. We just want to ensure that code path is executed. + BackendCluster: envoygatewayv1alpha1.BackendCluster{ + BackendRef: &gatewayv1.BackendObjectReference{}, + }, + }, + }, + }, + }, + expectedErrors: field.ErrorList{ + field.Forbidden(field.NewPath("spec", "oidc", "provider").Child("backendRef"), ""), + }, + }, + "invalid OIDC provider secret refs": { + securityPolicy: &envoygatewayv1alpha1.SecurityPolicy{ + Spec: envoygatewayv1alpha1.SecurityPolicySpec{ + OIDC: &envoygatewayv1alpha1.OIDC{ + ClientIDRef: &gatewayv1.SecretObjectReference{ + Name: "test-secret", + Namespace: ptr.To(gatewayv1.Namespace("other-namespace")), + }, + ClientSecret: gatewayv1.SecretObjectReference{ + Name: "test-secret", + Namespace: ptr.To(gatewayv1.Namespace("other-namespace")), + }, + }, + }, + }, + expectedErrors: field.ErrorList{ + field.Forbidden(field.NewPath("spec", "oidc", "clientIDRef", "namespace"), "must not be set"), + field.Forbidden(field.NewPath("spec", "oidc", "clientSecret", "namespace"), "must not be set"), + }, + }, + "invalid oidc scopes, resources, and refresh token ttl": { + securityPolicy: &envoygatewayv1alpha1.SecurityPolicy{ + Spec: envoygatewayv1alpha1.SecurityPolicySpec{ + OIDC: &envoygatewayv1alpha1.OIDC{ + Scopes: []string{"scope1", "scope2", "scope3"}, + Resources: []string{"resource1", "resource2", "resource3"}, + DefaultRefreshTokenTTL: ptr.To(gatewayv1.Duration("1s")), + }, + }, + }, + opts: SecurityPolicyValidationOptions{ + OIDC: OIDCValidationOptions{ + MaxScopes: 2, + MaxResources: 2, + MinRefreshTokenTTL: 1 * time.Minute, + }, + }, + expectedErrors: field.ErrorList{ + field.TooMany(field.NewPath("spec", "oidc", "scopes"), 0, 0), + field.TooMany(field.NewPath("spec", "oidc", "resources"), 0, 0), + field.Invalid(field.NewPath("spec", "oidc", "defaultRefreshTokenTTL"), 0, ""), + }, + }, + "too many authorization rules": { + securityPolicy: &envoygatewayv1alpha1.SecurityPolicy{ + Spec: envoygatewayv1alpha1.SecurityPolicySpec{ + Authorization: &envoygatewayv1alpha1.Authorization{ + Rules: []envoygatewayv1alpha1.AuthorizationRule{ + {}, + {}, + }, + }, + }, + }, + opts: SecurityPolicyValidationOptions{ + Authorization: AuthorizationValidationOptions{ + MaxRules: 1, + }, + }, + expectedErrors: field.ErrorList{ + field.TooMany(field.NewPath("spec", "authorization", "rules"), 2, 1), + }, + }, + "authorization rule with too many CIDRs": { + securityPolicy: &envoygatewayv1alpha1.SecurityPolicy{ + Spec: envoygatewayv1alpha1.SecurityPolicySpec{ + Authorization: &envoygatewayv1alpha1.Authorization{ + Rules: []envoygatewayv1alpha1.AuthorizationRule{ + { + Principal: envoygatewayv1alpha1.Principal{ + ClientCIDRs: []envoygatewayv1alpha1.CIDR{ + "1.1.1.1/32", + "1.1.1.2/32", + "1.1.1.3/32", + }, + }, + }, + }, + }, + }, + }, + opts: SecurityPolicyValidationOptions{ + Authorization: AuthorizationValidationOptions{ + MaxRules: 1, + MaxClientCIDRs: 2, + }, + }, + expectedErrors: field.ErrorList{ + field.TooMany(field.NewPath("spec", "authorization", "rules").Index(0).Child("principal", "clientCIDRs"), 3, 2), + }, + }, + } + + for name, scenario := range scenarios { + t.Run(name, func(t *testing.T) { + errs := ValidateSecurityPolicy(scenario.securityPolicy, scenario.opts) + delta := cmp.Diff(scenario.expectedErrors, errs, cmpopts.IgnoreFields(field.Error{}, "BadValue", "Detail")) + if delta != "" { + t.Errorf("Testcase %s - expected errors '%v', got '%v', diff: '%v'", name, scenario.expectedErrors, errs, delta) + } + }) + } +} diff --git a/internal/webhook/v1alpha1/httproutefilter_webhook.go b/internal/webhook/v1alpha1/httproutefilter_webhook.go index 18f65228..ac0faba0 100644 --- a/internal/webhook/v1alpha1/httproutefilter_webhook.go +++ b/internal/webhook/v1alpha1/httproutefilter_webhook.go @@ -21,15 +21,19 @@ import ( // SetupHTTPRouteFilterWebhookWithManager registers the webhook for HTTPRouteFilter in the manager. func SetupHTTPRouteFilterWebhookWithManager(mgr mcmanager.Manager) error { + validationOpts := validation.HTTPRouteFilterValidationOptions{ + MaxInlineBodySize: 1024, + } return ctrl.NewWebhookManagedBy(mgr.GetLocalManager()).For(&gatewayv1alpha1.HTTPRouteFilter{}). - WithValidator(&HTTPRouteFilterCustomValidator{mgr: mgr}). + WithValidator(&HTTPRouteFilterCustomValidator{mgr: mgr, validationOpts: validationOpts}). Complete() } // +kubebuilder:webhook:path=/validate-gateway-envoyproxy-io-v1alpha1-httproutefilter,mutating=false,failurePolicy=fail,sideEffects=None,groups=gateway.envoyproxy.io,resources=httproutefilters,verbs=create;update,versions=v1alpha1,name=vhttproutefilter-v1alpha1.kb.io,admissionReviewVersions=v1 type HTTPRouteFilterCustomValidator struct { - mgr mcmanager.Manager + mgr mcmanager.Manager + validationOpts validation.HTTPRouteFilterValidationOptions } var _ webhook.CustomValidator = &HTTPRouteFilterCustomValidator{} @@ -49,7 +53,7 @@ func (v *HTTPRouteFilterCustomValidator) ValidateCreate(ctx context.Context, obj log := logf.FromContext(ctx).WithValues("cluster", clusterName) log.Info("Validating HTTPRouteFilter", "name", httpRouteFilter.GetName(), "cluster", clusterName) - if errs := validation.ValidateHTTPRouteFilter(httpRouteFilter); len(errs) > 0 { + if errs := validation.ValidateHTTPRouteFilter(httpRouteFilter, v.validationOpts); len(errs) > 0 { return nil, apierrors.NewInvalid(obj.GetObjectKind().GroupVersionKind().GroupKind(), httpRouteFilter.GetName(), errs) } @@ -71,7 +75,7 @@ func (v *HTTPRouteFilterCustomValidator) ValidateUpdate(ctx context.Context, old log := logf.FromContext(ctx).WithValues("cluster", clusterName) log.Info("Validating HTTPRouteFilter", "name", httpRouteFilter.GetName(), "cluster", clusterName) - if errs := validation.ValidateHTTPRouteFilter(httpRouteFilter); len(errs) > 0 { + if errs := validation.ValidateHTTPRouteFilter(httpRouteFilter, v.validationOpts); len(errs) > 0 { return nil, apierrors.NewInvalid(oldObj.GetObjectKind().GroupVersionKind().GroupKind(), httpRouteFilter.GetName(), errs) } diff --git a/internal/webhook/v1alpha1/securitypolicy_webhook.go b/internal/webhook/v1alpha1/securitypolicy_webhook.go index e96c942f..f9712391 100644 --- a/internal/webhook/v1alpha1/securitypolicy_webhook.go +++ b/internal/webhook/v1alpha1/securitypolicy_webhook.go @@ -21,15 +21,23 @@ import ( // SetupSecurityPolicyWebhookWithManager registers the webhook for SecurityPolicy in the manager. func SetupSecurityPolicyWebhookWithManager(mgr mcmanager.Manager) error { + validationOpts := validation.SecurityPolicyValidationOptions{ + APIKeyAuth: validation.APIKeyAuthValidationOptions{ + MaxCredentialRefs: 5, + MaxExtractFrom: 5, + MaxExtractFromFieldLength: 5, + }, + } return ctrl.NewWebhookManagedBy(mgr.GetLocalManager()).For(&gatewayv1alpha1.SecurityPolicy{}). - WithValidator(&SecurityPolicyCustomValidator{mgr: mgr}). + WithValidator(&SecurityPolicyCustomValidator{mgr: mgr, validationOpts: validationOpts}). Complete() } // +kubebuilder:webhook:path=/validate-gateway-envoyproxy-io-v1alpha1-securitypolicy,mutating=false,failurePolicy=fail,sideEffects=None,groups=gateway.envoyproxy.io,resources=securitypolicies,verbs=create;update,versions=v1alpha1,name=vsecuritypolicy-v1alpha1.kb.io,admissionReviewVersions=v1 type SecurityPolicyCustomValidator struct { - mgr mcmanager.Manager + mgr mcmanager.Manager + validationOpts validation.SecurityPolicyValidationOptions } var _ webhook.CustomValidator = &SecurityPolicyCustomValidator{} @@ -49,7 +57,7 @@ func (v *SecurityPolicyCustomValidator) ValidateCreate(ctx context.Context, obj log := logf.FromContext(ctx).WithValues("cluster", clusterName) log.Info("Validating SecurityPolicy", "name", securityPolicy.GetName(), "cluster", clusterName) - if errs := validation.ValidateSecurityPolicy(securityPolicy); len(errs) > 0 { + if errs := validation.ValidateSecurityPolicy(securityPolicy, v.validationOpts); len(errs) > 0 { return nil, apierrors.NewInvalid(obj.GetObjectKind().GroupVersionKind().GroupKind(), securityPolicy.GetName(), errs) } @@ -71,7 +79,7 @@ func (v *SecurityPolicyCustomValidator) ValidateUpdate(ctx context.Context, oldO log := logf.FromContext(ctx).WithValues("cluster", clusterName) log.Info("Validating SecurityPolicy", "name", securityPolicy.GetName(), "cluster", clusterName) - if errs := validation.ValidateSecurityPolicy(securityPolicy); len(errs) > 0 { + if errs := validation.ValidateSecurityPolicy(securityPolicy, v.validationOpts); len(errs) > 0 { return nil, apierrors.NewInvalid(oldObj.GetObjectKind().GroupVersionKind().GroupKind(), securityPolicy.GetName(), errs) } From bc4c29d8c57bb7d0a631d28101c68178b445bce8 Mon Sep 17 00:00:00 2001 From: Joshua Reese Date: Tue, 23 Sep 2025 15:52:15 -0500 Subject: [PATCH 2/3] Wire Envoy Gateway Extension API validation settings into service configuration, setting sane defaults. --- cmd/main.go | 6 +- internal/config/config.go | 252 ++++++++++++++++++ internal/config/zz_generated.deepcopy.go | 74 +++++ internal/config/zz_generated.defaults.go | 39 +++ .../backendtrafficpolicy_validation.go | 68 +++-- .../backendtrafficpolicy_validation_test.go | 46 +++- .../validation/httproutefilter_validation.go | 9 +- .../httproutefilter_validation_test.go | 6 +- .../validation/securitypolicy_validation.go | 75 ++---- .../securitypolicy_validation_test.go | 40 +-- .../v1alpha1/backendtrafficpolicy_webhook.go | 12 +- .../v1alpha1/httproutefilter_webhook.go | 10 +- .../v1alpha1/securitypolicy_webhook.go | 14 +- 13 files changed, 508 insertions(+), 143 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 342f4ac9..d6be5eef 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -265,17 +265,17 @@ func main() { os.Exit(1) } - if err = webhookgatewayv1alpha1.SetupBackendTrafficPolicyWebhookWithManager(mgr); err != nil { + if err = webhookgatewayv1alpha1.SetupBackendTrafficPolicyWebhookWithManager(mgr, serverConfig); err != nil { setupLog.Error(err, "unable to create webhook", "webhook", "BackendTrafficPolicy") os.Exit(1) } - if err = webhookgatewayv1alpha1.SetupSecurityPolicyWebhookWithManager(mgr); err != nil { + if err = webhookgatewayv1alpha1.SetupSecurityPolicyWebhookWithManager(mgr, serverConfig); err != nil { setupLog.Error(err, "unable to create webhook", "webhook", "SecurityPolicy") os.Exit(1) } - if err = webhookgatewayv1alpha1.SetupHTTPRouteFilterWebhookWithManager(mgr); err != nil { + if err = webhookgatewayv1alpha1.SetupHTTPRouteFilterWebhookWithManager(mgr, serverConfig); err != nil { setupLog.Error(err, "unable to create webhook", "webhook", "HTTPRouteFilter") os.Exit(1) } diff --git a/internal/config/config.go b/internal/config/config.go index 67a66158..823fb3b0 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -10,6 +10,7 @@ import ( "time" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/rest" @@ -357,6 +358,10 @@ type GatewayConfig struct { // // +default={"80": ["HTTP"], "443": ["HTTPS"]} ValidProtocolTypes map[int][]gatewayv1.ProtocolType `json:"validProtocolTypes,omitempty"` + + // ExtensionAPIValidationOptions provides configuration for validation of + // extension APIs used by the Gateway. + ExtensionAPIValidationOptions ExtensionAPIValidationOptions `json:"extensionAPIValidationOptions,omitempty"` } func (c *GatewayConfig) GatewayDNSAddress(gateway *gatewayv1.Gateway) string { @@ -365,6 +370,253 @@ func (c *GatewayConfig) GatewayDNSAddress(gateway *gatewayv1.Gateway) string { // +k8s:deepcopy-gen=true +type ExtensionAPIValidationOptions struct { + // BackendTrafficPolicies specifies validation options for BackendTrafficPolicy resources. + BackendTrafficPolicies BackendTrafficPolicyValidationOptions `json:"backendTrafficPolicies"` + + // HTTPRouteFilters specifies validation options for HTTPRouteFilter resources. + HTTPRouteFilters HTTPRouteFilterValidationOptions `json:"httpRouteFilters"` + + // SecurityPolicies specifies validation options for SecurityPolicy resources. + SecurityPolicies SecurityPolicyValidationOptions `json:"securityPolicies"` +} + +// +k8s:deepcopy-gen=true + +type BackendTrafficPolicyValidationOptions struct { + ClusterSettings ClusterSettingsValidationOptions +} + +// +k8s:deepcopy-gen=true + +type ClusterSettingsValidationOptions struct { + // Minimum amount for the total number of unacknowledged probes to send before + // deciding the connection is dead. + // + // Default: 9 + TCPKeepaliveMinProbes uint32 + + // Minimum amount for the duration a connection needs to be idle before + // keep-alive probes start being sent. + // + // Default: 5m + TCPKeepaliveMinIdleTime time.Duration + + // Minimum amount for the duration between keep-alive probes. + // + // Default: 30s + TCPKeepaliveMinInterval time.Duration + + // Maximum time allowed for a connection timeout + // + // Default: 10s + TCPMaxConnectionTimeout time.Duration + + // Maximum amount for the duration a connection can be idle. + // + // Default: 1h + HTTPMaxConnectionIdleTimeout time.Duration + + // Maximum amount for the duration of a connection. + // + // Default: 1h + HTTPMaxConnectionDuration time.Duration + + // Maximum amount for the duration until an entire request is received by the + // upstream. + // + // Default: 1h + HTTPMaxRequestTimeout time.Duration + + // Maximum size for upstream connection buffers + // + // Default: 512Ki + ConnectionMaxBufferLimit resource.Quantity + + // Minimum amount for the duration between DNS refreshes. + // + // Default: 30s + DNSMinRefreshRate time.Duration + + // Maximum size for the initial stream window size for HTTP/2 connections. + // + // Default: 64Ki + HTTP2MaxInitialStreamWindowSize resource.Quantity + + // Maximum size for the initial connection window size for HTTP/2 connections. + // + // Default: 1Mi + HTTP2MaxInitialConnectionWindowSize resource.Quantity + + // Maximum number of concurrent streams for HTTP/2 connections. + // + // Default: 1024 + HTTP2MaxConcurrentStreams uint32 +} + +func SetDefaults_ClusterSettingsValidationOptions(obj *ClusterSettingsValidationOptions) { + if obj.TCPKeepaliveMinProbes == 0 { + obj.TCPKeepaliveMinProbes = 9 + } + + if obj.TCPKeepaliveMinIdleTime == 0 { + obj.TCPKeepaliveMinIdleTime = 5 * time.Minute + } + + if obj.TCPKeepaliveMinInterval == 0 { + obj.TCPKeepaliveMinInterval = 30 * time.Second + } + + if obj.TCPMaxConnectionTimeout == 0 { + obj.TCPMaxConnectionTimeout = 10 * time.Second + } + + if obj.HTTPMaxConnectionIdleTimeout == 0 { + obj.HTTPMaxConnectionIdleTimeout = 1 * time.Hour + } + + if obj.HTTPMaxConnectionDuration == 0 { + obj.HTTPMaxConnectionDuration = 1 * time.Hour + } + + if obj.HTTPMaxRequestTimeout == 0 { + obj.HTTPMaxRequestTimeout = 1 * time.Hour + } + + if obj.ConnectionMaxBufferLimit.IsZero() { + obj.ConnectionMaxBufferLimit = resource.MustParse("512Ki") + } + + if obj.DNSMinRefreshRate == 0 { + obj.DNSMinRefreshRate = 30 * time.Second + } + + if obj.HTTP2MaxInitialStreamWindowSize.IsZero() { + obj.HTTP2MaxInitialStreamWindowSize = resource.MustParse("64Ki") + } + + if obj.HTTP2MaxInitialConnectionWindowSize.IsZero() { + obj.HTTP2MaxInitialConnectionWindowSize = resource.MustParse("1Mi") + } + + if obj.HTTP2MaxConcurrentStreams == 0 { + obj.HTTP2MaxConcurrentStreams = 1024 + } +} + +type HTTPRouteFilterValidationOptions struct { + // MaxInlineBodySize is the maximum allowed size for an inline body in a + // direct response filter. + // + // +default=1024 + MaxInlineBodySize int +} + +// +k8s:deepcopy-gen=true + +type SecurityPolicyValidationOptions struct { + // APIKeyAuth specifies validation options for API key authentication + APIKeyAuth APIKeyAuthValidationOptions + + // CORS specifies validation options for CORS + CORS CORSValidationOptions + + // JWTProvider specifies validation options for JWT providers + JWTProvider JWTProviderValidationOptions + + // OIDC specifies validation options for OIDC + OIDC OIDCValidationOptions + + // Authorization specifies validation options for authorization + Authorization AuthorizationValidationOptions + + // ClusterSettings specifies validation options for cluster settings used + // within security policies. + ClusterSettings ClusterSettingsValidationOptions +} + +type APIKeyAuthValidationOptions struct { + // MaxCredentialRefs is the maximum number of credential references per + // SecurityPolicy. + // + // +default=5 + MaxCredentialRefs int + + // MaxExtractFrom is the maximum number of extractFrom entries per SecurityPolicy + // + // +default=5 + MaxExtractFrom int + + // MaxExtractFromFieldLength is the maximum length of each field in an + // extractFrom entry. + // + // +default=10 + MaxExtractFromFieldLength int + + // MaxForwardClientIDHeaderLength is the maximum length for the name of the + // header to use when forwarding the client identity to the upstream service. + // + // +default=256 + MaxForwardClientIDHeaderLength int +} + +type CORSValidationOptions struct { + // MaxFieldLength is the maximum length for each field in a CORS policy. + // + // +default=10 + MaxFieldLength int +} + +type JWTProviderValidationOptions struct { + // MaxClaimToHeaders is the maximum number of claim to header mappings per + // JWT provider. + // + // +default=5 + MaxClaimToHeaders int + + // MaxExtractorLength is the maximum length of each extractor field. + // + // +default=5 + MaxExtractorLength int +} + +type OIDCValidationOptions struct { + // MaxScopes is the maximum number of scopes per OIDC configuration. + // + // +default=5 + MaxScopes int + + // MaxResources is the maximum number of resources per OIDC configuration. + // + // +default=5 + MaxResources int + + // MinRefreshTokenTTL is the minimum allowed TTL for refresh tokens. + // + // Default: 1m + MinRefreshTokenTTL time.Duration +} + +func SetDefaults_OIDCValidationOptions(obj *OIDCValidationOptions) { + if obj.MinRefreshTokenTTL == 0 { + obj.MinRefreshTokenTTL = 1 * time.Minute + } +} + +type AuthorizationValidationOptions struct { + // MaxRules is the maximum number of authorization rules per SecurityPolicy. + // + // +default=20 + MaxRules int + + // MaxClientCIDRs is the maximum number of client CIDRs per authorization rule. + // + // +default=5 + MaxClientCIDRs int +} + +// +k8s:deepcopy-gen=true + type GatewayResourceReplicatorConfig struct { // Resources lists the upstream resource types that should be mirrored into // the downstream control plane. diff --git a/internal/config/zz_generated.deepcopy.go b/internal/config/zz_generated.deepcopy.go index d8da19bf..f06515e8 100644 --- a/internal/config/zz_generated.deepcopy.go +++ b/internal/config/zz_generated.deepcopy.go @@ -14,6 +14,40 @@ import ( apisv1 "sigs.k8s.io/gateway-api/apis/v1" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BackendTrafficPolicyValidationOptions) DeepCopyInto(out *BackendTrafficPolicyValidationOptions) { + *out = *in + in.ClusterSettings.DeepCopyInto(&out.ClusterSettings) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackendTrafficPolicyValidationOptions. +func (in *BackendTrafficPolicyValidationOptions) DeepCopy() *BackendTrafficPolicyValidationOptions { + if in == nil { + return nil + } + out := new(BackendTrafficPolicyValidationOptions) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterSettingsValidationOptions) DeepCopyInto(out *ClusterSettingsValidationOptions) { + *out = *in + out.ConnectionMaxBufferLimit = in.ConnectionMaxBufferLimit.DeepCopy() + out.HTTP2MaxInitialStreamWindowSize = in.HTTP2MaxInitialStreamWindowSize.DeepCopy() + out.HTTP2MaxInitialConnectionWindowSize = in.HTTP2MaxInitialConnectionWindowSize.DeepCopy() +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterSettingsValidationOptions. +func (in *ClusterSettingsValidationOptions) DeepCopy() *ClusterSettingsValidationOptions { + if in == nil { + return nil + } + out := new(ClusterSettingsValidationOptions) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CustomHostnameAllowListEntry) DeepCopyInto(out *CustomHostnameAllowListEntry) { *out = *in @@ -86,6 +120,24 @@ func (in *DownstreamResourceManagementConfig) DeepCopy() *DownstreamResourceMana return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExtensionAPIValidationOptions) DeepCopyInto(out *ExtensionAPIValidationOptions) { + *out = *in + in.BackendTrafficPolicies.DeepCopyInto(&out.BackendTrafficPolicies) + out.HTTPRouteFilters = in.HTTPRouteFilters + in.SecurityPolicies.DeepCopyInto(&out.SecurityPolicies) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExtensionAPIValidationOptions. +func (in *ExtensionAPIValidationOptions) DeepCopy() *ExtensionAPIValidationOptions { + if in == nil { + return nil + } + out := new(ExtensionAPIValidationOptions) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GatewayConfig) DeepCopyInto(out *GatewayConfig) { *out = *in @@ -145,6 +197,7 @@ func (in *GatewayConfig) DeepCopyInto(out *GatewayConfig) { (*out)[key] = outVal } } + in.ExtensionAPIValidationOptions.DeepCopyInto(&out.ExtensionAPIValidationOptions) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GatewayConfig. @@ -288,6 +341,27 @@ func (in *RetryInterval) DeepCopy() *RetryInterval { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecurityPolicyValidationOptions) DeepCopyInto(out *SecurityPolicyValidationOptions) { + *out = *in + out.APIKeyAuth = in.APIKeyAuth + out.CORS = in.CORS + out.JWTProvider = in.JWTProvider + out.OIDC = in.OIDC + out.Authorization = in.Authorization + in.ClusterSettings.DeepCopyInto(&out.ClusterSettings) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecurityPolicyValidationOptions. +func (in *SecurityPolicyValidationOptions) DeepCopy() *SecurityPolicyValidationOptions { + if in == nil { + return nil + } + out := new(SecurityPolicyValidationOptions) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TLSConfig) DeepCopyInto(out *TLSConfig) { *out = *in diff --git a/internal/config/zz_generated.defaults.go b/internal/config/zz_generated.defaults.go index cab9c3f5..9948ff56 100644 --- a/internal/config/zz_generated.defaults.go +++ b/internal/config/zz_generated.defaults.go @@ -42,6 +42,45 @@ func SetObjectDefaults_NetworkServicesOperator(in *NetworkServicesOperator) { panic(err) } } + SetDefaults_ClusterSettingsValidationOptions(&in.Gateway.ExtensionAPIValidationOptions.BackendTrafficPolicies.ClusterSettings) + if in.Gateway.ExtensionAPIValidationOptions.HTTPRouteFilters.MaxInlineBodySize == 0 { + in.Gateway.ExtensionAPIValidationOptions.HTTPRouteFilters.MaxInlineBodySize = 1024 + } + if in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.APIKeyAuth.MaxCredentialRefs == 0 { + in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.APIKeyAuth.MaxCredentialRefs = 5 + } + if in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.APIKeyAuth.MaxExtractFrom == 0 { + in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.APIKeyAuth.MaxExtractFrom = 5 + } + if in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.APIKeyAuth.MaxExtractFromFieldLength == 0 { + in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.APIKeyAuth.MaxExtractFromFieldLength = 10 + } + if in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.APIKeyAuth.MaxForwardClientIDHeaderLength == 0 { + in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.APIKeyAuth.MaxForwardClientIDHeaderLength = 256 + } + if in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.CORS.MaxFieldLength == 0 { + in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.CORS.MaxFieldLength = 10 + } + if in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.JWTProvider.MaxClaimToHeaders == 0 { + in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.JWTProvider.MaxClaimToHeaders = 5 + } + if in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.JWTProvider.MaxExtractorLength == 0 { + in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.JWTProvider.MaxExtractorLength = 5 + } + SetDefaults_OIDCValidationOptions(&in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.OIDC) + if in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.OIDC.MaxScopes == 0 { + in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.OIDC.MaxScopes = 5 + } + if in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.OIDC.MaxResources == 0 { + in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.OIDC.MaxResources = 5 + } + if in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.Authorization.MaxRules == 0 { + in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.Authorization.MaxRules = 20 + } + if in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.Authorization.MaxClientCIDRs == 0 { + in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.Authorization.MaxClientCIDRs = 5 + } + SetDefaults_ClusterSettingsValidationOptions(&in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.ClusterSettings) SetDefaults_GatewayResourceReplicatorConfig(&in.GatewayResourceReplicator) if in.HTTPProxy.GatewayClassName == "" { in.HTTPProxy.GatewayClassName = "datum-external-global-proxy" diff --git a/internal/validation/backendtrafficpolicy_validation.go b/internal/validation/backendtrafficpolicy_validation.go index 02e375d8..044cdcf4 100644 --- a/internal/validation/backendtrafficpolicy_validation.go +++ b/internal/validation/backendtrafficpolicy_validation.go @@ -6,14 +6,14 @@ import ( "time" envoygatewayv1alpha1 "github.com/envoyproxy/gateway/api/v1alpha1" - "k8s.io/apimachinery/pkg/api/resource" "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/utils/ptr" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" + + "go.datum.net/network-services-operator/internal/config" ) -// TODO(jreese) move limits to config -func ValidateBackendTrafficPolicy(backendTrafficPolicy *envoygatewayv1alpha1.BackendTrafficPolicy) field.ErrorList { +func ValidateBackendTrafficPolicy(backendTrafficPolicy *envoygatewayv1alpha1.BackendTrafficPolicy, opts config.BackendTrafficPolicyValidationOptions) field.ErrorList { allErrs := field.ErrorList{} specPath := field.NewPath("spec") @@ -23,14 +23,14 @@ func ValidateBackendTrafficPolicy(backendTrafficPolicy *envoygatewayv1alpha1.Bac allErrs = append(allErrs, field.Forbidden(specPath.Child("targetRef"), "deprecated field, use spec.targetRefs or spec.targetSelectors instead")) } - allErrs = append(allErrs, validateGatewayClusterSettings(backendTrafficPolicy.Spec.ClusterSettings, specPath)...) + allErrs = append(allErrs, validateGatewayClusterSettings(backendTrafficPolicy.Spec.ClusterSettings, specPath, opts.ClusterSettings)...) allErrs = append(allErrs, validateBackendTrafficPolicyRateLimit(backendTrafficPolicy.Spec.RateLimit, specPath.Child("rateLimit"))...) allErrs = append(allErrs, validateBackendTrafficPolicyFaultInjection(backendTrafficPolicy.Spec.FaultInjection, specPath.Child("faultInjection"))...) return allErrs } -func validateGatewayClusterSettings(clusterSettings envoygatewayv1alpha1.ClusterSettings, fldPath *field.Path) field.ErrorList { +func validateGatewayClusterSettings(clusterSettings envoygatewayv1alpha1.ClusterSettings, fldPath *field.Path, opts config.ClusterSettingsValidationOptions) field.ErrorList { allErrs := field.ErrorList{} allErrs = append(allErrs, validateGatewayBackendLoadBalancer(clusterSettings.LoadBalancer, fldPath.Child("loadBalancer"))...) @@ -39,12 +39,12 @@ func validateGatewayClusterSettings(clusterSettings envoygatewayv1alpha1.Cluster allErrs = append(allErrs, field.Forbidden(fldPath.Child("retry"), "retry settings are not permitted")) } - allErrs = append(allErrs, validateGatewayClusterSettingsTCPKeepalive(clusterSettings.TCPKeepalive, fldPath.Child("tcpKeepalive"))...) + allErrs = append(allErrs, validateGatewayClusterSettingsTCPKeepalive(clusterSettings.TCPKeepalive, fldPath.Child("tcpKeepalive"), opts)...) allErrs = append(allErrs, validateGatewayClusterSettingsHealthCheck(clusterSettings.HealthCheck, fldPath.Child("healthCheck"))...) - allErrs = append(allErrs, validateGatewayClusterSettingsTimeout(clusterSettings.Timeout, fldPath.Child("timeout"))...) - allErrs = append(allErrs, validateGatewayClusterSettingsConnection(clusterSettings.Connection, fldPath.Child("connection"))...) - allErrs = append(allErrs, validateGatewayClusterSettingsDNS(clusterSettings.DNS, fldPath.Child("dns"))...) - allErrs = append(allErrs, validateGatewayClusterSettingsHTTP2(clusterSettings.HTTP2, fldPath.Child("http2"))...) + allErrs = append(allErrs, validateGatewayClusterSettingsTimeout(clusterSettings.Timeout, fldPath.Child("timeout"), opts)...) + allErrs = append(allErrs, validateGatewayClusterSettingsConnection(clusterSettings.Connection, fldPath.Child("connection"), opts)...) + allErrs = append(allErrs, validateGatewayClusterSettingsDNS(clusterSettings.DNS, fldPath.Child("dns"), opts)...) + allErrs = append(allErrs, validateGatewayClusterSettingsHTTP2(clusterSettings.HTTP2, fldPath.Child("http2"), opts)...) return allErrs } @@ -63,25 +63,25 @@ func validateGatewayBackendLoadBalancer(loadBalancer *envoygatewayv1alpha1.LoadB return allErrs } -func validateGatewayClusterSettingsTCPKeepalive(tcpKeepalive *envoygatewayv1alpha1.TCPKeepalive, fldPath *field.Path) field.ErrorList { +func validateGatewayClusterSettingsTCPKeepalive(tcpKeepalive *envoygatewayv1alpha1.TCPKeepalive, fldPath *field.Path, opts config.ClusterSettingsValidationOptions) field.ErrorList { if tcpKeepalive == nil { return nil } allErrs := field.ErrorList{} - if tcpKeepalive.Probes != nil && *tcpKeepalive.Probes < 9 { - allErrs = append(allErrs, field.Invalid(fldPath.Child("probes"), strconv.FormatUint(uint64(*tcpKeepalive.Probes), 10), "must be greater than or equal to 9")) + if tcpKeepalive.Probes != nil && *tcpKeepalive.Probes < opts.TCPKeepaliveMinProbes { + allErrs = append(allErrs, field.Invalid(fldPath.Child("probes"), strconv.FormatUint(uint64(*tcpKeepalive.Probes), 10), fmt.Sprintf("must be greater than or equal to %d", opts.TCPKeepaliveMinProbes))) } if v := tcpKeepalive.IdleTime; v != nil { idleTimeFieldPath := fldPath.Child("idleTime") - allErrs = append(allErrs, validateGatewayDuration(idleTimeFieldPath, v, ptr.To(5*time.Minute), nil)...) + allErrs = append(allErrs, validateGatewayDuration(idleTimeFieldPath, v, ptr.To(opts.TCPKeepaliveMinIdleTime), nil)...) } if v := tcpKeepalive.Interval; v != nil { intervalFieldPath := fldPath.Child("interval") - allErrs = append(allErrs, validateGatewayDuration(intervalFieldPath, v, ptr.To(30*time.Second), nil)...) + allErrs = append(allErrs, validateGatewayDuration(intervalFieldPath, v, ptr.To(opts.TCPKeepaliveMinInterval), nil)...) } return allErrs @@ -101,7 +101,7 @@ func validateGatewayClusterSettingsHealthCheck(healthCheck *envoygatewayv1alpha1 return allErrs } -func validateGatewayClusterSettingsTimeout(timeout *envoygatewayv1alpha1.Timeout, fldPath *field.Path) field.ErrorList { +func validateGatewayClusterSettingsTimeout(timeout *envoygatewayv1alpha1.Timeout, fldPath *field.Path, opts config.ClusterSettingsValidationOptions) field.ErrorList { if timeout == nil { return nil } @@ -111,7 +111,7 @@ func validateGatewayClusterSettingsTimeout(timeout *envoygatewayv1alpha1.Timeout if timeout.TCP != nil { if v := timeout.TCP.ConnectTimeout; v != nil { connectTimeoutFieldPath := fldPath.Child("tcp").Child("connectTimeout") - allErrs = append(allErrs, validateGatewayDuration(connectTimeoutFieldPath, v, nil, ptr.To(10*time.Second))...) + allErrs = append(allErrs, validateGatewayDuration(connectTimeoutFieldPath, v, nil, ptr.To(opts.TCPMaxConnectionTimeout))...) } } @@ -120,17 +120,17 @@ func validateGatewayClusterSettingsTimeout(timeout *envoygatewayv1alpha1.Timeout if v := timeout.HTTP.ConnectionIdleTimeout; v != nil { connectionIdleTimeoutFieldPath := httpTimeoutFieldPath.Child("connectionIdleTimeout") - allErrs = append(allErrs, validateGatewayDuration(connectionIdleTimeoutFieldPath, v, nil, ptr.To(1*time.Hour))...) + allErrs = append(allErrs, validateGatewayDuration(connectionIdleTimeoutFieldPath, v, nil, ptr.To(opts.HTTPMaxConnectionIdleTimeout))...) } if v := timeout.HTTP.MaxConnectionDuration; v != nil { maxConnectionDurationFieldPath := httpTimeoutFieldPath.Child("maxConnectionDuration") - allErrs = append(allErrs, validateGatewayDuration(maxConnectionDurationFieldPath, v, nil, ptr.To(1*time.Hour))...) + allErrs = append(allErrs, validateGatewayDuration(maxConnectionDurationFieldPath, v, nil, ptr.To(opts.HTTPMaxConnectionDuration))...) } if v := timeout.HTTP.RequestTimeout; v != nil { requestTimeoutFieldPath := httpTimeoutFieldPath.Child("requestTimeout") - allErrs = append(allErrs, validateGatewayDuration(requestTimeoutFieldPath, v, nil, ptr.To(1*time.Hour))...) + allErrs = append(allErrs, validateGatewayDuration(requestTimeoutFieldPath, v, nil, ptr.To(opts.HTTPMaxRequestTimeout))...) } } @@ -138,22 +138,21 @@ func validateGatewayClusterSettingsTimeout(timeout *envoygatewayv1alpha1.Timeout return allErrs } -func validateGatewayClusterSettingsConnection(connection *envoygatewayv1alpha1.BackendConnection, fldPath *field.Path) field.ErrorList { +func validateGatewayClusterSettingsConnection(connection *envoygatewayv1alpha1.BackendConnection, fldPath *field.Path, opts config.ClusterSettingsValidationOptions) field.ErrorList { if connection == nil { return nil } - limit := resource.MustParse("512Ki") - if connection.BufferLimit != nil && connection.BufferLimit.Cmp(limit) == 1 { + if connection.BufferLimit != nil && connection.BufferLimit.Cmp(opts.ConnectionMaxBufferLimit) == 1 { return field.ErrorList{ - field.Invalid(fldPath.Child("bufferLimit"), connection.BufferLimit.String(), fmt.Sprintf("must be less than or equal to %s", limit.String())), + field.Invalid(fldPath.Child("bufferLimit"), connection.BufferLimit.String(), fmt.Sprintf("must be less than or equal to %s", opts.ConnectionMaxBufferLimit.String())), } } return nil } -func validateGatewayClusterSettingsDNS(dns *envoygatewayv1alpha1.DNS, fldPath *field.Path) field.ErrorList { +func validateGatewayClusterSettingsDNS(dns *envoygatewayv1alpha1.DNS, fldPath *field.Path, opts config.ClusterSettingsValidationOptions) field.ErrorList { if dns == nil { return nil } @@ -162,7 +161,7 @@ func validateGatewayClusterSettingsDNS(dns *envoygatewayv1alpha1.DNS, fldPath *f if v := dns.DNSRefreshRate; v != nil { dnsRefreshRateFieldPath := fldPath.Child("dnsRefreshRate") - allErrs = append(allErrs, validateGatewayDuration(dnsRefreshRateFieldPath, v, ptr.To(30*time.Second), nil)...) + allErrs = append(allErrs, validateGatewayDuration(dnsRefreshRateFieldPath, v, ptr.To(opts.DNSMinRefreshRate), nil)...) } if dns.RespectDNSTTL != nil && !*dns.RespectDNSTTL { @@ -172,30 +171,27 @@ func validateGatewayClusterSettingsDNS(dns *envoygatewayv1alpha1.DNS, fldPath *f return allErrs } -func validateGatewayClusterSettingsHTTP2(http2 *envoygatewayv1alpha1.HTTP2Settings, fldPath *field.Path) field.ErrorList { +func validateGatewayClusterSettingsHTTP2(http2 *envoygatewayv1alpha1.HTTP2Settings, fldPath *field.Path, opts config.ClusterSettingsValidationOptions) field.ErrorList { if http2 == nil { return nil } allErrs := field.ErrorList{} if http2.InitialStreamWindowSize != nil { - limit := resource.MustParse("64Ki") - if http2.InitialStreamWindowSize.Cmp(limit) == 1 { - allErrs = append(allErrs, field.Invalid(fldPath.Child("initialStreamWindowSize"), http2.InitialStreamWindowSize.String(), fmt.Sprintf("must be less than or equal to %s", limit.String()))) + if http2.InitialStreamWindowSize.Cmp(opts.HTTP2MaxInitialStreamWindowSize) == 1 { + allErrs = append(allErrs, field.Invalid(fldPath.Child("initialStreamWindowSize"), http2.InitialStreamWindowSize.String(), fmt.Sprintf("must be less than or equal to %s", opts.HTTP2MaxInitialStreamWindowSize.String()))) } } if http2.InitialConnectionWindowSize != nil { - limit := resource.MustParse("1Mi") - if http2.InitialConnectionWindowSize.Cmp(limit) == 1 { - allErrs = append(allErrs, field.Invalid(fldPath.Child("initialConnectionWindowSize"), http2.InitialConnectionWindowSize.String(), fmt.Sprintf("must be less than or equal to %s", limit.String()))) + if http2.InitialConnectionWindowSize.Cmp(opts.HTTP2MaxInitialConnectionWindowSize) == 1 { + allErrs = append(allErrs, field.Invalid(fldPath.Child("initialConnectionWindowSize"), http2.InitialConnectionWindowSize.String(), fmt.Sprintf("must be less than or equal to %s", opts.HTTP2MaxInitialConnectionWindowSize.String()))) } } if http2.MaxConcurrentStreams != nil { - limit := uint32(1024) - if *http2.MaxConcurrentStreams > limit { - allErrs = append(allErrs, field.Invalid(fldPath.Child("maxConcurrentStreams"), http2.MaxConcurrentStreams, fmt.Sprintf("must be less than or equal to %d", limit))) + if *http2.MaxConcurrentStreams > opts.HTTP2MaxConcurrentStreams { + allErrs = append(allErrs, field.Invalid(fldPath.Child("maxConcurrentStreams"), http2.MaxConcurrentStreams, fmt.Sprintf("must be less than or equal to %d", opts.HTTP2MaxConcurrentStreams))) } } diff --git a/internal/validation/backendtrafficpolicy_validation_test.go b/internal/validation/backendtrafficpolicy_validation_test.go index 52d3d9c8..0b44b5fa 100644 --- a/internal/validation/backendtrafficpolicy_validation_test.go +++ b/internal/validation/backendtrafficpolicy_validation_test.go @@ -2,6 +2,7 @@ package validation import ( "testing" + "time" envoygatewayv1alpha1 "github.com/envoyproxy/gateway/api/v1alpha1" "github.com/google/go-cmp/cmp" @@ -11,11 +12,14 @@ import ( "k8s.io/utils/ptr" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + + "go.datum.net/network-services-operator/internal/config" ) func TestValidateBackendTrafficPolicy(t *testing.T) { scenarios := map[string]struct { backendTrafficPolicy *envoygatewayv1alpha1.BackendTrafficPolicy + opts config.BackendTrafficPolicyValidationOptions expectedErrors field.ErrorList }{ "spec.targetRef forbidden": { @@ -72,6 +76,11 @@ func TestValidateBackendTrafficPolicy(t *testing.T) { }, }, }, + opts: config.BackendTrafficPolicyValidationOptions{ + ClusterSettings: config.ClusterSettingsValidationOptions{ + TCPKeepaliveMinProbes: 2, + }, + }, expectedErrors: field.ErrorList{ field.Invalid(field.NewPath("spec", "tcpKeepalive", "probes"), int32(0), ""), }, @@ -86,6 +95,11 @@ func TestValidateBackendTrafficPolicy(t *testing.T) { }, }, }, + opts: config.BackendTrafficPolicyValidationOptions{ + ClusterSettings: config.ClusterSettingsValidationOptions{ + TCPKeepaliveMinIdleTime: 1 * time.Minute, + }, + }, expectedErrors: field.ErrorList{ field.Invalid(field.NewPath("spec", "tcpKeepalive", "idleTime"), "4m0s", ""), }, @@ -100,6 +114,11 @@ func TestValidateBackendTrafficPolicy(t *testing.T) { }, }, }, + opts: config.BackendTrafficPolicyValidationOptions{ + ClusterSettings: config.ClusterSettingsValidationOptions{ + TCPKeepaliveMinInterval: 1 * time.Minute, + }, + }, expectedErrors: field.ErrorList{ field.Invalid(field.NewPath("spec", "tcpKeepalive", "interval"), "4m0s", ""), }, @@ -146,6 +165,11 @@ func TestValidateBackendTrafficPolicy(t *testing.T) { }, }, }, + opts: config.BackendTrafficPolicyValidationOptions{ + ClusterSettings: config.ClusterSettingsValidationOptions{ + HTTPMaxConnectionIdleTimeout: 1 * time.Hour, + }, + }, expectedErrors: field.ErrorList{ field.Invalid(field.NewPath("spec", "timeout", "http", "connectionIdleTimeout"), "", ""), }, @@ -162,6 +186,11 @@ func TestValidateBackendTrafficPolicy(t *testing.T) { }, }, }, + opts: config.BackendTrafficPolicyValidationOptions{ + ClusterSettings: config.ClusterSettingsValidationOptions{ + HTTPMaxConnectionDuration: 1 * time.Hour, + }, + }, expectedErrors: field.ErrorList{ field.Invalid(field.NewPath("spec", "timeout", "http", "maxConnectionDuration"), "", ""), }, @@ -178,6 +207,11 @@ func TestValidateBackendTrafficPolicy(t *testing.T) { }, }, }, + opts: config.BackendTrafficPolicyValidationOptions{ + ClusterSettings: config.ClusterSettingsValidationOptions{ + HTTPMaxRequestTimeout: 1 * time.Hour, + }, + }, expectedErrors: field.ErrorList{ field.Invalid(field.NewPath("spec", "timeout", "http", "requestTimeout"), "", ""), }, @@ -192,6 +226,11 @@ func TestValidateBackendTrafficPolicy(t *testing.T) { }, }, }, + opts: config.BackendTrafficPolicyValidationOptions{ + ClusterSettings: config.ClusterSettingsValidationOptions{ + ConnectionMaxBufferLimit: resource.MustParse("512Ki"), + }, + }, expectedErrors: field.ErrorList{ field.Invalid(field.NewPath("spec", "connection", "bufferLimit"), "", ""), }, @@ -206,6 +245,11 @@ func TestValidateBackendTrafficPolicy(t *testing.T) { }, }, }, + opts: config.BackendTrafficPolicyValidationOptions{ + ClusterSettings: config.ClusterSettingsValidationOptions{ + DNSMinRefreshRate: 1 * time.Second, + }, + }, expectedErrors: field.ErrorList{ field.Invalid(field.NewPath("spec", "dns", "dnsRefreshRate"), "", ""), }, @@ -294,7 +338,7 @@ func TestValidateBackendTrafficPolicy(t *testing.T) { for name, scenario := range scenarios { t.Run(name, func(t *testing.T) { - errs := ValidateBackendTrafficPolicy(scenario.backendTrafficPolicy) + errs := ValidateBackendTrafficPolicy(scenario.backendTrafficPolicy, scenario.opts) delta := cmp.Diff(scenario.expectedErrors, errs, cmpopts.IgnoreFields(field.Error{}, "BadValue", "Detail")) if delta != "" { t.Errorf("Testcase %s - expected errors '%v', got '%v', diff: '%v'", name, scenario.expectedErrors, errs, delta) diff --git a/internal/validation/httproutefilter_validation.go b/internal/validation/httproutefilter_validation.go index bff42e9b..b222442e 100644 --- a/internal/validation/httproutefilter_validation.go +++ b/internal/validation/httproutefilter_validation.go @@ -3,14 +3,11 @@ package validation import ( envoygatewayv1alpha1 "github.com/envoyproxy/gateway/api/v1alpha1" "k8s.io/apimachinery/pkg/util/validation/field" -) -type HTTPRouteFilterValidationOptions struct { - // MaxInlineBodySize is the maximum allowed size for an inline body in a direct response filter. - MaxInlineBodySize int -} + "go.datum.net/network-services-operator/internal/config" +) -func ValidateHTTPRouteFilter(httpRouteFilter *envoygatewayv1alpha1.HTTPRouteFilter, opts HTTPRouteFilterValidationOptions) field.ErrorList { +func ValidateHTTPRouteFilter(httpRouteFilter *envoygatewayv1alpha1.HTTPRouteFilter, opts config.HTTPRouteFilterValidationOptions) field.ErrorList { allErrs := field.ErrorList{} specPath := field.NewPath("spec") diff --git a/internal/validation/httproutefilter_validation_test.go b/internal/validation/httproutefilter_validation_test.go index 9c1c7616..4cea7cec 100644 --- a/internal/validation/httproutefilter_validation_test.go +++ b/internal/validation/httproutefilter_validation_test.go @@ -8,12 +8,14 @@ import ( "github.com/google/go-cmp/cmp/cmpopts" "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/utils/ptr" + + "go.datum.net/network-services-operator/internal/config" ) func TestValidateHTTPRouteFilter(t *testing.T) { scenarios := map[string]struct { httpRouteFilter *envoygatewayv1alpha1.HTTPRouteFilter - opts HTTPRouteFilterValidationOptions + opts config.HTTPRouteFilterValidationOptions expectedErrors field.ErrorList }{ "direct response - inline body too large": { @@ -26,7 +28,7 @@ func TestValidateHTTPRouteFilter(t *testing.T) { }, }, }, - opts: HTTPRouteFilterValidationOptions{ + opts: config.HTTPRouteFilterValidationOptions{ MaxInlineBodySize: 1, }, expectedErrors: field.ErrorList{ diff --git a/internal/validation/securitypolicy_validation.go b/internal/validation/securitypolicy_validation.go index 8fc65205..767960b1 100644 --- a/internal/validation/securitypolicy_validation.go +++ b/internal/validation/securitypolicy_validation.go @@ -1,49 +1,14 @@ package validation import ( - "time" - envoygatewayv1alpha1 "github.com/envoyproxy/gateway/api/v1alpha1" "k8s.io/apimachinery/pkg/util/validation/field" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" -) - -type SecurityPolicyValidationOptions struct { - APIKeyAuth APIKeyAuthValidationOptions - CORS CORSValidationOptions - JWTProvider JWTProviderValidationOptions - OIDC OIDCValidationOptions - Authorization AuthorizationValidationOptions -} - -type APIKeyAuthValidationOptions struct { - MaxCredentialRefs int - MaxExtractFrom int - MaxExtractFromFieldLength int - MaxForwardClientIDHeaderLength int -} - -type CORSValidationOptions struct { - MaxFieldLength int -} - -type JWTProviderValidationOptions struct { - MaxClaimToHeaders int - MaxExtractorLength int -} -type OIDCValidationOptions struct { - MaxScopes int - MaxResources int - MinRefreshTokenTTL time.Duration -} - -type AuthorizationValidationOptions struct { - MaxRules int - MaxClientCIDRs int -} + "go.datum.net/network-services-operator/internal/config" +) -func ValidateSecurityPolicy(securityPolicy *envoygatewayv1alpha1.SecurityPolicy, opts SecurityPolicyValidationOptions) field.ErrorList { +func ValidateSecurityPolicy(securityPolicy *envoygatewayv1alpha1.SecurityPolicy, opts config.SecurityPolicyValidationOptions) field.ErrorList { allErrs := field.ErrorList{} specPath := field.NewPath("spec") @@ -68,7 +33,7 @@ func ValidateSecurityPolicy(securityPolicy *envoygatewayv1alpha1.SecurityPolicy, return allErrs } -func validateSecurityPolicyAPIKeyAuth(apiKeyAuth *envoygatewayv1alpha1.APIKeyAuth, fldPath *field.Path, opts SecurityPolicyValidationOptions) field.ErrorList { +func validateSecurityPolicyAPIKeyAuth(apiKeyAuth *envoygatewayv1alpha1.APIKeyAuth, fldPath *field.Path, opts config.SecurityPolicyValidationOptions) field.ErrorList { if apiKeyAuth == nil { return nil } @@ -93,7 +58,7 @@ func validateSecurityPolicyAPIKeyAuth(apiKeyAuth *envoygatewayv1alpha1.APIKeyAut return allErrs } -func validateSecurityPolicyCORS(cors *envoygatewayv1alpha1.CORS, fldPath *field.Path, opts SecurityPolicyValidationOptions) field.ErrorList { +func validateSecurityPolicyCORS(cors *envoygatewayv1alpha1.CORS, fldPath *field.Path, opts config.SecurityPolicyValidationOptions) field.ErrorList { if cors == nil { return nil } @@ -131,7 +96,7 @@ func validateSecurityPolicyBasicAuth(basicAuth *envoygatewayv1alpha1.BasicAuth, return allErrs } -func validateSecurityPolicyJWT(jwt *envoygatewayv1alpha1.JWT, fldPath *field.Path, opts SecurityPolicyValidationOptions) field.ErrorList { +func validateSecurityPolicyJWT(jwt *envoygatewayv1alpha1.JWT, fldPath *field.Path, opts config.SecurityPolicyValidationOptions) field.ErrorList { if jwt == nil { return nil } @@ -148,13 +113,13 @@ func validateSecurityPolicyJWT(jwt *envoygatewayv1alpha1.JWT, fldPath *field.Pat return allErrs } -func validateSecurityPolicyOIDC(oidc *envoygatewayv1alpha1.OIDC, fldPath *field.Path, opts SecurityPolicyValidationOptions) field.ErrorList { +func validateSecurityPolicyOIDC(oidc *envoygatewayv1alpha1.OIDC, fldPath *field.Path, opts config.SecurityPolicyValidationOptions) field.ErrorList { if oidc == nil { return nil } allErrs := field.ErrorList{} - allErrs = append(allErrs, validateSecurityPolicyOIDCProvider(oidc.Provider, fldPath.Child("provider"))...) + allErrs = append(allErrs, validateSecurityPolicyOIDCProvider(oidc.Provider, fldPath.Child("provider"), opts)...) allErrs = append(allErrs, validateGatewaySecretObjectReference(oidc.ClientIDRef, fldPath.Child("clientIDRef"))...) allErrs = append(allErrs, validateGatewaySecretObjectReference(&oidc.ClientSecret, fldPath.Child("clientSecret"))...) @@ -173,7 +138,7 @@ func validateSecurityPolicyOIDC(oidc *envoygatewayv1alpha1.OIDC, fldPath *field. return allErrs } -func validateSecurityPolicyAuthorization(authorization *envoygatewayv1alpha1.Authorization, fldPath *field.Path, opts SecurityPolicyValidationOptions) field.ErrorList { +func validateSecurityPolicyAuthorization(authorization *envoygatewayv1alpha1.Authorization, fldPath *field.Path, opts config.SecurityPolicyValidationOptions) field.ErrorList { if authorization == nil { return nil } @@ -194,7 +159,7 @@ func validateSecurityPolicyAuthorization(authorization *envoygatewayv1alpha1.Aut return allErrs } -func validateAuthorizationPrincipal(principal envoygatewayv1alpha1.Principal, fldPath *field.Path, opts SecurityPolicyValidationOptions) field.ErrorList { +func validateAuthorizationPrincipal(principal envoygatewayv1alpha1.Principal, fldPath *field.Path, opts config.SecurityPolicyValidationOptions) field.ErrorList { allErrs := field.ErrorList{} @@ -205,18 +170,18 @@ func validateAuthorizationPrincipal(principal envoygatewayv1alpha1.Principal, fl return allErrs } -func validateSecurityPolicyOIDCProvider(provider envoygatewayv1alpha1.OIDCProvider, fldPath *field.Path) field.ErrorList { +func validateSecurityPolicyOIDCProvider(provider envoygatewayv1alpha1.OIDCProvider, fldPath *field.Path, opts config.SecurityPolicyValidationOptions) field.ErrorList { allErrs := field.ErrorList{} - allErrs = append(allErrs, validateGatewayBackendCluster(provider.BackendCluster, fldPath)...) + allErrs = append(allErrs, validateGatewayBackendCluster(provider.BackendCluster, fldPath, opts)...) return allErrs } -func validateGatewayJWTProvider(provider envoygatewayv1alpha1.JWTProvider, fldPath *field.Path, opts SecurityPolicyValidationOptions) field.ErrorList { +func validateGatewayJWTProvider(provider envoygatewayv1alpha1.JWTProvider, fldPath *field.Path, opts config.SecurityPolicyValidationOptions) field.ErrorList { allErrs := field.ErrorList{} - allErrs = append(allErrs, validateRemoteJWKS(provider.RemoteJWKS, fldPath.Child("remoteJWKS"))...) + allErrs = append(allErrs, validateRemoteJWKS(provider.RemoteJWKS, fldPath.Child("remoteJWKS"), opts)...) if len(provider.ClaimToHeaders) > opts.JWTProvider.MaxClaimToHeaders { allErrs = append(allErrs, field.TooMany(fldPath.Child("claimToHeaders"), len(provider.ClaimToHeaders), opts.JWTProvider.MaxClaimToHeaders)) @@ -239,19 +204,19 @@ func validateGatewayJWTProvider(provider envoygatewayv1alpha1.JWTProvider, fldPa return allErrs } -func validateRemoteJWKS(remoteJWKS *envoygatewayv1alpha1.RemoteJWKS, fldPath *field.Path) field.ErrorList { +func validateRemoteJWKS(remoteJWKS *envoygatewayv1alpha1.RemoteJWKS, fldPath *field.Path, opts config.SecurityPolicyValidationOptions) field.ErrorList { if remoteJWKS == nil { return nil } allErrs := field.ErrorList{} - allErrs = append(allErrs, validateGatewayBackendCluster(remoteJWKS.BackendCluster, fldPath)...) + allErrs = append(allErrs, validateGatewayBackendCluster(remoteJWKS.BackendCluster, fldPath, opts)...) return allErrs } -func validateGatewayBackendCluster(backendCluster envoygatewayv1alpha1.BackendCluster, fldPath *field.Path) field.ErrorList { +func validateGatewayBackendCluster(backendCluster envoygatewayv1alpha1.BackendCluster, fldPath *field.Path, opts config.SecurityPolicyValidationOptions) field.ErrorList { allErrs := field.ErrorList{} // nolint:staticcheck @@ -264,7 +229,7 @@ func validateGatewayBackendCluster(backendCluster envoygatewayv1alpha1.BackendCl } if backendCluster.BackendSettings != nil { - allErrs = append(allErrs, validateGatewayClusterSettings(*backendCluster.BackendSettings, fldPath.Child("backendSettings"))...) + allErrs = append(allErrs, validateGatewayClusterSettings(*backendCluster.BackendSettings, fldPath.Child("backendSettings"), opts.ClusterSettings)...) } return allErrs @@ -280,7 +245,7 @@ func validateBackendClusterBackendObjectReference(backendObjectRef gatewayv1.Bac return allErrs } -func validateSecurityPolicyExtractFrom(extractFrom *envoygatewayv1alpha1.ExtractFrom, fldPath *field.Path, opts SecurityPolicyValidationOptions) field.ErrorList { +func validateSecurityPolicyExtractFrom(extractFrom *envoygatewayv1alpha1.ExtractFrom, fldPath *field.Path, opts config.SecurityPolicyValidationOptions) field.ErrorList { if extractFrom == nil { return nil } @@ -302,7 +267,7 @@ func validateSecurityPolicyExtractFrom(extractFrom *envoygatewayv1alpha1.Extract return allErrs } -func validateSecurityPolicyCredentialRefs(refs []gatewayv1.SecretObjectReference, fldPath *field.Path, opts SecurityPolicyValidationOptions) field.ErrorList { +func validateSecurityPolicyCredentialRefs(refs []gatewayv1.SecretObjectReference, fldPath *field.Path, opts config.SecurityPolicyValidationOptions) field.ErrorList { allErrs := field.ErrorList{} if len(refs) > opts.APIKeyAuth.MaxCredentialRefs { diff --git a/internal/validation/securitypolicy_validation_test.go b/internal/validation/securitypolicy_validation_test.go index a529880b..c380351a 100644 --- a/internal/validation/securitypolicy_validation_test.go +++ b/internal/validation/securitypolicy_validation_test.go @@ -11,12 +11,14 @@ import ( "k8s.io/utils/ptr" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + + "go.datum.net/network-services-operator/internal/config" ) func TestValidateSecurityPolicy(t *testing.T) { scenarios := map[string]struct { securityPolicy *envoygatewayv1alpha1.SecurityPolicy - opts SecurityPolicyValidationOptions + opts config.SecurityPolicyValidationOptions expectedErrors field.ErrorList }{ "spec.targetRef forbidden": { @@ -49,8 +51,8 @@ func TestValidateSecurityPolicy(t *testing.T) { }, }, }, - opts: SecurityPolicyValidationOptions{ - APIKeyAuth: APIKeyAuthValidationOptions{ + opts: config.SecurityPolicyValidationOptions{ + APIKeyAuth: config.APIKeyAuthValidationOptions{ MaxCredentialRefs: 1, }, }, @@ -70,8 +72,8 @@ func TestValidateSecurityPolicy(t *testing.T) { }, }, }, - opts: SecurityPolicyValidationOptions{ - APIKeyAuth: APIKeyAuthValidationOptions{ + opts: config.SecurityPolicyValidationOptions{ + APIKeyAuth: config.APIKeyAuthValidationOptions{ MaxExtractFrom: 1, }, }, @@ -93,8 +95,8 @@ func TestValidateSecurityPolicy(t *testing.T) { }, }, }, - opts: SecurityPolicyValidationOptions{ - APIKeyAuth: APIKeyAuthValidationOptions{ + opts: config.SecurityPolicyValidationOptions{ + APIKeyAuth: config.APIKeyAuthValidationOptions{ MaxExtractFrom: 1, MaxExtractFromFieldLength: 1, }, @@ -113,8 +115,8 @@ func TestValidateSecurityPolicy(t *testing.T) { }, }, }, - opts: SecurityPolicyValidationOptions{ - APIKeyAuth: APIKeyAuthValidationOptions{ + opts: config.SecurityPolicyValidationOptions{ + APIKeyAuth: config.APIKeyAuthValidationOptions{ MaxForwardClientIDHeaderLength: 1, }, }, @@ -133,8 +135,8 @@ func TestValidateSecurityPolicy(t *testing.T) { }, }, }, - opts: SecurityPolicyValidationOptions{ - CORS: CORSValidationOptions{ + opts: config.SecurityPolicyValidationOptions{ + CORS: config.CORSValidationOptions{ MaxFieldLength: 1, }, }, @@ -246,8 +248,8 @@ func TestValidateSecurityPolicy(t *testing.T) { }, }, }, - opts: SecurityPolicyValidationOptions{ - JWTProvider: JWTProviderValidationOptions{ + opts: config.SecurityPolicyValidationOptions{ + JWTProvider: config.JWTProviderValidationOptions{ MaxClaimToHeaders: 1, MaxExtractorLength: 2, }, @@ -307,8 +309,8 @@ func TestValidateSecurityPolicy(t *testing.T) { }, }, }, - opts: SecurityPolicyValidationOptions{ - OIDC: OIDCValidationOptions{ + opts: config.SecurityPolicyValidationOptions{ + OIDC: config.OIDCValidationOptions{ MaxScopes: 2, MaxResources: 2, MinRefreshTokenTTL: 1 * time.Minute, @@ -331,8 +333,8 @@ func TestValidateSecurityPolicy(t *testing.T) { }, }, }, - opts: SecurityPolicyValidationOptions{ - Authorization: AuthorizationValidationOptions{ + opts: config.SecurityPolicyValidationOptions{ + Authorization: config.AuthorizationValidationOptions{ MaxRules: 1, }, }, @@ -358,8 +360,8 @@ func TestValidateSecurityPolicy(t *testing.T) { }, }, }, - opts: SecurityPolicyValidationOptions{ - Authorization: AuthorizationValidationOptions{ + opts: config.SecurityPolicyValidationOptions{ + Authorization: config.AuthorizationValidationOptions{ MaxRules: 1, MaxClientCIDRs: 2, }, diff --git a/internal/webhook/v1alpha1/backendtrafficpolicy_webhook.go b/internal/webhook/v1alpha1/backendtrafficpolicy_webhook.go index fd8199e6..43090480 100644 --- a/internal/webhook/v1alpha1/backendtrafficpolicy_webhook.go +++ b/internal/webhook/v1alpha1/backendtrafficpolicy_webhook.go @@ -16,20 +16,22 @@ import ( mccontext "sigs.k8s.io/multicluster-runtime/pkg/context" mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" + "go.datum.net/network-services-operator/internal/config" "go.datum.net/network-services-operator/internal/validation" ) // SetupBackendTrafficPolicyWebhookWithManager registers the webhook for BackendTrafficPolicy in the manager. -func SetupBackendTrafficPolicyWebhookWithManager(mgr mcmanager.Manager) error { +func SetupBackendTrafficPolicyWebhookWithManager(mgr mcmanager.Manager, cfg config.NetworkServicesOperator) error { return ctrl.NewWebhookManagedBy(mgr.GetLocalManager()).For(&gatewayv1alpha1.BackendTrafficPolicy{}). - WithValidator(&BackendTrafficPolicyCustomValidator{mgr: mgr}). + WithValidator(&BackendTrafficPolicyCustomValidator{mgr: mgr, validationOpts: cfg.Gateway.ExtensionAPIValidationOptions.BackendTrafficPolicies}). Complete() } // +kubebuilder:webhook:path=/validate-gateway-envoyproxy-io-v1alpha1-backendtrafficpolicy,mutating=false,failurePolicy=fail,sideEffects=None,groups=gateway.envoyproxy.io,resources=backendtrafficpolicies,verbs=create;update,versions=v1alpha1,name=vbackendtrafficpolicy-v1alpha1.kb.io,admissionReviewVersions=v1 type BackendTrafficPolicyCustomValidator struct { - mgr mcmanager.Manager + mgr mcmanager.Manager + validationOpts config.BackendTrafficPolicyValidationOptions } var _ webhook.CustomValidator = &BackendTrafficPolicyCustomValidator{} @@ -49,7 +51,7 @@ func (v *BackendTrafficPolicyCustomValidator) ValidateCreate(ctx context.Context log := logf.FromContext(ctx).WithValues("cluster", clusterName) log.Info("Validating BackendTrafficPolicy", "name", backendTrafficPolicy.GetName(), "cluster", clusterName) - if errs := validation.ValidateBackendTrafficPolicy(backendTrafficPolicy); len(errs) > 0 { + if errs := validation.ValidateBackendTrafficPolicy(backendTrafficPolicy, v.validationOpts); len(errs) > 0 { return nil, apierrors.NewInvalid(obj.GetObjectKind().GroupVersionKind().GroupKind(), backendTrafficPolicy.GetName(), errs) } @@ -71,7 +73,7 @@ func (v *BackendTrafficPolicyCustomValidator) ValidateUpdate(ctx context.Context log := logf.FromContext(ctx).WithValues("cluster", clusterName) log.Info("Validating BackendTrafficPolicy", "name", backendTrafficPolicy.GetName(), "cluster", clusterName) - if errs := validation.ValidateBackendTrafficPolicy(backendTrafficPolicy); len(errs) > 0 { + if errs := validation.ValidateBackendTrafficPolicy(backendTrafficPolicy, v.validationOpts); len(errs) > 0 { return nil, apierrors.NewInvalid(oldObj.GetObjectKind().GroupVersionKind().GroupKind(), backendTrafficPolicy.GetName(), errs) } return nil, nil diff --git a/internal/webhook/v1alpha1/httproutefilter_webhook.go b/internal/webhook/v1alpha1/httproutefilter_webhook.go index ac0faba0..247e30d4 100644 --- a/internal/webhook/v1alpha1/httproutefilter_webhook.go +++ b/internal/webhook/v1alpha1/httproutefilter_webhook.go @@ -16,16 +16,14 @@ import ( mccontext "sigs.k8s.io/multicluster-runtime/pkg/context" mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" + "go.datum.net/network-services-operator/internal/config" "go.datum.net/network-services-operator/internal/validation" ) // SetupHTTPRouteFilterWebhookWithManager registers the webhook for HTTPRouteFilter in the manager. -func SetupHTTPRouteFilterWebhookWithManager(mgr mcmanager.Manager) error { - validationOpts := validation.HTTPRouteFilterValidationOptions{ - MaxInlineBodySize: 1024, - } +func SetupHTTPRouteFilterWebhookWithManager(mgr mcmanager.Manager, cfg config.NetworkServicesOperator) error { return ctrl.NewWebhookManagedBy(mgr.GetLocalManager()).For(&gatewayv1alpha1.HTTPRouteFilter{}). - WithValidator(&HTTPRouteFilterCustomValidator{mgr: mgr, validationOpts: validationOpts}). + WithValidator(&HTTPRouteFilterCustomValidator{mgr: mgr, validationOpts: cfg.Gateway.ExtensionAPIValidationOptions.HTTPRouteFilters}). Complete() } @@ -33,7 +31,7 @@ func SetupHTTPRouteFilterWebhookWithManager(mgr mcmanager.Manager) error { type HTTPRouteFilterCustomValidator struct { mgr mcmanager.Manager - validationOpts validation.HTTPRouteFilterValidationOptions + validationOpts config.HTTPRouteFilterValidationOptions } var _ webhook.CustomValidator = &HTTPRouteFilterCustomValidator{} diff --git a/internal/webhook/v1alpha1/securitypolicy_webhook.go b/internal/webhook/v1alpha1/securitypolicy_webhook.go index f9712391..14c24fcd 100644 --- a/internal/webhook/v1alpha1/securitypolicy_webhook.go +++ b/internal/webhook/v1alpha1/securitypolicy_webhook.go @@ -16,20 +16,14 @@ import ( mccontext "sigs.k8s.io/multicluster-runtime/pkg/context" mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" + "go.datum.net/network-services-operator/internal/config" "go.datum.net/network-services-operator/internal/validation" ) // SetupSecurityPolicyWebhookWithManager registers the webhook for SecurityPolicy in the manager. -func SetupSecurityPolicyWebhookWithManager(mgr mcmanager.Manager) error { - validationOpts := validation.SecurityPolicyValidationOptions{ - APIKeyAuth: validation.APIKeyAuthValidationOptions{ - MaxCredentialRefs: 5, - MaxExtractFrom: 5, - MaxExtractFromFieldLength: 5, - }, - } +func SetupSecurityPolicyWebhookWithManager(mgr mcmanager.Manager, cfg config.NetworkServicesOperator) error { return ctrl.NewWebhookManagedBy(mgr.GetLocalManager()).For(&gatewayv1alpha1.SecurityPolicy{}). - WithValidator(&SecurityPolicyCustomValidator{mgr: mgr, validationOpts: validationOpts}). + WithValidator(&SecurityPolicyCustomValidator{mgr: mgr, validationOpts: cfg.Gateway.ExtensionAPIValidationOptions.SecurityPolicies}). Complete() } @@ -37,7 +31,7 @@ func SetupSecurityPolicyWebhookWithManager(mgr mcmanager.Manager) error { type SecurityPolicyCustomValidator struct { mgr mcmanager.Manager - validationOpts validation.SecurityPolicyValidationOptions + validationOpts config.SecurityPolicyValidationOptions } var _ webhook.CustomValidator = &SecurityPolicyCustomValidator{} From a20d0b9e188503d8b9a995e46fcb308e58e6ba9f Mon Sep 17 00:00:00 2001 From: Joshua Reese Date: Tue, 23 Sep 2025 16:42:33 -0500 Subject: [PATCH 3/3] Adjust config field types to use metav1.Duration, which comes with a JSON marshaling implementation, leverage defaulter-gen instead of direct defaulting functions. --- internal/config/config.go | 106 ++++------------ internal/config/zz_generated.deepcopy.go | 75 ++++++++++- internal/config/zz_generated.defaults.go | 120 +++++++++++++++++- .../backendtrafficpolicy_validation.go | 20 +-- .../backendtrafficpolicy_validation_test.go | 61 ++++----- .../httproutefilter_validation_test.go | 16 ++- .../validation/securitypolicy_validation.go | 2 +- .../securitypolicy_validation_test.go | 77 +++++------ 8 files changed, 298 insertions(+), 179 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index 823fb3b0..4c8f8398 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -393,117 +393,67 @@ type ClusterSettingsValidationOptions struct { // Minimum amount for the total number of unacknowledged probes to send before // deciding the connection is dead. // - // Default: 9 + // +default=9 TCPKeepaliveMinProbes uint32 // Minimum amount for the duration a connection needs to be idle before // keep-alive probes start being sent. // - // Default: 5m - TCPKeepaliveMinIdleTime time.Duration + // +default="5m" + TCPKeepaliveMinIdleTime *metav1.Duration // Minimum amount for the duration between keep-alive probes. // - // Default: 30s - TCPKeepaliveMinInterval time.Duration + // +default="30s" + TCPKeepaliveMinInterval *metav1.Duration // Maximum time allowed for a connection timeout // - // Default: 10s - TCPMaxConnectionTimeout time.Duration + // +default="10s" + TCPMaxConnectionTimeout *metav1.Duration // Maximum amount for the duration a connection can be idle. // - // Default: 1h - HTTPMaxConnectionIdleTimeout time.Duration + // +default="1h" + HTTPMaxConnectionIdleTimeout *metav1.Duration // Maximum amount for the duration of a connection. // - // Default: 1h - HTTPMaxConnectionDuration time.Duration + // +default="1h" + HTTPMaxConnectionDuration *metav1.Duration // Maximum amount for the duration until an entire request is received by the // upstream. // - // Default: 1h - HTTPMaxRequestTimeout time.Duration + // +default="1h" + HTTPMaxRequestTimeout *metav1.Duration // Maximum size for upstream connection buffers // - // Default: 512Ki - ConnectionMaxBufferLimit resource.Quantity + // +default="512Ki" + ConnectionMaxBufferLimit *resource.Quantity // Minimum amount for the duration between DNS refreshes. // - // Default: 30s - DNSMinRefreshRate time.Duration + // +default="30s" + DNSMinRefreshRate *metav1.Duration // Maximum size for the initial stream window size for HTTP/2 connections. // - // Default: 64Ki - HTTP2MaxInitialStreamWindowSize resource.Quantity + // +default="64Ki" + HTTP2MaxInitialStreamWindowSize *resource.Quantity // Maximum size for the initial connection window size for HTTP/2 connections. // - // Default: 1Mi - HTTP2MaxInitialConnectionWindowSize resource.Quantity + // +default="1Mi" + HTTP2MaxInitialConnectionWindowSize *resource.Quantity // Maximum number of concurrent streams for HTTP/2 connections. // - // Default: 1024 + // +default=1024 HTTP2MaxConcurrentStreams uint32 } -func SetDefaults_ClusterSettingsValidationOptions(obj *ClusterSettingsValidationOptions) { - if obj.TCPKeepaliveMinProbes == 0 { - obj.TCPKeepaliveMinProbes = 9 - } - - if obj.TCPKeepaliveMinIdleTime == 0 { - obj.TCPKeepaliveMinIdleTime = 5 * time.Minute - } - - if obj.TCPKeepaliveMinInterval == 0 { - obj.TCPKeepaliveMinInterval = 30 * time.Second - } - - if obj.TCPMaxConnectionTimeout == 0 { - obj.TCPMaxConnectionTimeout = 10 * time.Second - } - - if obj.HTTPMaxConnectionIdleTimeout == 0 { - obj.HTTPMaxConnectionIdleTimeout = 1 * time.Hour - } - - if obj.HTTPMaxConnectionDuration == 0 { - obj.HTTPMaxConnectionDuration = 1 * time.Hour - } - - if obj.HTTPMaxRequestTimeout == 0 { - obj.HTTPMaxRequestTimeout = 1 * time.Hour - } - - if obj.ConnectionMaxBufferLimit.IsZero() { - obj.ConnectionMaxBufferLimit = resource.MustParse("512Ki") - } - - if obj.DNSMinRefreshRate == 0 { - obj.DNSMinRefreshRate = 30 * time.Second - } - - if obj.HTTP2MaxInitialStreamWindowSize.IsZero() { - obj.HTTP2MaxInitialStreamWindowSize = resource.MustParse("64Ki") - } - - if obj.HTTP2MaxInitialConnectionWindowSize.IsZero() { - obj.HTTP2MaxInitialConnectionWindowSize = resource.MustParse("1Mi") - } - - if obj.HTTP2MaxConcurrentStreams == 0 { - obj.HTTP2MaxConcurrentStreams = 1024 - } -} - type HTTPRouteFilterValidationOptions struct { // MaxInlineBodySize is the maximum allowed size for an inline body in a // direct response filter. @@ -580,6 +530,8 @@ type JWTProviderValidationOptions struct { MaxExtractorLength int } +// +k8s:deepcopy-gen=true + type OIDCValidationOptions struct { // MaxScopes is the maximum number of scopes per OIDC configuration. // @@ -593,14 +545,8 @@ type OIDCValidationOptions struct { // MinRefreshTokenTTL is the minimum allowed TTL for refresh tokens. // - // Default: 1m - MinRefreshTokenTTL time.Duration -} - -func SetDefaults_OIDCValidationOptions(obj *OIDCValidationOptions) { - if obj.MinRefreshTokenTTL == 0 { - obj.MinRefreshTokenTTL = 1 * time.Minute - } + // +default="5m" + MinRefreshTokenTTL *metav1.Duration } type AuthorizationValidationOptions struct { diff --git a/internal/config/zz_generated.deepcopy.go b/internal/config/zz_generated.deepcopy.go index f06515e8..d3144681 100644 --- a/internal/config/zz_generated.deepcopy.go +++ b/internal/config/zz_generated.deepcopy.go @@ -33,9 +33,56 @@ func (in *BackendTrafficPolicyValidationOptions) DeepCopy() *BackendTrafficPolic // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClusterSettingsValidationOptions) DeepCopyInto(out *ClusterSettingsValidationOptions) { *out = *in - out.ConnectionMaxBufferLimit = in.ConnectionMaxBufferLimit.DeepCopy() - out.HTTP2MaxInitialStreamWindowSize = in.HTTP2MaxInitialStreamWindowSize.DeepCopy() - out.HTTP2MaxInitialConnectionWindowSize = in.HTTP2MaxInitialConnectionWindowSize.DeepCopy() + if in.TCPKeepaliveMinIdleTime != nil { + in, out := &in.TCPKeepaliveMinIdleTime, &out.TCPKeepaliveMinIdleTime + *out = new(metav1.Duration) + **out = **in + } + if in.TCPKeepaliveMinInterval != nil { + in, out := &in.TCPKeepaliveMinInterval, &out.TCPKeepaliveMinInterval + *out = new(metav1.Duration) + **out = **in + } + if in.TCPMaxConnectionTimeout != nil { + in, out := &in.TCPMaxConnectionTimeout, &out.TCPMaxConnectionTimeout + *out = new(metav1.Duration) + **out = **in + } + if in.HTTPMaxConnectionIdleTimeout != nil { + in, out := &in.HTTPMaxConnectionIdleTimeout, &out.HTTPMaxConnectionIdleTimeout + *out = new(metav1.Duration) + **out = **in + } + if in.HTTPMaxConnectionDuration != nil { + in, out := &in.HTTPMaxConnectionDuration, &out.HTTPMaxConnectionDuration + *out = new(metav1.Duration) + **out = **in + } + if in.HTTPMaxRequestTimeout != nil { + in, out := &in.HTTPMaxRequestTimeout, &out.HTTPMaxRequestTimeout + *out = new(metav1.Duration) + **out = **in + } + if in.ConnectionMaxBufferLimit != nil { + in, out := &in.ConnectionMaxBufferLimit, &out.ConnectionMaxBufferLimit + x := (*in).DeepCopy() + *out = &x + } + if in.DNSMinRefreshRate != nil { + in, out := &in.DNSMinRefreshRate, &out.DNSMinRefreshRate + *out = new(metav1.Duration) + **out = **in + } + if in.HTTP2MaxInitialStreamWindowSize != nil { + in, out := &in.HTTP2MaxInitialStreamWindowSize, &out.HTTP2MaxInitialStreamWindowSize + x := (*in).DeepCopy() + *out = &x + } + if in.HTTP2MaxInitialConnectionWindowSize != nil { + in, out := &in.HTTP2MaxInitialConnectionWindowSize, &out.HTTP2MaxInitialConnectionWindowSize + x := (*in).DeepCopy() + *out = &x + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterSettingsValidationOptions. @@ -300,6 +347,26 @@ func (in *NetworkServicesOperator) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OIDCValidationOptions) DeepCopyInto(out *OIDCValidationOptions) { + *out = *in + if in.MinRefreshTokenTTL != nil { + in, out := &in.MinRefreshTokenTTL, &out.MinRefreshTokenTTL + *out = new(metav1.Duration) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OIDCValidationOptions. +func (in *OIDCValidationOptions) DeepCopy() *OIDCValidationOptions { + if in == nil { + return nil + } + out := new(OIDCValidationOptions) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ReplicatedResourceConfig) DeepCopyInto(out *ReplicatedResourceConfig) { *out = *in @@ -347,7 +414,7 @@ func (in *SecurityPolicyValidationOptions) DeepCopyInto(out *SecurityPolicyValid out.APIKeyAuth = in.APIKeyAuth out.CORS = in.CORS out.JWTProvider = in.JWTProvider - out.OIDC = in.OIDC + in.OIDC.DeepCopyInto(&out.OIDC) out.Authorization = in.Authorization in.ClusterSettings.DeepCopyInto(&out.ClusterSettings) } diff --git a/internal/config/zz_generated.defaults.go b/internal/config/zz_generated.defaults.go index 9948ff56..a1a69cc3 100644 --- a/internal/config/zz_generated.defaults.go +++ b/internal/config/zz_generated.defaults.go @@ -42,7 +42,62 @@ func SetObjectDefaults_NetworkServicesOperator(in *NetworkServicesOperator) { panic(err) } } - SetDefaults_ClusterSettingsValidationOptions(&in.Gateway.ExtensionAPIValidationOptions.BackendTrafficPolicies.ClusterSettings) + if in.Gateway.ExtensionAPIValidationOptions.BackendTrafficPolicies.ClusterSettings.TCPKeepaliveMinProbes == 0 { + in.Gateway.ExtensionAPIValidationOptions.BackendTrafficPolicies.ClusterSettings.TCPKeepaliveMinProbes = 9 + } + if in.Gateway.ExtensionAPIValidationOptions.BackendTrafficPolicies.ClusterSettings.TCPKeepaliveMinIdleTime == nil { + if err := json.Unmarshal([]byte(`"5m"`), &in.Gateway.ExtensionAPIValidationOptions.BackendTrafficPolicies.ClusterSettings.TCPKeepaliveMinIdleTime); err != nil { + panic(err) + } + } + if in.Gateway.ExtensionAPIValidationOptions.BackendTrafficPolicies.ClusterSettings.TCPKeepaliveMinInterval == nil { + if err := json.Unmarshal([]byte(`"30s"`), &in.Gateway.ExtensionAPIValidationOptions.BackendTrafficPolicies.ClusterSettings.TCPKeepaliveMinInterval); err != nil { + panic(err) + } + } + if in.Gateway.ExtensionAPIValidationOptions.BackendTrafficPolicies.ClusterSettings.TCPMaxConnectionTimeout == nil { + if err := json.Unmarshal([]byte(`"10s"`), &in.Gateway.ExtensionAPIValidationOptions.BackendTrafficPolicies.ClusterSettings.TCPMaxConnectionTimeout); err != nil { + panic(err) + } + } + if in.Gateway.ExtensionAPIValidationOptions.BackendTrafficPolicies.ClusterSettings.HTTPMaxConnectionIdleTimeout == nil { + if err := json.Unmarshal([]byte(`"1h"`), &in.Gateway.ExtensionAPIValidationOptions.BackendTrafficPolicies.ClusterSettings.HTTPMaxConnectionIdleTimeout); err != nil { + panic(err) + } + } + if in.Gateway.ExtensionAPIValidationOptions.BackendTrafficPolicies.ClusterSettings.HTTPMaxConnectionDuration == nil { + if err := json.Unmarshal([]byte(`"1h"`), &in.Gateway.ExtensionAPIValidationOptions.BackendTrafficPolicies.ClusterSettings.HTTPMaxConnectionDuration); err != nil { + panic(err) + } + } + if in.Gateway.ExtensionAPIValidationOptions.BackendTrafficPolicies.ClusterSettings.HTTPMaxRequestTimeout == nil { + if err := json.Unmarshal([]byte(`"1h"`), &in.Gateway.ExtensionAPIValidationOptions.BackendTrafficPolicies.ClusterSettings.HTTPMaxRequestTimeout); err != nil { + panic(err) + } + } + if in.Gateway.ExtensionAPIValidationOptions.BackendTrafficPolicies.ClusterSettings.ConnectionMaxBufferLimit == nil { + if err := json.Unmarshal([]byte(`"512Ki"`), &in.Gateway.ExtensionAPIValidationOptions.BackendTrafficPolicies.ClusterSettings.ConnectionMaxBufferLimit); err != nil { + panic(err) + } + } + if in.Gateway.ExtensionAPIValidationOptions.BackendTrafficPolicies.ClusterSettings.DNSMinRefreshRate == nil { + if err := json.Unmarshal([]byte(`"30s"`), &in.Gateway.ExtensionAPIValidationOptions.BackendTrafficPolicies.ClusterSettings.DNSMinRefreshRate); err != nil { + panic(err) + } + } + if in.Gateway.ExtensionAPIValidationOptions.BackendTrafficPolicies.ClusterSettings.HTTP2MaxInitialStreamWindowSize == nil { + if err := json.Unmarshal([]byte(`"64Ki"`), &in.Gateway.ExtensionAPIValidationOptions.BackendTrafficPolicies.ClusterSettings.HTTP2MaxInitialStreamWindowSize); err != nil { + panic(err) + } + } + if in.Gateway.ExtensionAPIValidationOptions.BackendTrafficPolicies.ClusterSettings.HTTP2MaxInitialConnectionWindowSize == nil { + if err := json.Unmarshal([]byte(`"1Mi"`), &in.Gateway.ExtensionAPIValidationOptions.BackendTrafficPolicies.ClusterSettings.HTTP2MaxInitialConnectionWindowSize); err != nil { + panic(err) + } + } + if in.Gateway.ExtensionAPIValidationOptions.BackendTrafficPolicies.ClusterSettings.HTTP2MaxConcurrentStreams == 0 { + in.Gateway.ExtensionAPIValidationOptions.BackendTrafficPolicies.ClusterSettings.HTTP2MaxConcurrentStreams = 1024 + } if in.Gateway.ExtensionAPIValidationOptions.HTTPRouteFilters.MaxInlineBodySize == 0 { in.Gateway.ExtensionAPIValidationOptions.HTTPRouteFilters.MaxInlineBodySize = 1024 } @@ -67,20 +122,79 @@ func SetObjectDefaults_NetworkServicesOperator(in *NetworkServicesOperator) { if in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.JWTProvider.MaxExtractorLength == 0 { in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.JWTProvider.MaxExtractorLength = 5 } - SetDefaults_OIDCValidationOptions(&in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.OIDC) if in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.OIDC.MaxScopes == 0 { in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.OIDC.MaxScopes = 5 } if in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.OIDC.MaxResources == 0 { in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.OIDC.MaxResources = 5 } + if in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.OIDC.MinRefreshTokenTTL == nil { + if err := json.Unmarshal([]byte(`"5m"`), &in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.OIDC.MinRefreshTokenTTL); err != nil { + panic(err) + } + } if in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.Authorization.MaxRules == 0 { in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.Authorization.MaxRules = 20 } if in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.Authorization.MaxClientCIDRs == 0 { in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.Authorization.MaxClientCIDRs = 5 } - SetDefaults_ClusterSettingsValidationOptions(&in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.ClusterSettings) + if in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.ClusterSettings.TCPKeepaliveMinProbes == 0 { + in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.ClusterSettings.TCPKeepaliveMinProbes = 9 + } + if in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.ClusterSettings.TCPKeepaliveMinIdleTime == nil { + if err := json.Unmarshal([]byte(`"5m"`), &in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.ClusterSettings.TCPKeepaliveMinIdleTime); err != nil { + panic(err) + } + } + if in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.ClusterSettings.TCPKeepaliveMinInterval == nil { + if err := json.Unmarshal([]byte(`"30s"`), &in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.ClusterSettings.TCPKeepaliveMinInterval); err != nil { + panic(err) + } + } + if in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.ClusterSettings.TCPMaxConnectionTimeout == nil { + if err := json.Unmarshal([]byte(`"10s"`), &in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.ClusterSettings.TCPMaxConnectionTimeout); err != nil { + panic(err) + } + } + if in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.ClusterSettings.HTTPMaxConnectionIdleTimeout == nil { + if err := json.Unmarshal([]byte(`"1h"`), &in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.ClusterSettings.HTTPMaxConnectionIdleTimeout); err != nil { + panic(err) + } + } + if in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.ClusterSettings.HTTPMaxConnectionDuration == nil { + if err := json.Unmarshal([]byte(`"1h"`), &in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.ClusterSettings.HTTPMaxConnectionDuration); err != nil { + panic(err) + } + } + if in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.ClusterSettings.HTTPMaxRequestTimeout == nil { + if err := json.Unmarshal([]byte(`"1h"`), &in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.ClusterSettings.HTTPMaxRequestTimeout); err != nil { + panic(err) + } + } + if in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.ClusterSettings.ConnectionMaxBufferLimit == nil { + if err := json.Unmarshal([]byte(`"512Ki"`), &in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.ClusterSettings.ConnectionMaxBufferLimit); err != nil { + panic(err) + } + } + if in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.ClusterSettings.DNSMinRefreshRate == nil { + if err := json.Unmarshal([]byte(`"30s"`), &in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.ClusterSettings.DNSMinRefreshRate); err != nil { + panic(err) + } + } + if in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.ClusterSettings.HTTP2MaxInitialStreamWindowSize == nil { + if err := json.Unmarshal([]byte(`"64Ki"`), &in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.ClusterSettings.HTTP2MaxInitialStreamWindowSize); err != nil { + panic(err) + } + } + if in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.ClusterSettings.HTTP2MaxInitialConnectionWindowSize == nil { + if err := json.Unmarshal([]byte(`"1Mi"`), &in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.ClusterSettings.HTTP2MaxInitialConnectionWindowSize); err != nil { + panic(err) + } + } + if in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.ClusterSettings.HTTP2MaxConcurrentStreams == 0 { + in.Gateway.ExtensionAPIValidationOptions.SecurityPolicies.ClusterSettings.HTTP2MaxConcurrentStreams = 1024 + } SetDefaults_GatewayResourceReplicatorConfig(&in.GatewayResourceReplicator) if in.HTTPProxy.GatewayClassName == "" { in.HTTPProxy.GatewayClassName = "datum-external-global-proxy" diff --git a/internal/validation/backendtrafficpolicy_validation.go b/internal/validation/backendtrafficpolicy_validation.go index 044cdcf4..f4cbc048 100644 --- a/internal/validation/backendtrafficpolicy_validation.go +++ b/internal/validation/backendtrafficpolicy_validation.go @@ -76,12 +76,12 @@ func validateGatewayClusterSettingsTCPKeepalive(tcpKeepalive *envoygatewayv1alph if v := tcpKeepalive.IdleTime; v != nil { idleTimeFieldPath := fldPath.Child("idleTime") - allErrs = append(allErrs, validateGatewayDuration(idleTimeFieldPath, v, ptr.To(opts.TCPKeepaliveMinIdleTime), nil)...) + allErrs = append(allErrs, validateGatewayDuration(idleTimeFieldPath, v, ptr.To(opts.TCPKeepaliveMinIdleTime.Duration), nil)...) } if v := tcpKeepalive.Interval; v != nil { intervalFieldPath := fldPath.Child("interval") - allErrs = append(allErrs, validateGatewayDuration(intervalFieldPath, v, ptr.To(opts.TCPKeepaliveMinInterval), nil)...) + allErrs = append(allErrs, validateGatewayDuration(intervalFieldPath, v, ptr.To(opts.TCPKeepaliveMinInterval.Duration), nil)...) } return allErrs @@ -111,7 +111,7 @@ func validateGatewayClusterSettingsTimeout(timeout *envoygatewayv1alpha1.Timeout if timeout.TCP != nil { if v := timeout.TCP.ConnectTimeout; v != nil { connectTimeoutFieldPath := fldPath.Child("tcp").Child("connectTimeout") - allErrs = append(allErrs, validateGatewayDuration(connectTimeoutFieldPath, v, nil, ptr.To(opts.TCPMaxConnectionTimeout))...) + allErrs = append(allErrs, validateGatewayDuration(connectTimeoutFieldPath, v, nil, ptr.To(opts.TCPMaxConnectionTimeout.Duration))...) } } @@ -120,17 +120,17 @@ func validateGatewayClusterSettingsTimeout(timeout *envoygatewayv1alpha1.Timeout if v := timeout.HTTP.ConnectionIdleTimeout; v != nil { connectionIdleTimeoutFieldPath := httpTimeoutFieldPath.Child("connectionIdleTimeout") - allErrs = append(allErrs, validateGatewayDuration(connectionIdleTimeoutFieldPath, v, nil, ptr.To(opts.HTTPMaxConnectionIdleTimeout))...) + allErrs = append(allErrs, validateGatewayDuration(connectionIdleTimeoutFieldPath, v, nil, ptr.To(opts.HTTPMaxConnectionIdleTimeout.Duration))...) } if v := timeout.HTTP.MaxConnectionDuration; v != nil { maxConnectionDurationFieldPath := httpTimeoutFieldPath.Child("maxConnectionDuration") - allErrs = append(allErrs, validateGatewayDuration(maxConnectionDurationFieldPath, v, nil, ptr.To(opts.HTTPMaxConnectionDuration))...) + allErrs = append(allErrs, validateGatewayDuration(maxConnectionDurationFieldPath, v, nil, ptr.To(opts.HTTPMaxConnectionDuration.Duration))...) } if v := timeout.HTTP.RequestTimeout; v != nil { requestTimeoutFieldPath := httpTimeoutFieldPath.Child("requestTimeout") - allErrs = append(allErrs, validateGatewayDuration(requestTimeoutFieldPath, v, nil, ptr.To(opts.HTTPMaxRequestTimeout))...) + allErrs = append(allErrs, validateGatewayDuration(requestTimeoutFieldPath, v, nil, ptr.To(opts.HTTPMaxRequestTimeout.Duration))...) } } @@ -143,7 +143,7 @@ func validateGatewayClusterSettingsConnection(connection *envoygatewayv1alpha1.B return nil } - if connection.BufferLimit != nil && connection.BufferLimit.Cmp(opts.ConnectionMaxBufferLimit) == 1 { + if connection.BufferLimit != nil && connection.BufferLimit.Cmp(*opts.ConnectionMaxBufferLimit) == 1 { return field.ErrorList{ field.Invalid(fldPath.Child("bufferLimit"), connection.BufferLimit.String(), fmt.Sprintf("must be less than or equal to %s", opts.ConnectionMaxBufferLimit.String())), } @@ -161,7 +161,7 @@ func validateGatewayClusterSettingsDNS(dns *envoygatewayv1alpha1.DNS, fldPath *f if v := dns.DNSRefreshRate; v != nil { dnsRefreshRateFieldPath := fldPath.Child("dnsRefreshRate") - allErrs = append(allErrs, validateGatewayDuration(dnsRefreshRateFieldPath, v, ptr.To(opts.DNSMinRefreshRate), nil)...) + allErrs = append(allErrs, validateGatewayDuration(dnsRefreshRateFieldPath, v, ptr.To(opts.DNSMinRefreshRate.Duration), nil)...) } if dns.RespectDNSTTL != nil && !*dns.RespectDNSTTL { @@ -178,13 +178,13 @@ func validateGatewayClusterSettingsHTTP2(http2 *envoygatewayv1alpha1.HTTP2Settin allErrs := field.ErrorList{} if http2.InitialStreamWindowSize != nil { - if http2.InitialStreamWindowSize.Cmp(opts.HTTP2MaxInitialStreamWindowSize) == 1 { + if http2.InitialStreamWindowSize.Cmp(*opts.HTTP2MaxInitialStreamWindowSize) == 1 { allErrs = append(allErrs, field.Invalid(fldPath.Child("initialStreamWindowSize"), http2.InitialStreamWindowSize.String(), fmt.Sprintf("must be less than or equal to %s", opts.HTTP2MaxInitialStreamWindowSize.String()))) } } if http2.InitialConnectionWindowSize != nil { - if http2.InitialConnectionWindowSize.Cmp(opts.HTTP2MaxInitialConnectionWindowSize) == 1 { + if http2.InitialConnectionWindowSize.Cmp(*opts.HTTP2MaxInitialConnectionWindowSize) == 1 { allErrs = append(allErrs, field.Invalid(fldPath.Child("initialConnectionWindowSize"), http2.InitialConnectionWindowSize.String(), fmt.Sprintf("must be less than or equal to %s", opts.HTTP2MaxInitialConnectionWindowSize.String()))) } } diff --git a/internal/validation/backendtrafficpolicy_validation_test.go b/internal/validation/backendtrafficpolicy_validation_test.go index 0b44b5fa..eb2adc04 100644 --- a/internal/validation/backendtrafficpolicy_validation_test.go +++ b/internal/validation/backendtrafficpolicy_validation_test.go @@ -8,6 +8,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/utils/ptr" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" @@ -19,7 +20,7 @@ import ( func TestValidateBackendTrafficPolicy(t *testing.T) { scenarios := map[string]struct { backendTrafficPolicy *envoygatewayv1alpha1.BackendTrafficPolicy - opts config.BackendTrafficPolicyValidationOptions + opts func(*config.BackendTrafficPolicyValidationOptions) expectedErrors field.ErrorList }{ "spec.targetRef forbidden": { @@ -76,10 +77,8 @@ func TestValidateBackendTrafficPolicy(t *testing.T) { }, }, }, - opts: config.BackendTrafficPolicyValidationOptions{ - ClusterSettings: config.ClusterSettingsValidationOptions{ - TCPKeepaliveMinProbes: 2, - }, + opts: func(cfg *config.BackendTrafficPolicyValidationOptions) { + cfg.ClusterSettings.TCPKeepaliveMinProbes = 2 }, expectedErrors: field.ErrorList{ field.Invalid(field.NewPath("spec", "tcpKeepalive", "probes"), int32(0), ""), @@ -95,10 +94,8 @@ func TestValidateBackendTrafficPolicy(t *testing.T) { }, }, }, - opts: config.BackendTrafficPolicyValidationOptions{ - ClusterSettings: config.ClusterSettingsValidationOptions{ - TCPKeepaliveMinIdleTime: 1 * time.Minute, - }, + opts: func(cfg *config.BackendTrafficPolicyValidationOptions) { + cfg.ClusterSettings.TCPKeepaliveMinIdleTime = &metav1.Duration{Duration: 1 * time.Minute} }, expectedErrors: field.ErrorList{ field.Invalid(field.NewPath("spec", "tcpKeepalive", "idleTime"), "4m0s", ""), @@ -114,10 +111,8 @@ func TestValidateBackendTrafficPolicy(t *testing.T) { }, }, }, - opts: config.BackendTrafficPolicyValidationOptions{ - ClusterSettings: config.ClusterSettingsValidationOptions{ - TCPKeepaliveMinInterval: 1 * time.Minute, - }, + opts: func(cfg *config.BackendTrafficPolicyValidationOptions) { + cfg.ClusterSettings.TCPKeepaliveMinInterval = &metav1.Duration{Duration: 1 * time.Minute} }, expectedErrors: field.ErrorList{ field.Invalid(field.NewPath("spec", "tcpKeepalive", "interval"), "4m0s", ""), @@ -165,10 +160,8 @@ func TestValidateBackendTrafficPolicy(t *testing.T) { }, }, }, - opts: config.BackendTrafficPolicyValidationOptions{ - ClusterSettings: config.ClusterSettingsValidationOptions{ - HTTPMaxConnectionIdleTimeout: 1 * time.Hour, - }, + opts: func(cfg *config.BackendTrafficPolicyValidationOptions) { + cfg.ClusterSettings.HTTPMaxConnectionIdleTimeout = &metav1.Duration{Duration: 1 * time.Hour} }, expectedErrors: field.ErrorList{ field.Invalid(field.NewPath("spec", "timeout", "http", "connectionIdleTimeout"), "", ""), @@ -186,10 +179,8 @@ func TestValidateBackendTrafficPolicy(t *testing.T) { }, }, }, - opts: config.BackendTrafficPolicyValidationOptions{ - ClusterSettings: config.ClusterSettingsValidationOptions{ - HTTPMaxConnectionDuration: 1 * time.Hour, - }, + opts: func(cfg *config.BackendTrafficPolicyValidationOptions) { + cfg.ClusterSettings.HTTPMaxConnectionDuration = &metav1.Duration{Duration: 1 * time.Hour} }, expectedErrors: field.ErrorList{ field.Invalid(field.NewPath("spec", "timeout", "http", "maxConnectionDuration"), "", ""), @@ -207,10 +198,8 @@ func TestValidateBackendTrafficPolicy(t *testing.T) { }, }, }, - opts: config.BackendTrafficPolicyValidationOptions{ - ClusterSettings: config.ClusterSettingsValidationOptions{ - HTTPMaxRequestTimeout: 1 * time.Hour, - }, + opts: func(cfg *config.BackendTrafficPolicyValidationOptions) { + cfg.ClusterSettings.HTTPMaxRequestTimeout = &metav1.Duration{Duration: 1 * time.Hour} }, expectedErrors: field.ErrorList{ field.Invalid(field.NewPath("spec", "timeout", "http", "requestTimeout"), "", ""), @@ -226,10 +215,8 @@ func TestValidateBackendTrafficPolicy(t *testing.T) { }, }, }, - opts: config.BackendTrafficPolicyValidationOptions{ - ClusterSettings: config.ClusterSettingsValidationOptions{ - ConnectionMaxBufferLimit: resource.MustParse("512Ki"), - }, + opts: func(cfg *config.BackendTrafficPolicyValidationOptions) { + cfg.ClusterSettings.ConnectionMaxBufferLimit = ptr.To(resource.MustParse("512Ki")) }, expectedErrors: field.ErrorList{ field.Invalid(field.NewPath("spec", "connection", "bufferLimit"), "", ""), @@ -245,10 +232,8 @@ func TestValidateBackendTrafficPolicy(t *testing.T) { }, }, }, - opts: config.BackendTrafficPolicyValidationOptions{ - ClusterSettings: config.ClusterSettingsValidationOptions{ - DNSMinRefreshRate: 1 * time.Second, - }, + opts: func(cfg *config.BackendTrafficPolicyValidationOptions) { + cfg.ClusterSettings.DNSMinRefreshRate = &metav1.Duration{Duration: 1 * time.Second} }, expectedErrors: field.ErrorList{ field.Invalid(field.NewPath("spec", "dns", "dnsRefreshRate"), "", ""), @@ -338,7 +323,15 @@ func TestValidateBackendTrafficPolicy(t *testing.T) { for name, scenario := range scenarios { t.Run(name, func(t *testing.T) { - errs := ValidateBackendTrafficPolicy(scenario.backendTrafficPolicy, scenario.opts) + opts := &config.NetworkServicesOperator{} + config.SetObjectDefaults_NetworkServicesOperator(opts) + backendTrafficPolicyOpts := opts.Gateway.ExtensionAPIValidationOptions.BackendTrafficPolicies + + if scenario.opts != nil { + scenario.opts(&backendTrafficPolicyOpts) + } + + errs := ValidateBackendTrafficPolicy(scenario.backendTrafficPolicy, backendTrafficPolicyOpts) delta := cmp.Diff(scenario.expectedErrors, errs, cmpopts.IgnoreFields(field.Error{}, "BadValue", "Detail")) if delta != "" { t.Errorf("Testcase %s - expected errors '%v', got '%v', diff: '%v'", name, scenario.expectedErrors, errs, delta) diff --git a/internal/validation/httproutefilter_validation_test.go b/internal/validation/httproutefilter_validation_test.go index 4cea7cec..39223afc 100644 --- a/internal/validation/httproutefilter_validation_test.go +++ b/internal/validation/httproutefilter_validation_test.go @@ -15,7 +15,7 @@ import ( func TestValidateHTTPRouteFilter(t *testing.T) { scenarios := map[string]struct { httpRouteFilter *envoygatewayv1alpha1.HTTPRouteFilter - opts config.HTTPRouteFilterValidationOptions + opts func(*config.HTTPRouteFilterValidationOptions) expectedErrors field.ErrorList }{ "direct response - inline body too large": { @@ -28,8 +28,8 @@ func TestValidateHTTPRouteFilter(t *testing.T) { }, }, }, - opts: config.HTTPRouteFilterValidationOptions{ - MaxInlineBodySize: 1, + opts: func(cfg *config.HTTPRouteFilterValidationOptions) { + cfg.MaxInlineBodySize = 1 }, expectedErrors: field.ErrorList{ field.TooLong(field.NewPath("spec", "directResponse", "body", "inline"), 0, 0), @@ -39,7 +39,15 @@ func TestValidateHTTPRouteFilter(t *testing.T) { for name, scenario := range scenarios { t.Run(name, func(t *testing.T) { - errs := ValidateHTTPRouteFilter(scenario.httpRouteFilter, scenario.opts) + opts := &config.NetworkServicesOperator{} + config.SetObjectDefaults_NetworkServicesOperator(opts) + httpRouteFilterOpts := opts.Gateway.ExtensionAPIValidationOptions.HTTPRouteFilters + + if scenario.opts != nil { + scenario.opts(&httpRouteFilterOpts) + } + + errs := ValidateHTTPRouteFilter(scenario.httpRouteFilter, httpRouteFilterOpts) delta := cmp.Diff(scenario.expectedErrors, errs, cmpopts.IgnoreFields(field.Error{}, "BadValue", "Detail")) if delta != "" { t.Errorf("Testcase %s - expected errors '%v', got '%v', diff: '%v'", name, scenario.expectedErrors, errs, delta) diff --git a/internal/validation/securitypolicy_validation.go b/internal/validation/securitypolicy_validation.go index 767960b1..9d4985f7 100644 --- a/internal/validation/securitypolicy_validation.go +++ b/internal/validation/securitypolicy_validation.go @@ -132,7 +132,7 @@ func validateSecurityPolicyOIDC(oidc *envoygatewayv1alpha1.OIDC, fldPath *field. } if oidc.DefaultRefreshTokenTTL != nil { - allErrs = append(allErrs, validateGatewayDuration(fldPath.Child("defaultRefreshTokenTTL"), oidc.DefaultRefreshTokenTTL, &opts.OIDC.MinRefreshTokenTTL, nil)...) + allErrs = append(allErrs, validateGatewayDuration(fldPath.Child("defaultRefreshTokenTTL"), oidc.DefaultRefreshTokenTTL, &opts.OIDC.MinRefreshTokenTTL.Duration, nil)...) } return allErrs diff --git a/internal/validation/securitypolicy_validation_test.go b/internal/validation/securitypolicy_validation_test.go index c380351a..a2b7e249 100644 --- a/internal/validation/securitypolicy_validation_test.go +++ b/internal/validation/securitypolicy_validation_test.go @@ -7,6 +7,7 @@ import ( envoygatewayv1alpha1 "github.com/envoyproxy/gateway/api/v1alpha1" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/utils/ptr" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" @@ -18,7 +19,7 @@ import ( func TestValidateSecurityPolicy(t *testing.T) { scenarios := map[string]struct { securityPolicy *envoygatewayv1alpha1.SecurityPolicy - opts config.SecurityPolicyValidationOptions + opts func(*config.SecurityPolicyValidationOptions) expectedErrors field.ErrorList }{ "spec.targetRef forbidden": { @@ -51,10 +52,8 @@ func TestValidateSecurityPolicy(t *testing.T) { }, }, }, - opts: config.SecurityPolicyValidationOptions{ - APIKeyAuth: config.APIKeyAuthValidationOptions{ - MaxCredentialRefs: 1, - }, + opts: func(cfg *config.SecurityPolicyValidationOptions) { + cfg.APIKeyAuth.MaxCredentialRefs = 1 }, expectedErrors: field.ErrorList{ field.TooMany(field.NewPath("spec", "apiKeyAuth", "credentialRefs"), 2, 1), @@ -72,10 +71,8 @@ func TestValidateSecurityPolicy(t *testing.T) { }, }, }, - opts: config.SecurityPolicyValidationOptions{ - APIKeyAuth: config.APIKeyAuthValidationOptions{ - MaxExtractFrom: 1, - }, + opts: func(cfg *config.SecurityPolicyValidationOptions) { + cfg.APIKeyAuth.MaxExtractFrom = 1 }, expectedErrors: field.ErrorList{ field.TooMany(field.NewPath("spec", "apiKeyAuth", "extractFrom"), 2, 1), @@ -95,11 +92,9 @@ func TestValidateSecurityPolicy(t *testing.T) { }, }, }, - opts: config.SecurityPolicyValidationOptions{ - APIKeyAuth: config.APIKeyAuthValidationOptions{ - MaxExtractFrom: 1, - MaxExtractFromFieldLength: 1, - }, + opts: func(cfg *config.SecurityPolicyValidationOptions) { + cfg.APIKeyAuth.MaxExtractFrom = 1 + cfg.APIKeyAuth.MaxExtractFromFieldLength = 1 }, expectedErrors: field.ErrorList{ field.TooMany(field.NewPath("spec", "apiKeyAuth", "extractFrom").Index(0).Child("headers"), 0, 0), @@ -115,10 +110,8 @@ func TestValidateSecurityPolicy(t *testing.T) { }, }, }, - opts: config.SecurityPolicyValidationOptions{ - APIKeyAuth: config.APIKeyAuthValidationOptions{ - MaxForwardClientIDHeaderLength: 1, - }, + opts: func(cfg *config.SecurityPolicyValidationOptions) { + cfg.APIKeyAuth.MaxForwardClientIDHeaderLength = 1 }, expectedErrors: field.ErrorList{ field.TooLong(field.NewPath("spec", "apiKeyAuth", "forwardClientIDHeader"), 0, 0), @@ -135,10 +128,8 @@ func TestValidateSecurityPolicy(t *testing.T) { }, }, }, - opts: config.SecurityPolicyValidationOptions{ - CORS: config.CORSValidationOptions{ - MaxFieldLength: 1, - }, + opts: func(cfg *config.SecurityPolicyValidationOptions) { + cfg.CORS.MaxFieldLength = 1 }, expectedErrors: field.ErrorList{ field.TooMany(field.NewPath("spec", "cors", "allowOrigins"), 3, 2), @@ -248,11 +239,9 @@ func TestValidateSecurityPolicy(t *testing.T) { }, }, }, - opts: config.SecurityPolicyValidationOptions{ - JWTProvider: config.JWTProviderValidationOptions{ - MaxClaimToHeaders: 1, - MaxExtractorLength: 2, - }, + opts: func(cfg *config.SecurityPolicyValidationOptions) { + cfg.JWTProvider.MaxClaimToHeaders = 1 + cfg.JWTProvider.MaxExtractorLength = 2 }, expectedErrors: field.ErrorList{ field.TooMany(field.NewPath("spec", "jwt", "providers").Index(0).Child("claimToHeaders"), 2, 1), @@ -309,12 +298,10 @@ func TestValidateSecurityPolicy(t *testing.T) { }, }, }, - opts: config.SecurityPolicyValidationOptions{ - OIDC: config.OIDCValidationOptions{ - MaxScopes: 2, - MaxResources: 2, - MinRefreshTokenTTL: 1 * time.Minute, - }, + opts: func(cfg *config.SecurityPolicyValidationOptions) { + cfg.OIDC.MaxScopes = 2 + cfg.OIDC.MaxResources = 2 + cfg.OIDC.MinRefreshTokenTTL = &metav1.Duration{Duration: 1 * time.Minute} }, expectedErrors: field.ErrorList{ field.TooMany(field.NewPath("spec", "oidc", "scopes"), 0, 0), @@ -333,10 +320,8 @@ func TestValidateSecurityPolicy(t *testing.T) { }, }, }, - opts: config.SecurityPolicyValidationOptions{ - Authorization: config.AuthorizationValidationOptions{ - MaxRules: 1, - }, + opts: func(cfg *config.SecurityPolicyValidationOptions) { + cfg.Authorization.MaxRules = 1 }, expectedErrors: field.ErrorList{ field.TooMany(field.NewPath("spec", "authorization", "rules"), 2, 1), @@ -360,11 +345,9 @@ func TestValidateSecurityPolicy(t *testing.T) { }, }, }, - opts: config.SecurityPolicyValidationOptions{ - Authorization: config.AuthorizationValidationOptions{ - MaxRules: 1, - MaxClientCIDRs: 2, - }, + opts: func(cfg *config.SecurityPolicyValidationOptions) { + cfg.Authorization.MaxRules = 1 + cfg.Authorization.MaxClientCIDRs = 2 }, expectedErrors: field.ErrorList{ field.TooMany(field.NewPath("spec", "authorization", "rules").Index(0).Child("principal", "clientCIDRs"), 3, 2), @@ -374,7 +357,15 @@ func TestValidateSecurityPolicy(t *testing.T) { for name, scenario := range scenarios { t.Run(name, func(t *testing.T) { - errs := ValidateSecurityPolicy(scenario.securityPolicy, scenario.opts) + opts := &config.NetworkServicesOperator{} + config.SetObjectDefaults_NetworkServicesOperator(opts) + securityPolicyOpts := opts.Gateway.ExtensionAPIValidationOptions.SecurityPolicies + + if scenario.opts != nil { + scenario.opts(&securityPolicyOpts) + } + + errs := ValidateSecurityPolicy(scenario.securityPolicy, securityPolicyOpts) delta := cmp.Diff(scenario.expectedErrors, errs, cmpopts.IgnoreFields(field.Error{}, "BadValue", "Detail")) if delta != "" { t.Errorf("Testcase %s - expected errors '%v', got '%v', diff: '%v'", name, scenario.expectedErrors, errs, delta)