Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions api/envoy/config/filter/http/ext_authz/v2/ext_authz.proto
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,18 @@ message ExtAuthz {

// Enables filter to buffer the client request body and send it within the authorization request.
BufferSettings with_request_body = 5;

// Clears route cache in order to allow the external authorization service to correctly affect
// routing decisions. Filter clears all cached routes when:
//
// 1. The field is set to *true*.
//
Comment thread
gsagula marked this conversation as resolved.
// 2. The status returned from the authorization service is a HTTP 200 or gRPC 0.
//
// 3. At least one *authorization response header* is added to the client request, or is used for
// altering another client request header.
//
bool clear_route_cache = 6;
}

// Configuration for buffering the request data.
Expand Down
1 change: 1 addition & 0 deletions docs/root/intro/version_history.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Version history
================
* dubbo_proxy: support the :ref:`Dubbo proxy filter <config_network_filters_dubbo_proxy>`.
* event: added :ref:`loop duration and poll delay statistics <operations_performance>`.
* ext_authz: added option to `ext_authz` that allows the filter clearing route cache.
* eds: added support to specify max time for which endpoints can be used :ref:`gRPC filter <envoy_api_msg_ClusterLoadAssignment.Policy>`.
* http: mitigated a race condition with the :ref:`delayed_close_timeout<envoy_api_field_config.filter.network.http_connection_manager.v2.HttpConnectionManager.delayed_close_timeout>` where it could trigger while actively flushing a pending write buffer for a downstream connection.
* redis: added :ref:`prefix routing <envoy_api_field_config.filter.network.redis_proxy.v2.RedisProxy.prefix_routes>` to enable routing commands based on their key's prefix to different upstream.
Expand Down
6 changes: 6 additions & 0 deletions source/extensions/filters/http/ext_authz/ext_authz.cc
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,12 @@ void Filter::onComplete(Filters::Common::ExtAuthz::ResponsePtr&& response) {
// Only send headers if the response is ok.
if (response->status == CheckStatus::OK) {
ENVOY_STREAM_LOG(trace, "ext_authz filter added header(s) to the request:", *callbacks_);
if (config_->clearRouteCache() &&
(!response->headers_to_add.empty() || !response->headers_to_append.empty())) {
Comment thread
gsagula marked this conversation as resolved.
ENVOY_STREAM_LOG(debug, "ext_authz is clearing route cache", *callbacks_);
callbacks_->clearRouteCache();
}

for (const auto& header : response->headers_to_add) {
Http::HeaderEntry* header_to_modify = request_headers_->get(header.first);
if (header_to_modify) {
Expand Down
4 changes: 4 additions & 0 deletions source/extensions/filters/http/ext_authz/ext_authz.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class FilterConfig {
Runtime::Loader& runtime, Http::Context& http_context)
: allow_partial_message_(config.with_request_body().allow_partial_message()),
failure_mode_allow_(config.failure_mode_allow()),
clear_route_cache_(config.clear_route_cache()),
max_request_bytes_(config.with_request_body().max_request_bytes()), local_info_(local_info),
scope_(scope), runtime_(runtime), http_context_(http_context) {}

Expand All @@ -50,6 +51,8 @@ class FilterConfig {

bool failureModeAllow() const { return failure_mode_allow_; }

bool clearRouteCache() const { return clear_route_cache_; }

uint32_t maxRequestBytes() const { return max_request_bytes_; }

const LocalInfo::LocalInfo& localInfo() const { return local_info_; }
Expand All @@ -63,6 +66,7 @@ class FilterConfig {
private:
const bool allow_partial_message_;
const bool failure_mode_allow_;
const bool clear_route_cache_;
const uint32_t max_request_bytes_;
const LocalInfo::LocalInfo& local_info_;
Stats::Scope& scope_;
Expand Down
221 changes: 220 additions & 1 deletion test/extensions/filters/http/ext_authz/ext_authz_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,225 @@ TEST_F(HttpFilterTest, HeaderOnlyRequestWithStream) {
EXPECT_EQ(Http::FilterTrailersStatus::StopIteration, filter_->decodeTrailers(request_headers_));
}

// Verifies that the filter clears the route cache when an authorization response:
// 1. is an OK response.
// 2. has headers to append.
// 3. has headers to add.
TEST_F(HttpFilterTest, ClearCache) {
InSequence s;

initialize(R"EOF(
grpc_service:
envoy_grpc:
cluster_name: "ext_authz_server"
clear_route_cache: true
)EOF");

prepareCheck();

EXPECT_CALL(*client_, check(_, _, testing::A<Tracing::Span&>()))
.WillOnce(
WithArgs<0>(Invoke([&](Filters::Common::ExtAuthz::RequestCallbacks& callbacks) -> void {
request_callbacks_ = &callbacks;
})));
EXPECT_CALL(filter_callbacks_, clearRouteCache()).Times(1);
EXPECT_EQ(Http::FilterHeadersStatus::StopAllIterationAndWatermark,
filter_->decodeHeaders(request_headers_, false));
EXPECT_EQ(Http::FilterDataStatus::Continue, filter_->decodeData(data_, false));
EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_->decodeTrailers(request_headers_));
EXPECT_CALL(filter_callbacks_, continueDecoding());
EXPECT_CALL(filter_callbacks_.stream_info_,
setResponseFlag(Envoy::StreamInfo::ResponseFlag::UnauthorizedExternalService))
.Times(0);

Filters::Common::ExtAuthz::Response response{};
response.status = Filters::Common::ExtAuthz::CheckStatus::OK;
response.headers_to_append = Http::HeaderVector{{Http::LowerCaseString{"foo"}, "bar"}};
response.headers_to_add = Http::HeaderVector{{Http::LowerCaseString{"bar"}, "foo"}};
request_callbacks_->onComplete(std::make_unique<Filters::Common::ExtAuthz::Response>(response));
EXPECT_EQ(1U, filter_callbacks_.clusterInfo()->statsScope().counter("ext_authz.ok").value());
}

// Verifies that the filter clears the route cache when an authorization response:
// 1. is an OK response.
// 2. has headers to append.
// 3. has NO headers to add.
TEST_F(HttpFilterTest, ClearCacheRouteHeadersToAppendOnly) {
InSequence s;

initialize(R"EOF(
grpc_service:
envoy_grpc:
cluster_name: "ext_authz_server"
clear_route_cache: true
)EOF");

prepareCheck();

EXPECT_CALL(*client_, check(_, _, testing::A<Tracing::Span&>()))
.WillOnce(
WithArgs<0>(Invoke([&](Filters::Common::ExtAuthz::RequestCallbacks& callbacks) -> void {
request_callbacks_ = &callbacks;
})));
EXPECT_CALL(filter_callbacks_, clearRouteCache()).Times(1);
EXPECT_EQ(Http::FilterHeadersStatus::StopAllIterationAndWatermark,
filter_->decodeHeaders(request_headers_, false));
EXPECT_EQ(Http::FilterDataStatus::Continue, filter_->decodeData(data_, false));
EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_->decodeTrailers(request_headers_));
EXPECT_CALL(filter_callbacks_, continueDecoding());
EXPECT_CALL(filter_callbacks_.stream_info_,
setResponseFlag(Envoy::StreamInfo::ResponseFlag::UnauthorizedExternalService))
.Times(0);

Filters::Common::ExtAuthz::Response response{};
response.status = Filters::Common::ExtAuthz::CheckStatus::OK;
response.headers_to_append = Http::HeaderVector{{Http::LowerCaseString{"foo"}, "bar"}};
request_callbacks_->onComplete(std::make_unique<Filters::Common::ExtAuthz::Response>(response));
EXPECT_EQ(1U, filter_callbacks_.clusterInfo()->statsScope().counter("ext_authz.ok").value());
}

// Verifies that the filter clears the route cache when an authorization response:
// 1. is an OK response.
// 2. has headers to add.
// 3. has NO headers to append.
TEST_F(HttpFilterTest, ClearCacheRouteHeadersToAddOnly) {
InSequence s;

initialize(R"EOF(
grpc_service:
envoy_grpc:
cluster_name: "ext_authz_server"
clear_route_cache: true
)EOF");

prepareCheck();

EXPECT_CALL(*client_, check(_, _, testing::A<Tracing::Span&>()))
.WillOnce(
WithArgs<0>(Invoke([&](Filters::Common::ExtAuthz::RequestCallbacks& callbacks) -> void {
request_callbacks_ = &callbacks;
})));
EXPECT_CALL(filter_callbacks_, clearRouteCache()).Times(1);
EXPECT_EQ(Http::FilterHeadersStatus::StopAllIterationAndWatermark,
filter_->decodeHeaders(request_headers_, false));
EXPECT_EQ(Http::FilterDataStatus::Continue, filter_->decodeData(data_, false));
EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_->decodeTrailers(request_headers_));
EXPECT_CALL(filter_callbacks_, continueDecoding());
EXPECT_CALL(filter_callbacks_.stream_info_,
setResponseFlag(Envoy::StreamInfo::ResponseFlag::UnauthorizedExternalService))
.Times(0);

Filters::Common::ExtAuthz::Response response{};
response.status = Filters::Common::ExtAuthz::CheckStatus::OK;
response.headers_to_add = Http::HeaderVector{{Http::LowerCaseString{"foo"}, "bar"}};
request_callbacks_->onComplete(std::make_unique<Filters::Common::ExtAuthz::Response>(response));
EXPECT_EQ(1U, filter_callbacks_.clusterInfo()->statsScope().counter("ext_authz.ok").value());
}

// Verifies that the filter DOES NOT clear the route cache when an authorization response:
// 1. is an OK response.
// 2. has NO headers to add or to append.
TEST_F(HttpFilterTest, NoClearCacheRoute) {
InSequence s;

initialize(R"EOF(
grpc_service:
envoy_grpc:
cluster_name: "ext_authz_server"
clear_route_cache: true
)EOF");

prepareCheck();

EXPECT_CALL(*client_, check(_, _, testing::A<Tracing::Span&>()))
.WillOnce(
WithArgs<0>(Invoke([&](Filters::Common::ExtAuthz::RequestCallbacks& callbacks) -> void {
request_callbacks_ = &callbacks;
})));
EXPECT_CALL(filter_callbacks_, clearRouteCache()).Times(0);
EXPECT_EQ(Http::FilterHeadersStatus::StopAllIterationAndWatermark,
filter_->decodeHeaders(request_headers_, false));
EXPECT_EQ(Http::FilterDataStatus::Continue, filter_->decodeData(data_, false));
EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_->decodeTrailers(request_headers_));
EXPECT_CALL(filter_callbacks_, continueDecoding());
EXPECT_CALL(filter_callbacks_.stream_info_,
setResponseFlag(Envoy::StreamInfo::ResponseFlag::UnauthorizedExternalService))
.Times(0);

Filters::Common::ExtAuthz::Response response{};
response.status = Filters::Common::ExtAuthz::CheckStatus::OK;
request_callbacks_->onComplete(std::make_unique<Filters::Common::ExtAuthz::Response>(response));
EXPECT_EQ(1U, filter_callbacks_.clusterInfo()->statsScope().counter("ext_authz.ok").value());
}

// Verifies that the filter DOES NOT clear the route cache when clear_route_cache is set to false.
TEST_F(HttpFilterTest, NoClearCacheRouteConfig) {
InSequence s;

initialize(R"EOF(
grpc_service:
envoy_grpc:
cluster_name: "ext_authz_server"
)EOF");

prepareCheck();

EXPECT_CALL(*client_, check(_, _, testing::A<Tracing::Span&>()))
.WillOnce(
WithArgs<0>(Invoke([&](Filters::Common::ExtAuthz::RequestCallbacks& callbacks) -> void {
request_callbacks_ = &callbacks;
})));
EXPECT_CALL(filter_callbacks_, clearRouteCache()).Times(0);
EXPECT_EQ(Http::FilterHeadersStatus::StopAllIterationAndWatermark,
filter_->decodeHeaders(request_headers_, false));
EXPECT_EQ(Http::FilterDataStatus::Continue, filter_->decodeData(data_, false));
EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_->decodeTrailers(request_headers_));
EXPECT_CALL(filter_callbacks_, continueDecoding());
EXPECT_CALL(filter_callbacks_.stream_info_,
setResponseFlag(Envoy::StreamInfo::ResponseFlag::UnauthorizedExternalService))
.Times(0);

Filters::Common::ExtAuthz::Response response{};
response.status = Filters::Common::ExtAuthz::CheckStatus::OK;
response.headers_to_append = Http::HeaderVector{{Http::LowerCaseString{"foo"}, "bar"}};
response.headers_to_add = Http::HeaderVector{{Http::LowerCaseString{"bar"}, "foo"}};
request_callbacks_->onComplete(std::make_unique<Filters::Common::ExtAuthz::Response>(response));
EXPECT_EQ(1U, filter_callbacks_.clusterInfo()->statsScope().counter("ext_authz.ok").value());
}

// Verifies that the filter DOES NOT clear the route cache when authorization response is NOT OK.
TEST_F(HttpFilterTest, NoClearCacheRouteDeniedResponse) {
InSequence s;

initialize(R"EOF(
grpc_service:
envoy_grpc:
cluster_name: "ext_authz_server"
clear_route_cache: true
)EOF");

prepareCheck();

Filters::Common::ExtAuthz::Response response{};
response.status = Filters::Common::ExtAuthz::CheckStatus::Denied;
response.status_code = Http::Code::Unauthorized;
response.headers_to_add = Http::HeaderVector{{Http::LowerCaseString{"foo"}, "bar"}};
auto response_ptr = std::make_unique<Filters::Common::ExtAuthz::Response>(response);

EXPECT_CALL(*client_, check(_, _, testing::A<Tracing::Span&>()))
.WillOnce(
WithArgs<0>(Invoke([&](Filters::Common::ExtAuthz::RequestCallbacks& callbacks) -> void {
callbacks.onComplete(std::move(response_ptr));
})));
EXPECT_CALL(filter_callbacks_, clearRouteCache()).Times(0);
EXPECT_CALL(filter_callbacks_, continueDecoding()).Times(0);
EXPECT_EQ(Http::FilterHeadersStatus::StopAllIterationAndWatermark,
filter_->decodeHeaders(request_headers_, false));
EXPECT_EQ(Http::FilterDataStatus::Continue, filter_->decodeData(data_, false));
EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_->decodeTrailers(request_headers_));
EXPECT_EQ(1U, filter_callbacks_.clusterInfo()->statsScope().counter("ext_authz.denied").value());
}

// -------------------
// Parameterized Tests
// -------------------
Expand Down Expand Up @@ -563,7 +782,7 @@ TEST_F(HttpFilterTestParam, DisabledOnRoute) {

// baseline: make sure that when not disabled, check is called
test_disable(false);
EXPECT_CALL(*client_, check(_, _, _)).Times(1);
EXPECT_CALL(*client_, check(_, _, testing::A<Tracing::Span&>())).Times(1);
// Engage the filter.
EXPECT_EQ(Http::FilterHeadersStatus::StopAllIterationAndWatermark,
filter_->decodeHeaders(request_headers_, false));
Expand Down