Skip to content

Commit

Permalink
Propagate route metadata in ext_authz (envoyproxy#30477)
Browse files Browse the repository at this point in the history
Add the ability to ext_authz that collect specified namespaces from route metadata, and propagate them to external auth service. envoyproxy#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 <[email protected]>
  • Loading branch information
ikimonogakari authored Nov 1, 2023
1 parent d1831a6 commit 6b78796
Show file tree
Hide file tree
Showing 10 changed files with 298 additions and 58 deletions.
14 changes: 13 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 @@ -28,7 +28,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE;
// External Authorization :ref:`configuration overview <config_http_filters_ext_authz>`.
// [#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";
Expand Down Expand Up @@ -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 <envoy_v3_api_field_service.auth.v3.AttributeContext.route_metadata_context>` in
// :ref:`CheckRequest <envoy_v3_api_field_service.auth.v3.CheckRequest.attributes>`.
// :ref:`filter_metadata <envoy_v3_api_field_config.core.v3.Metadata.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 <envoy_v3_api_field_service.auth.v3.AttributeContext.route_metadata_context>` in
// :ref:`CheckRequest <envoy_v3_api_field_service.auth.v3.CheckRequest.attributes>`.
// :ref:`typed_filter_metadata <envoy_v3_api_field_config.core.v3.Metadata.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 <envoy_v3_api_field_config.core.v3.RuntimeFractionalPercent.runtime_key>` is specified,
Expand Down
5 changes: 4 additions & 1 deletion api/envoy/service/auth/v3/attribute_context.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 <config_http_filters_ext_authz>` is set to true.
Expand Down
10 changes: 10 additions & 0 deletions changelogs/current.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -124,5 +124,15 @@ new_features:
New config parameter :ref:`charge_cluster_response_stats
<envoy_v3_api_field_extensions.filters.http.ext_authz.v3.ExtAuthz.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 <envoy_v3_api_field_config.core.v3.Metadata.filter_metadata>` selected by
:ref:`route_metadata_context_namespaces
<envoy_v3_api_field_extensions.filters.http.ext_authz.v3.ExtAuthz.route_metadata_context_namespaces>`
and :ref:`typed_filter_metadata <envoy_v3_api_field_config.core.v3.Metadata.typed_filter_metadata>` selected by
:ref:`route_typed_metadata_context_namespaces
<envoy_v3_api_field_extensions.filters.http.ext_authz.v3.ExtAuthz.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:
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ void CheckRequestUtils::createHttpCheck(
const Envoy::Http::RequestHeaderMap& headers,
Protobuf::Map<std::string, std::string>&& 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<std::string, std::string>& destination_labels,
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ class CheckRequestUtils {
const Envoy::Http::RequestHeaderMap& headers,
Protobuf::Map<std::string, std::string>&& 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,
Expand Down
97 changes: 61 additions & 36 deletions source/extensions/filters/http/ext_authz/ext_authz.cc
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
#include "source/extensions/filters/http/ext_authz/ext_authz.h"

#include <chrono>
#include <optional>
#include <string>
#include <vector>

#include "envoy/config/core/v3/base.pb.h"

Expand All @@ -15,6 +18,46 @@ namespace Extensions {
namespace HttpFilters {
namespace ExtAuthz {

namespace {

using MetadataProto = ::envoy::config::core::v3::Metadata;

void fillMetadataContext(const std::vector<const MetadataProto*>& source_metadata,
const std::vector<std::string>& metadata_context_namespaces,
const std::vector<std::string>& 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.
Expand All @@ -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
Expand Down
15 changes: 15 additions & 0 deletions source/extensions/filters/http/ext_authz/ext_authz.h
Original file line number Diff line number Diff line change
Expand Up @@ -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_(
Expand Down Expand Up @@ -169,6 +174,14 @@ class FilterConfig {
return typed_metadata_context_namespaces_;
}

const std::vector<std::string>& routeMetadataContextNamespaces() {
return route_metadata_context_namespaces_;
}

const std::vector<std::string>& routeTypedMetadataContextNamespaces() {
return route_typed_metadata_context_namespaces_;
}

const ExtAuthzFilterStats& stats() const { return stats_; }

void incCounter(Stats::Scope& scope, Stats::StatName name) {
Expand Down Expand Up @@ -231,6 +244,8 @@ class FilterConfig {

const std::vector<std::string> metadata_context_namespaces_;
const std::vector<std::string> typed_metadata_context_namespaces_;
const std::vector<std::string> route_metadata_context_namespaces_;
const std::vector<std::string> route_typed_metadata_context_namespaces_;

const bool include_peer_certificate_;
const bool include_tls_session_;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,26 +66,26 @@ 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());
EXPECT_EQ("foo", request.attributes().source().service());
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()
.at("meta.key")
.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());
Expand Down Expand Up @@ -190,7 +190,7 @@ TEST_F(CheckRequestUtilsTest, BasicHttp) {
expectBasicHttp();
CheckRequestUtils::createHttpCheck(
&callbacks_, request_headers, Protobuf::Map<std::string, std::string>(),
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<std::string, std::string>(), nullptr);
ASSERT_EQ(size, request_.attributes().request().http().body().size());
Expand Down Expand Up @@ -218,9 +218,9 @@ TEST_F(CheckRequestUtilsTest, BasicHttpWithDuplicateHeaders) {
expectBasicHttp();
CheckRequestUtils::createHttpCheck(
&callbacks_, request_headers, Protobuf::Map<std::string, std::string>(),
envoy::config::core::v3::Metadata(), request_, size,
/*pack_as_bytes=*/false, /*include_peer_certificate=*/false,
/*include_tls_session=*/false, Protobuf::Map<std::string, std::string>(), 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<std::string, std::string>(), 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"));
Expand All @@ -247,7 +247,7 @@ TEST_F(CheckRequestUtilsTest, BasicHttpWithRequestHeaderMatchers) {

CheckRequestUtils::createHttpCheck(
&callbacks_, request_headers, Protobuf::Map<std::string, std::string>(),
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<std::string, std::string>(), createRequestHeaderMatchers());
ASSERT_EQ(size, request_.attributes().request().http().body().size());
Expand All @@ -270,9 +270,9 @@ TEST_F(CheckRequestUtilsTest, BasicHttpWithPartialBody) {
expectBasicHttp();
CheckRequestUtils::createHttpCheck(
&callbacks_, headers_, Protobuf::Map<std::string, std::string>(),
envoy::config::core::v3::Metadata(), request_, size,
/*pack_as_bytes=*/false, /*include_peer_certificate=*/false,
/*include_tls_session=*/false, Protobuf::Map<std::string, std::string>(), 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<std::string, std::string>(), 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(
Expand All @@ -290,9 +290,9 @@ TEST_F(CheckRequestUtilsTest, BasicHttpWithFullBody) {
expectBasicHttp();
CheckRequestUtils::createHttpCheck(
&callbacks_, headers_, Protobuf::Map<std::string, std::string>(),
envoy::config::core::v3::Metadata(), request_, buffer_->length(), /*pack_as_bytes=*/false,
/*include_peer_certificate=*/false, /*include_tls_session=*/false,
Protobuf::Map<std::string, std::string>(), 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<std::string, std::string>(), nullptr);
ASSERT_EQ(buffer_->length(), request_.attributes().request().http().body().size());
EXPECT_EQ(buffer_->toString().substr(0, buffer_->length()),
request_.attributes().request().http().body());
Expand Down Expand Up @@ -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<std::string, std::string>(),
envoy::config::core::v3::Metadata(), request_, buffer_->length(), /*pack_as_bytes=*/true,
/*include_peer_certificate=*/false, /*include_tls_session=*/false,
Protobuf::Map<std::string, std::string>(), 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<std::string, std::string>(), nullptr);

// TODO(dio): Find a way to test this without using function from testing::internal namespace.
testing::internal::CaptureStderr();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading

0 comments on commit 6b78796

Please sign in to comment.