Skip to content

Commit

Permalink
ratelimit: added support for rate limiting based on query parameters
Browse files Browse the repository at this point in the history
Signed-off-by: Rohit Agrawal <[email protected]>
  • Loading branch information
agrawroh committed Jan 4, 2025
1 parent b0d58be commit 0f93a8a
Show file tree
Hide file tree
Showing 8 changed files with 327 additions and 0 deletions.
31 changes: 31 additions & 0 deletions api/envoy/config/route/v3/route_components.proto
Original file line number Diff line number Diff line change
Expand Up @@ -1935,6 +1935,34 @@ message RateLimit {
bool skip_if_absent = 3;
}

// The following descriptor entry is appended when a query parameter contains a key that matches the
// ``query_parameter_name``:
//
// .. code-block:: cpp
//
// ("<descriptor_key>", "<query_parameter_value_queried_from_query_parameter>")
message QueryParameters {
// The name of the query parameter to use for rate limiting. Value of this query parameter is used to populate
// the value of the descriptor entry for the descriptor_key.
string query_parameter_name = 1 [(validate.rules).string = {min_len: 1}];

// The key to use when creating the rate limit descriptor entry. his descriptor key will be used to identify the
// rate limit rule in the rate limiting service.
string descriptor_key = 2 [(validate.rules).string = {min_len: 1}];

// Controls the behavior when the specified query parameter is not present in the request.
//
// If set to ``true`` (default is ``false``):
// - The rate limiting service will **NOT** be called for this descriptor.
// - Useful when the query parameter is optional and you want to skip rate limiting.
//
// If set to ``false``:
// - The rate limiting service will be called.
// - Useful when you want to enforce rate limiting even if the query parameter is missing.
//
bool skip_if_absent = 3;
}

