From 6b78796398528e6d9d5c60c24ab1f54bdc57db8e Mon Sep 17 00:00:00 2001 From: Yujian Zhao Date: Wed, 1 Nov 2023 15:21:26 -0700 Subject: [PATCH] Propagate route metadata in ext_authz (#30477) Add the ability to ext_authz that collect specified namespaces from route metadata, and propagate them to external auth service. #30252 The instruction of what namespace to select from route metadata, and the field in CheckRequest where the metadata context from route is filled are totally separate from those metadata context from connection or request. Risk Level: Low Testing: Unit tests Signed-off-by: Yujian Zhao --- .../filters/http/ext_authz/v3/ext_authz.proto | 14 +- .../service/auth/v3/attribute_context.proto | 5 +- changelogs/current.yaml | 10 + .../common/ext_authz/check_request_utils.cc | 2 + .../common/ext_authz/check_request_utils.h | 1 + .../filters/http/ext_authz/ext_authz.cc | 97 ++++++---- .../filters/http/ext_authz/ext_authz.h | 15 ++ .../ext_authz/check_request_utils_test.cc | 40 ++-- .../ext_authz/ext_authz_integration_test.cc | 1 + .../filters/http/ext_authz/ext_authz_test.cc | 171 ++++++++++++++++++ 10 files changed, 298 insertions(+), 58 deletions(-) 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 d81b8d8e8a35..9ea4703b6d71 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 @@ -28,7 +28,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // External Authorization :ref:`configuration overview `. // [#extension: envoy.filters.http.ext_authz] -// [#next-free-field: 21] +// [#next-free-field: 23] message ExtAuthz { option (udpa.annotations.versioning).previous_message_type = "envoy.config.filter.http.ext_authz.v2.ExtAuthz"; @@ -120,6 +120,18 @@ message ExtAuthz { // repeated string typed_metadata_context_namespaces = 16; + // Specifies a list of route metadata namespaces whose values, if present, will be passed to the + // ext_authz service at :ref:`route_metadata_context ` in + // :ref:`CheckRequest `. + // :ref:`filter_metadata ` is passed as an opaque ``protobuf::Struct``. + repeated string route_metadata_context_namespaces = 21; + + // Specifies a list of route metadata namespaces whose values, if present, will be passed to the + // ext_authz service at :ref:`route_metadata_context ` in + // :ref:`CheckRequest `. + // :ref:`typed_filter_metadata ` is passed as an ``protobuf::Any``. + repeated string route_typed_metadata_context_namespaces = 22; + // Specifies if the filter is enabled. // // If :ref:`runtime_key ` is specified, diff --git a/api/envoy/service/auth/v3/attribute_context.proto b/api/envoy/service/auth/v3/attribute_context.proto index 77af84436de9..152672685bcc 100644 --- a/api/envoy/service/auth/v3/attribute_context.proto +++ b/api/envoy/service/auth/v3/attribute_context.proto @@ -38,7 +38,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // - field mask to send // - which return values from request_context are copied back // - which return values are copied into request_headers] -// [#next-free-field: 13] +// [#next-free-field: 14] message AttributeContext { option (udpa.annotations.versioning).previous_message_type = "envoy.service.auth.v2.AttributeContext"; @@ -183,6 +183,9 @@ message AttributeContext { // Dynamic metadata associated with the request. config.core.v3.Metadata metadata_context = 11; + // Metadata associated with the selected route. + config.core.v3.Metadata route_metadata_context = 13; + // TLS session details of the underlying connection. // This is not populated by default and will be populated if ext_authz filter's // :ref:`include_tls_session ` is set to true. diff --git a/changelogs/current.yaml b/changelogs/current.yaml index 00f8ef6da138..ef01b2c786be 100644 --- a/changelogs/current.yaml +++ b/changelogs/current.yaml @@ -124,5 +124,15 @@ new_features: New config parameter :ref:`charge_cluster_response_stats ` for not incrementing cluster statistics on ext_authz response. Default true, no behavior change. +- area: ext_authz + change: | + forward :ref:`filter_metadata ` selected by + :ref:`route_metadata_context_namespaces + ` + and :ref:`typed_filter_metadata ` selected by + :ref:`route_typed_metadata_context_namespaces + ` + from the metadata of the selected route to external auth service. + This metadata propagation is independent from the dynamic metadata from connection and request. deprecated: diff --git a/source/extensions/filters/common/ext_authz/check_request_utils.cc b/source/extensions/filters/common/ext_authz/check_request_utils.cc index 6d65dc7d4a4d..8a62aa1cdc98 100644 --- a/source/extensions/filters/common/ext_authz/check_request_utils.cc +++ b/source/extensions/filters/common/ext_authz/check_request_utils.cc @@ -192,6 +192,7 @@ void CheckRequestUtils::createHttpCheck( const Envoy::Http::RequestHeaderMap& headers, Protobuf::Map&& context_extensions, envoy::config::core::v3::Metadata&& metadata_context, + envoy::config::core::v3::Metadata&& route_metadata_context, envoy::service::auth::v3::CheckRequest& request, uint64_t max_request_bytes, bool pack_as_bytes, bool include_peer_certificate, bool include_tls_session, const Protobuf::Map& destination_labels, @@ -224,6 +225,7 @@ void CheckRequestUtils::createHttpCheck( // Fill in the context extensions and metadata context. (*attrs->mutable_context_extensions()) = std::move(context_extensions); (*attrs->mutable_metadata_context()) = std::move(metadata_context); + (*attrs->mutable_route_metadata_context()) = std::move(route_metadata_context); } void CheckRequestUtils::createTcpCheck( diff --git a/source/extensions/filters/common/ext_authz/check_request_utils.h b/source/extensions/filters/common/ext_authz/check_request_utils.h index 96d10334bcbb..1390485c0ae0 100644 --- a/source/extensions/filters/common/ext_authz/check_request_utils.h +++ b/source/extensions/filters/common/ext_authz/check_request_utils.h @@ -93,6 +93,7 @@ class CheckRequestUtils { const Envoy::Http::RequestHeaderMap& headers, Protobuf::Map&& context_extensions, envoy::config::core::v3::Metadata&& metadata_context, + envoy::config::core::v3::Metadata&& route_metadata_context, envoy::service::auth::v3::CheckRequest& request, uint64_t max_request_bytes, bool pack_as_bytes, bool include_peer_certificate, bool include_tls_session, diff --git a/source/extensions/filters/http/ext_authz/ext_authz.cc b/source/extensions/filters/http/ext_authz/ext_authz.cc index 7fad55fe9517..07db5ae73433 100644 --- a/source/extensions/filters/http/ext_authz/ext_authz.cc +++ b/source/extensions/filters/http/ext_authz/ext_authz.cc @@ -1,6 +1,9 @@ #include "source/extensions/filters/http/ext_authz/ext_authz.h" #include +#include +#include +#include #include "envoy/config/core/v3/base.pb.h" @@ -15,6 +18,46 @@ namespace Extensions { namespace HttpFilters { namespace ExtAuthz { +namespace { + +using MetadataProto = ::envoy::config::core::v3::Metadata; + +void fillMetadataContext(const std::vector& source_metadata, + const std::vector& metadata_context_namespaces, + const std::vector& typed_metadata_context_namespaces, + MetadataProto& metadata_context) { + for (const auto& context_key : metadata_context_namespaces) { + for (const MetadataProto* metadata : source_metadata) { + if (metadata == nullptr) { + continue; + } + const auto& filter_metadata = metadata->filter_metadata(); + if (const auto metadata_it = filter_metadata.find(context_key); + metadata_it != filter_metadata.end()) { + (*metadata_context.mutable_filter_metadata())[metadata_it->first] = metadata_it->second; + break; + } + } + } + + for (const auto& context_key : typed_metadata_context_namespaces) { + for (const MetadataProto* metadata : source_metadata) { + if (metadata == nullptr) { + continue; + } + const auto& typed_filter_metadata = metadata->typed_filter_metadata(); + if (const auto metadata_it = typed_filter_metadata.find(context_key); + metadata_it != typed_filter_metadata.end()) { + (*metadata_context.mutable_typed_filter_metadata())[metadata_it->first] = + metadata_it->second; + break; + } + } + } +} + +} // namespace + void FilterConfigPerRoute::merge(const FilterConfigPerRoute& other) { // We only merge context extensions here, and leave boolean flags untouched since those flags are // not used from the merged config. @@ -41,47 +84,29 @@ void Filter::initiateCall(const Http::RequestHeaderMap& headers) { context_extensions = maybe_merged_per_route_config.value().takeContextExtensions(); } + // If metadata_context_namespaces or typed_metadata_context_namespaces is specified, + // pass matching filter metadata to the ext_authz service. + // If metadata key is set in both the connection and request metadata, + // then the value will be the request metadata value. envoy::config::core::v3::Metadata metadata_context; - - // If metadata_context_namespaces is specified, pass matching filter metadata to the ext_authz - // service. If metadata key is set in both the connection and request metadata then the value - // will be the request metadata value. - const auto& connection_metadata = - decoder_callbacks_->connection()->streamInfo().dynamicMetadata().filter_metadata(); - const auto& request_metadata = - decoder_callbacks_->streamInfo().dynamicMetadata().filter_metadata(); - for (const auto& context_key : config_->metadataContextNamespaces()) { - if (const auto metadata_it = request_metadata.find(context_key); - metadata_it != request_metadata.end()) { - (*metadata_context.mutable_filter_metadata())[metadata_it->first] = metadata_it->second; - } else if (const auto metadata_it = connection_metadata.find(context_key); - metadata_it != connection_metadata.end()) { - (*metadata_context.mutable_filter_metadata())[metadata_it->first] = metadata_it->second; - } - } - - // If typed_metadata_context_namespaces is specified, pass matching typed filter metadata to the - // ext_authz service. If metadata key is set in both the connection and request metadata then - // the value will be the request metadata value. - const auto& connection_typed_metadata = - decoder_callbacks_->connection()->streamInfo().dynamicMetadata().typed_filter_metadata(); - const auto& request_typed_metadata = - decoder_callbacks_->streamInfo().dynamicMetadata().typed_filter_metadata(); - for (const auto& context_key : config_->typedMetadataContextNamespaces()) { - if (const auto metadata_it = request_typed_metadata.find(context_key); - metadata_it != request_typed_metadata.end()) { - (*metadata_context.mutable_typed_filter_metadata())[metadata_it->first] = metadata_it->second; - } else if (const auto metadata_it = connection_typed_metadata.find(context_key); - metadata_it != connection_typed_metadata.end()) { - (*metadata_context.mutable_typed_filter_metadata())[metadata_it->first] = metadata_it->second; - } + fillMetadataContext({&decoder_callbacks_->streamInfo().dynamicMetadata(), + &decoder_callbacks_->connection()->streamInfo().dynamicMetadata()}, + config_->metadataContextNamespaces(), + config_->typedMetadataContextNamespaces(), metadata_context); + + // Fill route_metadata_context from the selected route's metadata. + envoy::config::core::v3::Metadata route_metadata_context; + if (decoder_callbacks_->route() != nullptr) { + fillMetadataContext({&decoder_callbacks_->route()->metadata()}, + config_->routeMetadataContextNamespaces(), + config_->routeTypedMetadataContextNamespaces(), route_metadata_context); } Filters::Common::ExtAuthz::CheckRequestUtils::createHttpCheck( decoder_callbacks_, headers, std::move(context_extensions), std::move(metadata_context), - check_request_, config_->maxRequestBytes(), config_->packAsBytes(), - config_->includePeerCertificate(), config_->includeTLSSession(), config_->destinationLabels(), - config_->requestHeaderMatchers()); + std::move(route_metadata_context), check_request_, config_->maxRequestBytes(), + config_->packAsBytes(), config_->includePeerCertificate(), config_->includeTLSSession(), + config_->destinationLabels(), config_->requestHeaderMatchers()); ENVOY_STREAM_LOG(trace, "ext_authz filter calling authorization server", *decoder_callbacks_); // Store start time of ext_authz filter call diff --git a/source/extensions/filters/http/ext_authz/ext_authz.h b/source/extensions/filters/http/ext_authz/ext_authz.h index d49dec93c320..084bff2704e6 100644 --- a/source/extensions/filters/http/ext_authz/ext_authz.h +++ b/source/extensions/filters/http/ext_authz/ext_authz.h @@ -88,6 +88,11 @@ class FilterConfig { config.metadata_context_namespaces().end()), typed_metadata_context_namespaces_(config.typed_metadata_context_namespaces().begin(), config.typed_metadata_context_namespaces().end()), + route_metadata_context_namespaces_(config.route_metadata_context_namespaces().begin(), + config.route_metadata_context_namespaces().end()), + route_typed_metadata_context_namespaces_( + config.route_typed_metadata_context_namespaces().begin(), + config.route_typed_metadata_context_namespaces().end()), include_peer_certificate_(config.include_peer_certificate()), include_tls_session_(config.include_tls_session()), charge_cluster_response_stats_( @@ -169,6 +174,14 @@ class FilterConfig { return typed_metadata_context_namespaces_; } + const std::vector& routeMetadataContextNamespaces() { + return route_metadata_context_namespaces_; + } + + const std::vector& routeTypedMetadataContextNamespaces() { + return route_typed_metadata_context_namespaces_; + } + const ExtAuthzFilterStats& stats() const { return stats_; } void incCounter(Stats::Scope& scope, Stats::StatName name) { @@ -231,6 +244,8 @@ class FilterConfig { const std::vector metadata_context_namespaces_; const std::vector typed_metadata_context_namespaces_; + const std::vector route_metadata_context_namespaces_; + const std::vector route_typed_metadata_context_namespaces_; const bool include_peer_certificate_; const bool include_tls_session_; diff --git a/test/extensions/filters/common/ext_authz/check_request_utils_test.cc b/test/extensions/filters/common/ext_authz/check_request_utils_test.cc index 83674fc8f177..9486088ba523 100644 --- a/test/extensions/filters/common/ext_authz/check_request_utils_test.cc +++ b/test/extensions/filters/common/ext_authz/check_request_utils_test.cc @@ -66,11 +66,11 @@ class CheckRequestUtilsTest : public testing::Test { auto metadata_val = MessageUtil::keyValueStruct("foo", "bar"); (*metadata_context.mutable_filter_metadata())["meta.key"] = metadata_val; - CheckRequestUtils::createHttpCheck(&callbacks_, request_headers, std::move(context_extensions), - std::move(metadata_context), request, - /*max_request_bytes=*/0, /*pack_as_bytes=*/false, - include_peer_certificate, want_tls_session != nullptr, - labels, nullptr); + CheckRequestUtils::createHttpCheck( + &callbacks_, request_headers, std::move(context_extensions), std::move(metadata_context), + envoy::config::core::v3::Metadata(), request, /*max_request_bytes=*/0, + /*pack_as_bytes=*/false, include_peer_certificate, want_tls_session != nullptr, labels, + nullptr); EXPECT_EQ("source", request.attributes().source().principal()); EXPECT_EQ("destination", request.attributes().destination().principal()); @@ -78,7 +78,6 @@ class CheckRequestUtilsTest : public testing::Test { EXPECT_EQ("value", request.attributes().context_extensions().at("key")); EXPECT_EQ("value_1", request.attributes().destination().labels().at("label_1")); EXPECT_EQ("value_2", request.attributes().destination().labels().at("label_2")); - EXPECT_EQ("bar", request.attributes() .metadata_context() .filter_metadata() @@ -86,6 +85,7 @@ class CheckRequestUtilsTest : public testing::Test { .fields() .at("foo") .string_value()); + EXPECT_TRUE(request.attributes().has_route_metadata_context()); if (include_peer_certificate) { EXPECT_EQ(cert_data_, request.attributes().source().certificate()); @@ -190,7 +190,7 @@ TEST_F(CheckRequestUtilsTest, BasicHttp) { expectBasicHttp(); CheckRequestUtils::createHttpCheck( &callbacks_, request_headers, Protobuf::Map(), - envoy::config::core::v3::Metadata(), request_, size, + envoy::config::core::v3::Metadata(), envoy::config::core::v3::Metadata(), request_, size, /*pack_as_bytes=*/false, /*include_peer_certificate=*/false, /*include_tls_session=*/false, Protobuf::Map(), nullptr); ASSERT_EQ(size, request_.attributes().request().http().body().size()); @@ -218,9 +218,9 @@ TEST_F(CheckRequestUtilsTest, BasicHttpWithDuplicateHeaders) { expectBasicHttp(); CheckRequestUtils::createHttpCheck( &callbacks_, request_headers, Protobuf::Map(), - envoy::config::core::v3::Metadata(), request_, size, - /*pack_as_bytes=*/false, /*include_peer_certificate=*/false, - /*include_tls_session=*/false, Protobuf::Map(), nullptr); + envoy::config::core::v3::Metadata(), envoy::config::core::v3::Metadata(), request_, size, + /*pack_as_bytes=*/false, /*include_peer_certificate=*/false, /*include_tls_session=*/false, + Protobuf::Map(), nullptr); ASSERT_EQ(size, request_.attributes().request().http().body().size()); EXPECT_EQ(buffer_->toString().substr(0, size), request_.attributes().request().http().body()); EXPECT_EQ(",foo,bar", request_.attributes().request().http().headers().at("x-duplicate-header")); @@ -247,7 +247,7 @@ TEST_F(CheckRequestUtilsTest, BasicHttpWithRequestHeaderMatchers) { CheckRequestUtils::createHttpCheck( &callbacks_, request_headers, Protobuf::Map(), - envoy::config::core::v3::Metadata(), request_, size, + envoy::config::core::v3::Metadata(), envoy::config::core::v3::Metadata(), request_, size, /*pack_as_bytes=*/false, /*include_peer_certificate=*/false, /*include_tls_session=*/false, Protobuf::Map(), createRequestHeaderMatchers()); ASSERT_EQ(size, request_.attributes().request().http().body().size()); @@ -270,9 +270,9 @@ TEST_F(CheckRequestUtilsTest, BasicHttpWithPartialBody) { expectBasicHttp(); CheckRequestUtils::createHttpCheck( &callbacks_, headers_, Protobuf::Map(), - envoy::config::core::v3::Metadata(), request_, size, - /*pack_as_bytes=*/false, /*include_peer_certificate=*/false, - /*include_tls_session=*/false, Protobuf::Map(), nullptr); + envoy::config::core::v3::Metadata(), envoy::config::core::v3::Metadata(), request_, size, + /*pack_as_bytes=*/false, /*include_peer_certificate=*/false, /*include_tls_session=*/false, + Protobuf::Map(), nullptr); ASSERT_EQ(size, request_.attributes().request().http().body().size()); EXPECT_EQ(buffer_->toString().substr(0, size), request_.attributes().request().http().body()); EXPECT_EQ("true", request_.attributes().request().http().headers().at( @@ -290,9 +290,9 @@ TEST_F(CheckRequestUtilsTest, BasicHttpWithFullBody) { expectBasicHttp(); CheckRequestUtils::createHttpCheck( &callbacks_, headers_, Protobuf::Map(), - envoy::config::core::v3::Metadata(), request_, buffer_->length(), /*pack_as_bytes=*/false, - /*include_peer_certificate=*/false, /*include_tls_session=*/false, - Protobuf::Map(), nullptr); + envoy::config::core::v3::Metadata(), envoy::config::core::v3::Metadata(), request_, + buffer_->length(), /*pack_as_bytes=*/false, /*include_peer_certificate=*/false, + /*include_tls_session=*/false, Protobuf::Map(), nullptr); ASSERT_EQ(buffer_->length(), request_.attributes().request().http().body().size()); EXPECT_EQ(buffer_->toString().substr(0, buffer_->length()), request_.attributes().request().http().body()); @@ -323,9 +323,9 @@ TEST_F(CheckRequestUtilsTest, BasicHttpWithFullBodyPackAsBytes) { // request_.SerializeToString() still returns "true" when it is failed to serialize the data. CheckRequestUtils::createHttpCheck( &callbacks_, headers_, Protobuf::Map(), - envoy::config::core::v3::Metadata(), request_, buffer_->length(), /*pack_as_bytes=*/true, - /*include_peer_certificate=*/false, /*include_tls_session=*/false, - Protobuf::Map(), nullptr); + envoy::config::core::v3::Metadata(), envoy::config::core::v3::Metadata(), request_, + buffer_->length(), /*pack_as_bytes=*/true, /*include_peer_certificate=*/false, + /*include_tls_session=*/false, Protobuf::Map(), nullptr); // TODO(dio): Find a way to test this without using function from testing::internal namespace. testing::internal::CaptureStderr(); diff --git a/test/extensions/filters/http/ext_authz/ext_authz_integration_test.cc b/test/extensions/filters/http/ext_authz/ext_authz_integration_test.cc index 5ce46084511f..a7b48eb4f768 100644 --- a/test/extensions/filters/http/ext_authz/ext_authz_integration_test.cc +++ b/test/extensions/filters/http/ext_authz/ext_authz_integration_test.cc @@ -184,6 +184,7 @@ class ExtAuthzGrpcIntegrationTest : public Grpc::GrpcClientIntegrationParamTest, attributes->clear_source(); attributes->clear_destination(); attributes->clear_metadata_context(); + attributes->clear_route_metadata_context(); attributes->mutable_request()->clear_time(); http_request->clear_id(); http_request->clear_headers(); diff --git a/test/extensions/filters/http/ext_authz/ext_authz_test.cc b/test/extensions/filters/http/ext_authz/ext_authz_test.cc index d67b842d4836..6f902ef0e36f 100644 --- a/test/extensions/filters/http/ext_authz/ext_authz_test.cc +++ b/test/extensions/filters/http/ext_authz/ext_authz_test.cc @@ -36,9 +36,12 @@ using Envoy::Http::LowerCaseString; using testing::_; +using testing::Contains; using testing::InSequence; using testing::Invoke; +using testing::Key; using testing::NiceMock; +using testing::Not; using testing::Return; using testing::ReturnRef; using testing::Values; @@ -1508,6 +1511,174 @@ TEST_F(HttpFilterTest, ConnectionMetadataContext) { "not.selected.data")); } +// Verifies that specified route metadata is passed along in the check request +TEST_F(HttpFilterTest, RouteMetadataContext) { + initialize(R"EOF( + transport_api_version: V3 + grpc_service: + envoy_grpc: + cluster_name: "ext_authz_server" + route_metadata_context_namespaces: + - request.connection.route.have.data + - request.route.have.data + - connection.route.have.data + - route.has.data + - request.has.data + - untyped.and.typed.route.data + - typed.route.data + - untyped.route.data + route_typed_metadata_context_namespaces: + - untyped.and.typed.route.data + - typed.route.data + - untyped.route.data + metadata_context_namespaces: + - request.connection.route.have.data + - request.route.have.data + - connection.route.have.data + - connection.has.data + - route.has.data + )EOF"); + + const std::string route_yaml = R"EOF( + filter_metadata: + request.connection.route.have.data: + data: route + request.route.have.data: + data: route + connection.route.have.data: + data: route + route.has.data: + data: route + untyped.and.typed.route.data: + data: route_untyped + untyped.route.data: + data: route_untyped + typed_filter_metadata: + untyped.and.typed.route.data: + '@type': type.googleapis.com/helloworld.HelloRequest + name: route_typed + typed.route.data: + '@type': type.googleapis.com/helloworld.HelloRequest + name: route_typed + )EOF"; + + const std::string request_yaml = R"EOF( + filter_metadata: + request.connection.route.have.data: + data: request + request.route.have.data: + data: request + )EOF"; + + const std::string connection_yaml = R"EOF( + filter_metadata: + request.connection.route.have.data: + data: connection + connection.route.have.data: + data: connection + connection.has.data: + data: connection + )EOF"; + + prepareCheck(); + + envoy::config::core::v3::Metadata request_metadata, connection_metadata, route_metadata; + TestUtility::loadFromYaml(request_yaml, request_metadata); + TestUtility::loadFromYaml(connection_yaml, connection_metadata); + TestUtility::loadFromYaml(route_yaml, route_metadata); + ON_CALL(decoder_filter_callbacks_.stream_info_, dynamicMetadata()) + .WillByDefault(ReturnRef(request_metadata)); + connection_.stream_info_.metadata_ = connection_metadata; + ON_CALL(*decoder_filter_callbacks_.route_, metadata()).WillByDefault(ReturnRef(route_metadata)); + + envoy::service::auth::v3::CheckRequest check_request; + EXPECT_CALL(*client_, check(_, _, _, _)) + .WillOnce( + Invoke([&](Filters::Common::ExtAuthz::RequestCallbacks&, + const envoy::service::auth::v3::CheckRequest& check_param, Tracing::Span&, + const StreamInfo::StreamInfo&) -> void { check_request = check_param; })); + + filter_->decodeHeaders(request_headers_, false); + Http::MetadataMap metadata_map{{"metadata", "metadata"}}; + EXPECT_EQ(Http::FilterMetadataStatus::Continue, filter_->decodeMetadata(metadata_map)); + + for (const auto& namespace_from_route : std::vector{ + "request.connection.route.have.data", + "request.route.have.data", + "connection.route.have.data", + "route.has.data", + }) { + ASSERT_THAT(check_request.attributes().route_metadata_context().filter_metadata(), + Contains(Key(namespace_from_route))); + EXPECT_EQ("route", check_request.attributes() + .route_metadata_context() + .filter_metadata() + .at(namespace_from_route) + .fields() + .at("data") + .string_value()); + } + EXPECT_THAT(check_request.attributes().route_metadata_context().filter_metadata(), + Not(Contains(Key("request.has.data")))); + + for (const auto& namespace_from_request : + std::vector{"request.connection.route.have.data", "request.route.have.data"}) { + ASSERT_THAT(check_request.attributes().metadata_context().filter_metadata(), + Contains(Key(namespace_from_request))); + EXPECT_EQ("request", check_request.attributes() + .metadata_context() + .filter_metadata() + .at(namespace_from_request) + .fields() + .at("data") + .string_value()); + } + for (const auto& namespace_from_connection : + std::vector{"connection.route.have.data", "connection.has.data"}) { + ASSERT_THAT(check_request.attributes().metadata_context().filter_metadata(), + Contains(Key(namespace_from_connection))); + EXPECT_EQ("connection", check_request.attributes() + .metadata_context() + .filter_metadata() + .at(namespace_from_connection) + .fields() + .at("data") + .string_value()); + } + EXPECT_THAT(check_request.attributes().metadata_context().filter_metadata(), + Not(Contains(Key("route.has.data")))); + + for (const auto& namespace_from_route_untyped : + std::vector{"untyped.and.typed.route.data", "untyped.route.data"}) { + ASSERT_THAT(check_request.attributes().route_metadata_context().filter_metadata(), + Contains(Key(namespace_from_route_untyped))); + EXPECT_EQ("route_untyped", check_request.attributes() + .route_metadata_context() + .filter_metadata() + .at(namespace_from_route_untyped) + .fields() + .at("data") + .string_value()); + } + EXPECT_THAT(check_request.attributes().route_metadata_context().filter_metadata(), + Not(Contains(Key("typed.route.data")))); + + for (const auto& namespace_from_route_typed : + std::vector{"untyped.and.typed.route.data", "typed.route.data"}) { + ASSERT_THAT(check_request.attributes().route_metadata_context().typed_filter_metadata(), + Contains(Key(namespace_from_route_typed))); + helloworld::HelloRequest hello; + EXPECT_TRUE(check_request.attributes() + .route_metadata_context() + .typed_filter_metadata() + .at(namespace_from_route_typed) + .UnpackTo(&hello)); + EXPECT_EQ("route_typed", hello.name()); + } + EXPECT_THAT(check_request.attributes().route_metadata_context().typed_filter_metadata(), + Not(Contains(Key("untyped.route.data")))); +} + // Test that filter can be disabled via the filter_enabled field. TEST_F(HttpFilterTest, FilterDisabled) { initialize(R"EOF(