Skip to content

Commit

Permalink
ext_authz: add metadata_context to ext_authz filter (#7818)
Browse files Browse the repository at this point in the history
This adds the ability to specify dynamic metadata (by namespace) to
send with the ext_authz check request. This allows one filter to
specify information that can be then used in evaluating an
authorization decision.

Risk Level: Medium. Optional feature/extension of existing filter
Testing: Unit testing
Docs Changes: Inline in attribute_context.proto and ext_authz.proto

Fixes #7699

Signed-off-by: Ben Plotnick <[email protected]>
  • Loading branch information
Ben Plotnick authored and htuch committed Aug 20, 2019
1 parent f90e1b0 commit d2e2cd6
Show file tree
Hide file tree
Showing 9 changed files with 124 additions and 7 deletions.
14 changes: 14 additions & 0 deletions api/envoy/config/filter/http/ext_authz/v2/ext_authz.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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
// <envoy_api_field_config.filter.http.jwt_authn.v2alpha.JwtProvider.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.
Expand Down
4 changes: 4 additions & 0 deletions api/envoy/service/auth/v2/attribute_context.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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<string, string> 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
Expand Down
1 change: 1 addition & 0 deletions docs/root/intro/version_history.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <envoy_api_field_core.ConfigSource.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 <arch_overview_initialization>` for more details.
* config: added stat :ref:`init_fetch_timeout <config_cluster_manager_cds>`.
* ext_authz: added :ref:`configurable ability <envoy_api_field_config.filter.http.ext_authz.v2.ExtAuthz.metadata_context_namespaces>` to send dynamic metadata to the `ext_authz` service.
* fault: added overrides for default runtime keys in :ref:`HTTPFault <envoy_api_msg_config.filter.http.fault.v2.HTTPFault>` filter.
* grpc: added :ref:`AWS IAM grpc credentials extension <envoy_api_file_envoy/config/grpc_credential/v2alpha/aws_iam.proto>` for AWS-managed xDS.
* grpc-json: added support for :ref:`ignoring unknown query parameters<envoy_api_field_config.filter.http.transcoder.v2.GrpcJsonTranscoder.ignore_unknown_query_parameters>`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ void CheckRequestUtils::createHttpCheck(
const Envoy::Http::StreamDecoderFilterCallbacks* callbacks,
const Envoy::Http::HeaderMap& headers,
Protobuf::Map<std::string, std::string>&& 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();
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class CheckRequestUtils {
static void createHttpCheck(const Envoy::Http::StreamDecoderFilterCallbacks* callbacks,
const Envoy::Http::HeaderMap& headers,
Protobuf::Map<std::string, std::string>&& context_extensions,
envoy::api::v2::core::Metadata&& metadata_context,
envoy::service::auth::v2::CheckRequest& request,
uint64_t max_request_bytes);

Expand Down
15 changes: 13 additions & 2 deletions source/extensions/filters/http/ext_authz/ext_authz.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
9 changes: 9 additions & 0 deletions source/extensions/filters/http/ext_authz/ext_authz.h
Original file line number Diff line number Diff line change
Expand Up @@ -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")) {}
Expand Down Expand Up @@ -75,6 +77,10 @@ class FilterConfig {
scope.counterFromStatName(name).inc();
}

const std::vector<std::string>& metadataContextNamespaces() {
return metadata_context_namespaces_;
}

private:
static Http::Code toErrorCode(uint64_t status) {
const auto code = static_cast<Http::Code>(status);
Expand All @@ -93,8 +99,11 @@ class FilterConfig {
Stats::Scope& scope_;
Runtime::Loader& runtime_;
Http::Context& http_context_;

Stats::StatNamePool pool_;

const std::vector<std::string> metadata_context_namespaces_;

public:
const Stats::StatName ext_authz_ok_;
const Stats::StatName ext_authz_denied_;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ TEST_F(CheckRequestUtilsTest, BasicHttp) {

ExpectBasicHttp();
CheckRequestUtils::createHttpCheck(&callbacks_, request_headers,
Protobuf::Map<std::string, std::string>(), request_, size);
Protobuf::Map<std::string, std::string>(),
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(),
Expand All @@ -102,7 +103,8 @@ TEST_F(CheckRequestUtilsTest, BasicHttpWithPartialBody) {

ExpectBasicHttp();
CheckRequestUtils::createHttpCheck(&callbacks_, headers_,
Protobuf::Map<std::string, std::string>(), request_, size);
Protobuf::Map<std::string, std::string>(),
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(
Expand All @@ -116,8 +118,8 @@ TEST_F(CheckRequestUtilsTest, BasicHttpWithFullBody) {

ExpectBasicHttp();
CheckRequestUtils::createHttpCheck(&callbacks_, headers_,
Protobuf::Map<std::string, std::string>(), request_,
buffer_->length());
Protobuf::Map<std::string, std::string>(),
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());
Expand Down Expand Up @@ -146,13 +148,24 @@ TEST_F(CheckRequestUtilsTest, CheckAttrContextPeer) {
Protobuf::Map<std::string, std::string> 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
Expand Down
62 changes: 62 additions & 0 deletions test/extensions/filters/http/ext_authz/ext_authz_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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
// -------------------
Expand Down

0 comments on commit d2e2cd6

Please sign in to comment.