Skip to content
Closed
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
1 change: 1 addition & 0 deletions docs/root/version_history/current.rst
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ Bug Fixes

* active http health checks: properly handles HTTP/2 GOAWAY frames from the upstream. Previously a GOAWAY frame due to a graceful listener drain could cause improper failed health checks due to streams being refused by the upstream on a connection that is going away. To revert to old GOAWAY handling behavior, set the runtime feature `envoy.reloadable_features.health_check.graceful_goaway_handling` to false.
* buffer: tighten network connection read and write buffer high watermarks in preparation to more careful enforcement of read limits. Buffer high-watermark is now set to the exact configured value; previously it was set to value + 1.
* ext_authz: fix a bug that skipped external authorization checks for routes that contained only a direct response.
* fault injection: stop counting as active fault after delay elapsed. Previously fault injection filter continues to count the injected delay as an active fault even after it has elapsed. This produces incorrect output statistics and impacts the max number of consecutive faults allowed (e.g., for long-lived streams). This change decreases the active fault count when the delay fault is the only active and has gone finished.
* grpc-web: fix local reply and non-proto-encoded gRPC response handling for small response bodies. This fix can be temporarily reverted by setting `envoy.reloadable_features.grpc_web_fix_non_proto_encoded_response_handling` to false.
* http: disallowing "host:" in request_headers_to_add for behavioral consistency with rejecting :authority header. This behavior can be temporarily reverted by setting `envoy.reloadable_features.treat_host_like_authority` to false.
Expand Down
14 changes: 11 additions & 3 deletions source/extensions/filters/http/ext_authz/ext_authz.cc
Original file line number Diff line number Diff line change
Expand Up @@ -376,8 +376,16 @@ void Filter::continueDecoding() {
}

Filter::PerRouteFlags Filter::getPerRouteFlags(const Router::RouteConstSharedPtr& route) const {
if (route == nullptr || route->routeEntry() == nullptr) {
return PerRouteFlags{true /*skip_check_*/, false /*skip_request_body_buffering_*/};
if (route == nullptr) {
// If there's no route, skip authorization checks. This is an optimization to avoid work
// if we know the request won't be forwarded upstream anyway.
return PerRouteFlags::skipCheckFlags();
}

if (route->routeEntry() == nullptr && route->directResponseEntry() == nullptr) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you quickly scan in the docs, if we have the opportunity to inform this explicitly in the docs?

@esmet esmet Feb 16, 2021

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dio Good callout. I read the docs. I think we're actually going to skip authz checks for redirect actions still. Maybe this feature would be more complete if we:

  • Documented that only routes that proxy requests upstream will be covered by ext_authz by default
  • Add a knob to enable ext_authz on direct response actions
  • Add a knob to enable ext_authz on redirect actions

This way, this "bugfix" is now instead a new feature that won't break existing users that expect the current behavior. Principle of least surprise is violated, in theory, but we can mitigate that with documentation and I think this is better than possibly introducing a breaking change when we meant to make things better.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By the way, I was wrong. Redirects and direct replies are both "direct responses" on the route object so I believe this change will correctly apply to them all. I should probably add an integration test.

// If there's a route, but no route entry nor direct response entry, then skip authorization
// checks. This is another optimization.
return PerRouteFlags::skipCheckFlags();
}

const auto* specific_per_route_config =
Expand All @@ -388,7 +396,7 @@ Filter::PerRouteFlags Filter::getPerRouteFlags(const Router::RouteConstSharedPtr
specific_per_route_config->disableRequestBodyBuffering()};
}

return PerRouteFlags{false /*skip_check_*/, false /*skip_request_body_buffering_*/};
return PerRouteFlags::defaultFlags();
}

} // namespace ExtAuthz
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 @@ -268,6 +268,10 @@ class Filter : public Logger::Loggable<Logger::Id::filter>,
struct PerRouteFlags {
const bool skip_check_;
const bool skip_request_body_buffering_;

static constexpr PerRouteFlags defaultFlags() { return PerRouteFlags{false, false}; }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we add comments on what the bools are here and below?

e.g., false /*skip_check_*/, false /*skip_request_body_buffering_*/


static constexpr PerRouteFlags skipCheckFlags() { return PerRouteFlags{true, false}; }
};
PerRouteFlags getPerRouteFlags(const Router::RouteConstSharedPtr& route) const;

Expand Down
37 changes: 35 additions & 2 deletions test/extensions/filters/http/ext_authz/ext_authz_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1589,9 +1589,42 @@ TEST_P(HttpFilterTestParam, DisabledOnRouteWithRequestBody) {
EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_->decodeTrailers(request_trailers_));
}

// Test that the request continues when the filter_callbacks has no route.
TEST_P(HttpFilterTestParam, NoRoute) {
// Test that the request stops at decodeHeaders when filter_callbacks has a route entry.
TEST_P(HttpFilterTestParam, StopIterationWithRouteEntry) {
NiceMock<Router::MockRouteEntry> route_entry;
prepareCheck();
EXPECT_CALL(*client_, check(_, _, _, _));
EXPECT_CALL(*filter_callbacks_.route_, routeEntry()).WillRepeatedly(Return(&route_entry));
EXPECT_EQ(Http::FilterHeadersStatus::StopAllIterationAndWatermark,
filter_->decodeHeaders(request_headers_, true));
}

// Test that the request stops at decodeHeaders when filter_callbacks has no route entry, but it
// does have a direct response entry.
TEST_P(HttpFilterTestParam, StopIterationWithDirectResponseEntry) {
NiceMock<Router::MockDirectResponseEntry> direct_response_entry;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we have a test with redirect?

prepareCheck();
EXPECT_CALL(*client_, check(_, _, _, _));
EXPECT_CALL(*filter_callbacks_.route_, routeEntry()).WillRepeatedly(Return(nullptr));
EXPECT_CALL(*filter_callbacks_.route_, directResponseEntry())
.WillOnce(Return(&direct_response_entry));
EXPECT_EQ(Http::FilterHeadersStatus::StopAllIterationAndWatermark,
filter_->decodeHeaders(request_headers_, true));
}

// Test that the request continues when filter_callbacks has no route
TEST_P(HttpFilterTestParam, ContinueIterationWithNoRoute) {
EXPECT_CALL(filter_callbacks_, route()).WillRepeatedly(Return(nullptr));
EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers_, false));
EXPECT_EQ(Http::FilterDataStatus::Continue, filter_->decodeData(data_, false));
EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_->decodeTrailers(request_trailers_));
}

// Test that the request continues when filter_callbacks has no route entry / direct response entry.
TEST_P(HttpFilterTestParam, ContinueIterationWithNoRouteEntryOrDirectResponseEntry) {
EXPECT_CALL(*client_, check(_, _, _, _)).Times(0);
EXPECT_CALL(*filter_callbacks_.route_, routeEntry()).WillOnce(Return(nullptr));
EXPECT_CALL(*filter_callbacks_.route_, directResponseEntry()).WillOnce(Return(nullptr));
EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers_, false));
EXPECT_EQ(Http::FilterDataStatus::Continue, filter_->decodeData(data_, false));
EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_->decodeTrailers(request_trailers_));
Expand Down