diff --git a/api/envoy/config/filter/http/ext_authz/v2/ext_authz.proto b/api/envoy/config/filter/http/ext_authz/v2/ext_authz.proto index 8e2ea7661e1f..de105eff3c80 100644 --- a/api/envoy/config/filter/http/ext_authz/v2/ext_authz.proto +++ b/api/envoy/config/filter/http/ext_authz/v2/ext_authz.proto @@ -72,6 +72,20 @@ message ExtAuthz { // Sets the HTTP status that is returned to the client when there is a network error between the // filter and the authorization server. The default status is HTTP 403 Forbidden. envoy.type.HttpStatus status_on_error = 7; + + // Specifies a list of metadata namespaces whose values, if present, will be passed to the + // ext_authz service as an opaque *protobuf::Struct*. + // + // For example, if the *jwt_authn* filter is used and :ref:`payload_in_metadata + // ` is set, + // then the following will pass the jwt payload to the authorization server. + // + // .. code-block:: yaml + // + // metadata_context_namespaces: + // - envoy.filters.http.jwt_authn + // + repeated string metadata_context_namespaces = 8; } // Configuration for buffering the request data. diff --git a/api/envoy/service/auth/v2/attribute_context.proto b/api/envoy/service/auth/v2/attribute_context.proto index f5b723e7b633..822e361dd81b 100644 --- a/api/envoy/service/auth/v2/attribute_context.proto +++ b/api/envoy/service/auth/v2/attribute_context.proto @@ -7,6 +7,7 @@ option java_multiple_files = true; option java_package = "io.envoyproxy.envoy.service.auth.v2"; import "envoy/api/v2/core/address.proto"; +import "envoy/api/v2/core/base.proto"; import "google/protobuf/timestamp.proto"; import "gogoproto/gogo.proto"; @@ -135,6 +136,9 @@ message AttributeContext { // information to the auth server without modifying the proto definition. It maps to the // internal opaque context in the filter chain. map context_extensions = 10; + + // Dynamic metadata associated with the request. + envoy.api.v2.core.Metadata metadata_context = 11; } // The following items are left out of this proto diff --git a/docs/root/intro/version_history.rst b/docs/root/intro/version_history.rst index 83998544fdf1..c40a3454e4b1 100644 --- a/docs/root/intro/version_history.rst +++ b/docs/root/intro/version_history.rst @@ -19,6 +19,7 @@ Version history * config: async data access for local and remote data source. * config: changed the default value of :ref:`initial_fetch_timeout ` from 0s to 15s. This is a change in behaviour in the sense that Envoy will move to the next initialization phase, even if the first config is not delivered in 15s. Refer to :ref:`initialization process ` for more details. * config: added stat :ref:`init_fetch_timeout `. +* ext_authz: added :ref:`configurable ability ` to send dynamic metadata to the `ext_authz` service. * fault: added overrides for default runtime keys in :ref:`HTTPFault ` filter. * grpc: added :ref:`AWS IAM grpc credentials extension ` for AWS-managed xDS. * grpc-json: added support for :ref:`ignoring unknown query parameters`. 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 d243e04c5978..b98752cc5a14 100644 --- a/source/extensions/filters/common/ext_authz/check_request_utils.cc +++ b/source/extensions/filters/common/ext_authz/check_request_utils.cc @@ -143,6 +143,7 @@ void CheckRequestUtils::createHttpCheck( const Envoy::Http::StreamDecoderFilterCallbacks* callbacks, const Envoy::Http::HeaderMap& headers, Protobuf::Map&& context_extensions, + envoy::api::v2::core::Metadata&& metadata_context, envoy::service::auth::v2::CheckRequest& request, uint64_t max_request_bytes) { auto attrs = request.mutable_attributes(); @@ -158,6 +159,7 @@ void CheckRequestUtils::createHttpCheck( // Fill in the context extensions: (*attrs->mutable_context_extensions()) = std::move(context_extensions); + (*attrs->mutable_metadata_context()) = std::move(metadata_context); } void CheckRequestUtils::createTcpCheck(const Network::ReadFilterCallbacks* callbacks, 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 5fa997c80a52..6f90d8d86b1a 100644 --- a/source/extensions/filters/common/ext_authz/check_request_utils.h +++ b/source/extensions/filters/common/ext_authz/check_request_utils.h @@ -48,6 +48,7 @@ class CheckRequestUtils { static void createHttpCheck(const Envoy::Http::StreamDecoderFilterCallbacks* callbacks, const Envoy::Http::HeaderMap& headers, Protobuf::Map&& context_extensions, + envoy::api::v2::core::Metadata&& metadata_context, envoy::service::auth::v2::CheckRequest& request, uint64_t max_request_bytes); diff --git a/source/extensions/filters/http/ext_authz/ext_authz.cc b/source/extensions/filters/http/ext_authz/ext_authz.cc index 87b566079927..d7685ab1a057 100644 --- a/source/extensions/filters/http/ext_authz/ext_authz.cc +++ b/source/extensions/filters/http/ext_authz/ext_authz.cc @@ -65,9 +65,20 @@ void Filter::initiateCall(const Http::HeaderMap& headers) { if (maybe_merged_per_route_config) { context_extensions = maybe_merged_per_route_config.value().takeContextExtensions(); } + + // If metadata_context_namespaces is specified, pass matching metadata to the ext_authz service + envoy::api::v2::core::Metadata metadata_context; + const auto& request_metadata = callbacks_->streamInfo().dynamicMetadata().filter_metadata(); + for (const auto& context_key : config_->metadataContextNamespaces()) { + const auto& metadata_it = request_metadata.find(context_key); + if (metadata_it != request_metadata.end()) { + (*metadata_context.mutable_filter_metadata())[metadata_it->first] = metadata_it->second; + } + } + Filters::Common::ExtAuthz::CheckRequestUtils::createHttpCheck( - callbacks_, headers, std::move(context_extensions), check_request_, - config_->maxRequestBytes()); + callbacks_, headers, std::move(context_extensions), std::move(metadata_context), + check_request_, config_->maxRequestBytes()); ENVOY_STREAM_LOG(trace, "ext_authz filter calling authorization server", *callbacks_); state_ = State::Calling; diff --git a/source/extensions/filters/http/ext_authz/ext_authz.h b/source/extensions/filters/http/ext_authz/ext_authz.h index 0b0528b53403..60ec7ca10f48 100644 --- a/source/extensions/filters/http/ext_authz/ext_authz.h +++ b/source/extensions/filters/http/ext_authz/ext_authz.h @@ -47,6 +47,8 @@ class FilterConfig { max_request_bytes_(config.with_request_body().max_request_bytes()), status_on_error_(toErrorCode(config.status_on_error().code())), local_info_(local_info), scope_(scope), runtime_(runtime), http_context_(http_context), pool_(scope.symbolTable()), + metadata_context_namespaces_(config.metadata_context_namespaces().begin(), + config.metadata_context_namespaces().end()), ext_authz_ok_(pool_.add("ext_authz.ok")), ext_authz_denied_(pool_.add("ext_authz.denied")), ext_authz_error_(pool_.add("ext_authz.error")), ext_authz_failure_mode_allowed_(pool_.add("ext_authz.failure_mode_allowed")) {} @@ -75,6 +77,10 @@ class FilterConfig { scope.counterFromStatName(name).inc(); } + const std::vector& metadataContextNamespaces() { + return metadata_context_namespaces_; + } + private: static Http::Code toErrorCode(uint64_t status) { const auto code = static_cast(status); @@ -93,8 +99,11 @@ class FilterConfig { Stats::Scope& scope_; Runtime::Loader& runtime_; Http::Context& http_context_; + Stats::StatNamePool pool_; + const std::vector metadata_context_namespaces_; + public: const Stats::StatName ext_authz_ok_; const Stats::StatName ext_authz_denied_; 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 7567a9a95023..fa925bf6b8bf 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 @@ -86,7 +86,8 @@ TEST_F(CheckRequestUtilsTest, BasicHttp) { ExpectBasicHttp(); CheckRequestUtils::createHttpCheck(&callbacks_, request_headers, - Protobuf::Map(), request_, size); + Protobuf::Map(), + envoy::api::v2::core::Metadata(), request_, size); ASSERT_EQ(size, request_.attributes().request().http().body().size()); EXPECT_EQ(buffer_->toString().substr(0, size), request_.attributes().request().http().body()); EXPECT_EQ(request_.attributes().request().http().headers().end(), @@ -102,7 +103,8 @@ TEST_F(CheckRequestUtilsTest, BasicHttpWithPartialBody) { ExpectBasicHttp(); CheckRequestUtils::createHttpCheck(&callbacks_, headers_, - Protobuf::Map(), request_, size); + Protobuf::Map(), + envoy::api::v2::core::Metadata(), request_, size); 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( @@ -116,8 +118,8 @@ TEST_F(CheckRequestUtilsTest, BasicHttpWithFullBody) { ExpectBasicHttp(); CheckRequestUtils::createHttpCheck(&callbacks_, headers_, - Protobuf::Map(), request_, - buffer_->length()); + Protobuf::Map(), + envoy::api::v2::core::Metadata(), request_, buffer_->length()); ASSERT_EQ(buffer_->length(), request_.attributes().request().http().body().size()); EXPECT_EQ(buffer_->toString().substr(0, buffer_->length()), request_.attributes().request().http().body()); @@ -146,13 +148,24 @@ TEST_F(CheckRequestUtilsTest, CheckAttrContextPeer) { Protobuf::Map context_extensions; context_extensions["key"] = "value"; + envoy::api::v2::core::Metadata metadata_context; + 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), - request, false); + std::move(metadata_context), request, false); EXPECT_EQ("source", request.attributes().source().principal()); EXPECT_EQ("destination", request.attributes().destination().principal()); EXPECT_EQ("foo", request.attributes().source().service()); EXPECT_EQ("value", request.attributes().context_extensions().at("key")); + EXPECT_EQ("bar", request.attributes() + .metadata_context() + .filter_metadata() + .at("meta.key") + .fields() + .at("foo") + .string_value()); } } // namespace 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 d82354c0e917..3586d9d9b550 100644 --- a/test/extensions/filters/http/ext_authz/ext_authz_test.cc +++ b/test/extensions/filters/http/ext_authz/ext_authz_test.cc @@ -768,6 +768,68 @@ TEST_F(HttpFilterTest, NoClearCacheRouteDeniedResponse) { EXPECT_EQ("ext_authz_denied", filter_callbacks_.details_); } +// Verifies that specified metadata is passed along in the check request +TEST_F(HttpFilterTest, MetadataContext) { + initialize(R"EOF( + grpc_service: + envoy_grpc: + cluster_name: "ext_authz_server" + metadata_context_namespaces: + - jazz.sax + - rock.guitar + - hiphop.drums + )EOF"); + + const std::string yaml = R"EOF( + filter_metadata: + jazz.sax: + coltrane: john + parker: charlie + jazz.piano: + monk: thelonious + hancock: herbie + rock.guitar: + hendrix: jimi + richards: keith + )EOF"; + + envoy::api::v2::core::Metadata metadata; + TestUtility::loadFromYaml(yaml, metadata); + ON_CALL(filter_callbacks_.stream_info_, dynamicMetadata()).WillByDefault(ReturnRef(metadata)); + + prepareCheck(); + + envoy::service::auth::v2::CheckRequest check_request; + EXPECT_CALL(*client_, check(_, _, _)) + .WillOnce(WithArgs<1>(Invoke([&](const envoy::service::auth::v2::CheckRequest& check_param) + -> 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)); + + EXPECT_EQ("john", check_request.attributes() + .metadata_context() + .filter_metadata() + .at("jazz.sax") + .fields() + .at("coltrane") + .string_value()); + + EXPECT_EQ("jimi", check_request.attributes() + .metadata_context() + .filter_metadata() + .at("rock.guitar") + .fields() + .at("hendrix") + .string_value()); + + EXPECT_EQ(0, check_request.attributes().metadata_context().filter_metadata().count("jazz.piano")); + + EXPECT_EQ(0, + check_request.attributes().metadata_context().filter_metadata().count("hiphop.drums")); +} + // ------------------- // Parameterized Tests // -------------------