diff --git a/api/envoy/extensions/filters/http/local_ratelimit/v3/BUILD b/api/envoy/extensions/filters/http/local_ratelimit/v3/BUILD index 1ef2f0c9bf47..ac9fd7c8abe8 100644 --- a/api/envoy/extensions/filters/http/local_ratelimit/v3/BUILD +++ b/api/envoy/extensions/filters/http/local_ratelimit/v3/BUILD @@ -7,6 +7,7 @@ licenses(["notice"]) # Apache 2 api_proto_package( deps = [ "//envoy/config/core/v3:pkg", + "//envoy/config/route/v3:pkg", "//envoy/extensions/common/ratelimit/v3:pkg", "//envoy/type/v3:pkg", "@com_github_cncf_xds//udpa/annotations:pkg", diff --git a/api/envoy/extensions/filters/http/local_ratelimit/v3/local_rate_limit.proto b/api/envoy/extensions/filters/http/local_ratelimit/v3/local_rate_limit.proto index a32475f352f3..82e38ed91d5a 100644 --- a/api/envoy/extensions/filters/http/local_ratelimit/v3/local_rate_limit.proto +++ b/api/envoy/extensions/filters/http/local_ratelimit/v3/local_rate_limit.proto @@ -3,6 +3,7 @@ syntax = "proto3"; package envoy.extensions.filters.http.local_ratelimit.v3; import "envoy/config/core/v3/base.proto"; +import "envoy/config/route/v3/route_components.proto"; import "envoy/extensions/common/ratelimit/v3/ratelimit.proto"; import "envoy/type/v3/http_status.proto"; import "envoy/type/v3/token_bucket.proto"; @@ -22,7 +23,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // Local Rate limit :ref:`configuration overview `. // [#extension: envoy.filters.http.local_ratelimit] -// [#next-free-field: 17] +// [#next-free-field: 18] message LocalRateLimit { // The human readable prefix to use when emitting stats. string stat_prefix = 1 [(validate.rules).string = {min_len: 1}]; @@ -147,4 +148,23 @@ message LocalRateLimit { // of the default ``UNAVAILABLE`` gRPC code for a rate limited gRPC call. The // HTTP code will be 200 for a gRPC response. bool rate_limited_as_resource_exhausted = 15; + + // Rate limit configuration that is used to generate a list of descriptor entries based on + // the request context. The generated entries will be used to find one or multiple matched rate + // limit rule from the ``descriptors``. + // If this is set, then + // :ref:`VirtualHost.rate_limits` or + // :ref:`RouteAction.rate_limits` fields + // will be ignored. + // + // .. note:: + // Not all configuration fields of + // :ref:`rate limit config ` is supported at here. + // Following fields are not supported: + // + // 1. :ref:`rate limit stage `. + // 2. :ref:`dynamic metadata `. + // 3. :ref:`disable_key `. + // 4. :ref:`override limit `. + repeated config.route.v3.RateLimit rate_limits = 17; } diff --git a/changelogs/current.yaml b/changelogs/current.yaml index b773fb76f626..3e5d60af9e33 100644 --- a/changelogs/current.yaml +++ b/changelogs/current.yaml @@ -431,6 +431,14 @@ new_features: change: | Added two new methods ``oidsPeerCertificate()`` and ``oidsLocalCertificate()`` to SSL connection object API :ref:`SSL connection info object `. +- area: local_ratelimit + change: | + Add the :ref:`rate_limits + ` + field to generate rate limit descriptors. If this field is set, the + :ref:`VirtualHost.rate_limits` or + :ref:`RouteAction.rate_limits` fields + will be ignored. - area: basic_auth change: | Added support to provide an override diff --git a/source/common/router/router_ratelimit.cc b/source/common/router/router_ratelimit.cc index db2dcc45e298..77c285a85a24 100644 --- a/source/common/router/router_ratelimit.cc +++ b/source/common/router/router_ratelimit.cc @@ -12,8 +12,6 @@ #include "source/common/common/empty_string.h" #include "source/common/config/metadata.h" #include "source/common/config/utility.h" -#include "source/common/http/matching/data_impl.h" -#include "source/common/matcher/matcher.h" #include "source/common/protobuf/utility.h" namespace Envoy { @@ -39,44 +37,24 @@ bool populateDescriptor(const std::vector& act return result; } -class RateLimitDescriptorValidationVisitor - : public Matcher::MatchTreeValidationVisitor { -public: - absl::Status performDataInputValidation(const Matcher::DataInputFactory&, - absl::string_view) override { - return absl::OkStatus(); +} // namespace + +// Ratelimit::DescriptorProducer +bool MatchInputRateLimitDescriptor::populateDescriptor(RateLimit::DescriptorEntry& descriptor_entry, + const std::string&, + const Http::RequestHeaderMap& headers, + const StreamInfo::StreamInfo& info) const { + Http::Matching::HttpMatchingDataImpl data(info); + data.onRequestHeaders(headers); + auto result = data_input_->get(data); + if (!absl::holds_alternative(result.data_)) { + return false; } -}; - -class MatchInputRateLimitDescriptor : public RateLimit::DescriptorProducer { -public: - MatchInputRateLimitDescriptor(const std::string& descriptor_key, - Matcher::DataInputPtr&& data_input) - : descriptor_key_(descriptor_key), data_input_(std::move(data_input)) {} - - // Ratelimit::DescriptorProducer - bool populateDescriptor(RateLimit::DescriptorEntry& descriptor_entry, const std::string&, - const Http::RequestHeaderMap& headers, - const StreamInfo::StreamInfo& info) const override { - Http::Matching::HttpMatchingDataImpl data(info); - data.onRequestHeaders(headers); - auto result = data_input_->get(data); - if (absl::holds_alternative(result.data_)) { - return false; - } - const std::string& str = absl::get(result.data_); - if (!str.empty()) { - descriptor_entry = {descriptor_key_, str}; - } - return true; + if (absl::string_view str = absl::get(result.data_); !str.empty()) { + descriptor_entry = {descriptor_key_, std::string(str)}; } - -private: - const std::string descriptor_key_; - Matcher::DataInputPtr data_input_; -}; - -} // namespace + return true; +} const uint64_t RateLimitPolicyImpl::MAX_STAGE_NUMBER = 10UL; @@ -256,7 +234,8 @@ QueryParameterValueMatchAction::QueryParameterValueMatchAction( : descriptor_value_(action.descriptor_value()), descriptor_key_(!action.descriptor_key().empty() ? action.descriptor_key() : "query_match"), expect_match_(PROTOBUF_GET_WRAPPED_OR_DEFAULT(action, expect_match, true)), - action_query_parameters_(buildQueryParameterMatcherVector(action, context)) {} + action_query_parameters_( + buildQueryParameterMatcherVector(action.query_parameters(), context)) {} bool QueryParameterValueMatchAction::populateDescriptor( RateLimit::DescriptorEntry& descriptor_entry, const std::string&, @@ -274,10 +253,11 @@ bool QueryParameterValueMatchAction::populateDescriptor( std::vector QueryParameterValueMatchAction::buildQueryParameterMatcherVector( - const envoy::config::route::v3::RateLimit::Action::QueryParameterValueMatch& action, + const Protobuf::RepeatedPtrField& + query_parameters, Server::Configuration::CommonFactoryContext& context) { std::vector ret; - for (const auto& query_parameter : action.query_parameters()) { + for (const auto& query_parameter : query_parameters) { ret.push_back(std::make_unique(query_parameter, context)); } return ret; diff --git a/source/common/router/router_ratelimit.h b/source/common/router/router_ratelimit.h index b069af8d7ed6..3fb5149a4cc2 100644 --- a/source/common/router/router_ratelimit.h +++ b/source/common/router/router_ratelimit.h @@ -13,6 +13,8 @@ #include "source/common/config/metadata.h" #include "source/common/http/header_utility.h" +#include "source/common/http/matching/data_impl.h" +#include "source/common/matcher/matcher.h" #include "source/common/network/cidr_range.h" #include "source/common/protobuf/utility.h" #include "source/common/router/config_utility.h" @@ -146,6 +148,7 @@ class MetaDataAction : public RateLimit::DescriptorProducer { MetaDataAction(const envoy::config::route::v3::RateLimit::Action::MetaData& action); // for maintaining backward compatibility with the deprecated DynamicMetaData action MetaDataAction(const envoy::config::route::v3::RateLimit::Action::DynamicMetaData& action); + // Ratelimit::DescriptorProducer bool populateDescriptor(RateLimit::DescriptorEntry& descriptor_entry, const std::string& local_service_cluster, @@ -198,7 +201,8 @@ class QueryParameterValueMatchAction : public RateLimit::DescriptorProducer { const StreamInfo::StreamInfo& info) const override; std::vector buildQueryParameterMatcherVector( - const envoy::config::route::v3::RateLimit::Action::QueryParameterValueMatch& action, + const Protobuf::RepeatedPtrField& + query_parameters, Server::Configuration::CommonFactoryContext& context); private: @@ -208,6 +212,31 @@ class QueryParameterValueMatchAction : public RateLimit::DescriptorProducer { const std::vector action_query_parameters_; }; +class RateLimitDescriptorValidationVisitor + : public Matcher::MatchTreeValidationVisitor { +public: + absl::Status performDataInputValidation(const Matcher::DataInputFactory&, + absl::string_view) override { + return absl::OkStatus(); + } +}; + +class MatchInputRateLimitDescriptor : public RateLimit::DescriptorProducer { +public: + MatchInputRateLimitDescriptor(const std::string& descriptor_key, + Matcher::DataInputPtr&& data_input) + : descriptor_key_(descriptor_key), data_input_(std::move(data_input)) {} + + // Ratelimit::DescriptorProducer + bool populateDescriptor(RateLimit::DescriptorEntry& descriptor_entry, const std::string&, + const Http::RequestHeaderMap& headers, + const StreamInfo::StreamInfo& info) const override; + +private: + const std::string descriptor_key_; + Matcher::DataInputPtr data_input_; +}; + /* * Implementation of RateLimitPolicyEntry that holds the action for the configuration. */ diff --git a/source/extensions/filters/common/ratelimit_config/BUILD b/source/extensions/filters/common/ratelimit_config/BUILD new file mode 100644 index 000000000000..a89b1d3676af --- /dev/null +++ b/source/extensions/filters/common/ratelimit_config/BUILD @@ -0,0 +1,22 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_library", + "envoy_extension_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_extension_package() + +envoy_cc_library( + name = "ratelimit_config_lib", + srcs = ["ratelimit_config.cc"], + hdrs = ["ratelimit_config.h"], + deps = [ + "//envoy/ratelimit:ratelimit_interface", + "//source/common/router:router_ratelimit_lib", + "@com_google_absl//absl/container:inlined_vector", + "@com_google_absl//absl/strings", + "@envoy_api//envoy/config/route/v3:pkg_cc_proto", + ], +) diff --git a/source/extensions/filters/common/ratelimit_config/ratelimit_config.cc b/source/extensions/filters/common/ratelimit_config/ratelimit_config.cc new file mode 100644 index 000000000000..c3fec0f4f691 --- /dev/null +++ b/source/extensions/filters/common/ratelimit_config/ratelimit_config.cc @@ -0,0 +1,143 @@ +#include "source/extensions/filters/common/ratelimit_config/ratelimit_config.h" + +#include "source/common/config/utility.h" +#include "source/common/http/matching/data_impl.h" +#include "source/common/matcher/matcher.h" + +namespace Envoy { +namespace Extensions { +namespace Filters { +namespace Common { +namespace RateLimit { + +RateLimitPolicy::RateLimitPolicy(const ProtoRateLimit& config, + Server::Configuration::CommonFactoryContext& context, + absl::Status& creation_status, bool no_limit) { + if (config.has_stage() || !config.disable_key().empty()) { + creation_status = + absl::InvalidArgumentError("'stage' field and 'disable_key' field are not supported"); + return; + } + + if (config.has_limit()) { + if (no_limit) { + creation_status = absl::InvalidArgumentError("'limit' field is not supported"); + return; + } + } + + for (const ProtoRateLimit::Action& action : config.actions()) { + switch (action.action_specifier_case()) { + case ProtoRateLimit::Action::ActionSpecifierCase::kSourceCluster: + actions_.emplace_back(new Envoy::Router::SourceClusterAction()); + break; + case ProtoRateLimit::Action::ActionSpecifierCase::kDestinationCluster: + actions_.emplace_back(new Envoy::Router::DestinationClusterAction()); + break; + case ProtoRateLimit::Action::ActionSpecifierCase::kRequestHeaders: + actions_.emplace_back(new Envoy::Router::RequestHeadersAction(action.request_headers())); + break; + case ProtoRateLimit::Action::ActionSpecifierCase::kRemoteAddress: + actions_.emplace_back(new Envoy::Router::RemoteAddressAction()); + break; + case ProtoRateLimit::Action::ActionSpecifierCase::kGenericKey: + actions_.emplace_back(new Envoy::Router::GenericKeyAction(action.generic_key())); + break; + case ProtoRateLimit::Action::ActionSpecifierCase::kMetadata: + actions_.emplace_back(new Envoy::Router::MetaDataAction(action.metadata())); + break; + case ProtoRateLimit::Action::ActionSpecifierCase::kHeaderValueMatch: + actions_.emplace_back( + new Envoy::Router::HeaderValueMatchAction(action.header_value_match(), context)); + break; + case ProtoRateLimit::Action::ActionSpecifierCase::kExtension: { + ProtobufMessage::ValidationVisitor& validator = context.messageValidationVisitor(); + auto* factory = + Envoy::Config::Utility::getFactory( + action.extension()); + if (!factory) { + // If no descriptor extension is found, fallback to using HTTP matcher + // input functions. Note that if the same extension name or type was + // dual registered as an extension descriptor and an HTTP matcher input + // function, the descriptor extension takes priority. + Router::RateLimitDescriptorValidationVisitor validation_visitor; + Matcher::MatchInputFactory input_factory(validator, + validation_visitor); + Matcher::DataInputFactoryCb data_input_cb = + input_factory.createDataInput(action.extension()); + actions_.emplace_back(std::make_unique( + action.extension().name(), data_input_cb())); + break; + } + auto message = Envoy::Config::Utility::translateAnyToFactoryConfig( + action.extension().typed_config(), validator, *factory); + Envoy::RateLimit::DescriptorProducerPtr producer = + factory->createDescriptorProducerFromProto(*message, context); + if (producer) { + actions_.emplace_back(std::move(producer)); + } else { + creation_status = absl::InvalidArgumentError( + absl::StrCat("Rate limit descriptor extension failed: ", action.extension().name())); + return; + } + break; + } + case ProtoRateLimit::Action::ActionSpecifierCase::kMaskedRemoteAddress: + actions_.emplace_back(new Router::MaskedRemoteAddressAction(action.masked_remote_address())); + break; + case ProtoRateLimit::Action::ActionSpecifierCase::kQueryParameterValueMatch: + actions_.emplace_back(new Router::QueryParameterValueMatchAction( + action.query_parameter_value_match(), context)); + break; + default: + creation_status = absl::InvalidArgumentError(fmt::format( + "Unsupported rate limit action: {}", static_cast(action.action_specifier_case()))); + return; + } + } +} + +void RateLimitPolicy::populateDescriptors(const Http::RequestHeaderMap& headers, + const StreamInfo::StreamInfo& stream_info, + const std::string& local_service_cluster, + RateLimitDescriptors& descriptors) const { + Envoy::RateLimit::LocalDescriptor descriptor; + for (const Envoy::RateLimit::DescriptorProducerPtr& action : actions_) { + Envoy::RateLimit::DescriptorEntry entry; + if (!action->populateDescriptor(entry, local_service_cluster, headers, stream_info)) { + return; + } + if (!entry.key_.empty()) { + descriptor.entries_.emplace_back(std::move(entry)); + } + } + descriptors.emplace_back(std::move(descriptor)); +} + +RateLimitConfig::RateLimitConfig(const Protobuf::RepeatedPtrField& configs, + Server::Configuration::CommonFactoryContext& context, + absl::Status& creation_status, bool no_limit) { + for (const ProtoRateLimit& config : configs) { + auto descriptor_generator = + std::make_unique(config, context, creation_status, no_limit); + if (!creation_status.ok()) { + return; + } + rate_limit_policies_.emplace_back(std::move(descriptor_generator)); + } +} + +void RateLimitConfig::populateDescriptors(const Http::RequestHeaderMap& headers, + const StreamInfo::StreamInfo& stream_info, + const std::string& local_service_cluster, + RateLimitDescriptors& descriptors) const { + for (const auto& generator : rate_limit_policies_) { + generator->populateDescriptors(headers, stream_info, local_service_cluster, descriptors); + } +} + +} // namespace RateLimit +} // namespace Common +} // namespace Filters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/common/ratelimit_config/ratelimit_config.h b/source/extensions/filters/common/ratelimit_config/ratelimit_config.h new file mode 100644 index 000000000000..743e69360c47 --- /dev/null +++ b/source/extensions/filters/common/ratelimit_config/ratelimit_config.h @@ -0,0 +1,57 @@ +#pragma once + +#include "envoy/config/route/v3/route_components.pb.h" +#include "envoy/ratelimit/ratelimit.h" + +#include "source/common/router/router_ratelimit.h" + +#include "absl/container/inlined_vector.h" + +namespace Envoy { +namespace Extensions { +namespace Filters { +namespace Common { +namespace RateLimit { + +using ProtoRateLimit = envoy::config::route::v3::RateLimit; +using RateLimitDescriptors = std::vector; + +class RateLimitPolicy : Logger::Loggable { +public: + RateLimitPolicy(const ProtoRateLimit& config, + Server::Configuration::CommonFactoryContext& context, + absl::Status& creation_status, bool no_limit = true); + + void populateDescriptors(const Http::RequestHeaderMap& headers, + const StreamInfo::StreamInfo& info, + const std::string& local_service_cluster, + RateLimitDescriptors& descriptors) const; + +private: + std::vector actions_; +}; + +class RateLimitConfig : Logger::Loggable { +public: + RateLimitConfig(const Protobuf::RepeatedPtrField& configs, + Server::Configuration::CommonFactoryContext& context, + absl::Status& creation_status, bool no_limit = true); + + bool empty() const { return rate_limit_policies_.empty(); } + + size_t size() const { return rate_limit_policies_.size(); } + + void populateDescriptors(const Http::RequestHeaderMap& headers, + const StreamInfo::StreamInfo& info, + const std::string& local_service_cluster, + RateLimitDescriptors& descriptors) const; + +private: + std::vector> rate_limit_policies_; +}; + +} // namespace RateLimit +} // namespace Common +} // namespace Filters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/local_ratelimit/BUILD b/source/extensions/filters/http/local_ratelimit/BUILD index 88d4042e9809..f20a2b04118e 100644 --- a/source/extensions/filters/http/local_ratelimit/BUILD +++ b/source/extensions/filters/http/local_ratelimit/BUILD @@ -27,6 +27,7 @@ envoy_cc_library( "//source/common/runtime:runtime_lib", "//source/extensions/filters/common/local_ratelimit:local_ratelimit_lib", "//source/extensions/filters/common/ratelimit:ratelimit_lib", + "//source/extensions/filters/common/ratelimit_config:ratelimit_config_lib", "//source/extensions/filters/http/common:pass_through_filter_lib", "//source/extensions/filters/http/common:ratelimit_headers_lib", "@envoy_api//envoy/extensions/common/ratelimit/v3:pkg_cc_proto", diff --git a/source/extensions/filters/http/local_ratelimit/config.cc b/source/extensions/filters/http/local_ratelimit/config.cc index e0990cf4598f..2228619acc75 100644 --- a/source/extensions/filters/http/local_ratelimit/config.cc +++ b/source/extensions/filters/http/local_ratelimit/config.cc @@ -15,12 +15,9 @@ namespace LocalRateLimitFilter { Http::FilterFactoryCb LocalRateLimitFilterConfig::createFilterFactoryFromProtoTyped( const envoy::extensions::filters::http::local_ratelimit::v3::LocalRateLimit& proto_config, const std::string&, Server::Configuration::FactoryContext& context) { - auto& server_context = context.serverFactoryContext(); - FilterConfigSharedPtr filter_config = std::make_shared( - proto_config, server_context.localInfo(), server_context.mainThreadDispatcher(), - server_context.clusterManager(), server_context.singletonManager(), context.scope(), - server_context.runtime()); + FilterConfigSharedPtr filter_config = + std::make_shared(proto_config, context.serverFactoryContext(), context.scope()); return [filter_config](Http::FilterChainFactoryCallbacks& callbacks) -> void { callbacks.addStreamFilter(std::make_shared(filter_config)); }; @@ -30,9 +27,7 @@ Router::RouteSpecificFilterConfigConstSharedPtr LocalRateLimitFilterConfig::createRouteSpecificFilterConfigTyped( const envoy::extensions::filters::http::local_ratelimit::v3::LocalRateLimit& proto_config, Server::Configuration::ServerFactoryContext& context, ProtobufMessage::ValidationVisitor&) { - return std::make_shared( - proto_config, context.localInfo(), context.mainThreadDispatcher(), context.clusterManager(), - context.singletonManager(), context.scope(), context.runtime(), true); + return std::make_shared(proto_config, context, context.scope(), true); } /** diff --git a/source/extensions/filters/http/local_ratelimit/local_ratelimit.cc b/source/extensions/filters/http/local_ratelimit/local_ratelimit.cc index 8356fa49b652..0e12da02bfdd 100644 --- a/source/extensions/filters/http/local_ratelimit/local_ratelimit.cc +++ b/source/extensions/filters/http/local_ratelimit/local_ratelimit.cc @@ -23,10 +23,8 @@ const std::string& PerConnectionRateLimiter::key() { FilterConfig::FilterConfig( const envoy::extensions::filters::http::local_ratelimit::v3::LocalRateLimit& config, - const LocalInfo::LocalInfo& local_info, Event::Dispatcher& dispatcher, - Upstream::ClusterManager& cm, Singleton::Manager& singleton_manager, Stats::Scope& scope, - Runtime::Loader& runtime, const bool per_route) - : dispatcher_(dispatcher), status_(toErrorCode(config.status().code())), + Server::Configuration::CommonFactoryContext& context, Stats::Scope& scope, const bool per_route) + : dispatcher_(context.mainThreadDispatcher()), status_(toErrorCode(config.status().code())), stats_(generateStats(config.stat_prefix(), scope)), fill_interval_(std::chrono::milliseconds( PROTOBUF_GET_MS_OR_DEFAULT(config.token_bucket(), fill_interval, 0))), @@ -38,7 +36,7 @@ FilterConfig::FilterConfig( config.has_always_consume_default_token_bucket() ? config.always_consume_default_token_bucket().value() : true), - local_info_(local_info), runtime_(runtime), + local_info_(context.localInfo()), runtime_(context.runtime()), filter_enabled_( config.has_filter_enabled() ? absl::optional( @@ -73,20 +71,35 @@ FilterConfig::FilterConfig( throw EnvoyException("local rate limit token bucket must be set for per filter configs"); } + absl::Status creation_status; + rate_limit_config_ = std::make_unique( + config.rate_limits(), context, creation_status); + THROW_IF_NOT_OK_REF(creation_status); + + if (rate_limit_config_->empty()) { + if (!config.descriptors().empty()) { + ENVOY_LOG_FIRST_N( + warn, 20, + "'descriptors' is set but only used by route configuration. Please configure the local " + "rate limit filter using the embedded 'rate_limits' field as route configuration for " + "local rate limits will be ignored in the future."); + } + } + Filters::Common::LocalRateLimit::ShareProviderSharedPtr share_provider; if (config.has_local_cluster_rate_limit()) { if (rate_limit_per_connection_) { throw EnvoyException("local_cluster_rate_limit is set and " "local_rate_limit_per_downstream_connection is set to true"); } - if (!cm.localClusterName().has_value()) { + if (!context.clusterManager().localClusterName().has_value()) { throw EnvoyException("local_cluster_rate_limit is set but no local cluster name is present"); } // If the local cluster name is set then the relevant cluster must exist or the cluster // manager will fail to initialize. share_provider_manager_ = Filters::Common::LocalRateLimit::ShareProviderManager::singleton( - dispatcher, cm, singleton_manager); + dispatcher_, context.clusterManager(), context.singletonManager()); if (!share_provider_manager_) { throw EnvoyException("local_cluster_rate_limit is set but no local cluster is present"); } @@ -95,7 +108,7 @@ FilterConfig::FilterConfig( } rate_limiter_ = std::make_unique( - fill_interval_, max_tokens_, tokens_per_fill_, dispatcher, descriptors_, + fill_interval_, max_tokens_, tokens_per_fill_, dispatcher_, descriptors_, always_consume_default_token_bucket_, std::move(share_provider)); } @@ -137,7 +150,11 @@ Http::FilterHeadersStatus Filter::decodeHeaders(Http::RequestHeaderMap& headers, std::vector descriptors; if (used_config_->hasDescriptors()) { - populateDescriptors(descriptors, headers); + if (used_config_->hasRateLimitConfigs()) { + used_config_->populateDescriptors(headers, decoder_callbacks_->streamInfo(), descriptors); + } else { + populateDescriptors(descriptors, headers); + } } if (ENVOY_LOG_CHECK_LEVEL(debug)) { diff --git a/source/extensions/filters/http/local_ratelimit/local_ratelimit.h b/source/extensions/filters/http/local_ratelimit/local_ratelimit.h index d23f7228f03c..2b3af4ec7fb8 100644 --- a/source/extensions/filters/http/local_ratelimit/local_ratelimit.h +++ b/source/extensions/filters/http/local_ratelimit/local_ratelimit.h @@ -19,6 +19,7 @@ #include "source/common/runtime/runtime_protos.h" #include "source/extensions/filters/common/local_ratelimit/local_ratelimit_impl.h" #include "source/extensions/filters/common/ratelimit/ratelimit.h" +#include "source/extensions/filters/common/ratelimit_config/ratelimit_config.h" #include "source/extensions/filters/http/common/pass_through_filter.h" namespace Envoy { @@ -69,12 +70,12 @@ class PerConnectionRateLimiter : public StreamInfo::FilterState::Object { /** * Global configuration for the HTTP local rate limit filter. */ -class FilterConfig : public Router::RouteSpecificFilterConfig { +class FilterConfig : public Router::RouteSpecificFilterConfig, + Logger::Loggable { public: FilterConfig(const envoy::extensions::filters::http::local_ratelimit::v3::LocalRateLimit& config, - const LocalInfo::LocalInfo& local_info, Event::Dispatcher& dispatcher, - Upstream::ClusterManager& cm, Singleton::Manager& singleton_manager, - Stats::Scope& scope, Runtime::Loader& runtime, bool per_route = false); + Server::Configuration::CommonFactoryContext& context, Stats::Scope& scope, + const bool per_route = false); ~FilterConfig() override { // Ensure that the LocalRateLimiterImpl instance will be destroyed on the thread where its inner // timer is created and running. @@ -113,6 +114,18 @@ class FilterConfig : public Router::RouteSpecificFilterConfig { return rate_limited_grpc_status_; } + bool hasRateLimitConfigs() const { + ASSERT(rate_limit_config_ != nullptr); + return !rate_limit_config_->empty(); + } + + void populateDescriptors(const Http::RequestHeaderMap& headers, + const StreamInfo::StreamInfo& info, + Filters::Common::RateLimit::RateLimitDescriptors& descriptors) const { + ASSERT(rate_limit_config_ != nullptr); + rate_limit_config_->populateDescriptors(headers, info, local_info_.clusterName(), descriptors); + } + private: friend class FilterTest; @@ -150,6 +163,7 @@ class FilterConfig : public Router::RouteSpecificFilterConfig { const bool enable_x_rate_limit_headers_; const envoy::extensions::common::ratelimit::v3::VhRateLimitsOptions vh_rate_limits_; const absl::optional rate_limited_grpc_status_; + std::unique_ptr rate_limit_config_; }; using FilterConfigSharedPtr = std::shared_ptr; diff --git a/test/extensions/filters/common/ratelimit_config/BUILD b/test/extensions/filters/common/ratelimit_config/BUILD new file mode 100644 index 000000000000..bffabf03a6cd --- /dev/null +++ b/test/extensions/filters/common/ratelimit_config/BUILD @@ -0,0 +1,37 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_test", + "envoy_package", + "envoy_proto_library", +) + +licenses(["notice"]) # Apache 2 + +envoy_package() + +envoy_proto_library( + name = "ratelimit_config_test_proto", + srcs = ["ratelimit_config_test.proto"], + deps = [ + "@envoy_api//envoy/config/route/v3:pkg", + ], +) + +envoy_cc_test( + name = "ratelimit_config_test", + srcs = ["ratelimit_config_test.cc"], + deps = [ + ":ratelimit_config_test_proto_cc_proto", + "//source/common/http:header_map_lib", + "//source/common/protobuf:utility_lib", + "//source/common/router:config_lib", + "//source/extensions/filters/common/ratelimit_config:ratelimit_config_lib", + "//test/mocks/http:http_mocks", + "//test/mocks/ratelimit:ratelimit_mocks", + "//test/mocks/router:router_mocks", + "//test/mocks/server:instance_mocks", + "//test/test_common:registry_lib", + "//test/test_common:utility_lib", + "@envoy_api//envoy/config/route/v3:pkg_cc_proto", + ], +) diff --git a/test/extensions/filters/common/ratelimit_config/ratelimit_config_test.cc b/test/extensions/filters/common/ratelimit_config/ratelimit_config_test.cc new file mode 100644 index 000000000000..aea1832038bc --- /dev/null +++ b/test/extensions/filters/common/ratelimit_config/ratelimit_config_test.cc @@ -0,0 +1,1121 @@ +#include +#include +#include + +#include "envoy/config/route/v3/route.pb.h" +#include "envoy/config/route/v3/route_components.pb.h" +#include "envoy/config/route/v3/route_components.pb.validate.h" + +#include "source/common/http/header_map_impl.h" +#include "source/common/network/address_impl.h" +#include "source/common/protobuf/utility.h" +#include "source/extensions/filters/common/ratelimit_config/ratelimit_config.h" + +#include "test/extensions/filters/common/ratelimit_config/ratelimit_config_test.pb.h" +#include "test/extensions/filters/common/ratelimit_config/ratelimit_config_test.pb.validate.h" +#include "test/mocks/http/mocks.h" +#include "test/mocks/ratelimit/mocks.h" +#include "test/mocks/router/mocks.h" +#include "test/mocks/server/instance.h" +#include "test/test_common/printers.h" +#include "test/test_common/registry.h" +#include "test/test_common/utility.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::NiceMock; + +namespace Envoy { +namespace Extensions { +namespace Filters { +namespace Common { +namespace RateLimit { +namespace { + +ProtoRateLimit parseRateLimitFromV3Yaml(const std::string& yaml_string) { + ProtoRateLimit rate_limit; + TestUtility::loadFromYaml(yaml_string, rate_limit); + TestUtility::validate(rate_limit); + return rate_limit; +} + +TEST(BadRateLimitConfiguration, MissingActions) { + EXPECT_THROW_WITH_REGEX(parseRateLimitFromV3Yaml("{}"), EnvoyException, + "value must contain at least"); +} + +TEST(BadRateLimitConfiguration, ActionsMissingRequiredFields) { + const std::string yaml_one = R"EOF( +actions: +- request_headers: {} + )EOF"; + + EXPECT_THROW_WITH_REGEX(parseRateLimitFromV3Yaml(yaml_one), EnvoyException, + "value length must be at least"); + + const std::string yaml_two = R"EOF( +actions: +- request_headers: + header_name: test + )EOF"; + + EXPECT_THROW_WITH_REGEX(parseRateLimitFromV3Yaml(yaml_two), EnvoyException, + "value length must be at least"); + + const std::string yaml_three = R"EOF( +actions: +- request_headers: + descriptor_key: test + )EOF"; + + EXPECT_THROW_WITH_REGEX(parseRateLimitFromV3Yaml(yaml_three), EnvoyException, + "value length must be at least"); +} + +class RateLimitConfigTest : public testing::Test { +public: + void setupTest(const std::string& yaml) { + test::extensions::filters::common::ratelimit_config::TestRateLimitConfig proto_config; + TestUtility::loadFromYaml(yaml, proto_config); + config_ = std::make_unique( + proto_config.rate_limits(), factory_context_, creation_status_); + stream_info_.downstream_connection_info_provider_->setRemoteAddress(default_remote_address_); + ON_CALL(Const(stream_info_), route()).WillByDefault(testing::Return(route_)); + } + + NiceMock factory_context_; + ProtobufMessage::NullValidationVisitorImpl any_validation_visitor_; + absl::Status creation_status_{}; + std::unique_ptr config_; + Http::TestRequestHeaderMapImpl headers_; + std::shared_ptr route_{new NiceMock()}; + Network::Address::InstanceConstSharedPtr default_remote_address_{ + new Network::Address::Ipv4Instance("10.0.0.1")}; + NiceMock stream_info_; +}; + +TEST_F(RateLimitConfigTest, DisableKeyIsNotAllowed) { + { + const std::string yaml = R"EOF( + rate_limits: + - actions: + - remote_address: {} + stage: 2 + disable_key: anything + limit: + dynamic_metadata: + metadata_key: + key: key + path: + - key: key + )EOF"; + + factory_context_.cluster_manager_.initializeClusters({"www2"}, {}); + setupTest(yaml); + EXPECT_FALSE(creation_status_.ok()); + EXPECT_EQ(creation_status_.message(), + "'stage' field and 'disable_key' field are not supported"); + } +} + +TEST_F(RateLimitConfigTest, LimitIsNotAllowed) { + { + const std::string yaml = R"EOF( + rate_limits: + - actions: + - remote_address: {} + limit: + dynamic_metadata: + metadata_key: + key: key + path: + - key: key + )EOF"; + + factory_context_.cluster_manager_.initializeClusters({"www2"}, {}); + setupTest(yaml); + EXPECT_FALSE(creation_status_.ok()); + EXPECT_EQ(creation_status_.message(), "'limit' field is not supported"); + } +} + +TEST_F(RateLimitConfigTest, NoAction) { + { + const std::string yaml = R"EOF( +actions: +- {} + )EOF"; + + ProtoRateLimit rate_limit; + TestUtility::loadFromYaml(yaml, rate_limit); + + absl::Status creation_status; + RateLimitPolicy policy(rate_limit, factory_context_, creation_status); + + EXPECT_TRUE(absl::StartsWith(creation_status.message(), "Unsupported rate limit action:")); + } + + { + const std::string yaml = R"EOF( + rate_limits: + - actions: + - remote_address: {} + - {} + )EOF"; + + factory_context_.cluster_manager_.initializeClusters({"www2"}, {}); + setupTest(yaml); + + EXPECT_TRUE(absl::StartsWith(creation_status_.message(), "Unsupported rate limit action:")); + } +} + +TEST_F(RateLimitConfigTest, EmptyRateLimit) { + const std::string yaml = R"EOF( +rate_limits: [] + )EOF"; + + factory_context_.cluster_manager_.initializeClusters({"www2"}, {}); + setupTest(yaml); + + EXPECT_TRUE(config_->empty()); +} + +TEST_F(RateLimitConfigTest, SinglePolicy) { + const std::string yaml = R"EOF( + rate_limits: + - actions: + - remote_address: {} + )EOF"; + + factory_context_.cluster_manager_.initializeClusters({"www2"}, {}); + setupTest(yaml); + + EXPECT_EQ(1U, config_->size()); + + std::vector descriptors; + config_->populateDescriptors(headers_, stream_info_, "", descriptors); + EXPECT_THAT(std::vector({{{{"remote_address", "10.0.0.1"}}}}), + testing::ContainerEq(descriptors)); +} + +TEST_F(RateLimitConfigTest, MultiplePoliciesAndMultipleActions) { + const std::string yaml = R"EOF( + rate_limits: + - actions: + - remote_address: {} + - destination_cluster: {} + - actions: + - destination_cluster: {} + )EOF"; + + setupTest(yaml); + + std::vector descriptors; + + config_->populateDescriptors(headers_, stream_info_, "", descriptors); + + EXPECT_THAT(std::vector( + {Envoy::RateLimit::LocalDescriptor{ + {{"remote_address", "10.0.0.1"}, {"destination_cluster", "fake_cluster"}}}, + Envoy::RateLimit::LocalDescriptor{{{"destination_cluster", "fake_cluster"}}}}), + testing::ContainerEq(descriptors)); +} + +class RateLimitPolicyTest : public testing::Test { +public: + void setupTest(const std::string& yaml) { + rate_limit_entry_ = std::make_unique(parseRateLimitFromV3Yaml(yaml), + factory_context_, creation_status_); + descriptors_.clear(); + stream_info_.downstream_connection_info_provider_->setRemoteAddress(default_remote_address_); + ON_CALL(Const(stream_info_), route()).WillByDefault(testing::Return(route_)); + } + + NiceMock factory_context_; + std::unique_ptr rate_limit_entry_; + absl::Status creation_status_{}; + Http::TestRequestHeaderMapImpl headers_; + std::shared_ptr route_{new NiceMock()}; + + std::vector descriptors_; + Network::Address::InstanceConstSharedPtr default_remote_address_{ + new Network::Address::Ipv4Instance("10.0.0.1")}; + NiceMock stream_info_; +}; + +class RateLimitPolicyIpv6Test : public testing::Test { +public: + void setupTest(const std::string& yaml) { + absl::Status creation_status; + rate_limit_entry_ = std::make_unique(parseRateLimitFromV3Yaml(yaml), + factory_context_, creation_status); + THROW_IF_NOT_OK(creation_status); // NOLINT + descriptors_.clear(); + stream_info_.downstream_connection_info_provider_->setRemoteAddress(default_remote_address_); + ON_CALL(Const(stream_info_), route()).WillByDefault(testing::Return(route_)); + } + + NiceMock factory_context_; + std::unique_ptr rate_limit_entry_; + Http::TestRequestHeaderMapImpl headers_; + std::vector descriptors_; + std::shared_ptr route_{new NiceMock()}; + + Network::Address::InstanceConstSharedPtr default_remote_address_{ + new Network::Address::Ipv6Instance("2001:abcd:ef01:2345:6789:abcd:ef01:234")}; + NiceMock stream_info_; +}; + +TEST_F(RateLimitPolicyTest, RemoteAddress) { + const std::string yaml = R"EOF( +actions: +- remote_address: {} + )EOF"; + + setupTest(yaml); + + rate_limit_entry_->populateDescriptors(headers_, stream_info_, "", descriptors_); + + EXPECT_THAT(std::vector({{{{"remote_address", "10.0.0.1"}}}}), + testing::ContainerEq(descriptors_)); +} + +TEST_F(RateLimitPolicyTest, MaskedRemoteAddressIpv4Default) { + const std::string yaml = R"EOF( +actions: +- masked_remote_address: {} + )EOF"; + + setupTest(yaml); + + rate_limit_entry_->populateDescriptors(headers_, stream_info_, "", descriptors_); + + EXPECT_THAT(std::vector( + {{{{"masked_remote_address", "10.0.0.1/32"}}}}), + testing::ContainerEq(descriptors_)); +} + +TEST_F(RateLimitPolicyTest, MaskedRemoteAddressIpv4) { + const std::string yaml = R"EOF( +actions: +- masked_remote_address: + v4_prefix_mask_len: 16 + )EOF"; + + setupTest(yaml); + + rate_limit_entry_->populateDescriptors(headers_, stream_info_, "", descriptors_); + + EXPECT_THAT(std::vector( + {{{{"masked_remote_address", "10.0.0.0/16"}}}}), + testing::ContainerEq(descriptors_)); +} + +TEST_F(RateLimitPolicyIpv6Test, MaskedRemoteAddressIpv6Default) { + const std::string yaml = R"EOF( +actions: +- masked_remote_address: {} + )EOF"; + + setupTest(yaml); + + rate_limit_entry_->populateDescriptors(headers_, stream_info_, "", descriptors_); + + EXPECT_THAT(std::vector( + {{{{"masked_remote_address", "2001:abcd:ef01:2345:6789:abcd:ef01:234/128"}}}}), + testing::ContainerEq(descriptors_)); +} + +TEST_F(RateLimitPolicyIpv6Test, MaskedRemoteAddressIpv6) { + const std::string yaml = R"EOF( +actions: +- masked_remote_address: + v6_prefix_mask_len: 64 + )EOF"; + + setupTest(yaml); + + rate_limit_entry_->populateDescriptors(headers_, stream_info_, "", descriptors_); + + EXPECT_THAT(std::vector( + {{{{"masked_remote_address", "2001:abcd:ef01:2345::/64"}}}}), + testing::ContainerEq(descriptors_)); +} + +// Verify no descriptor is emitted if remote is a pipe. +TEST_F(RateLimitPolicyTest, PipeAddress) { + const std::string yaml = R"EOF( +actions: +- remote_address: {} + )EOF"; + + setupTest(yaml); + + stream_info_.downstream_connection_info_provider_->setRemoteAddress( + *Network::Address::PipeInstance::create("/hello")); + rate_limit_entry_->populateDescriptors(headers_, stream_info_, "", descriptors_); + + EXPECT_TRUE(descriptors_.empty()); +} + +TEST_F(RateLimitPolicyTest, SourceService) { + const std::string yaml = R"EOF( +actions: +- source_cluster: {} + )EOF"; + + setupTest(yaml); + + rate_limit_entry_->populateDescriptors(headers_, stream_info_, "service_cluster", descriptors_); + + EXPECT_THAT( + std::vector({{{{"source_cluster", "service_cluster"}}}}), + testing::ContainerEq(descriptors_)); +} + +TEST_F(RateLimitPolicyTest, DestinationService) { + const std::string yaml = R"EOF( +actions: +- destination_cluster: {} + )EOF"; + + setupTest(yaml); + + rate_limit_entry_->populateDescriptors(headers_, stream_info_, "service_cluster", descriptors_); + + EXPECT_THAT( + std::vector({{{{"destination_cluster", "fake_cluster"}}}}), + testing::ContainerEq(descriptors_)); +} + +TEST_F(RateLimitPolicyTest, RequestHeaders) { + const std::string yaml = R"EOF( +actions: +- request_headers: + header_name: x-header-name + descriptor_key: my_header_name + )EOF"; + + setupTest(yaml); + headers_.setCopy(Http::LowerCaseString("x-header-name"), "test_value"); + + rate_limit_entry_->populateDescriptors(headers_, stream_info_, "service_cluster", descriptors_); + + EXPECT_THAT( + std::vector({{{{"my_header_name", "test_value"}}}}), + testing::ContainerEq(descriptors_)); +} + +// Validate that a descriptor is added if the missing request header +// has skip_if_absent set to true +TEST_F(RateLimitPolicyTest, RequestHeadersWithSkipIfAbsent) { + const std::string yaml = R"EOF( +actions: +- request_headers: + header_name: x-header-name + descriptor_key: my_header_name + skip_if_absent: false +- request_headers: + header_name: x-header + descriptor_key: my_header + skip_if_absent: true + )EOF"; + + setupTest(yaml); + headers_.setCopy(Http::LowerCaseString("x-header-name"), "test_value"); + + rate_limit_entry_->populateDescriptors(headers_, stream_info_, "service_cluster", descriptors_); + + EXPECT_THAT( + std::vector({{{{"my_header_name", "test_value"}}}}), + testing::ContainerEq(descriptors_)); +} + +// Tests if the descriptors are added if one of the headers is missing +// and skip_if_absent is set to default value which is false +TEST_F(RateLimitPolicyTest, RequestHeadersWithDefaultSkipIfAbsent) { + const std::string yaml = R"EOF( +actions: +- request_headers: + header_name: x-header-name + descriptor_key: my_header_name + skip_if_absent: false +- request_headers: + header_name: x-header + descriptor_key: my_header + skip_if_absent: false + )EOF"; + + setupTest(yaml); + Http::TestRequestHeaderMapImpl header{{"x-header-test", "test_value"}}; + + rate_limit_entry_->populateDescriptors(headers_, stream_info_, "service_cluster", descriptors_); + + EXPECT_TRUE(descriptors_.empty()); +} + +TEST_F(RateLimitPolicyTest, RequestHeadersNoMatch) { + const std::string yaml = R"EOF( +actions: +- request_headers: + header_name: x-header + descriptor_key: my_header_name + )EOF"; + + setupTest(yaml); + headers_.setCopy(Http::LowerCaseString("x-header-name"), "test_value"); + + rate_limit_entry_->populateDescriptors(headers_, stream_info_, "service_cluster", descriptors_); + + EXPECT_TRUE(descriptors_.empty()); +} + +TEST_F(RateLimitPolicyTest, RateLimitKey) { + const std::string yaml = R"EOF( +actions: +- generic_key: + descriptor_value: fake_key + )EOF"; + + setupTest(yaml); + + rate_limit_entry_->populateDescriptors(headers_, stream_info_, "", descriptors_); + + EXPECT_THAT(std::vector({{{{"generic_key", "fake_key"}}}}), + testing::ContainerEq(descriptors_)); +} + +TEST_F(RateLimitPolicyTest, GenericKeyWithSetDescriptorKey) { + const std::string yaml = R"EOF( +actions: +- generic_key: + descriptor_key: fake_key + descriptor_value: fake_value + )EOF"; + + setupTest(yaml); + + rate_limit_entry_->populateDescriptors(headers_, stream_info_, "", descriptors_); + + EXPECT_THAT(std::vector({{{{"fake_key", "fake_value"}}}}), + testing::ContainerEq(descriptors_)); +} + +TEST_F(RateLimitPolicyTest, GenericKeyWithEmptyDescriptorKey) { + const std::string yaml = R"EOF( +actions: +- generic_key: + descriptor_key: "" + descriptor_value: fake_value + )EOF"; + + setupTest(yaml); + + rate_limit_entry_->populateDescriptors(headers_, stream_info_, "", descriptors_); + + EXPECT_THAT(std::vector({{{{"generic_key", "fake_value"}}}}), + testing::ContainerEq(descriptors_)); +} + +TEST_F(RateLimitPolicyTest, MetaDataMatchDynamicSourceByDefault) { + const std::string yaml = R"EOF( +actions: +- metadata: + descriptor_key: fake_key + default_value: fake_value + metadata_key: + key: 'envoy.xxx' + path: + - key: test + - key: prop + )EOF"; + + setupTest(yaml); + + std::string metadata_yaml = R"EOF( +filter_metadata: + envoy.xxx: + test: + prop: foo + )EOF"; + + TestUtility::loadFromYaml(metadata_yaml, stream_info_.dynamicMetadata()); + rate_limit_entry_->populateDescriptors(headers_, stream_info_, "", descriptors_); + + EXPECT_THAT(std::vector({{{{"fake_key", "foo"}}}}), + testing::ContainerEq(descriptors_)); +} + +TEST_F(RateLimitPolicyTest, MetaDataMatchDynamicSource) { + const std::string yaml = R"EOF( +actions: +- metadata: + descriptor_key: fake_key + default_value: fake_value + metadata_key: + key: 'envoy.xxx' + path: + - key: test + - key: prop + source: DYNAMIC + )EOF"; + + setupTest(yaml); + + std::string metadata_yaml = R"EOF( +filter_metadata: + envoy.xxx: + test: + prop: foo + )EOF"; + + TestUtility::loadFromYaml(metadata_yaml, stream_info_.dynamicMetadata()); + rate_limit_entry_->populateDescriptors(headers_, stream_info_, "", descriptors_); + + EXPECT_THAT(std::vector({{{{"fake_key", "foo"}}}}), + testing::ContainerEq(descriptors_)); +} + +TEST_F(RateLimitPolicyTest, MetaDataMatchRouteEntrySource) { + const std::string yaml = R"EOF( +actions: +- metadata: + descriptor_key: fake_key + default_value: fake_value + metadata_key: + key: 'envoy.xxx' + path: + - key: test + - key: prop + source: ROUTE_ENTRY + )EOF"; + + setupTest(yaml); + + std::string metadata_yaml = R"EOF( +filter_metadata: + envoy.xxx: + test: + prop: foo + )EOF"; + + TestUtility::loadFromYaml(metadata_yaml, route_->metadata_); + + rate_limit_entry_->populateDescriptors(headers_, stream_info_, "", descriptors_); + + EXPECT_THAT(std::vector({{{{"fake_key", "foo"}}}}), + testing::ContainerEq(descriptors_)); +} + +// Tests that the default_value is used in the descriptor when the metadata_key is empty. +TEST_F(RateLimitPolicyTest, MetaDataNoMatchWithDefaultValue) { + const std::string yaml = R"EOF( +actions: +- metadata: + descriptor_key: fake_key + default_value: fake_value + metadata_key: + key: 'envoy.xxx' + path: + - key: test + - key: prop + )EOF"; + + setupTest(yaml); + + std::string metadata_yaml = R"EOF( +filter_metadata: + envoy.xxx: + another_key: + prop: foo + )EOF"; + + TestUtility::loadFromYaml(metadata_yaml, stream_info_.dynamicMetadata()); + rate_limit_entry_->populateDescriptors(headers_, stream_info_, "", descriptors_); + + EXPECT_THAT(std::vector({{{{"fake_key", "fake_value"}}}}), + testing::ContainerEq(descriptors_)); +} + +TEST_F(RateLimitPolicyTest, MetaDataNoMatch) { + const std::string yaml = R"EOF( +actions: +- metadata: + descriptor_key: fake_key + metadata_key: + key: 'envoy.xxx' + path: + - key: test + - key: prop + )EOF"; + + setupTest(yaml); + + std::string metadata_yaml = R"EOF( +filter_metadata: + envoy.xxx: + another_key: + prop: foo + )EOF"; + + TestUtility::loadFromYaml(metadata_yaml, stream_info_.dynamicMetadata()); + rate_limit_entry_->populateDescriptors(headers_, stream_info_, "", descriptors_); + + EXPECT_TRUE(descriptors_.empty()); +} + +TEST_F(RateLimitPolicyTest, MetaDataEmptyValue) { + const std::string yaml = R"EOF( +actions: +- metadata: + descriptor_key: fake_key + metadata_key: + key: 'envoy.xxx' + path: + - key: test + - key: prop + )EOF"; + + setupTest(yaml); + + std::string metadata_yaml = R"EOF( +filter_metadata: + envoy.xxx: + test: + prop: "" + )EOF"; + + TestUtility::loadFromYaml(metadata_yaml, stream_info_.dynamicMetadata()); + rate_limit_entry_->populateDescriptors(headers_, stream_info_, "", descriptors_); + + EXPECT_TRUE(descriptors_.empty()); +} + +// Tests that no descriptors are generated when both the metadata_key and default_value are empty. +TEST_F(RateLimitPolicyTest, MetaDataAndDefaultValueEmpty) { + const std::string yaml = R"EOF( +actions: +- generic_key: + descriptor_key: fake_key + descriptor_value: fake_value +- metadata: + descriptor_key: fake_key + default_value: "" + metadata_key: + key: 'envoy.xxx' + path: + - key: test + - key: prop + )EOF"; + + setupTest(yaml); + + std::string metadata_yaml = R"EOF( +filter_metadata: + envoy.xxx: + another_key: + prop: "" + )EOF"; + + TestUtility::loadFromYaml(metadata_yaml, stream_info_.dynamicMetadata()); + rate_limit_entry_->populateDescriptors(headers_, stream_info_, "", descriptors_); + + EXPECT_TRUE(descriptors_.empty()); +} + +// Tests that no descriptor is generated when both the metadata_key and default_value are empty, +// and skip_if_absent is set to true. +TEST_F(RateLimitPolicyTest, MetaDataAndDefaultValueEmptySkipIfAbsent) { + const std::string yaml = R"EOF( +actions: +- generic_key: + descriptor_key: fake_key + descriptor_value: fake_value +- metadata: + descriptor_key: fake_key + default_value: "" + metadata_key: + key: 'envoy.xxx' + path: + - key: test + - key: prop + skip_if_absent: true + )EOF"; + + setupTest(yaml); + + std::string metadata_yaml = R"EOF( +filter_metadata: + envoy.xxx: + another_key: + prop: "" + )EOF"; + + TestUtility::loadFromYaml(metadata_yaml, stream_info_.dynamicMetadata()); + rate_limit_entry_->populateDescriptors(headers_, stream_info_, "", descriptors_); + + EXPECT_THAT(std::vector({{{{"fake_key", "fake_value"}}}}), + testing::ContainerEq(descriptors_)); +} + +TEST_F(RateLimitPolicyTest, MetaDataNonStringNoMatch) { + const std::string yaml = R"EOF( +actions: +- metadata: + descriptor_key: fake_key + metadata_key: + key: 'envoy.xxx' + path: + - key: test + - key: prop + )EOF"; + + setupTest(yaml); + + std::string metadata_yaml = R"EOF( +filter_metadata: + envoy.xxx: + test: + prop: + foo: bar + )EOF"; + + TestUtility::loadFromYaml(metadata_yaml, stream_info_.dynamicMetadata()); + rate_limit_entry_->populateDescriptors(headers_, stream_info_, "", descriptors_); + + EXPECT_TRUE(descriptors_.empty()); +} + +TEST_F(RateLimitPolicyTest, HeaderValueMatch) { + const std::string yaml = R"EOF( +actions: +- header_value_match: + descriptor_value: fake_value + headers: + - name: x-header-name + string_match: + exact: test_value + )EOF"; + + setupTest(yaml); + headers_.setCopy(Http::LowerCaseString("x-header-name"), "test_value"); + + rate_limit_entry_->populateDescriptors(headers_, stream_info_, "", descriptors_); + + EXPECT_THAT(std::vector({{{{"header_match", "fake_value"}}}}), + testing::ContainerEq(descriptors_)); +} + +TEST_F(RateLimitPolicyTest, HeaderValueMatchDescriptorKey) { + const std::string yaml = R"EOF( +actions: +- header_value_match: + descriptor_key: fake_key + descriptor_value: fake_value + headers: + - name: x-header-name + string_match: + exact: test_value + )EOF"; + + setupTest(yaml); + headers_.setCopy(Http::LowerCaseString("x-header-name"), "test_value"); + + rate_limit_entry_->populateDescriptors(headers_, stream_info_, "", descriptors_); + + EXPECT_THAT(std::vector({{{{"fake_key", "fake_value"}}}}), + testing::ContainerEq(descriptors_)); +} + +TEST_F(RateLimitPolicyTest, HeaderValueMatchNoMatch) { + const std::string yaml = R"EOF( +actions: +- header_value_match: + descriptor_value: fake_value + headers: + - name: x-header-name + string_match: + exact: test_value + )EOF"; + + setupTest(yaml); + headers_.setCopy(Http::LowerCaseString("x-header-name"), "not_same_value"); + + rate_limit_entry_->populateDescriptors(headers_, stream_info_, "", descriptors_); + + EXPECT_TRUE(descriptors_.empty()); +} + +TEST_F(RateLimitPolicyTest, HeaderValueMatchHeadersNotPresent) { + const std::string yaml = R"EOF( +actions: +- header_value_match: + descriptor_value: fake_value + expect_match: false + headers: + - name: x-header-name + string_match: + exact: test_value + )EOF"; + + setupTest(yaml); + headers_.setCopy(Http::LowerCaseString("x-header-name"), "not_same_value"); + + rate_limit_entry_->populateDescriptors(headers_, stream_info_, "", descriptors_); + + EXPECT_THAT(std::vector({{{{"header_match", "fake_value"}}}}), + testing::ContainerEq(descriptors_)); +} + +TEST_F(RateLimitPolicyTest, HeaderValueMatchHeadersPresent) { + const std::string yaml = R"EOF( +actions: +- header_value_match: + descriptor_value: fake_value + expect_match: false + headers: + - name: x-header-name + string_match: + exact: test_value + )EOF"; + + setupTest(yaml); + headers_.setCopy(Http::LowerCaseString("x-header-name"), "test_value"); + + rate_limit_entry_->populateDescriptors(headers_, stream_info_, "", descriptors_); + + EXPECT_TRUE(descriptors_.empty()); +} + +TEST_F(RateLimitPolicyTest, QueryParameterValueMatch) { + const std::string yaml = R"EOF( +actions: +- query_parameter_value_match: + descriptor_value: fake_value + query_parameters: + - name: x-parameter-name + string_match: + exact: test_value + )EOF"; + + setupTest(yaml); + Http::TestRequestHeaderMapImpl header{{":path", "/?x-parameter-name=test_value"}}; + + rate_limit_entry_->populateDescriptors(header, stream_info_, "", descriptors_); + + EXPECT_THAT(std::vector({{{{"query_match", "fake_value"}}}}), + testing::ContainerEq(descriptors_)); +} + +TEST_F(RateLimitPolicyTest, QueryParameterValueMatchDescriptorKey) { + const std::string yaml = R"EOF( +actions: +- query_parameter_value_match: + descriptor_key: fake_key + descriptor_value: fake_value + query_parameters: + - name: x-parameter-name + string_match: + exact: test_value + )EOF"; + + setupTest(yaml); + Http::TestRequestHeaderMapImpl header{{":path", "/?x-parameter-name=test_value"}}; + + rate_limit_entry_->populateDescriptors(header, stream_info_, "", descriptors_); + + EXPECT_THAT(std::vector({{{{"fake_key", "fake_value"}}}}), + testing::ContainerEq(descriptors_)); +} + +TEST_F(RateLimitPolicyTest, QueryParameterValueMatchNoMatch) { + const std::string yaml = R"EOF( +actions: +- query_parameter_value_match: + descriptor_value: fake_value + query_parameters: + - name: x-parameter-name + string_match: + exact: test_value + )EOF"; + + setupTest(yaml); + Http::TestRequestHeaderMapImpl header{{":path", "/?x-parameter-name=not_same_value"}}; + + rate_limit_entry_->populateDescriptors(header, stream_info_, "", descriptors_); + + EXPECT_TRUE(descriptors_.empty()); +} + +TEST_F(RateLimitPolicyTest, QueryParameterValueMatchExpectNoMatch) { + const std::string yaml = R"EOF( +actions: +- query_parameter_value_match: + descriptor_value: fake_value + expect_match: false + query_parameters: + - name: x-parameter-name + string_match: + exact: test_value + )EOF"; + + setupTest(yaml); + Http::TestRequestHeaderMapImpl header{{":path", "/?x-parameter-name=not_same_value"}}; + + rate_limit_entry_->populateDescriptors(header, stream_info_, "", descriptors_); + + EXPECT_THAT(std::vector({{{{"query_match", "fake_value"}}}}), + testing::ContainerEq(descriptors_)); +} + +TEST_F(RateLimitPolicyTest, QueryParameterValueMatchExpectNoMatchFailed) { + const std::string yaml = R"EOF( +actions: +- query_parameter_value_match: + descriptor_value: fake_value + expect_match: false + query_parameters: + - name: x-parameter-name + string_match: + exact: test_value + )EOF"; + + setupTest(yaml); + Http::TestRequestHeaderMapImpl header{{":path", "/?x-parameter-name=test_value"}}; + + rate_limit_entry_->populateDescriptors(header, stream_info_, "", descriptors_); + + EXPECT_TRUE(descriptors_.empty()); +} + +TEST_F(RateLimitPolicyTest, CompoundActions) { + const std::string yaml = R"EOF( +actions: +- destination_cluster: {} +- source_cluster: {} + )EOF"; + + setupTest(yaml); + + rate_limit_entry_->populateDescriptors(headers_, stream_info_, "service_cluster", descriptors_); + + EXPECT_THAT( + std::vector( + {{{{"destination_cluster", "fake_cluster"}, {"source_cluster", "service_cluster"}}}}), + testing::ContainerEq(descriptors_)); +} + +TEST_F(RateLimitPolicyTest, CompoundActionsNoDescriptor) { + const std::string yaml = R"EOF( +actions: +- destination_cluster: {} +- header_value_match: + descriptor_value: fake_value + headers: + - name: x-header-name + string_match: + exact: test_value + )EOF"; + + setupTest(yaml); + + rate_limit_entry_->populateDescriptors(headers_, stream_info_, "service_cluster", descriptors_); + + EXPECT_TRUE(descriptors_.empty()); +} + +const std::string RequestHeaderMatchInputDescriptor = R"EOF( +actions: +- extension: + name: my_header_name + typed_config: + "@type": type.googleapis.com/envoy.type.matcher.v3.HttpRequestHeaderMatchInput + header_name: x-header-name + )EOF"; + +TEST_F(RateLimitPolicyTest, RequestMatchInput) { + setupTest(RequestHeaderMatchInputDescriptor); + headers_.setCopy(Http::LowerCaseString("x-header-name"), "test_value"); + + rate_limit_entry_->populateDescriptors(headers_, stream_info_, "service_cluster", descriptors_); + + EXPECT_THAT( + std::vector({{{{"my_header_name", "test_value"}}}}), + testing::ContainerEq(descriptors_)); +} + +TEST_F(RateLimitPolicyTest, RequestMatchInputEmpty) { + setupTest(RequestHeaderMatchInputDescriptor); + headers_.setCopy(Http::LowerCaseString("x-header-name"), ""); + + rate_limit_entry_->populateDescriptors(headers_, stream_info_, "service_cluster", descriptors_); + + EXPECT_FALSE(descriptors_.empty()); +} + +TEST_F(RateLimitPolicyTest, RequestMatchInputSkip) { + setupTest(RequestHeaderMatchInputDescriptor); + + rate_limit_entry_->populateDescriptors(headers_, stream_info_, "service_cluster", descriptors_); + + EXPECT_TRUE(descriptors_.empty()); +} + +class ExtensionDescriptorFactory : public Envoy::RateLimit::DescriptorProducerFactory { +public: + ProtobufTypes::MessagePtr createEmptyConfigProto() override { + return std::make_unique(); + } + std::string name() const override { return "test.descriptor_producer"; } + + Envoy::RateLimit::DescriptorProducerPtr + createDescriptorProducerFromProto(const Protobuf::Message&, + Server::Configuration::CommonFactoryContext&) override { + return return_valid_producer_ ? std::make_unique() : nullptr; + } + bool return_valid_producer_{true}; +}; + +TEST_F(RateLimitPolicyTest, ExtensionDescriptorProducer) { + const std::string ExtensionDescriptor = R"EOF( +actions: +- extension: + name: test.descriptor_producer + typed_config: + "@type": type.googleapis.com/google.protobuf.Struct + value: + key: value + )EOF"; + + { + ExtensionDescriptorFactory factory; + Registry::InjectFactory registration(factory); + + setupTest(ExtensionDescriptor); + + rate_limit_entry_->populateDescriptors(headers_, stream_info_, "service_cluster", descriptors_); + EXPECT_THAT( + std::vector({{{{"source_cluster", "service_cluster"}}}}), + testing::ContainerEq(descriptors_)); + } + + { + ExtensionDescriptorFactory factory; + factory.return_valid_producer_ = false; + Registry::InjectFactory registration(factory); + + setupTest(ExtensionDescriptor); + + EXPECT_TRUE( + absl::StartsWith(creation_status_.message(), "Rate limit descriptor extension failed:")); + } +} + +} // namespace +} // namespace RateLimit +} // namespace Common +} // namespace Filters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/common/ratelimit_config/ratelimit_config_test.proto b/test/extensions/filters/common/ratelimit_config/ratelimit_config_test.proto new file mode 100644 index 000000000000..08d48af9cb86 --- /dev/null +++ b/test/extensions/filters/common/ratelimit_config/ratelimit_config_test.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +package test.extensions.filters.common.ratelimit_config; + +import "envoy/config/route/v3/route_components.proto"; + +message TestRateLimitConfig { + repeated envoy.config.route.v3.RateLimit rate_limits = 1; +} diff --git a/test/extensions/filters/http/local_ratelimit/BUILD b/test/extensions/filters/http/local_ratelimit/BUILD index bb876c07f70c..e71f31db0857 100644 --- a/test/extensions/filters/http/local_ratelimit/BUILD +++ b/test/extensions/filters/http/local_ratelimit/BUILD @@ -22,8 +22,7 @@ envoy_extension_cc_test( "//source/extensions/filters/http/local_ratelimit:local_ratelimit_lib", "//test/common/stream_info:test_util", "//test/mocks/http:http_mocks", - "//test/mocks/local_info:local_info_mocks", - "//test/mocks/upstream:cluster_manager_mocks", + "//test/mocks/server:server_mocks", "//test/test_common:test_runtime_lib", "//test/test_common:utility_lib", "@envoy_api//envoy/extensions/filters/http/local_ratelimit/v3:pkg_cc_proto", diff --git a/test/extensions/filters/http/local_ratelimit/filter_test.cc b/test/extensions/filters/http/local_ratelimit/filter_test.cc index 4495dcaf7a6a..cfecb31560b0 100644 --- a/test/extensions/filters/http/local_ratelimit/filter_test.cc +++ b/test/extensions/filters/http/local_ratelimit/filter_test.cc @@ -4,8 +4,7 @@ #include "source/extensions/filters/http/local_ratelimit/local_ratelimit.h" #include "test/mocks/http/mocks.h" -#include "test/mocks/local_info/mocks.h" -#include "test/mocks/upstream/cluster_manager.h" +#include "test/mocks/server/mocks.h" #include "test/test_common/test_runtime.h" #include "test/test_common/thread_factory_for_test.h" @@ -64,12 +63,12 @@ class FilterTest : public testing::Test { void setupPerRoute(const std::string& yaml, const bool enabled = true, const bool enforced = true, const bool per_route = false) { EXPECT_CALL( - runtime_.snapshot_, + factory_context_.runtime_loader_.snapshot_, featureEnabled(absl::string_view("test_enabled"), testing::Matcher(Percent(100)))) .WillRepeatedly(testing::Return(enabled)); EXPECT_CALL( - runtime_.snapshot_, + factory_context_.runtime_loader_.snapshot_, featureEnabled(absl::string_view("test_enforced"), testing::Matcher(Percent(100)))) .WillRepeatedly(testing::Return(enforced)); @@ -80,8 +79,7 @@ class FilterTest : public testing::Test { envoy::extensions::filters::http::local_ratelimit::v3::LocalRateLimit config; TestUtility::loadFromYaml(yaml, config); config_ = - std::make_shared(config, local_info_, dispatcher_, cm_, singleton_manager_, - *stats_.rootScope(), runtime_, per_route); + std::make_shared(config, factory_context_, *stats_.rootScope(), per_route); filter_ = std::make_shared(config_); filter_->setDecoderFilterCallbacks(decoder_callbacks_); @@ -103,10 +101,8 @@ class FilterTest : public testing::Test { testing::NiceMock decoder_callbacks_; testing::NiceMock decoder_callbacks_2_; NiceMock dispatcher_; - NiceMock runtime_; - NiceMock local_info_; - NiceMock cm_; - Singleton::ManagerImpl singleton_manager_; + + NiceMock factory_context_; std::shared_ptr config_; std::shared_ptr filter_; @@ -115,7 +111,7 @@ class FilterTest : public testing::Test { TEST_F(FilterTest, Runtime) { setup(fmt::format(config_yaml, "false", "1", "false", "\"OFF\""), false, false); - EXPECT_EQ(&runtime_, &(config_->runtime())); + EXPECT_EQ(&factory_context_.runtime_loader_, &(config_->runtime())); } TEST_F(FilterTest, ToErrorCode) { @@ -400,6 +396,56 @@ enable_x_ratelimit_headers: {} stage: {} )"; +static constexpr absl::string_view inlined_descriptor_config_yaml = R"( +stat_prefix: test +token_bucket: + max_tokens: {} + tokens_per_fill: 1 + fill_interval: 60s +filter_enabled: + runtime_key: test_enabled + default_value: + numerator: 100 + denominator: HUNDRED +filter_enforced: + runtime_key: test_enforced + default_value: + numerator: 100 + denominator: HUNDRED +response_headers_to_add: + - append_action: OVERWRITE_IF_EXISTS_OR_ADD + header: + key: x-test-rate-limit + value: 'true' +enable_x_ratelimit_headers: {} +descriptors: +- entries: + - key: hello + value: world + - key: foo + value: bar + token_bucket: + max_tokens: 10 + tokens_per_fill: 10 + fill_interval: 60s +- entries: + - key: foo2 + value: bar2 + token_bucket: + max_tokens: {} + tokens_per_fill: 1 + fill_interval: 60s +rate_limits: +- actions: + - header_value_match: + descriptor_key: foo2 + descriptor_value: bar2 + headers: + - name: x-header-name + string_match: + exact: test_value + )"; + static constexpr absl::string_view consume_default_token_config_yaml = R"( stat_prefix: test token_bucket: @@ -938,6 +984,24 @@ TEST_F(DescriptorFilterTest, IncludeVirtualHostRateLimitsSetTrue) { EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(headers, false)); } +TEST_F(DescriptorFilterTest, UseInlinedRateLimitConfig) { + setUpTest(fmt::format(inlined_descriptor_config_yaml, "10", R"("OFF")", "1")); + + auto headers = Http::TestRequestHeaderMapImpl(); + // Requests will not be blocked because the requests don't match any descriptor and + // the global token bucket has enough tokens. + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(headers, false)); + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(headers, false)); + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(headers, false)); + + headers.setCopy(Http::LowerCaseString("x-header-name"), "test_value"); + + // Only one request is allowed in 60s for the matched request. + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(headers, false)); + EXPECT_CALL(decoder_callbacks_, sendLocalReply(Http::Code::TooManyRequests, _, _, _, _)); + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, filter_->decodeHeaders(headers, false)); +} + } // namespace LocalRateLimitFilter } // namespace HttpFilters } // namespace Extensions