Skip to content

Commit 8c03b2a

Browse files
authored
ext_authz: add grpc_service field on the per-route filter (#40169)
## Description This PR adds support for per-route gRPC service override in the `ext_authz` HTTP filter, allowing different routes to use different external authorization backends. Routes would now be able to specify a different authorization service by configuring `grpc_service` in the per-route `check_settings`. --- Commit Message: ext_authz: add grpc_service field on the per-route filter Additional Description: Add a new `grpc_service` field on the per-route ExtAuthZ filter to be able to override the AuthService backend on a per-route basis. Risk Level: Low Testing: Added Unit & Integration Tests Docs Changes: Added Release Notes: Added --------- Signed-off-by: Rohit Agrawal <rohit.agrawal@databricks.com>
1 parent 0c7818d commit 8c03b2a

File tree

11 files changed

+1725
-217
lines changed

11 files changed

+1725
-217
lines changed

api/envoy/extensions/filters/http/ext_authz/v3/ext_authz.proto

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,7 @@ message ExtAuthzPerRoute {
476476
}
477477

478478
// Extra settings for the check request.
479+
// [#next-free-field: 6]
479480
message CheckSettings {
480481
option (udpa.annotations.versioning).previous_message_type =
481482
"envoy.config.filter.http.ext_authz.v2.CheckSettings";
@@ -513,4 +514,16 @@ message CheckSettings {
513514
// :ref:`disable_request_body_buffering <envoy_v3_api_field_extensions.filters.http.ext_authz.v3.CheckSettings.disable_request_body_buffering>`
514515
// may be specified.
515516
BufferSettings with_request_body = 3;
517+
518+
// Override the external authorization service for this route.
519+
// This allows different routes to use different external authorization service backends
520+
// and service types (gRPC or HTTP). If specified, this overrides the filter-level service
521+
// configuration regardless of the original service type.
522+
oneof service_override {
523+
// Override with a gRPC service configuration.
524+
config.core.v3.GrpcService grpc_service = 4;
525+
526+
// Override with an HTTP service configuration.
527+
HttpService http_service = 5;
528+
}
516529
}

changelogs/current.yaml

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -203,9 +203,8 @@ new_features:
203203
for more details.
204204
- area: socket
205205
change: |
206-
Added :ref:``network_namespace_filepath <envoy_v3_api_msg_config.core.v3.SocketAddress.network_namespace_filepath>`` to
207-
:ref:`SocketAddress <envoy_v3_api_msg_config.core.v3.SocketAddress>`. This field allows specifying a Linux network namespace filepath
208-
for socket creation, enabling network isolation in containerized environments.
206+
Added ``network_namespace_filepath`` to :ref:`SocketAddress <envoy_v3_api_msg_config.core.v3.SocketAddress>`. This field allows
207+
specifying a Linux network namespace filepath for socket creation, enabling network isolation in containerized environments.
209208
- area: ratelimit
210209
change: |
211210
Add the :ref:`rate_limits
@@ -256,6 +255,13 @@ new_features:
256255
Added ``virtualHost()`` to the Stream handle API, allowing Lua scripts to retrieve virtual host information. So far, the only method
257256
implemented is ``metadata()``, allowing Lua scripts to access virtual host metadata scoped to the specific filter name. See
258257
:ref:`Virtual host object API <config_http_filters_lua_virtual_host_wrapper>` for more details.
258+
- area: ext_authz
259+
change: |
260+
Added support for per-route gRPC service override in the ``ext_authz`` HTTP filter. This allows different routes
261+
to use different external authorization backends by configuring a
262+
:ref:`grpc_service <envoy_v3_api_field_extensions.filters.http.ext_authz.v3.CheckSettings.grpc_service>`
263+
in the per-route ``check_settings``. Routes without this configuration continue to use the default
264+
authorization service.
259265
- area: tracing
260266
change: |
261267
Added :ref:`trace_context_option <envoy_v3_api_field_config.trace.v3.ZipkinConfig.trace_context_option>` enum

source/extensions/filters/common/ext_authz/ext_authz_http_impl.cc

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,30 @@ ClientConfig::ClientConfig(const envoy::extensions::filters::http::ext_authz::v3
133133
Router::HeaderParserPtr)),
134134
encode_raw_headers_(config.encode_raw_headers()) {}
135135

136+
ClientConfig::ClientConfig(
137+
const envoy::extensions::filters::http::ext_authz::v3::HttpService& http_service,
138+
bool encode_raw_headers, uint32_t timeout, Server::Configuration::CommonFactoryContext& context)
139+
: client_header_matchers_(toClientMatchers(
140+
http_service.authorization_response().allowed_client_headers(), context)),
141+
client_header_on_success_matchers_(toClientMatchersOnSuccess(
142+
http_service.authorization_response().allowed_client_headers_on_success(), context)),
143+
to_dynamic_metadata_matchers_(toDynamicMetadataMatchers(
144+
http_service.authorization_response().dynamic_metadata_from_headers(), context)),
145+
upstream_header_matchers_(toUpstreamMatchers(
146+
http_service.authorization_response().allowed_upstream_headers(), context)),
147+
upstream_header_to_append_matchers_(toUpstreamMatchers(
148+
http_service.authorization_response().allowed_upstream_headers_to_append(), context)),
149+
cluster_name_(http_service.server_uri().cluster()), timeout_(timeout),
150+
path_prefix_(
151+
THROW_OR_RETURN_VALUE(validatePathPrefix(http_service.path_prefix()), std::string)),
152+
tracing_name_(fmt::format("async {} egress", http_service.server_uri().cluster())),
153+
request_headers_parser_(THROW_OR_RETURN_VALUE(
154+
Router::HeaderParser::configure(
155+
http_service.authorization_request().headers_to_add(),
156+
envoy::config::core::v3::HeaderValueOption::OVERWRITE_IF_EXISTS_OR_ADD),
157+
Router::HeaderParserPtr)),
158+
encode_raw_headers_(encode_raw_headers) {}
159+
136160
MatcherSharedPtr
137161
ClientConfig::toClientMatchersOnSuccess(const envoy::type::matcher::v3::ListStringMatcher& list,
138162
Server::Configuration::CommonFactoryContext& context) {

source/extensions/filters/common/ext_authz/ext_authz_http_impl.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ class ClientConfig {
2828
uint32_t timeout, absl::string_view path_prefix,
2929
Server::Configuration::CommonFactoryContext& context);
3030

31+
// Build config directly from HttpService without constructing a temporary ExtAuthz.
32+
ClientConfig(const envoy::extensions::filters::http::ext_authz::v3::HttpService& http_service,
33+
bool encode_raw_headers, uint32_t timeout,
34+
Server::Configuration::CommonFactoryContext& context);
35+
3136
/**
3237
* Returns the name of the authorization cluster.
3338
*/

source/extensions/filters/http/ext_authz/config.cc

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ Http::FilterFactoryCb ExtAuthzFilterConfig::createFilterFactoryFromProtoWithServ
3939
&server_context](Http::FilterChainFactoryCallbacks& callbacks) {
4040
auto client = std::make_unique<Extensions::Filters::Common::ExtAuthz::RawHttpClientImpl>(
4141
server_context.clusterManager(), client_config);
42-
callbacks.addStreamFilter(std::make_shared<Filter>(filter_config, std::move(client)));
42+
callbacks.addStreamFilter(
43+
std::make_shared<Filter>(filter_config, std::move(client), server_context));
4344
};
4445
} else {
4546
// gRPC client.
@@ -57,7 +58,8 @@ Http::FilterFactoryCb ExtAuthzFilterConfig::createFilterFactoryFromProtoWithServ
5758
THROW_IF_NOT_OK_REF(client_or_error.status());
5859
auto client = std::make_unique<Filters::Common::ExtAuthz::GrpcClientImpl>(
5960
client_or_error.value(), std::chrono::milliseconds(timeout_ms));
60-
callbacks.addStreamFilter(std::make_shared<Filter>(filter_config, std::move(client)));
61+
callbacks.addStreamFilter(
62+
std::make_shared<Filter>(filter_config, std::move(client), server_context));
6163
};
6264
}
6365
return callback;

source/extensions/filters/http/ext_authz/ext_authz.cc

Lines changed: 129 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
#include "ext_authz.h"
21
#include "source/extensions/filters/http/ext_authz/ext_authz.h"
32

43
#include <chrono>
@@ -21,6 +20,9 @@ namespace ExtAuthz {
2120

2221
namespace {
2322

23+
// Default timeout for per-route gRPC client creation.
24+
constexpr uint32_t kDefaultPerRouteTimeoutMs = 200;
25+
2426
using MetadataProto = ::envoy::config::core::v3::Metadata;
2527
using Filters::Common::MutationRules::CheckOperation;
2628
using Filters::Common::MutationRules::CheckResult;
@@ -172,6 +174,86 @@ void FilterConfigPerRoute::merge(const FilterConfigPerRoute& other) {
172174
}
173175
}
174176

177+
// Constructor used for merging configurations from different levels (vhost, route, etc.)
178+
FilterConfigPerRoute::FilterConfigPerRoute(const FilterConfigPerRoute& less_specific,
179+
const FilterConfigPerRoute& more_specific)
180+
: context_extensions_(less_specific.context_extensions_),
181+
check_settings_(more_specific.check_settings_), disabled_(more_specific.disabled_),
182+
// Only use the most specific per-route override. Do not inherit overrides from less
183+
// specific configuration. If the more specific configuration has no override, leave both
184+
// unset so that the main filter configuration is used.
185+
grpc_service_(more_specific.grpc_service_.has_value() ? more_specific.grpc_service_
186+
: absl::nullopt),
187+
http_service_(more_specific.http_service_.has_value() ? more_specific.http_service_
188+
: absl::nullopt) {
189+
// Merge context extensions from more specific configuration, overriding less specific ones.
190+
for (const auto& extension : more_specific.context_extensions_) {
191+
context_extensions_[extension.first] = extension.second;
192+
}
193+
}
194+
195+
Filters::Common::ExtAuthz::ClientPtr
196+
Filter::createPerRouteGrpcClient(const envoy::config::core::v3::GrpcService& grpc_service) {
197+
if (server_context_ == nullptr) {
198+
ENVOY_STREAM_LOG(
199+
debug, "ext_authz filter: server context not available for per-route gRPC client creation.",
200+
*decoder_callbacks_);
201+
return nullptr;
202+
}
203+
204+
// Use the timeout from the gRPC service configuration, use default if not specified.
205+
const uint32_t timeout_ms =
206+
PROTOBUF_GET_MS_OR_DEFAULT(grpc_service, timeout, kDefaultPerRouteTimeoutMs);
207+
208+
// We can skip transport version check for per-route gRPC service here.
209+
// The transport version is already validated at the main configuration level.
210+
Envoy::Grpc::GrpcServiceConfigWithHashKey config_with_hash_key =
211+
Envoy::Grpc::GrpcServiceConfigWithHashKey(grpc_service);
212+
213+
auto client_or_error = server_context_->clusterManager()
214+
.grpcAsyncClientManager()
215+
.getOrCreateRawAsyncClientWithHashKey(config_with_hash_key,
216+
server_context_->scope(), true);
217+
if (!client_or_error.ok()) {
218+
ENVOY_STREAM_LOG(warn,
219+
"ext_authz filter: failed to create per-route gRPC client: {}. Falling back "
220+
"to default client.",
221+
*decoder_callbacks_, client_or_error.status().ToString());
222+
return nullptr;
223+
}
224+
225+
ENVOY_STREAM_LOG(debug, "ext_authz filter: created per-route gRPC client for cluster: {}.",
226+
*decoder_callbacks_,
227+
grpc_service.has_envoy_grpc() ? grpc_service.envoy_grpc().cluster_name()
228+
: "google_grpc");
229+
230+
return std::make_unique<Filters::Common::ExtAuthz::GrpcClientImpl>(
231+
client_or_error.value(), std::chrono::milliseconds(timeout_ms));
232+
}
233+
234+
Filters::Common::ExtAuthz::ClientPtr Filter::createPerRouteHttpClient(
235+
const envoy::extensions::filters::http::ext_authz::v3::HttpService& http_service) {
236+
if (server_context_ == nullptr) {
237+
ENVOY_STREAM_LOG(
238+
debug, "ext_authz filter: server context not available for per-route HTTP client creation.",
239+
*decoder_callbacks_);
240+
return nullptr;
241+
}
242+
243+
// Use the timeout from the HTTP service configuration, use default if not specified.
244+
const uint32_t timeout_ms =
245+
PROTOBUF_GET_MS_OR_DEFAULT(http_service.server_uri(), timeout, kDefaultPerRouteTimeoutMs);
246+
247+
ENVOY_STREAM_LOG(debug, "ext_authz filter: creating per-route HTTP client for URI: {}.",
248+
*decoder_callbacks_, http_service.server_uri().uri());
249+
250+
const auto client_config = std::make_shared<Extensions::Filters::Common::ExtAuthz::ClientConfig>(
251+
http_service, config_->headersAsBytes(), timeout_ms, *server_context_);
252+
253+
return std::make_unique<Extensions::Filters::Common::ExtAuthz::RawHttpClientImpl>(
254+
server_context_->clusterManager(), client_config);
255+
}
256+
175257
void Filter::initiateCall(const Http::RequestHeaderMap& headers) {
176258
if (filter_return_ == FilterReturn::StopDecoding) {
177259
return;
@@ -205,9 +287,10 @@ void Filter::initiateCall(const Http::RequestHeaderMap& headers) {
205287
for (const FilterConfigPerRoute& cfg :
206288
Http::Utility::getAllPerFilterConfig<FilterConfigPerRoute>(decoder_callbacks_)) {
207289
if (maybe_merged_per_route_config.has_value()) {
208-
maybe_merged_per_route_config.value().merge(cfg);
290+
FilterConfigPerRoute current_config = maybe_merged_per_route_config.value();
291+
maybe_merged_per_route_config.emplace(current_config, cfg);
209292
} else {
210-
maybe_merged_per_route_config = cfg;
293+
maybe_merged_per_route_config.emplace(cfg);
211294
}
212295
}
213296

@@ -216,6 +299,46 @@ void Filter::initiateCall(const Http::RequestHeaderMap& headers) {
216299
context_extensions = maybe_merged_per_route_config.value().takeContextExtensions();
217300
}
218301

302+
// Check if we need to use a per-route service override (gRPC or HTTP).
303+
Filters::Common::ExtAuthz::Client* client_to_use = client_.get();
304+
if (maybe_merged_per_route_config) {
305+
if (maybe_merged_per_route_config->grpcService().has_value()) {
306+
const auto& grpc_service = maybe_merged_per_route_config->grpcService().value();
307+
ENVOY_STREAM_LOG(debug, "ext_authz filter: using per-route gRPC service configuration.",
308+
*decoder_callbacks_);
309+
310+
// Create a new gRPC client for this route.
311+
per_route_client_ = createPerRouteGrpcClient(grpc_service);
312+
if (per_route_client_ != nullptr) {
313+
client_to_use = per_route_client_.get();
314+
ENVOY_STREAM_LOG(debug, "ext_authz filter: successfully created per-route gRPC client.",
315+
*decoder_callbacks_);
316+
} else {
317+
ENVOY_STREAM_LOG(
318+
warn,
319+
"ext_authz filter: failed to create per-route gRPC client, falling back to default.",
320+
*decoder_callbacks_);
321+
}
322+
} else if (maybe_merged_per_route_config->httpService().has_value()) {
323+
const auto& http_service = maybe_merged_per_route_config->httpService().value();
324+
ENVOY_STREAM_LOG(debug, "ext_authz filter: using per-route HTTP service configuration.",
325+
*decoder_callbacks_);
326+
327+
// Create a new HTTP client for this route.
328+
per_route_client_ = createPerRouteHttpClient(http_service);
329+
if (per_route_client_ != nullptr) {
330+
client_to_use = per_route_client_.get();
331+
ENVOY_STREAM_LOG(debug, "ext_authz filter: successfully created per-route HTTP client.",
332+
*decoder_callbacks_);
333+
} else {
334+
ENVOY_STREAM_LOG(
335+
warn,
336+
"ext_authz filter: failed to create per-route HTTP client, falling back to default.",
337+
*decoder_callbacks_);
338+
}
339+
}
340+
}
341+
219342
// If metadata_context_namespaces or typed_metadata_context_namespaces is specified,
220343
// pass matching filter metadata to the ext_authz service.
221344
// If metadata key is set in both the connection and request metadata,
@@ -241,7 +364,7 @@ void Filter::initiateCall(const Http::RequestHeaderMap& headers) {
241364
config_->destinationLabels(), config_->allowedHeadersMatcher(),
242365
config_->disallowedHeadersMatcher());
243366

244-
ENVOY_STREAM_LOG(trace, "ext_authz filter calling authorization server", *decoder_callbacks_);
367+
ENVOY_STREAM_LOG(trace, "ext_authz filter calling authorization server.", *decoder_callbacks_);
245368
// Store start time of ext_authz filter call
246369
start_time_ = decoder_callbacks_->dispatcher().timeSource().monotonicTime();
247370

@@ -250,8 +373,8 @@ void Filter::initiateCall(const Http::RequestHeaderMap& headers) {
250373
// going to invoke check call.
251374
cluster_ = decoder_callbacks_->clusterInfo();
252375
initiating_call_ = true;
253-
client_->check(*this, check_request_, decoder_callbacks_->activeSpan(),
254-
decoder_callbacks_->streamInfo());
376+
client_to_use->check(*this, check_request_, decoder_callbacks_->activeSpan(),
377+
decoder_callbacks_->streamInfo());
255378
initiating_call_ = false;
256379
}
257380

0 commit comments

Comments
 (0)