diff --git a/api/envoy/extensions/filters/http/ext_authz/v3/ext_authz.proto b/api/envoy/extensions/filters/http/ext_authz/v3/ext_authz.proto index 705629496d942..38a6cfb9d3af6 100644 --- a/api/envoy/extensions/filters/http/ext_authz/v3/ext_authz.proto +++ b/api/envoy/extensions/filters/http/ext_authz/v3/ext_authz.proto @@ -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"; @@ -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 `. is set, authorization + // When this :ref:`list ` 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*, @@ -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 ` 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: + // + // - :ref:`envoy.filters.http.ext_authz ` for HTTP filter. + // - :ref:`envoy.filters.network.ext_authz ` for network filter. + type.matcher.v3.ListStringMatcher dynamic_metadata_from_headers = 5; } // Extra settings on a per virtualhost/route/weighted-cluster level. diff --git a/api/envoy/extensions/filters/http/ext_authz/v4alpha/ext_authz.proto b/api/envoy/extensions/filters/http/ext_authz/v4alpha/ext_authz.proto index 014c8263e61c3..7333dfbdacbdc 100644 --- a/api/envoy/extensions/filters/http/ext_authz/v4alpha/ext_authz.proto +++ b/api/envoy/extensions/filters/http/ext_authz/v4alpha/ext_authz.proto @@ -235,6 +235,7 @@ message AuthorizationRequest { repeated config.core.v4alpha.HeaderValue headers_to_add = 2; } +// [#next-free-field: 6] message AuthorizationResponse { option (udpa.annotations.versioning).previous_message_type = "envoy.extensions.filters.http.ext_authz.v3.AuthorizationResponse"; @@ -249,7 +250,7 @@ message AuthorizationResponse { // that coexistent headers will be appended. type.matcher.v4alpha.ListStringMatcher allowed_upstream_headers_to_append = 3; - // When this :ref:`list `. is set, authorization + // When this :ref:`list ` 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*, @@ -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.v4alpha.ListStringMatcher allowed_client_headers_on_success = 4; + + // When this :ref:`list ` 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: + // + // - :ref:`envoy.filters.http.ext_authz ` for HTTP filter. + // - :ref:`envoy.filters.network.ext_authz ` for network filter. + type.matcher.v4alpha.ListStringMatcher dynamic_metadata_from_headers = 5; } // Extra settings on a per virtualhost/route/weighted-cluster level. diff --git a/docs/root/version_history/current.rst b/docs/root/version_history/current.rst index 9ab8bdcfd4d50..212e3cf197e8e 100644 --- a/docs/root/version_history/current.rst +++ b/docs/root/version_history/current.rst @@ -83,6 +83,7 @@ New Features * compression: add brotli :ref:`compressor ` and :ref:`decompressor `. * 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 ` to support emitting dynamic metadata from headers returned by an external authorization service via HTTP. * ext_authz: added :ref:`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 ` 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 ` to reject invalid requests early. diff --git a/generated_api_shadow/envoy/extensions/filters/http/ext_authz/v3/ext_authz.proto b/generated_api_shadow/envoy/extensions/filters/http/ext_authz/v3/ext_authz.proto index 81be512556f19..0c8d2bf6ba958 100644 --- a/generated_api_shadow/envoy/extensions/filters/http/ext_authz/v3/ext_authz.proto +++ b/generated_api_shadow/envoy/extensions/filters/http/ext_authz/v3/ext_authz.proto @@ -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"; @@ -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 `. is set, authorization + // When this :ref:`list ` 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*, @@ -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 ` 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: + // + // - :ref:`envoy.filters.http.ext_authz ` for HTTP filter. + // - :ref:`envoy.filters.network.ext_authz ` for network filter. + type.matcher.v3.ListStringMatcher dynamic_metadata_from_headers = 5; } // Extra settings on a per virtualhost/route/weighted-cluster level. diff --git a/generated_api_shadow/envoy/extensions/filters/http/ext_authz/v4alpha/ext_authz.proto b/generated_api_shadow/envoy/extensions/filters/http/ext_authz/v4alpha/ext_authz.proto index 014c8263e61c3..7333dfbdacbdc 100644 --- a/generated_api_shadow/envoy/extensions/filters/http/ext_authz/v4alpha/ext_authz.proto +++ b/generated_api_shadow/envoy/extensions/filters/http/ext_authz/v4alpha/ext_authz.proto @@ -235,6 +235,7 @@ message AuthorizationRequest { repeated config.core.v4alpha.HeaderValue headers_to_add = 2; } +// [#next-free-field: 6] message AuthorizationResponse { option (udpa.annotations.versioning).previous_message_type = "envoy.extensions.filters.http.ext_authz.v3.AuthorizationResponse"; @@ -249,7 +250,7 @@ message AuthorizationResponse { // that coexistent headers will be appended. type.matcher.v4alpha.ListStringMatcher allowed_upstream_headers_to_append = 3; - // When this :ref:`list `. is set, authorization + // When this :ref:`list ` 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*, @@ -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.v4alpha.ListStringMatcher allowed_client_headers_on_success = 4; + + // When this :ref:`list ` 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: + // + // - :ref:`envoy.filters.http.ext_authz ` for HTTP filter. + // - :ref:`envoy.filters.network.ext_authz ` for network filter. + type.matcher.v4alpha.ListStringMatcher dynamic_metadata_from_headers = 5; } // Extra settings on a per virtualhost/route/weighted-cluster level. diff --git a/source/extensions/filters/common/ext_authz/ext_authz_http_impl.cc b/source/extensions/filters/common/ext_authz/ext_authz_http_impl.cc index 11da8bc4f7dd8..1daa6acdfd61d 100644 --- a/source/extensions/filters/common/ext_authz/ext_authz_http_impl.cc +++ b/source/extensions/filters/common/ext_authz/ext_authz_http_impl.cc @@ -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_matchers_(response_matchers), + to_dynamic_metadata_matchers_(dynamic_metadata_matchers), + response_(std::make_unique(response)) { headers_.iterate([this](const Http::HeaderEntry& header) -> Http::HeaderMap::Iterate { // UpstreamHeaderMatcher if (matchers_->matches(header.key().getStringView())) { @@ -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_; }; @@ -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( @@ -148,6 +160,12 @@ ClientConfig::toClientMatchersOnSuccess(const envoy::type::matcher::v3::ListStri return std::make_shared(std::move(matchers)); } +MatcherSharedPtr +ClientConfig::toDynamicMetadataMatchers(const envoy::type::matcher::v3::ListStringMatcher& list) { + std::vector matchers(createStringMatchers(list)); + return std::make_shared(std::move(matchers)); +} + MatcherSharedPtr ClientConfig::toClientMatchers(const envoy::type::matcher::v3::ListStringMatcher& list) { std::vector matchers(createStringMatchers(list)); @@ -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{}, diff --git a/source/extensions/filters/common/ext_authz/ext_authz_http_impl.h b/source/extensions/filters/common/ext_authz/ext_authz_http_impl.h index f2e6e5726973d..cd897dcc3d8ac 100644 --- a/source/extensions/filters/common/ext_authz/ext_authz_http_impl.h +++ b/source/extensions/filters/common/ext_authz/ext_authz_http_impl.h @@ -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. @@ -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_; diff --git a/test/extensions/filters/common/ext_authz/ext_authz_http_impl_test.cc b/test/extensions/filters/common/ext_authz/ext_authz_http_impl_test.cc index 97e51e11a505c..56fe0d0aafa20 100644 --- a/test/extensions/filters/common/ext_authz/ext_authz_http_impl_test.cc +++ b/test/extensions/filters/common/ext_authz/ext_authz_http_impl_test.cc @@ -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( + 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}});