Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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
1 change: 1 addition & 0 deletions api/envoy/config/filter/http/ext_authz/v2/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ api_proto_library_internal(
"//envoy/api/v2/core:base",
"//envoy/api/v2/core:grpc_service",
"//envoy/api/v2/core:http_uri",
"//envoy/type:http_status",
"//envoy/type/matcher:string",
],
)
6 changes: 6 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 @@ -11,6 +11,7 @@ import "envoy/api/v2/core/base.proto";
import "envoy/api/v2/core/grpc_service.proto";
import "envoy/api/v2/core/http_uri.proto";

import "envoy/type/http_status.proto";
import "envoy/type/matcher/string.proto";

import "validate/validate.proto";
Expand Down Expand Up @@ -67,6 +68,11 @@ message ExtAuthz {
// altering another client request header.
//
bool clear_route_cache = 6;

// Sets the HTTP status that is returned to the client when a network error compromises the
// communication between the filter and the authorization server. The default status is HTTP 403
// Forbidden.
envoy.type.HttpStatus status_on_error = 7;
}

// Configuration for buffering the request data.
Expand Down
5 changes: 3 additions & 2 deletions docs/root/intro/version_history.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ Version history
================
* access log: added a new field for response code details in :ref:`file access logger<config_access_log_format_response_code_details>` and :ref:`gRPC access logger<envoy_api_field_data.accesslog.v2.HTTPResponseProperties.response_code_details>`.
* dubbo_proxy: support the :ref:`Dubbo proxy filter <config_network_filters_dubbo_proxy>`.
* eds: added support to specify max time for which endpoints can be used :ref:`gRPC filter <envoy_api_msg_ClusterLoadAssignment.Policy>`.
* event: added :ref:`loop duration and poll delay statistics <operations_performance>`.
* ext_authz: added a `x-envoy-auth-partial-body` metadata header set to `false|true` indicating if there is a partial body sent in the authorization request message.
* ext_authz: added configurable status code that allows customizing HTTP responses on filter check status errors.
* 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>`.
* event: added :ref:`loop duration and poll delay statistics <operations_performance>`.
Comment thread
gsagula marked this conversation as resolved.
Outdated
* 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.
* jwt_authn: make filter's parsing of JWT more flexible, allowing syntax like ``jwt=eyJhbGciOiJS...ZFnFIw,extra=7,realm=123``
* 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
114 changes: 59 additions & 55 deletions source/extensions/filters/http/ext_authz/ext_authz.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

#include "common/common/assert.h"
#include "common/common/enum_to_int.h"
#include "common/http/codes.h"
#include "common/http/utility.h"
#include "common/router/config_impl.h"

Expand All @@ -13,6 +12,8 @@ namespace Extensions {
namespace HttpFilters {
namespace ExtAuthz {

using Filters::Common::ExtAuthz::CheckStatus;

void FilterConfigPerRoute::merge(const FilterConfigPerRoute& other) {
disabled_ = other.disabled_;
auto begin_it = other.context_extensions_.begin();
Expand Down Expand Up @@ -129,16 +130,39 @@ void Filter::onDestroy() {
void Filter::onComplete(Filters::Common::ExtAuthz::ResponsePtr&& response) {
ASSERT(cluster_);
state_ = State::Complete;
using Filters::Common::ExtAuthz::CheckStatus;

switch (response->status) {
case CheckStatus::OK:
case CheckStatus::OK: {
ENVOY_STREAM_LOG(trace, "ext_authz filter added header(s) to the request:", *callbacks_);
Comment thread
gsagula marked this conversation as resolved.
if (config_->clearRouteCache() &&
(!response->headers_to_add.empty() || !response->headers_to_append.empty())) {
ENVOY_STREAM_LOG(debug, "ext_authz is clearing route cache", *callbacks_);
callbacks_->clearRouteCache();
}
for (const auto& header : response->headers_to_add) {
ENVOY_STREAM_LOG(trace, " '{}':'{}'", *callbacks_, header.first.get(), header.second);
Http::HeaderEntry* header_to_modify = request_headers_->get(header.first);
if (header_to_modify) {
header_to_modify->value(header.second.c_str(), header.second.size());
} else {
request_headers_->addCopy(header.first, header.second);
}
}
for (const auto& header : response->headers_to_append) {
Http::HeaderEntry* header_to_modify = request_headers_->get(header.first);
if (header_to_modify) {
ENVOY_STREAM_LOG(trace, " '{}':'{}'", *callbacks_, header.first.get(), header.second);
Http::HeaderMapImpl::appendToHeader(header_to_modify->value(), header.second);
}
}
cluster_->statsScope().counter("ext_authz.ok").inc();
continueDecoding();
break;
case CheckStatus::Error:
cluster_->statsScope().counter("ext_authz.error").inc();
break;
case CheckStatus::Denied:
}

case CheckStatus::Denied: {
ENVOY_STREAM_LOG(trace, "ext_authz filter rejected the request. Response status code: '{}",
*callbacks_, enumToInt(response->status_code));
cluster_->statsScope().counter("ext_authz.denied").inc();
Http::CodeStats::ResponseStatInfo info{config_->scope(),
cluster_->statsScope(),
Expand All @@ -151,70 +175,43 @@ void Filter::onComplete(Filters::Common::ExtAuthz::ResponsePtr&& response) {
EMPTY_STRING,
false};
config_->httpContext().codeStats().chargeResponseStat(info);
break;
}

ENVOY_STREAM_LOG(trace, "ext_authz filter received status code {}", *callbacks_,
enumToInt(response->status_code));

// We fail open/fail close based of filter config
// if there is an error contacting the service.
if (response->status == CheckStatus::Denied ||
(response->status == CheckStatus::Error && !config_->failureModeAllow())) {
ENVOY_STREAM_LOG(debug, "ext_authz filter rejected the request", *callbacks_);
callbacks_->sendLocalReply(
response->status_code, response->body,
[& headers = response->headers_to_add,
&callbacks = *callbacks_](Http::HeaderMap& response_headers) -> void {
ENVOY_STREAM_LOG(trace,
"ext_authz filter added header(s) to the local response:", callbacks);
for (const auto& header : headers) {
ENVOY_STREAM_LOG(trace, " '{}':'{}'", callbacks, header.first.get(), header.second);
response_headers.remove(header.first);
response_headers.addCopy(header.first, header.second);
ENVOY_STREAM_LOG(trace, " '{}':'{}'", callbacks, header.first.get(), header.second);
}
},
absl::nullopt);
callbacks_->streamInfo().setResponseFlag(StreamInfo::ResponseFlag::UnauthorizedExternalService);
} else {
ENVOY_STREAM_LOG(debug, "ext_authz filter accepted the request", *callbacks_);
// Let the filter chain continue.
filter_return_ = FilterReturn::ContinueDecoding;
if (config_->failureModeAllow() && response->status == CheckStatus::Error) {
// Status is Error and yet we are allowing the request. Click a counter.
cluster_->statsScope().counter("ext_authz.failure_mode_allowed").inc();
}
// 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())) {
ENVOY_STREAM_LOG(debug, "ext_authz is clearing route cache", *callbacks_);
callbacks_->clearRouteCache();
}
break;
}

for (const auto& header : response->headers_to_add) {
Http::HeaderEntry* header_to_modify = request_headers_->get(header.first);
if (header_to_modify) {
header_to_modify->value(header.second.c_str(), header.second.size());
} else {
request_headers_->addCopy(header.first, header.second);
}
ENVOY_STREAM_LOG(trace, " '{}':'{}'", *callbacks_, header.first.get(), header.second);
}
for (const auto& header : response->headers_to_append) {
Http::HeaderEntry* header_to_modify = request_headers_->get(header.first);
if (header_to_modify) {
Http::HeaderMapImpl::appendToHeader(header_to_modify->value(), header.second);
ENVOY_STREAM_LOG(trace, " '{}':'{}'", *callbacks_, header.first.get(), header.second);
}
}
case CheckStatus::Error: {
cluster_->statsScope().counter("ext_authz.error").inc();
if (config_->failureModeAllow()) {
ENVOY_STREAM_LOG(trace, "ext_authz filter allowed the request with error", *callbacks_);
cluster_->statsScope().counter("ext_authz.failure_mode_allowed").inc();
continueDecoding();
} else {
ENVOY_STREAM_LOG(
trace, "ext_authz filter rejected the request with an error. Response status code: {}",
*callbacks_, enumToInt(config_->statusOnError()));
callbacks_->streamInfo().setResponseFlag(
StreamInfo::ResponseFlag::UnauthorizedExternalService);
callbacks_->sendLocalReply(config_->statusOnError(), EMPTY_STRING, nullptr, absl::nullopt);
}
break;
}

if (!initiating_call_) {
// We got completion async. Let the filter chain continue.
callbacks_->continueDecoding();
}
default:
NOT_REACHED_GCOVR_EXCL_LINE;
break;
}
}

Expand All @@ -226,6 +223,13 @@ bool Filter::isBufferFull() {
return false;
}

void Filter::continueDecoding() {
filter_return_ = FilterReturn::ContinueDecoding;
if (!initiating_call_) {
callbacks_->continueDecoding();
}
}

} // namespace ExtAuthz
} // namespace HttpFilters
} // namespace Extensions
Expand Down
16 changes: 15 additions & 1 deletion source/extensions/filters/http/ext_authz/ext_authz.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@
#include "envoy/local_info/local_info.h"
#include "envoy/runtime/runtime.h"
#include "envoy/stats/scope.h"
#include "envoy/type/http_status.pb.h"
#include "envoy/upstream/cluster_manager.h"

#include "common/common/assert.h"
#include "common/common/logger.h"
#include "common/common/matchers.h"
#include "common/http/codes.h"
#include "common/http/header_map_impl.h"

#include "extensions/filters/common/ext_authz/ext_authz.h"
Expand Down Expand Up @@ -42,7 +44,8 @@ class FilterConfig {
: 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),
max_request_bytes_(config.with_request_body().max_request_bytes()),
status_on_error_(toErrorCode(config.status_on_error().code())), local_info_(local_info),
scope_(scope), runtime_(runtime), http_context_(http_context) {}

bool allowPartialMessage() const { return allow_partial_message_; }
Expand All @@ -57,17 +60,27 @@ class FilterConfig {

const LocalInfo::LocalInfo& localInfo() const { return local_info_; }

Http::Code statusOnError() const { return status_on_error_; }

Runtime::Loader& runtime() { return runtime_; }

Stats::Scope& scope() { return scope_; }

Http::Context& httpContext() { return http_context_; }

private:
static Http::Code toErrorCode(uint64_t status) {
Comment thread
gsagula marked this conversation as resolved.
if (Http::CodeUtility::is5xx(status)) {
return static_cast<Http::Code>(status);
}
return Http::Code::Forbidden;
}

const bool allow_partial_message_;
const bool failure_mode_allow_;
const bool clear_route_cache_;
const uint32_t max_request_bytes_;
const Http::Code status_on_error_;
const LocalInfo::LocalInfo& local_info_;
Stats::Scope& scope_;
Runtime::Loader& runtime_;
Expand Down Expand Up @@ -134,6 +147,7 @@ class Filter : public Logger::Loggable<Logger::Id::filter>,
private:
void addResponseHeaders(Http::HeaderMap& header_map, const Http::HeaderVector& headers);
void initiateCall(const Http::HeaderMap& headers);
void continueDecoding();
bool isBufferFull();

// State of this filter's communication with the external authorization service.
Expand Down
41 changes: 41 additions & 0 deletions test/extensions/filters/http/ext_authz/ext_authz_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,47 @@ TEST_F(HttpFilterTest, ErrorFailClose) {
EXPECT_EQ(Http::FilterHeadersStatus::StopAllIterationAndWatermark,
filter_->decodeHeaders(request_headers_, false));
EXPECT_CALL(filter_callbacks_, continueDecoding()).Times(0);
EXPECT_CALL(filter_callbacks_, encodeHeaders_(_, true))
.WillOnce(Invoke([&](const Http::HeaderMap& headers, bool) -> void {
EXPECT_EQ(headers.Status()->value().getStringView(),
std::to_string(enumToInt(Http::Code::Forbidden)));
}));

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

// Verifies that the filter responds with a configurable HTTP status when an network error occurs.
TEST_F(HttpFilterTest, ErrorCustomStatusCode) {
InSequence s;

initialize(R"EOF(
grpc_service:
envoy_grpc:
cluster_name: "ext_authz_server"
failure_mode_allow: false
status_on_error:
code: 503
)EOF");

ON_CALL(filter_callbacks_, connection()).WillByDefault(Return(&connection_));
EXPECT_CALL(connection_, remoteAddress()).WillOnce(ReturnRef(addr_));
EXPECT_CALL(connection_, localAddress()).WillOnce(ReturnRef(addr_));
EXPECT_CALL(*client_, check(_, _, _))
.WillOnce(
WithArgs<0>(Invoke([&](Filters::Common::ExtAuthz::RequestCallbacks& callbacks) -> void {
request_callbacks_ = &callbacks;
})));
EXPECT_EQ(Http::FilterHeadersStatus::StopAllIterationAndWatermark,
filter_->decodeHeaders(request_headers_, false));
EXPECT_CALL(filter_callbacks_, continueDecoding()).Times(0);
EXPECT_CALL(filter_callbacks_, encodeHeaders_(_, true))
.WillOnce(Invoke([&](const Http::HeaderMap& headers, bool) -> void {
EXPECT_EQ(headers.Status()->value().getStringView(),
std::to_string(enumToInt(Http::Code::ServiceUnavailable)));
}));

Filters::Common::ExtAuthz::Response response{};
response.status = Filters::Common::ExtAuthz::CheckStatus::Error;
Expand Down