Skip to content
Closed
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 @@ -235,6 +235,7 @@ message AuthorizationRequest {
repeated config.core.v3.HeaderValue headers_to_add = 2;
}

// [#next-free-field: 6]
message AuthorizationResponse {
option (udpa.annotations.versioning).previous_message_type =
"envoy.config.filter.http.ext_authz.v2.AuthorizationResponse";
Expand All @@ -249,7 +250,7 @@ message AuthorizationResponse {
// that coexistent headers will be appended.
type.matcher.v3.ListStringMatcher allowed_upstream_headers_to_append = 3;

// When this :ref:`list <envoy_api_msg_type.matcher.v3.ListStringMatcher>`. is set, authorization
// When this :ref:`list <envoy_api_msg_type.matcher.v3.ListStringMatcher>` is set, authorization
// response headers that have a correspondent match will be added to the client's response. Note
// that when this list is *not* set, all the authorization response headers, except *Authority
// (Host)* will be in the response to the client. When a header is included in this list, *Path*,
Expand All @@ -261,6 +262,16 @@ message AuthorizationResponse {
// the authorization response itself is successful, i.e. not failed or denied. When this list is
// *not* set, no additional headers will be added to the client's response on success.
type.matcher.v3.ListStringMatcher allowed_client_headers_on_success = 4;

// When this :ref:`list <envoy_api_msg_type.matcher.v3.ListStringMatcher>` is set, authorization
// response headers that have a correspondent match will be emitted as dynamic metadata, whose keys
// correspond to the name of the matched header and whose values are the respective values. When this
// list is *not* set, no additional dynamic metadata will be emitted. This metadata lives in a namespace
// specified by the canonical name of extension filter that requires it:
Copy link
Member

@htuch htuch Mar 10, 2021

Choose a reason for hiding this comment

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

@lizan @kyessenov I'm little concerned that we've moved to a model where type URL is the canonical name for extension filters but we continue to use these names in the API for other purposes. What exists in this PR is consistent with other places we do this, e.g. RLS, but curious what to do going forward. Do we need some API style guidance on which one to use where here?

CC @envoyproxy/api-shepherds @phlax

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, we need guidance what keys to use for metadata (and filter state). It's wild west right now. As long as the name is unique, it should be OK to use any name, but we need to phrase it this way and avoid canonical name.

Copy link
Member

Choose a reason for hiding this comment

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

+1 anything we can do here to centralize this in TypedExtensionConfig and have common documentation would be hugely helpful. This is incredibly confusing IMO.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right, this PR uses the same metadata key name that is documented in the proto used by the gRPC authorization server. What can I do to help here, and is it something that should be addressed in this PR or in a followup PR?

Copy link
Member

Choose a reason for hiding this comment

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

For now can you potentially just add an example to these docs? It's pretty hard to understand for the casual user. Thank you!

/wait

//
// - :ref:`envoy.filters.http.ext_authz <config_http_filters_ext_authz_dynamic_metadata>` for HTTP filter.
// - :ref:`envoy.filters.network.ext_authz <config_network_filters_ext_authz_dynamic_metadata>` for network filter.
type.matcher.v3.ListStringMatcher dynamic_metadata_from_headers = 5;
}

// Extra settings on a per virtualhost/route/weighted-cluster level.
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions docs/root/version_history/current.rst
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ New Features
* compression: add brotli :ref:`compressor <envoy_v3_api_msg_extensions.compression.brotli.compressor.v3.Brotli>` and :ref:`decompressor <envoy_v3_api_msg_extensions.compression.brotli.decompressor.v3.Brotli>`.
* config: add `envoy.features.fail_on_any_deprecated_feature` runtime key, which matches the behaviour of compile-time flag `ENVOY_DISABLE_DEPRECATED_FEATURES`, i.e. use of deprecated fields will cause a crash.
* dispatcher: supports a stack of `Envoy::ScopeTrackedObject` instead of a single tracked object. This will allow Envoy to dump more debug information on crash.
* ext_authz: added :ref:`dynamic_metadata_from_headers <envoy_v3_api_field_extensions.filters.http.ext_authz.v3.AuthorizationResponse.dynamic_metadata_from_headers>` to support emitting dynamic metadata from headers returned by an external authorization service via HTTP.
* ext_authz: added :ref:`response_headers_to_add <envoy_v3_api_field_service.auth.v3.OkHttpResponse.response_headers_to_add>` to support sending response headers to downstream clients on OK authorization checks via gRPC.
* ext_authz: added :ref:`allowed_client_headers_on_success <envoy_v3_api_field_extensions.filters.http.ext_authz.v3.AuthorizationResponse.allowed_client_headers_on_success>` to support sending response headers to downstream clients on OK external authorization checks via HTTP.
* grpc_json_transcoder: added option :ref:`strict_http_request_validation <envoy_v3_api_field_extensions.filters.http.grpc_json_transcoder.v3.GrpcJsonTranscoder.strict_http_request_validation>` to reject invalid requests early.
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

41 changes: 32 additions & 9 deletions source/extensions/filters/common/ext_authz/ext_authz_http_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,12 @@ const Response& errorResponse() {
struct SuccessResponse {
SuccessResponse(const Http::HeaderMap& headers, const MatcherSharedPtr& matchers,
const MatcherSharedPtr& append_matchers,
const MatcherSharedPtr& response_matchers, Response&& response)
const MatcherSharedPtr& response_matchers,
const MatcherSharedPtr& dynamic_metadata_matchers, Response&& response)
: headers_(headers), matchers_(matchers), append_matchers_(append_matchers),
response_matchers_(response_matchers), response_(std::make_unique<Response>(response)) {
response_matchers_(response_matchers),
to_dynamic_metadata_matchers_(dynamic_metadata_matchers),
response_(std::make_unique<Response>(response)) {
headers_.iterate([this](const Http::HeaderEntry& header) -> Http::HeaderMap::Iterate {
// UpstreamHeaderMatcher
if (matchers_->matches(header.key().getStringView())) {
Expand All @@ -71,14 +74,21 @@ struct SuccessResponse {
Http::LowerCaseString{std::string(header.key().getStringView())},
std::string(header.value().getStringView()));
}
if (to_dynamic_metadata_matchers_->matches(header.key().getStringView())) {
const std::string key{header.key().getStringView()};
const std::string value{header.value().getStringView()};
(*response_->dynamic_metadata.mutable_fields())[key] = ValueUtil::stringValue(value);
}
return Http::HeaderMap::Iterate::Continue;
});
}

const Http::HeaderMap& headers_;
// All matchers below are used on headers_.
const MatcherSharedPtr& matchers_;
const MatcherSharedPtr& append_matchers_;
const MatcherSharedPtr& response_matchers_;
const MatcherSharedPtr& to_dynamic_metadata_matchers_;
ResponsePtr response_;
};

Expand Down Expand Up @@ -116,6 +126,8 @@ ClientConfig::ClientConfig(const envoy::extensions::filters::http::ext_authz::v3
config.http_service().authorization_response().allowed_client_headers())),
client_header_on_success_matchers_(toClientMatchersOnSuccess(
config.http_service().authorization_response().allowed_client_headers_on_success())),
to_dynamic_metadata_matchers_(toDynamicMetadataMatchers(
config.http_service().authorization_response().dynamic_metadata_from_headers())),
upstream_header_matchers_(toUpstreamMatchers(
config.http_service().authorization_response().allowed_upstream_headers())),
upstream_header_to_append_matchers_(toUpstreamMatchers(
Expand Down Expand Up @@ -148,6 +160,12 @@ ClientConfig::toClientMatchersOnSuccess(const envoy::type::matcher::v3::ListStri
return std::make_shared<HeaderKeyMatcher>(std::move(matchers));
}

MatcherSharedPtr
ClientConfig::toDynamicMetadataMatchers(const envoy::type::matcher::v3::ListStringMatcher& list) {
std::vector<Matchers::StringMatcherPtr> matchers(createStringMatchers(list));
return std::make_shared<HeaderKeyMatcher>(std::move(matchers));
}

MatcherSharedPtr
ClientConfig::toClientMatchers(const envoy::type::matcher::v3::ListStringMatcher& list) {
std::vector<Matchers::StringMatcherPtr> matchers(createStringMatchers(list));
Expand Down Expand Up @@ -316,19 +334,24 @@ ResponsePtr RawHttpClientImpl::toResponse(Http::ResponseMessagePtr message) {

// Create an Ok authorization response.
if (status_code == enumToInt(Http::Code::OK)) {
SuccessResponse ok{
message->headers(), config_->upstreamHeaderMatchers(),
config_->upstreamHeaderToAppendMatchers(), config_->clientHeaderOnSuccessMatchers(),
Response{CheckStatus::OK, Http::HeaderVector{}, Http::HeaderVector{}, Http::HeaderVector{},
Http::HeaderVector{}, std::move(headers_to_remove), EMPTY_STRING, Http::Code::OK,
ProtobufWkt::Struct{}}};
SuccessResponse ok{message->headers(),
config_->upstreamHeaderMatchers(),
config_->upstreamHeaderToAppendMatchers(),
config_->clientHeaderOnSuccessMatchers(),
config_->dynamicMetadataMatchers(),
Response{CheckStatus::OK, Http::HeaderVector{}, Http::HeaderVector{},
Http::HeaderVector{}, Http::HeaderVector{},
std::move(headers_to_remove), EMPTY_STRING, Http::Code::OK,
ProtobufWkt::Struct{}}};
return std::move(ok.response_);
}

// Create a Denied authorization response.
SuccessResponse denied{message->headers(), config_->clientHeaderMatchers(),
SuccessResponse denied{message->headers(),
config_->clientHeaderMatchers(),
config_->upstreamHeaderToAppendMatchers(),
config_->clientHeaderOnSuccessMatchers(),
config_->dynamicMetadataMatchers(),
Response{CheckStatus::Denied,
Http::HeaderVector{},
Http::HeaderVector{},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,11 @@ class ClientConfig {
return client_header_on_success_matchers_;
}

/**
* Returns a list of matchers used for selecting the headers to emit as dynamic metadata.
*/
const MatcherSharedPtr& dynamicMetadataMatchers() const { return to_dynamic_metadata_matchers_; }

/**
* Returns a list of matchers used for selecting the authorization response headers that
* should be send to an the upstream server.
Expand Down Expand Up @@ -132,11 +137,14 @@ class ClientConfig {
static MatcherSharedPtr
toClientMatchersOnSuccess(const envoy::type::matcher::v3::ListStringMatcher& list);
static MatcherSharedPtr
toDynamicMetadataMatchers(const envoy::type::matcher::v3::ListStringMatcher& list);
static MatcherSharedPtr
toUpstreamMatchers(const envoy::type::matcher::v3::ListStringMatcher& list);

const MatcherSharedPtr request_header_matchers_;
const MatcherSharedPtr client_header_matchers_;
const MatcherSharedPtr client_header_on_success_matchers_;
const MatcherSharedPtr to_dynamic_metadata_matchers_;
const MatcherSharedPtr upstream_header_matchers_;
const MatcherSharedPtr upstream_header_to_append_matchers_;
const std::string cluster_name_;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,53 @@ TEST_F(ExtAuthzHttpClientTest, AuthorizationOkWithHeadersToRemove) {
client_->onSuccess(async_request_, std::move(http_response));
}

// Test the client when an OK response is received with dynamic metadata in that OK response.
TEST_F(ExtAuthzHttpClientTest, AuthorizationOkWithDynamicMetadata) {
const std::string yaml = R"EOF(
http_service:
server_uri:
uri: "ext_authz:9000"
cluster: "ext_authz"
timeout: 0.25s
authorization_response:
dynamic_metadata_from_headers:
patterns:
- prefix: "X-Metadata-"
ignore_case: true
failure_mode_allow: true
)EOF";

initialize(yaml);
envoy::service::auth::v3::CheckRequest request;
client_->check(request_callbacks_, request, parent_span_, stream_info_);

ProtobufWkt::Struct expected_dynamic_metadata;
auto* metadata_fields = expected_dynamic_metadata.mutable_fields();
(*metadata_fields)["x-metadata-header-0"] = ValueUtil::stringValue("zero");
(*metadata_fields)["x-metadata-header-1"] = ValueUtil::stringValue("2");
(*metadata_fields)["x-metadata-header-2"] = ValueUtil::stringValue("4");

// When we call onSuccess() at the bottom of the test we expect that all the
// dynamic metadata values that we set above to be present in the authz Response
// below.
Response authz_response;
authz_response.status = CheckStatus::OK;
authz_response.dynamic_metadata = expected_dynamic_metadata;
EXPECT_CALL(request_callbacks_, onComplete_(WhenDynamicCastTo<ResponsePtr&>(
AuthzResponseNoAttributes(authz_response))));

const HeaderValueOptionVector http_response_headers = TestCommon::makeHeaderValueOption({
{":status", "200", false},
{"bar", "nope", false},
{"x-metadata-header-0", "zero", false},
{"x-metadata-header-1", "2", false},
{"x-foo", "nah", false},
{"x-metadata-header-2", "4", false},
});
Http::ResponseMessagePtr http_response = TestCommon::makeMessageResponse(http_response_headers);
client_->onSuccess(async_request_, std::move(http_response));
}

// Test the client when a denied response is received.
TEST_F(ExtAuthzHttpClientTest, AuthorizationDenied) {
const auto expected_headers = TestCommon::makeHeaderValueOption({{":status", "403", false}});
Expand Down