Skip to content

Commit a7d4e04

Browse files
authored
[ProtoApiScrubber] Fixing header and trailer propagation in field checker (#42293)
Signed-off-by: Sumit Kumar <sumitkmr@google.com> Signed-off-by: sumitkmr2 <sumitkmr@google.com>
1 parent 0fc325f commit a7d4e04

File tree

6 files changed

+361
-33
lines changed

6 files changed

+361
-33
lines changed

source/extensions/filters/http/proto_api_scrubber/filter.cc

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,9 @@ ProtoApiScrubberFilter::createRequestProtoScrubber() {
411411
RETURN_IF_NOT_OK(request_type_or_status.status());
412412

413413
request_match_tree_field_checker_ = std::make_unique<FieldChecker>(
414-
ScrubberContext::kRequestScrubbing, &decoder_callbacks_->streamInfo(), method_name_,
414+
ScrubberContext::kRequestScrubbing, &decoder_callbacks_->streamInfo(),
415+
decoder_callbacks_->requestHeaders(), OptRef<const Http::ResponseHeaderMap>{},
416+
decoder_callbacks_->requestTrailers(), OptRef<const Http::ResponseTrailerMap>{}, method_name_,
415417
&filter_config_);
416418

417419
return std::make_unique<ProtoScrubber>(
@@ -427,7 +429,9 @@ ProtoApiScrubberFilter::createResponseProtoScrubber() {
427429
RETURN_IF_NOT_OK(response_type_or_status.status());
428430

429431
response_match_tree_field_checker_ = std::make_unique<FieldChecker>(
430-
ScrubberContext::kResponseScrubbing, &encoder_callbacks_->streamInfo(), method_name_,
432+
ScrubberContext::kResponseScrubbing, &encoder_callbacks_->streamInfo(),
433+
decoder_callbacks_->requestHeaders(), encoder_callbacks_->responseHeaders(),
434+
decoder_callbacks_->requestTrailers(), encoder_callbacks_->responseTrailers(), method_name_,
431435
&filter_config_);
432436

433437
return std::make_unique<ProtoScrubber>(

source/extensions/filters/http/proto_api_scrubber/scrubbing_util_lib/field_checker.h

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,27 @@ using proto_processing_lib::proto_scrubber::ScrubberContext;
3232
class FieldChecker : public FieldCheckerInterface, public Logger::Loggable<Logger::Id::filter> {
3333
public:
3434
FieldChecker(const ScrubberContext scrubber_context,
35-
const Envoy::StreamInfo::StreamInfo* stream_info, const std::string& method_name,
36-
const ProtoApiScrubberFilterConfig* filter_config)
35+
const Envoy::StreamInfo::StreamInfo* stream_info,
36+
OptRef<const Http::RequestHeaderMap> request_headers,
37+
OptRef<const Http::ResponseHeaderMap> response_headers,
38+
OptRef<const Http::RequestTrailerMap> request_trailers,
39+
OptRef<const Http::ResponseTrailerMap> response_trailers,
40+
const std::string& method_name, const ProtoApiScrubberFilterConfig* filter_config)
3741
: scrubber_context_(scrubber_context), matching_data_(*stream_info),
38-
method_name_(method_name), filter_config_ptr_(filter_config) {}
42+
method_name_(method_name), filter_config_ptr_(filter_config) {
43+
if (request_headers.has_value()) {
44+
matching_data_.onRequestHeaders(request_headers.ref());
45+
}
46+
if (response_headers.has_value()) {
47+
matching_data_.onResponseHeaders(response_headers.ref());
48+
}
49+
if (request_trailers.has_value()) {
50+
matching_data_.onRequestTrailers(request_trailers.ref());
51+
}
52+
if (response_trailers.has_value()) {
53+
matching_data_.onResponseTrailers(response_trailers.ref());
54+
}
55+
}
3956

4057
// This type is neither copyable nor movable.
4158
FieldChecker(const FieldChecker&) = delete;

test/extensions/filters/http/proto_api_scrubber/BUILD

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,14 +77,20 @@ envoy_cc_test(
7777
rbe_pool = "6gig",
7878
deps = [
7979
"//source/common/grpc:common_lib",
80+
"//source/common/http/matching:inputs_lib",
81+
"//source/common/router:string_accessor_lib",
8082
"//source/extensions/filters/http/proto_api_scrubber:config",
8183
"//source/extensions/matching/http/cel_input:cel_input_lib",
8284
"//source/extensions/matching/input_matchers/cel_matcher:config",
85+
"//source/extensions/matching/network/common:inputs_lib",
8386
"//test/extensions/filters/http/grpc_field_extraction/message_converter:message_converter_test_lib",
8487
"//test/integration:http_protocol_integration_lib",
8588
"//test/proto:apikeys_proto_cc_proto",
89+
"//test/test_common:registry_lib",
8690
"@com_google_absl//absl/strings:str_format",
8791
"@com_google_cel_cpp//parser",
8892
"@envoy_api//envoy/extensions/filters/http/proto_api_scrubber/v3:pkg_cc_proto",
93+
"@envoy_api//envoy/extensions/matching/common_inputs/network/v3:pkg_cc_proto",
94+
"@envoy_api//envoy/type/matcher/v3:pkg_cc_proto",
8995
],
9096
)

test/extensions/filters/http/proto_api_scrubber/integration_test.cc

Lines changed: 225 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
#include "envoy/extensions/filters/http/proto_api_scrubber/v3/config.pb.h"
2+
#include "envoy/extensions/matching/common_inputs/network/v3/network_inputs.pb.h"
23
#include "envoy/grpc/status.h"
4+
#include "envoy/type/matcher/v3/http_inputs.pb.h"
35

6+
#include "source/common/router/string_accessor_impl.h"
47
#include "source/extensions/filters/http/proto_api_scrubber/scrubbing_util_lib/field_checker.h"
58

69
#include "test/extensions/filters/http/grpc_field_extraction/message_converter/message_converter_test_lib.h"
710
#include "test/integration/http_protocol_integration.h"
811
#include "test/proto/apikeys.pb.h"
12+
#include "test/test_common/registry.h"
913

1014
#include "cel/expr/syntax.pb.h"
1115
#include "fmt/format.h"
@@ -29,17 +33,63 @@ std::string apikeysDescriptorPath() {
2933
}
3034

3135
const std::string kCreateApiKeyMethod = "/apikeys.ApiKeys/CreateApiKey";
36+
const std::string kFilterStateLabelKey = "filter_state_label_key";
37+
const std::string kFilterStateLabelValue = "LABEL1,LABEL2,LABEL3";
38+
39+
// This filter injects data into filter_state.
40+
class MetadataInjectorFilter : public ::Envoy::Http::PassThroughDecoderFilter {
41+
public:
42+
::Envoy::Http::FilterHeadersStatus decodeHeaders(::Envoy::Http::RequestHeaderMap&,
43+
bool) override {
44+
const std::string key = kFilterStateLabelKey;
45+
const std::string value = kFilterStateLabelValue;
46+
decoder_callbacks_->streamInfo().filterState()->setData(
47+
key, std::make_shared<::Envoy::Router::StringAccessorImpl>(value),
48+
::Envoy::StreamInfo::FilterState::StateType::ReadOnly);
49+
return ::Envoy::Http::FilterHeadersStatus::Continue;
50+
}
51+
};
52+
53+
class MetadataInjectorConfigFactory
54+
: public ::Envoy::Server::Configuration::NamedHttpFilterConfigFactory {
55+
public:
56+
absl::StatusOr<::Envoy::Http::FilterFactoryCb>
57+
createFilterFactoryFromProto(const ::Envoy::Protobuf::Message&, const std::string&,
58+
::Envoy::Server::Configuration::FactoryContext&) override {
59+
return [](::Envoy::Http::FilterChainFactoryCallbacks& callbacks) {
60+
callbacks.addStreamDecoderFilter(std::make_shared<MetadataInjectorFilter>());
61+
};
62+
}
63+
64+
::Envoy::ProtobufTypes::MessagePtr createEmptyConfigProto() override {
65+
return std::make_unique<Protobuf::Struct>();
66+
}
67+
68+
std::string name() const override { return "test_injector"; }
69+
};
70+
71+
// RegisterFactory handles the singleton lifetime automatically and avoids the destruction crash.
72+
static ::Envoy::Registry::RegisterFactory<
73+
MetadataInjectorConfigFactory, ::Envoy::Server::Configuration::NamedHttpFilterConfigFactory>
74+
register_test_injector;
3275

3376
class ProtoApiScrubberIntegrationTest : public HttpProtocolIntegrationTest {
3477
public:
3578
void SetUp() override { HttpProtocolIntegrationTest::SetUp(); }
3679

3780
void TearDown() override {
81+
if (codec_client_) {
82+
// Close the client FIRST to prevent any "connection reset" callbacks.
83+
codec_client_->close();
84+
codec_client_.reset();
85+
}
86+
3887
cleanupUpstreamAndDownstream();
3988
HttpProtocolIntegrationTest::TearDown();
4089
}
4190

4291
enum class RestrictionType { Request, Response };
92+
enum class StringMatchType { Exact, Regex };
4393

4494
static xds::type::matcher::v3::Matcher::MatcherList::Predicate
4595
buildCelPredicate(absl::string_view cel_expression) {
@@ -64,6 +114,30 @@ class ProtoApiScrubberIntegrationTest : public HttpProtocolIntegrationTest {
64114
return predicate;
65115
}
66116

117+
static xds::type::matcher::v3::Matcher::MatcherList::Predicate
118+
buildStringMatcherPredicate(const std::string& input_extension_name,
119+
const Protobuf::Message& input_config,
120+
const std::string& match_pattern, StringMatchType match_type) {
121+
xds::type::matcher::v3::Matcher::MatcherList::Predicate predicate;
122+
auto* single = predicate.mutable_single_predicate();
123+
124+
// Configure the Data Input (The source of the string to be matched)
125+
single->mutable_input()->set_name(input_extension_name);
126+
single->mutable_input()->mutable_typed_config()->PackFrom(input_config);
127+
128+
// Configure the String Matcher (The logic to apply)
129+
auto* string_matcher = single->mutable_value_match();
130+
if (match_type == StringMatchType::Regex) {
131+
auto* regex = string_matcher->mutable_safe_regex();
132+
regex->mutable_google_re2();
133+
regex->set_regex(match_pattern);
134+
} else {
135+
string_matcher->set_exact(match_pattern);
136+
}
137+
138+
return predicate;
139+
}
140+
67141
// Helper to build the configuration with a generic predicate.
68142
std::string
69143
getFilterConfig(const std::string& descriptor_path, const std::string& method_name = "",
@@ -133,15 +207,33 @@ class ProtoApiScrubberIntegrationTest : public HttpProtocolIntegrationTest {
133207
}
134208

135209
template <typename T>
136-
IntegrationStreamDecoderPtr sendGrpcRequest(const T& request_msg,
137-
const std::string& method_path) {
210+
IntegrationStreamDecoderPtr
211+
sendGrpcRequest(const T& request_msg, const std::string& method_path,
212+
const Http::TestRequestHeaderMapImpl& custom_headers = {}) {
213+
// Close the existing connection in case it exists.
214+
// This can happen if this method is called more than once from a single test.
215+
if (codec_client_ != nullptr) {
216+
codec_client_->close();
217+
}
218+
138219
codec_client_ = makeHttpConnection(lookupPort("http"));
139220
auto request_buf = Grpc::Common::serializeToGrpcFrame(request_msg);
221+
222+
// Default headers
140223
auto request_headers = Http::TestRequestHeaderMapImpl{{":method", "POST"},
141224
{":path", method_path},
142225
{"content-type", "application/grpc"},
143226
{":authority", "host"},
144227
{":scheme", "http"}};
228+
229+
// Merge custom headers (overwriting defaults if keys match)
230+
custom_headers.iterate(
231+
[&request_headers](const Http::HeaderEntry& header) -> Http::HeaderMap::Iterate {
232+
request_headers.setCopy(Http::LowerCaseString(header.key().getStringView()),
233+
header.value().getStringView());
234+
return Http::HeaderMap::Iterate::Continue;
235+
});
236+
145237
return codec_client_->makeRequestWithBody(request_headers, request_buf->toString());
146238
}
147239
};
@@ -304,6 +396,137 @@ TEST_P(ProtoApiScrubberIntegrationTest, ScrubNestedField_MatcherFalse) {
304396
ASSERT_TRUE(response->waitForEndStream());
305397
}
306398

399+
// Tests scrubbing of nested fields in the request when a CEL matcher is configured to use request
400+
// headers.
401+
TEST_P(ProtoApiScrubberIntegrationTest, ScrubNestedField_CustomCelMatcher_RequestHeader) {
402+
config_helper_.prependFilter(getFilterConfig(
403+
apikeysDescriptorPath(), kCreateApiKeyMethod, "key.display_name", RestrictionType::Request,
404+
buildCelPredicate("request.headers['api-version'] == '2025-v1'")));
405+
initialize();
406+
407+
{
408+
// Tests that the field `key.display_name` is removed from the request as the CEL expression
409+
// ("request.headers['api-version'] == '2025_v1'") evaluates to true.
410+
auto original_proto = makeCreateApiKeyRequest(R"pb(
411+
parent: "public"
412+
key { display_name: "should-be-removed" }
413+
)pb");
414+
auto custom_headers = Http::TestRequestHeaderMapImpl{{"api-version", "2025-v1"}};
415+
416+
auto response = sendGrpcRequest(original_proto, kCreateApiKeyMethod, custom_headers);
417+
waitForNextUpstreamRequest();
418+
419+
apikeys::CreateApiKeyRequest expected = original_proto;
420+
expected.mutable_key()->clear_display_name();
421+
422+
Buffer::OwnedImpl data;
423+
data.add(upstream_request_->body());
424+
checkSerializedData<apikeys::CreateApiKeyRequest>(data, {expected});
425+
426+
upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, true);
427+
ASSERT_TRUE(response->waitForEndStream());
428+
}
429+
430+
{
431+
// Tests that the field `key.display_name` is preserved in the request as the CEL expression
432+
// ("request.headers['api-version'] == '2025_v1'") evaluates to false.
433+
auto original_proto = makeCreateApiKeyRequest(R"pb(
434+
parent: "public"
435+
key { display_name: "should-stay" }
436+
)pb");
437+
auto custom_headers = Http::TestRequestHeaderMapImpl{{"api-version", "2025-v2"}};
438+
439+
auto response = sendGrpcRequest(original_proto, kCreateApiKeyMethod, custom_headers);
440+
waitForNextUpstreamRequest();
441+
442+
Buffer::OwnedImpl data;
443+
data.add(upstream_request_->body());
444+
checkSerializedData<apikeys::CreateApiKeyRequest>(data, {original_proto});
445+
446+
upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, true);
447+
ASSERT_TRUE(response->waitForEndStream());
448+
}
449+
}
450+
451+
// Tests scrubbing of nested fields in the request when a String matcher is configured to use
452+
// request headers via exact match.
453+
TEST_P(ProtoApiScrubberIntegrationTest, ScrubNestedField_StringMatcher_RequestHeader_ExactMatch) {
454+
envoy::type::matcher::v3::HttpRequestHeaderMatchInput header_input;
455+
header_input.set_header_name("api-version");
456+
457+
auto predicate = buildStringMatcherPredicate("envoy.matching.inputs.request_headers",
458+
header_input, "2025-v1", StringMatchType::Exact);
459+
460+
config_helper_.prependFilter(getFilterConfig(apikeysDescriptorPath(), kCreateApiKeyMethod,
461+
"key.display_name", RestrictionType::Request,
462+
predicate));
463+
464+
initialize();
465+
466+
auto original_proto = makeCreateApiKeyRequest(R"pb(
467+
parent: "public"
468+
key { display_name: "should-be-removed" }
469+
)pb");
470+
auto custom_headers = Http::TestRequestHeaderMapImpl{{"api-version", "2025-v1"}};
471+
472+
auto response = sendGrpcRequest(original_proto, kCreateApiKeyMethod, custom_headers);
473+
waitForNextUpstreamRequest();
474+
475+
apikeys::CreateApiKeyRequest expected = original_proto;
476+
expected.mutable_key()->clear_display_name();
477+
478+
Buffer::OwnedImpl data;
479+
data.add(upstream_request_->body());
480+
checkSerializedData<apikeys::CreateApiKeyRequest>(data, {expected});
481+
482+
upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, true);
483+
ASSERT_TRUE(response->waitForEndStream());
484+
}
485+
486+
// Tests scrubbing of nested fields in the request when a String matcher is configured to use filter
487+
// state via regex match.
488+
TEST_P(ProtoApiScrubberIntegrationTest, ScrubNestedField_StringMatcher_FilterState_RegexMatch) {
489+
envoy::extensions::matching::common_inputs::network::v3::FilterStateInput filter_state_input;
490+
filter_state_input.set_key(kFilterStateLabelKey);
491+
492+
// The metadata injector filter (test_injector) sets the value to: "LABEL1,LABEL2,LABEL3"
493+
// We use a regex to verify that "LABEL2" is present in the list.
494+
auto predicate =
495+
buildStringMatcherPredicate("envoy.matching.inputs.filter_state", filter_state_input,
496+
".*LABEL2.*", StringMatchType::Regex);
497+
498+
config_helper_.prependFilter(getFilterConfig(apikeysDescriptorPath(), kCreateApiKeyMethod,
499+
"key.display_name", RestrictionType::Request,
500+
predicate));
501+
502+
config_helper_.prependFilter(R"yaml(
503+
name: test_injector
504+
typed_config:
505+
"@type": type.googleapis.com/google.protobuf.Struct
506+
)yaml");
507+
508+
initialize();
509+
510+
auto original_proto = makeCreateApiKeyRequest(R"pb(
511+
parent: "public"
512+
key { display_name: "should-be-removed" }
513+
)pb");
514+
515+
auto response = sendGrpcRequest(original_proto, kCreateApiKeyMethod);
516+
waitForNextUpstreamRequest();
517+
518+
// Since "LABEL1,LABEL2,LABEL3" matches ".*LABEL2.*", the field should be removed.
519+
apikeys::CreateApiKeyRequest expected = original_proto;
520+
expected.mutable_key()->clear_display_name();
521+
522+
Buffer::OwnedImpl data;
523+
data.add(upstream_request_->body());
524+
checkSerializedData<apikeys::CreateApiKeyRequest>(data, {expected});
525+
526+
upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, true);
527+
ASSERT_TRUE(response->waitForEndStream());
528+
}
529+
307530
// ============================================================================
308531
// TEST GROUP 3: VALIDATION & REJECTION
309532
// ============================================================================

test/extensions/filters/http/proto_api_scrubber/scrubbing_util_lib/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ envoy_cc_test(
1313
srcs = ["field_checker_test.cc"],
1414
data = ["//test/proto:apikeys_proto_descriptor"],
1515
deps = [
16+
"//envoy/common:optref_lib",
1617
"//source/common/protobuf",
1718
"//source/extensions/filters/http/proto_api_scrubber/scrubbing_util_lib",
1819
"//source/extensions/matching/http/cel_input:cel_input_lib",

0 commit comments

Comments
 (0)