// The following descriptor entry is appended to the descriptor and is populated using the
// trusted address from :ref:`x-forwarded-for <config_http_conn_man_headers_x-forwarded-for>`:
//
Expand Down Expand Up @@ -2111,6 +2139,9 @@ message RateLimit {
// Rate limit on request headers.
RequestHeaders request_headers = 3;

// Rate limit on query parameters.
QueryParameters query_parameters = 12;

// Rate limit on remote address.
RemoteAddress remote_address = 4;

Expand Down
10 changes: 10 additions & 0 deletions changelogs/current.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,16 @@ new_features:
change: |
Add the option to reduce the rate limit budget based on request/response contexts on stream done.
See :ref:`apply_on_stream_done <envoy_v3_api_field_config.route.v3.RateLimit.apply_on_stream_done>` for more details.
- area: ratelimit
change: |
added support for use of dynamic metadata :ref:`dynamic_metadata
<envoy_v3_api_field_config.route.v3.RateLimit.Action.dynamic_metadata>` as a ratelimit action.
- area: ratelimit
change: |
added support for query parameter rate limiting via the :ref:`query_parameters
<envoy_v3_api_field_config.route.v3.RateLimit.Action.query_parameters>` action across HTTP and Thrift. This allows
rate limiting based on specific query parameter values, with option to control the behavior when the query parameter
is absent.
deprecated:
- area: rbac
Expand Down
28 changes: 28 additions & 0 deletions source/common/router/router_ratelimit.cc
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,31 @@ bool MetaDataAction::populateDescriptor(RateLimit::DescriptorEntry& descriptor_e
return skip_if_absent_;
}

QueryParametersAction::QueryParametersAction(
const envoy::config::route::v3::RateLimit::Action::QueryParameters& action)
: query_param_name_(action.query_parameter_name()),
descriptor_key_(!action.descriptor_key().empty() ? action.descriptor_key() : "query_param"),
skip_if_absent_(action.skip_if_absent()) {}

bool QueryParametersAction::populateDescriptor(RateLimit::DescriptorEntry& descriptor_entry,
const std::string&,
const Http::RequestHeaderMap& headers,
const StreamInfo::StreamInfo&) const {
Http::Utility::QueryParamsMulti query_parameters =
Http::Utility::QueryParamsMulti::parseAndDecodeQueryString(headers.getPathValue());

const auto query_param_value = query_parameters.getFirstValue(query_param_name_);

// If query parameter is not present and ``skip_if_absent`` is ``true``, skip this descriptor.
// If ``skip_if_absent`` is ``false``, do not call rate limiting service.
if (!query_param_value.has_value()) {
return skip_if_absent_;
}

descriptor_entry = {descriptor_key_, query_param_value.value()};
return true;
}

HeaderValueMatchAction::HeaderValueMatchAction(
const envoy::config::route::v3::RateLimit::Action::HeaderValueMatch& action,
Server::Configuration::CommonFactoryContext& context)
Expand Down Expand Up @@ -277,6 +302,9 @@ RateLimitPolicyEntryImpl::RateLimitPolicyEntryImpl(
case envoy::config::route::v3::RateLimit::Action::ActionSpecifierCase::kDestinationCluster:
actions_.emplace_back(new DestinationClusterAction());
break;
case envoy::config::route::v3::RateLimit::Action::ActionSpecifierCase::kQueryParameters:
actions_.emplace_back(new QueryParametersAction(action.query_parameters()));
break;
case envoy::config::route::v3::RateLimit::Action::ActionSpecifierCase::kRequestHeaders:
actions_.emplace_back(new RequestHeadersAction(action.request_headers()));
break;
Expand Down
19 changes: 19 additions & 0 deletions source/common/router/router_ratelimit.h
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,25 @@ class MetaDataAction : public RateLimit::DescriptorProducer {
const bool skip_if_absent_;
};

/**
* Action for query parameters rate limiting.
*/
class QueryParametersAction : public RateLimit::DescriptorProducer {
public:
QueryParametersAction(const envoy::config::route::v3::RateLimit::Action::QueryParameters& action);

// Ratelimit::DescriptorProducer
bool populateDescriptor(RateLimit::DescriptorEntry& descriptor_entry,
const std::string& local_service_cluster,
const Http::RequestHeaderMap& headers,
const StreamInfo::StreamInfo& info) const override;

private:
const std::string query_param_name_;
const std::string descriptor_key_;
const bool skip_if_absent_;
};

/**
* Action for header value match rate limiting.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,25 @@ bool DestinationClusterAction::populateDescriptor(const RouteEntry& route,
return true;
}

bool QueryParametersAction::populateDescriptor(const RouteEntry&, RateLimit::Descriptor& descriptor,
const std::string&, const MessageMetadata& metadata,
const Network::Address::Instance&) const {
Http::Utility::QueryParamsMulti query_parameters =
Http::Utility::QueryParamsMulti::parseAndDecodeQueryString(
metadata.requestHeaders().getPathValue());

const auto query_param_value = query_parameters.getFirstValue(query_param_name_);

// If query parameter is not present and ``skip_if_absent`` is ``true``, skip this descriptor.
// If ``skip_if_absent`` is ``false``, do not call rate limiting service.
if (!query_param_value.has_value()) {
return skip_if_absent_;
}

descriptor.entries_.push_back({descriptor_key_, query_param_value.value()});
return true;
}

bool RequestHeadersAction::populateDescriptor(const RouteEntry&, RateLimit::Descriptor& descriptor,
const std::string&, const MessageMetadata& metadata,
const Network::Address::Instance&) const {
Expand Down Expand Up @@ -102,6 +121,9 @@ RateLimitPolicyEntryImpl::RateLimitPolicyEntryImpl(
case envoy::config::route::v3::RateLimit::Action::ActionSpecifierCase::kDestinationCluster:
actions_.emplace_back(new DestinationClusterAction());
break;
case envoy::config::route::v3::RateLimit::Action::ActionSpecifierCase::kQueryParameters:
actions_.emplace_back(new QueryParametersAction(action.query_parameters()));
break;
case envoy::config::route::v3::RateLimit::Action::ActionSpecifierCase::kRequestHeaders:
actions_.emplace_back(new RequestHeadersAction(action.request_headers()));
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,26 @@ class DestinationClusterAction : public RateLimitAction {
const Network::Address::Instance& remote_address) const override;
};

/**
* Action for query parameters rate limiting.
*/
class QueryParametersAction : public RateLimitAction {
public:
QueryParametersAction(const envoy::config::route::v3::RateLimit::Action::QueryParameters& action)
: query_param_name_(action.query_parameter_name()), descriptor_key_(action.descriptor_key()),
skip_if_absent_(action.skip_if_absent()) {}

// Ratelimit::RateLimitAction
bool populateDescriptor(const Router::RouteEntry& route, RateLimit::Descriptor& descriptor,
const std::string& local_service_cluster, const MessageMetadata& metadata,
const Network::Address::Instance& remote_address) const override;

private:
const std::string query_param_name_;
const std::string descriptor_key_;
const bool skip_if_absent_;
};

/**
* Action for request headers rate limiting.
*/
Expand Down
86 changes: 86 additions & 0 deletions test/common/router/router_ratelimit_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1239,6 +1239,92 @@ TEST_F(RateLimitPolicyEntryTest, RequestMatchInputSkip) {
EXPECT_TRUE(descriptors_.empty());
}

TEST_F(RateLimitPolicyEntryTest, QueryParametersBasicMatch) {
const std::string yaml = R"EOF(
actions:
- query_parameters:
query_parameter_name: x-parameter-name
descriptor_key: my_param
)EOF";

setupTest(yaml);
Http::TestRequestHeaderMapImpl header{{":path", "/?x-parameter-name=test_value"}};

rate_limit_entry_->populateDescriptors(descriptors_, "", header, stream_info_);
EXPECT_THAT(std::vector<Envoy::RateLimit::Descriptor>({{{{"my_param", "test_value"}}}}),
testing::ContainerEq(descriptors_));
}

TEST_F(RateLimitPolicyEntryTest, QueryParametersSkipIfAbsentFalse) {
const std::string yaml = R"EOF(
actions:
- query_parameters:
query_parameter_name: x-parameter-name
descriptor_key: my_param
skip_if_absent: false
)EOF";

setupTest(yaml);
// No matching query parameter
Http::TestRequestHeaderMapImpl header{{":path", "/no-match"}};

rate_limit_entry_->populateDescriptors(descriptors_, "", header, stream_info_);
EXPECT_TRUE(descriptors_.empty());
}

