Skip to content
Open
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
13 changes: 12 additions & 1 deletion api/envoy/extensions/filters/http/ext_authz/v3/ext_authz.proto
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE;
// External Authorization :ref:`configuration overview <config_http_filters_ext_authz>`.
// [#extension: envoy.filters.http.ext_authz]

// [#next-free-field: 30]
// [#next-free-field: 31]
message ExtAuthz {
option (udpa.annotations.versioning).previous_message_type =
"envoy.config.filter.http.ext_authz.v3.ExtAuthz";
Expand Down Expand Up @@ -311,6 +311,17 @@ message ExtAuthz {
// Field ``latency_us`` is exposed for CEL and logging when using gRPC or HTTP service.
// Fields ``bytesSent`` and ``bytesReceived`` are exposed for CEL and logging only when using gRPC service.
bool emit_filter_state_stats = 29;

// Sets the maximum size (in bytes) of the response body that the filter will send downstream
// when a request is denied by the external authorization service.
//
// If the authorization server returns a response body larger than this configured limit,
// the body will be truncated to ``max_denied_response_body_bytes`` before being sent to the
// downstream client.
//
// If this field is not set or is set to 0, no truncation will occur, and the entire
// denied response body will be forwarded.
uint32 max_denied_response_body_bytes = 30;
}

// Configuration for buffering the request data.
Expand Down
7 changes: 7 additions & 0 deletions changelogs/current.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,13 @@ new_features:
:ref:`grpc_service <envoy_v3_api_field_extensions.filters.http.ext_authz.v3.CheckSettings.grpc_service>`
in the per-route ``check_settings``. Routes without this configuration continue to use the default
authorization service.
- area: ext_authz
change: |
Added
:ref:`max_denied_response_body_bytes <envoy_v3_api_field_extensions.filters.http.ext_authz.v3.ExtAuthz.max_denied_response_body_bytes>`
to the ``ext_authz`` HTTP filter. This allows configuring the maximum size of the response body
returned to the downstream client when a request is denied by the external authorization service.
If the authorization server returns a response body larger than this limit, it will be truncated.
- area: ext_proc
change: |
Added :ref:`status_on_error <envoy_v3_api_field_extensions.filters.http.ext_proc.v3.ExternalProcessor.status_on_error>`
Expand Down
9 changes: 9 additions & 0 deletions source/extensions/filters/http/ext_authz/ext_authz.cc
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ FilterConfig::FilterConfig(const envoy::extensions::filters::http::ext_authz::v3
failure_mode_allow_header_add_(config.failure_mode_allow_header_add()),
clear_route_cache_(config.clear_route_cache()),
max_request_bytes_(config.with_request_body().max_request_bytes()),
max_denied_response_body_bytes_(config.max_denied_response_body_bytes()),

// `pack_as_bytes_` should be true when configured with the HTTP service because there is no
// difference to where the body is written in http requests, and a value of false here will
Expand Down Expand Up @@ -923,6 +924,14 @@ void Filter::onComplete(Filters::Common::ExtAuthz::ResponsePtr&& response) {
}
}

if (config_->maxDeniedResponseBodyBytes() > 0 &&
response->body.length() > config_->maxDeniedResponseBodyBytes()) {
ENVOY_STREAM_LOG(
trace, "ext_authz filter is truncating the response body from {} to {} bytes.",
*decoder_callbacks_, response->body.length(), config_->maxDeniedResponseBodyBytes());
response->body.resize(config_->maxDeniedResponseBodyBytes());
}

// setResponseFlag must be called before sendLocalReply
decoder_callbacks_->streamInfo().setResponseFlag(
StreamInfo::CoreResponseFlag::UnauthorizedExternalService);
Expand Down
3 changes: 3 additions & 0 deletions source/extensions/filters/http/ext_authz/ext_authz.h
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ class FilterConfig {

uint32_t maxRequestBytes() const { return max_request_bytes_; }

uint32_t maxDeniedResponseBodyBytes() const { return max_denied_response_body_bytes_; }

bool packAsBytes() const { return pack_as_bytes_; }

bool headersAsBytes() const { return encode_raw_headers_; }
Expand Down Expand Up @@ -245,6 +247,7 @@ class FilterConfig {
const bool failure_mode_allow_header_add_;
const bool clear_route_cache_;
const uint32_t max_request_bytes_;
const uint32_t max_denied_response_body_bytes_;
const bool pack_as_bytes_;
const bool encode_raw_headers_;
const Http::Code status_on_error_;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ struct GrpcInitializeConfigOpts {
uint64_t timeout_ms = 300'000; // 5 minutes.
bool validate_mutations = false;
bool retry_5xx = false;
uint32_t max_denied_response_body_bytes = 0;
// In some tests a request is never sent. If a request is never sent, stats are not set. In those
// tests, we need to be able to override this to false.
absl::optional<bool> expect_stats_override;
Expand Down Expand Up @@ -134,6 +135,7 @@ class ExtAuthzGrpcIntegrationTest
proto_config_.set_failure_mode_allow_header_add(opts.failure_mode_allow);
proto_config_.set_validate_mutations(opts.validate_mutations);
proto_config_.set_encode_raw_headers(encodeRawHeaders());
proto_config_.set_max_denied_response_body_bytes(opts.max_denied_response_body_bytes);

if (emitFilterStateStats()) {
proto_config_.set_emit_filter_state_stats(true);
Expand Down Expand Up @@ -1255,6 +1257,37 @@ TEST_P(ExtAuthzGrpcIntegrationTest, TimeoutFailClosed) {
cleanup();
}

// Test that a DENIED response with a body from the authorization service is truncated if the body
// size is larger than max_denied_response_body_bytes.
TEST_P(ExtAuthzGrpcIntegrationTest, DeniedResponseWithBodyTruncation) {
GrpcInitializeConfigOpts opts;
opts.max_denied_response_body_bytes = 10;
ext_authz_grpc_status_ = LoggingTestFilterConfig::PERMISSION_DENIED;
initializeConfig(opts);

setDownstreamProtocol(Http::CodecType::HTTP1);
HttpIntegrationTest::initialize();

initiateClientConnection(0);

waitForExtAuthzRequest(expectedCheckRequest(Http::CodecType::HTTP1));

ext_authz_request_->startGrpcStream();
envoy::service::auth::v3::CheckResponse check_response;
check_response.mutable_status()->set_code(Grpc::Status::WellKnownGrpcStatus::PermissionDenied);
check_response.mutable_denied_response()->set_body(
"this-is-a-long-body-that-should-be-truncated");
ext_authz_request_->sendGrpcMessage(check_response);
ext_authz_request_->finishGrpcStream(Grpc::Status::Ok);

ASSERT_TRUE(response_->waitForEndStream());
EXPECT_TRUE(response_->complete());
EXPECT_EQ("403", response_->headers().getStatusValue());
EXPECT_EQ("this-is-a-", response_->body()); // Truncated to 10 bytes

cleanup();
}

TEST_P(ExtAuthzGrpcIntegrationTest, Retry) {
if (clientType() == Grpc::ClientType::GoogleGrpc) {
GTEST_SKIP() << "Retry is only supported for Envoy gRPC";
Expand Down
118 changes: 118 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 @@ -1987,6 +1987,124 @@ TEST_F(HttpFilterTest, ClearCacheRouteHeadersToRemoveOnly) {
EXPECT_EQ(1U, config_->stats().ok_.value());
}

// Test that a DENIED response with a body from the authorization service is truncated if the body
// size is larger than max_denied_response_body_bytes.
TEST_F(HttpFilterTest, DeniedResponseWithBodyTruncation) {
InSequence s;

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

prepareCheck();

EXPECT_CALL(*client_, check(_, _, testing::A<Tracing::Span&>(), _))
.WillOnce(
Invoke([&](Filters::Common::ExtAuthz::RequestCallbacks& callbacks,
const envoy::service::auth::v3::CheckRequest&, Tracing::Span&,
const StreamInfo::StreamInfo&) -> void { request_callbacks_ = &callbacks; }));

EXPECT_EQ(Http::FilterHeadersStatus::StopAllIterationAndWatermark,
filter_->decodeHeaders(request_headers_, false));
EXPECT_CALL(decoder_filter_callbacks_, continueDecoding()).Times(0);
EXPECT_CALL(decoder_filter_callbacks_.stream_info_,
setResponseFlag(Envoy::StreamInfo::CoreResponseFlag::UnauthorizedExternalService));
// The body is truncated to 10 bytes.
EXPECT_CALL(decoder_filter_callbacks_,
sendLocalReply(Http::Code::Forbidden, "1234567890", _, _, _));

Filters::Common::ExtAuthz::Response response{};
response.status = Filters::Common::ExtAuthz::CheckStatus::Denied;
response.status_code = Http::Code::Forbidden;
response.body = "1234567890-this-should-be-truncated";
request_callbacks_->onComplete(std::make_unique<Filters::Common::ExtAuthz::Response>(response));
EXPECT_EQ(1U, decoder_filter_callbacks_.clusterInfo()
->statsScope()
.counterFromString("ext_authz.denied")
.value());
EXPECT_EQ(1U, config_->stats().denied_.value());
}

// Test that a DENIED response with a body from the authorization service is NOT truncated if the
// body size is smaller than max_denied_response_body_bytes.
TEST_F(HttpFilterTest, DeniedResponseWithBodyNotTruncated) {
InSequence s;

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

prepareCheck();

EXPECT_CALL(*client_, check(_, _, testing::A<Tracing::Span&>(), _))
.WillOnce(
Invoke([&](Filters::Common::ExtAuthz::RequestCallbacks& callbacks,
const envoy::service::auth::v3::CheckRequest&, Tracing::Span&,
const StreamInfo::StreamInfo&) -> void { request_callbacks_ = &callbacks; }));
EXPECT_EQ(Http::FilterHeadersStatus::StopAllIterationAndWatermark,
filter_->decodeHeaders(request_headers_, false));
EXPECT_CALL(decoder_filter_callbacks_, continueDecoding()).Times(0);
EXPECT_CALL(decoder_filter_callbacks_.stream_info_,
setResponseFlag(Envoy::StreamInfo::CoreResponseFlag::UnauthorizedExternalService));
const std::string body = "body-not-truncated";
EXPECT_CALL(decoder_filter_callbacks_, sendLocalReply(Http::Code::Forbidden, body, _, _, _));

Filters::Common::ExtAuthz::Response response{};
response.status = Filters::Common::ExtAuthz::CheckStatus::Denied;
response.status_code = Http::Code::Forbidden;
response.body = body;
request_callbacks_->onComplete(std::make_unique<Filters::Common::ExtAuthz::Response>(response));
EXPECT_EQ(1U, decoder_filter_callbacks_.clusterInfo()
->statsScope()
.counterFromString("ext_authz.denied")
.value());
EXPECT_EQ(1U, config_->stats().denied_.value());
}

// Test that a DENIED response with a body from the authorization service is NOT truncated if
// max_denied_response_body_bytes is not set (or zero).
TEST_F(HttpFilterTest, DeniedResponseWithBodyNotTruncatedWhenLimitIsZero) {
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(
Invoke([&](Filters::Common::ExtAuthz::RequestCallbacks& callbacks,
const envoy::service::auth::v3::CheckRequest&, Tracing::Span&,
const StreamInfo::StreamInfo&) -> void { request_callbacks_ = &callbacks; }));
EXPECT_EQ(Http::FilterHeadersStatus::StopAllIterationAndWatermark,
filter_->decodeHeaders(request_headers_, false));
EXPECT_CALL(decoder_filter_callbacks_, continueDecoding()).Times(0);
EXPECT_CALL(decoder_filter_callbacks_.stream_info_,
setResponseFlag(Envoy::StreamInfo::CoreResponseFlag::UnauthorizedExternalService));
const std::string body = "this-is-a-long-body-that-will-not-be-truncated";
EXPECT_CALL(decoder_filter_callbacks_, sendLocalReply(Http::Code::Forbidden, body, _, _, _));

Filters::Common::ExtAuthz::Response response{};
response.status = Filters::Common::ExtAuthz::CheckStatus::Denied;
response.status_code = Http::Code::Forbidden;
response.body = body;
request_callbacks_->onComplete(std::make_unique<Filters::Common::ExtAuthz::Response>(response));
EXPECT_EQ(1U, decoder_filter_callbacks_.clusterInfo()
->statsScope()
.counterFromString("ext_authz.denied")
.value());
EXPECT_EQ(1U, config_->stats().denied_.value());
}

// Verifies that the downstream request fails when the ext_authz response
// would cause the request headers to exceed their limit.
TEST_F(HttpFilterTest, DownstreamRequestFailsOnHeaderLimit) {
Expand Down