Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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
9 changes: 9 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,15 @@ 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*.
// 2. The status returned from the authorization service is a HTTP 200 or gRPC 0.
// 3. One or more *authorization response headers* are addressed to the upstream.
Comment thread
gsagula marked this conversation as resolved.
Outdated
//
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 @@ -3,6 +3,7 @@ Version history

1.11.0 (Pending)
================
* ext_authz: added option to `ext_authz` that allows the filter clearing route cache.
Comment thread
gsagula marked this conversation as resolved.
Outdated
* dubbo_proxy: support the :ref:`Dubbo proxy filter <config_network_filters_dubbo_proxy>`.
* 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: add support for zpopmax and zpopmin commands.
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 @@ -182,6 +182,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 @@ -500,6 +500,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::StopIteration,
filter_->decodeHeaders(request_headers_, false));
EXPECT_EQ(Http::FilterDataStatus::StopIterationAndWatermark, filter_->decodeData(data_, false));
EXPECT_EQ(Http::FilterTrailersStatus::StopIteration, 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::StopIteration,
filter_->decodeHeaders(request_headers_, false));
EXPECT_EQ(Http::FilterDataStatus::StopIterationAndWatermark, filter_->decodeData(data_, false));
EXPECT_EQ(Http::FilterTrailersStatus::StopIteration, 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::StopIteration,
filter_->decodeHeaders(request_headers_, false));
EXPECT_EQ(Http::FilterDataStatus::StopIterationAndWatermark, filter_->decodeData(data_, false));
EXPECT_EQ(Http::FilterTrailersStatus::StopIteration, 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::StopIteration,
filter_->decodeHeaders(request_headers_, false));
EXPECT_EQ(Http::FilterDataStatus::StopIterationAndWatermark, filter_->decodeData(data_, false));
EXPECT_EQ(Http::FilterTrailersStatus::StopIteration, 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::StopIteration,
filter_->decodeHeaders(request_headers_, false));
EXPECT_EQ(Http::FilterDataStatus::StopIterationAndWatermark, filter_->decodeData(data_, false));
EXPECT_EQ(Http::FilterTrailersStatus::StopIteration, 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::StopIteration,
filter_->decodeHeaders(request_headers_, false));
EXPECT_EQ(Http::FilterDataStatus::StopIterationAndWatermark, filter_->decodeData(data_, false));
EXPECT_EQ(Http::FilterTrailersStatus::StopIteration, filter_->decodeTrailers(request_headers_));
EXPECT_EQ(1U, filter_callbacks_.clusterInfo()->statsScope().counter("ext_authz.denied").value());
}

// -------------------
// Parameterized Tests
// -------------------
Expand Down Expand Up @@ -566,7 +785,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::StopIteration,
filter_->decodeHeaders(request_headers_, false));
Expand Down