TEST_F(RateLimitPolicyEntryTest, QueryParametersSkipIfAbsentTrue) {
const std::string yaml = R"EOF(
actions:
- query_parameters:
query_parameter_name: x-parameter-name
descriptor_key: my_param
skip_if_absent: true
)EOF";

setupTest(yaml);
// No matching query parameter
Http::TestRequestHeaderMapImpl header{{":path", "/no-match"}};

rate_limit_entry_->populateDescriptors(descriptors_, "", header, stream_info_);
// Descriptor should be added even if the query parameter is not present
EXPECT_FALSE(descriptors_.empty());
}

TEST_F(RateLimitPolicyEntryTest, QueryParametersMultipleValues) {
const std::string yaml = R"EOF(
actions:
- query_parameters:
query_parameter_name: x-parameter-name
descriptor_key: my_param
)EOF";

setupTest(yaml);
// Multiple values for the same query parameter
Http::TestRequestHeaderMapImpl header{
{":path", "/?x-parameter-name=value1&x-parameter-name=value2"}};

rate_limit_entry_->populateDescriptors(descriptors_, "", header, stream_info_);
EXPECT_THAT(std::vector<Envoy::RateLimit::Descriptor>({{{{"my_param", "value1"}}}}),
testing::ContainerEq(descriptors_));
}

TEST_F(RateLimitPolicyEntryTest, QueryParametersUrlEncoding) {
const std::string yaml = R"EOF(
actions:
- query_parameters:
query_parameter_name: test-parameter
descriptor_key: my_param
)EOF";

setupTest(yaml);
// URL-encoded query parameter
Http::TestRequestHeaderMapImpl header{{":path", "/?test-parameter=hello%20world"}};

rate_limit_entry_->populateDescriptors(descriptors_, "", header, stream_info_);
EXPECT_THAT(std::vector<Envoy::RateLimit::Descriptor>({{{{"my_param", "hello world"}}}}),
testing::ContainerEq(descriptors_));
}

} // namespace
} // namespace Router
} // namespace Envoy
Loading

0 comments on commit 0f93a8a

Please sign in to comment.