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..4c8f8398 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,199 @@ 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 *metav1.Duration + + // Minimum amount for the duration between keep-alive probes. + // + // +default="30s" + TCPKeepaliveMinInterval *metav1.Duration + + // Maximum time allowed for a connection timeout + // + // +default="10s" + TCPMaxConnectionTimeout *metav1.Duration + + // Maximum amount for the duration a connection can be idle. + // + // +default="1h" + HTTPMaxConnectionIdleTimeout *metav1.Duration + + // Maximum amount for the duration of a connection. + // + // +default="1h" + HTTPMaxConnectionDuration *metav1.Duration + + // Maximum amount for the duration until an entire request is received by the + // upstream. + // + // +default="1h" + HTTPMaxRequestTimeout *metav1.Duration + + // Maximum size for upstream connection buffers + // + // +default="512Ki" + ConnectionMaxBufferLimit *resource.Quantity + + // Minimum amount for the duration between DNS refreshes. + // + // +default="30s" + DNSMinRefreshRate *metav1.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 +} + +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 +} + +// +k8s:deepcopy-gen=true + +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="5m" + MinRefreshTokenTTL *metav1.Duration +} + +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..d3144681 100644 --- a/internal/config/zz_generated.deepcopy.go +++ b/internal/config/zz_generated.deepcopy.go @@ -14,6 +14,87 @@ 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 + 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. +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 +167,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 +244,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. @@ -247,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 @@ -288,6 +408,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 + in.OIDC.DeepCopyInto(&out.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..a1a69cc3 100644 --- a/internal/config/zz_generated.defaults.go +++ b/internal/config/zz_generated.defaults.go @@ -42,6 +42,159 @@ func SetObjectDefaults_NetworkServicesOperator(in *NetworkServicesOperator) { panic(err) } } + 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 + } + 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 + } + 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 + } + 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/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..f4cbc048 100644 --- a/internal/validation/backendtrafficpolicy_validation.go +++ b/internal/validation/backendtrafficpolicy_validation.go @@ -2,17 +2,18 @@ package validation import ( "fmt" + "strconv" "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") @@ -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, opts.ClusterSettings)...) + 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, opts config.ClusterSettingsValidationOptions) 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"), opts)...) + allErrs = append(allErrs, validateGatewayClusterSettingsHealthCheck(clusterSettings.HealthCheck, fldPath.Child("healthCheck"))...) + 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 } -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,31 +63,31 @@ func validateBackendTrafficPolicyLoadBalancer(loadBalancer *envoygatewayv1alpha1 return allErrs } -func validateBackendTrafficPolicyTCPKeepalive(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("probe"), *tcpKeepalive.Probes, "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.Duration), 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.Duration), nil)...) } 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, opts config.ClusterSettingsValidationOptions) 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(opts.TCPMaxConnectionTimeout.Duration))...) } } @@ -111,17 +120,17 @@ func validateBackendTrafficPolicyTimeout(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.Duration))...) } 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.Duration))...) } 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.Duration))...) } } @@ -129,22 +138,21 @@ 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, 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 validateBackendTrafficPolicyDNS(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 } @@ -153,7 +161,7 @@ func validateBackendTrafficPolicyDNS(dns *envoygatewayv1alpha1.DNS, fldPath *fie 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.Duration), nil)...) } if dns.RespectDNSTTL != nil && !*dns.RespectDNSTTL { @@ -163,30 +171,27 @@ 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, 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 22ab3c5f..eb2adc04 100644 --- a/internal/validation/backendtrafficpolicy_validation_test.go +++ b/internal/validation/backendtrafficpolicy_validation_test.go @@ -2,19 +2,25 @@ 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/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" 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 func(*config.BackendTrafficPolicyValidationOptions) expectedErrors field.ErrorList }{ "spec.targetRef forbidden": { @@ -31,11 +37,301 @@ 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)), + }, + }, + }, + }, + opts: func(cfg *config.BackendTrafficPolicyValidationOptions) { + cfg.ClusterSettings.TCPKeepaliveMinProbes = 2 + }, + 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")), + }, + }, + }, + }, + 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", ""), + }, + }, + "tcp interval too low": { + backendTrafficPolicy: &envoygatewayv1alpha1.BackendTrafficPolicy{ + Spec: envoygatewayv1alpha1.BackendTrafficPolicySpec{ + ClusterSettings: envoygatewayv1alpha1.ClusterSettings{ + TCPKeepalive: &envoygatewayv1alpha1.TCPKeepalive{ + Interval: ptr.To(gatewayv1.Duration("5s")), + }, + }, + }, + }, + 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", ""), + }, + }, + "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")), + }, + }, + }, + }, + }, + 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"), "", ""), + }, + }, + "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")), + }, + }, + }, + }, + }, + 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"), "", ""), + }, + }, + "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")), + }, + }, + }, + }, + }, + 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"), "", ""), + }, + }, + "connection buffer limit too high": { + backendTrafficPolicy: &envoygatewayv1alpha1.BackendTrafficPolicy{ + Spec: envoygatewayv1alpha1.BackendTrafficPolicySpec{ + ClusterSettings: envoygatewayv1alpha1.ClusterSettings{ + Connection: &envoygatewayv1alpha1.BackendConnection{ + BufferLimit: ptr.To(resource.MustParse("2Gi")), + }, + }, + }, + }, + opts: func(cfg *config.BackendTrafficPolicyValidationOptions) { + cfg.ClusterSettings.ConnectionMaxBufferLimit = ptr.To(resource.MustParse("512Ki")) + }, + 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")), + }, + }, + }, + }, + opts: func(cfg *config.BackendTrafficPolicyValidationOptions) { + cfg.ClusterSettings.DNSMinRefreshRate = &metav1.Duration{Duration: 1 * time.Second} + }, + 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 { t.Run(name, func(t *testing.T) { - errs := ValidateBackendTrafficPolicy(scenario.backendTrafficPolicy) + 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.go b/internal/validation/httproutefilter_validation.go index 55acc648..b222442e 100644 --- a/internal/validation/httproutefilter_validation.go +++ b/internal/validation/httproutefilter_validation.go @@ -3,8 +3,22 @@ package validation import ( envoygatewayv1alpha1 "github.com/envoyproxy/gateway/api/v1alpha1" "k8s.io/apimachinery/pkg/util/validation/field" + + "go.datum.net/network-services-operator/internal/config" ) -func ValidateHTTPRouteFilter(httpRouteFilter *envoygatewayv1alpha1.HTTPRouteFilter) field.ErrorList { - return nil +func ValidateHTTPRouteFilter(httpRouteFilter *envoygatewayv1alpha1.HTTPRouteFilter, opts config.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..39223afc --- /dev/null +++ b/internal/validation/httproutefilter_validation_test.go @@ -0,0 +1,57 @@ +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" + + "go.datum.net/network-services-operator/internal/config" +) + +func TestValidateHTTPRouteFilter(t *testing.T) { + scenarios := map[string]struct { + httpRouteFilter *envoygatewayv1alpha1.HTTPRouteFilter + opts func(*config.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: func(cfg *config.HTTPRouteFilterValidationOptions) { + cfg.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) { + 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 fc7492c9..9d4985f7 100644 --- a/internal/validation/securitypolicy_validation.go +++ b/internal/validation/securitypolicy_validation.go @@ -3,8 +3,296 @@ package validation import ( envoygatewayv1alpha1 "github.com/envoyproxy/gateway/api/v1alpha1" "k8s.io/apimachinery/pkg/util/validation/field" + gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" + + "go.datum.net/network-services-operator/internal/config" ) -func ValidateSecurityPolicy(securityPolicy *envoygatewayv1alpha1.SecurityPolicy) field.ErrorList { - return nil +func ValidateSecurityPolicy(securityPolicy *envoygatewayv1alpha1.SecurityPolicy, opts config.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 config.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 config.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 config.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 config.SecurityPolicyValidationOptions) field.ErrorList { + if oidc == nil { + return nil + } + + allErrs := field.ErrorList{} + 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"))...) + + 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.Duration, nil)...) + } + + return allErrs +} + +func validateSecurityPolicyAuthorization(authorization *envoygatewayv1alpha1.Authorization, fldPath *field.Path, opts config.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 config.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, opts config.SecurityPolicyValidationOptions) field.ErrorList { + allErrs := field.ErrorList{} + + allErrs = append(allErrs, validateGatewayBackendCluster(provider.BackendCluster, fldPath, opts)...) + + return allErrs +} + +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"), opts)...) + + 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, opts config.SecurityPolicyValidationOptions) field.ErrorList { + if remoteJWKS == nil { + return nil + } + + allErrs := field.ErrorList{} + + allErrs = append(allErrs, validateGatewayBackendCluster(remoteJWKS.BackendCluster, fldPath, opts)...) + + return allErrs +} + +func validateGatewayBackendCluster(backendCluster envoygatewayv1alpha1.BackendCluster, fldPath *field.Path, opts config.SecurityPolicyValidationOptions) 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"), opts.ClusterSettings)...) + } + + 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 config.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 config.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..a2b7e249 --- /dev/null +++ b/internal/validation/securitypolicy_validation_test.go @@ -0,0 +1,375 @@ +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" + 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" + 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 func(*config.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: func(cfg *config.SecurityPolicyValidationOptions) { + cfg.APIKeyAuth.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: func(cfg *config.SecurityPolicyValidationOptions) { + cfg.APIKeyAuth.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: 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), + 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: func(cfg *config.SecurityPolicyValidationOptions) { + cfg.APIKeyAuth.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: func(cfg *config.SecurityPolicyValidationOptions) { + cfg.CORS.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: 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), + 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: 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), + 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: func(cfg *config.SecurityPolicyValidationOptions) { + cfg.Authorization.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: 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), + }, + }, + } + + for name, scenario := range scenarios { + t.Run(name, func(t *testing.T) { + 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) + } + }) + } +} 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 18f65228..247e30d4 100644 --- a/internal/webhook/v1alpha1/httproutefilter_webhook.go +++ b/internal/webhook/v1alpha1/httproutefilter_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" ) // SetupHTTPRouteFilterWebhookWithManager registers the webhook for HTTPRouteFilter in the manager. -func SetupHTTPRouteFilterWebhookWithManager(mgr mcmanager.Manager) error { +func SetupHTTPRouteFilterWebhookWithManager(mgr mcmanager.Manager, cfg config.NetworkServicesOperator) error { return ctrl.NewWebhookManagedBy(mgr.GetLocalManager()).For(&gatewayv1alpha1.HTTPRouteFilter{}). - WithValidator(&HTTPRouteFilterCustomValidator{mgr: mgr}). + WithValidator(&HTTPRouteFilterCustomValidator{mgr: mgr, validationOpts: cfg.Gateway.ExtensionAPIValidationOptions.HTTPRouteFilters}). 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 config.HTTPRouteFilterValidationOptions } var _ webhook.CustomValidator = &HTTPRouteFilterCustomValidator{} @@ -49,7 +51,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 +73,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..14c24fcd 100644 --- a/internal/webhook/v1alpha1/securitypolicy_webhook.go +++ b/internal/webhook/v1alpha1/securitypolicy_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" ) // SetupSecurityPolicyWebhookWithManager registers the webhook for SecurityPolicy in the manager. -func SetupSecurityPolicyWebhookWithManager(mgr mcmanager.Manager) error { +func SetupSecurityPolicyWebhookWithManager(mgr mcmanager.Manager, cfg config.NetworkServicesOperator) error { return ctrl.NewWebhookManagedBy(mgr.GetLocalManager()).For(&gatewayv1alpha1.SecurityPolicy{}). - WithValidator(&SecurityPolicyCustomValidator{mgr: mgr}). + WithValidator(&SecurityPolicyCustomValidator{mgr: mgr, validationOpts: cfg.Gateway.ExtensionAPIValidationOptions.SecurityPolicies}). 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 config.SecurityPolicyValidationOptions } var _ webhook.CustomValidator = &SecurityPolicyCustomValidator{} @@ -49,7 +51,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 +73,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) }