Skip to content

Commit 0c00c89

Browse files
authored
ProtoApiScrubber: Add filter config parsing and storing logic (#39628)
Signed-off-by: Sumit Kumar <[email protected]>
1 parent d5a9abe commit 0c00c89

File tree

5 files changed

+881
-11
lines changed

5 files changed

+881
-11
lines changed

source/extensions/filters/http/proto_api_scrubber/BUILD

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ envoy_cc_extension(
1515
hdrs = ["filter_config.h"],
1616
deps = [
1717
"//source/common/common:minimal_logger_lib",
18-
"//source/extensions/filters/http/common:factory_base_lib",
18+
"//source/common/http:utility_lib",
19+
"//source/common/http/matching:data_impl_lib",
20+
"//source/common/matcher:matcher_lib",
21+
"@com_github_cncf_xds//xds/type/matcher/v3:pkg_cc_proto",
1922
"@envoy_api//envoy/extensions/filters/http/proto_api_scrubber/v3:pkg_cc_proto",
2023
],
2124
)

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

Lines changed: 163 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,176 @@
11
#include "source/extensions/filters/http/proto_api_scrubber/filter_config.h"
22

3+
#include <algorithm>
4+
5+
#include "envoy/extensions/filters/http/proto_api_scrubber/v3/matcher_actions.pb.h"
6+
#include "envoy/matcher/matcher.h"
7+
8+
#include "source/common/matcher/matcher.h"
9+
310
namespace Envoy {
411
namespace Extensions {
512
namespace HttpFilters {
613
namespace ProtoApiScrubber {
714
namespace {
815

9-
using ::envoy::extensions::filters::http::proto_api_scrubber::v3::ProtoApiScrubberConfig;
16+
static constexpr absl::string_view kConfigInitializationError =
17+
"Error encountered during config initialization.";
18+
19+
// Returns whether the fully qualified `api_name` is valid or not.
20+
// Checks for separator '.' in the name and verifies each substring between these separators are
21+
// non-empty. Note that it does not verify whether the API actually exists or not.
22+
bool isApiNameValid(absl::string_view api_name) {
23+
const std::vector<absl::string_view> api_name_parts = absl::StrSplit(api_name, '.');
24+
if (api_name_parts.size() <= 1) {
25+
return false;
26+
}
27+
28+
// Returns true if all of the api_name_parts are non-empty, otherwise returns false.
29+
return !std::any_of(api_name_parts.cbegin(), api_name_parts.cend(),
30+
[](const absl::string_view s) { return s.empty(); });
31+
}
32+
1033
} // namespace
1134

12-
FilterConfig::FilterConfig(const ProtoApiScrubberConfig&) {}
35+
absl::StatusOr<std::shared_ptr<const ProtoApiScrubberFilterConfig>>
36+
ProtoApiScrubberFilterConfig::create(const ProtoApiScrubberConfig& proto_config,
37+
Server::Configuration::FactoryContext& context) {
38+
std::shared_ptr<ProtoApiScrubberFilterConfig> filter_config_ptr =
39+
std::shared_ptr<ProtoApiScrubberFilterConfig>(new ProtoApiScrubberFilterConfig());
40+
RETURN_IF_ERROR(filter_config_ptr->initialize(proto_config, context));
41+
return filter_config_ptr;
42+
}
43+
44+
absl::Status
45+
ProtoApiScrubberFilterConfig::initialize(const ProtoApiScrubberConfig& proto_config,
46+
Server::Configuration::FactoryContext& context) {
47+
ENVOY_LOG(trace, "Initializing filter config from the proto config: {}",
48+
proto_config.DebugString());
49+
50+
FilteringMode filtering_mode = proto_config.filtering_mode();
51+
RETURN_IF_ERROR(validateFilteringMode(filtering_mode));
52+
filtering_mode_ = filtering_mode;
53+
54+
for (const auto& method_restriction : proto_config.restrictions().method_restrictions()) {
55+
std::string method_name = method_restriction.first;
56+
RETURN_IF_ERROR(validateMethodName(method_name));
57+
RETURN_IF_ERROR(initializeMethodRestrictions(
58+
method_name, request_field_restrictions_,
59+
method_restriction.second.request_field_restrictions(), context));
60+
RETURN_IF_ERROR(initializeMethodRestrictions(
61+
method_name, response_field_restrictions_,
62+
method_restriction.second.response_field_restrictions(), context));
63+
}
64+
65+
ENVOY_LOG(trace, "Filter config initialized successfully.");
66+
return absl::OkStatus();
67+
}
68+
69+
absl::Status ProtoApiScrubberFilterConfig::validateFilteringMode(FilteringMode filtering_mode) {
70+
switch (filtering_mode) {
71+
case FilteringMode::ProtoApiScrubberConfig_FilteringMode_OVERRIDE:
72+
return absl::OkStatus();
73+
default:
74+
return absl::InvalidArgumentError(
75+
fmt::format("{} Unsupported 'filtering_mode': {}.", kConfigInitializationError,
76+
envoy::extensions::filters::http::proto_api_scrubber::v3::
77+
ProtoApiScrubberConfig_FilteringMode_Name(filtering_mode)));
78+
}
79+
}
80+
81+
absl::Status ProtoApiScrubberFilterConfig::validateMethodName(absl::string_view method_name) {
82+
if (method_name.empty()) {
83+
return absl::InvalidArgumentError(
84+
fmt::format("{} Invalid method name: '{}'. Method name is empty.",
85+
kConfigInitializationError, method_name));
86+
}
87+
88+
if (absl::StrContains(method_name, '*')) {
89+
return absl::InvalidArgumentError(fmt::format(
90+
"{} Invalid method name: '{}'. Method name contains '*' which is not supported.",
91+
kConfigInitializationError, method_name));
92+
}
93+
94+
const std::vector<absl::string_view> method_name_parts = absl::StrSplit(method_name, '/');
95+
if (method_name_parts.size() != 3 || !method_name_parts[0].empty() ||
96+
method_name_parts[1].empty() || !isApiNameValid(method_name_parts[1]) ||
97+
method_name_parts[2].empty()) {
98+
return absl::InvalidArgumentError(
99+
fmt::format("{} Invalid method name: '{}'. Method name should follow the gRPC format "
100+
"('/package.ServiceName/MethodName').",
101+
kConfigInitializationError, method_name));
102+
}
103+
104+
return absl::OkStatus();
105+
}
106+
107+
absl::Status ProtoApiScrubberFilterConfig::validateFieldMask(absl::string_view field_mask) {
108+
if (field_mask.empty()) {
109+
return absl::InvalidArgumentError(
110+
fmt::format("{} Invalid field mask: '{}'. Field mask is empty.", kConfigInitializationError,
111+
field_mask));
112+
}
113+
114+
if (absl::StrContains(field_mask, '*')) {
115+
return absl::InvalidArgumentError(
116+
fmt::format("{} Invalid field mask: '{}'. Field mask contains '*' which is not supported.",
117+
kConfigInitializationError, field_mask));
118+
}
119+
120+
return absl::OkStatus();
121+
}
122+
123+
absl::Status ProtoApiScrubberFilterConfig::initializeMethodRestrictions(
124+
absl::string_view method_name, StringPairToMatchTreeMap& field_restrictions,
125+
const Map<std::string, RestrictionConfig>& restrictions,
126+
Envoy::Server::Configuration::FactoryContext& context) {
127+
for (const auto& restriction : restrictions) {
128+
absl::string_view field_mask = restriction.first;
129+
RETURN_IF_ERROR(validateFieldMask(field_mask));
130+
ProtoApiScrubberRemoveFieldAction remove_field_action;
131+
MatcherInputValidatorVisitor validation_visitor;
132+
Matcher::MatchTreeFactory<HttpMatchingData, ProtoApiScrubberRemoveFieldAction> matcher_factory(
133+
remove_field_action, context.serverFactoryContext(), validation_visitor);
134+
135+
absl::optional<Matcher::MatchTreeFactoryCb<HttpMatchingData>> factory_cb =
136+
matcher_factory.create(restriction.second.matcher());
137+
if (factory_cb.has_value()) {
138+
field_restrictions[std::make_pair(std::string(method_name), std::string(field_mask))] =
139+
factory_cb.value()();
140+
} else {
141+
return absl::InvalidArgumentError(fmt::format(
142+
"{} Failed to initialize matcher factory callback for method {} and field mask {}.",
143+
kConfigInitializationError, method_name, field_mask));
144+
}
145+
}
146+
147+
return absl::OkStatus();
148+
}
149+
150+
MatchTreeHttpMatchingDataSharedPtr
151+
ProtoApiScrubberFilterConfig::getRequestFieldMatcher(const std::string& method_name,
152+
const std::string& field_mask) const {
153+
if (auto it = request_field_restrictions_.find(std::make_pair(method_name, field_mask));
154+
it != request_field_restrictions_.end()) {
155+
return it->second;
156+
}
157+
158+
return nullptr;
159+
}
160+
161+
MatchTreeHttpMatchingDataSharedPtr
162+
ProtoApiScrubberFilterConfig::getResponseFieldMatcher(const std::string& method_name,
163+
const std::string& field_mask) const {
164+
if (auto it = response_field_restrictions_.find(std::make_pair(method_name, field_mask));
165+
it != response_field_restrictions_.end()) {
166+
return it->second;
167+
}
168+
169+
return nullptr;
170+
}
171+
172+
REGISTER_FACTORY(RemoveFilterActionFactory,
173+
Matcher::ActionFactory<ProtoApiScrubberRemoveFieldAction>);
13174

14175
} // namespace ProtoApiScrubber
15176
} // namespace HttpFilters

source/extensions/filters/http/proto_api_scrubber/filter_config.h

Lines changed: 112 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,129 @@
11
#pragma once
22

33
#include "envoy/extensions/filters/http/proto_api_scrubber/v3/config.pb.h"
4-
#include "envoy/extensions/filters/http/proto_api_scrubber/v3/config.pb.validate.h"
4+
#include "envoy/extensions/filters/http/proto_api_scrubber/v3/matcher_actions.pb.h"
55

66
#include "source/common/common/logger.h"
7+
#include "source/common/http/utility.h"
8+
#include "source/common/matcher/matcher.h"
9+
10+
#include "xds/type/matcher/v3/http_inputs.pb.h"
711

812
namespace Envoy {
913
namespace Extensions {
1014
namespace HttpFilters {
1115
namespace ProtoApiScrubber {
16+
namespace {
17+
using envoy::extensions::filters::http::proto_api_scrubber::v3::ProtoApiScrubberConfig;
18+
using envoy::extensions::filters::http::proto_api_scrubber::v3::RestrictionConfig;
19+
using Http::HttpMatchingData;
20+
using Protobuf::Map;
21+
using xds::type::matcher::v3::HttpAttributesCelMatchInput;
22+
using ProtoApiScrubberRemoveFieldAction =
23+
envoy::extensions::filters::http::proto_api_scrubber::v3::RemoveFieldAction;
24+
using FilteringMode = ProtoApiScrubberConfig::FilteringMode;
25+
using MatchTreeHttpMatchingDataSharedPtr = Matcher::MatchTreeSharedPtr<HttpMatchingData>;
26+
using StringPairToMatchTreeMap =
27+
absl::flat_hash_map<std::pair<std::string, std::string>, MatchTreeHttpMatchingDataSharedPtr>;
28+
} // namespace
29+
30+
// The config for Proto API Scrubber filter. As a thread-safe class, it should be constructed only
31+
// once and shared among filters for better performance.
32+
class ProtoApiScrubberFilterConfig : public Logger::Loggable<Logger::Id::filter> {
33+
public:
34+
// Creates and returns an instance of ProtoApiScrubberConfig.
35+
static absl::StatusOr<std::shared_ptr<const ProtoApiScrubberFilterConfig>>
36+
create(const ProtoApiScrubberConfig& proto_config,
37+
Server::Configuration::FactoryContext& context);
38+
39+
// Returns the match tree for a request payload field mask.
40+
MatchTreeHttpMatchingDataSharedPtr getRequestFieldMatcher(const std::string& method_name,
41+
const std::string& field_mask) const;
42+
43+
// Returns the match tree for a response payload field mask.
44+
MatchTreeHttpMatchingDataSharedPtr getResponseFieldMatcher(const std::string& method_name,
45+
const std::string& field_mask) const;
46+
47+
FilteringMode filteringMode() const { return filtering_mode_; }
48+
49+
private:
50+
// Private constructor to make sure that this class is used in a factory fashion using the
51+
// public `create` method.
52+
ProtoApiScrubberFilterConfig() = default;
53+
54+
// Validates the filtering mode. Currently, only FilteringMode::OVERRIDE is supported.
55+
// For any unsupported FilteringMode, it returns absl::InvalidArgument.
56+
absl::Status validateFilteringMode(FilteringMode);
57+
58+
// Validates the method name in the filter config.
59+
// The method should name should be of gRPC method name format i.e., '/package.service/method'
60+
// For any invalid method name, it returns absl::InvalidArgument with an appropriate error
61+
// message.
62+
absl::Status validateMethodName(absl::string_view);
63+
64+
// Validates the field mask in the filter config.
65+
// The currently supported field mask is of format 'a.b.c'
66+
// Wildcards (e.g., '*') are not supported.
67+
// For any invalid field mask, it returns absl::InvalidArgument with an appropriate error message.
68+
absl::Status validateFieldMask(absl::string_view);
69+
70+
// Initializes the request and response field mask maps using the proto_config.
71+
absl::Status initialize(const ProtoApiScrubberConfig& proto_config,
72+
Envoy::Server::Configuration::FactoryContext& context);
1273

13-
// The config for Proto API Scrubber filter. As a thread-safe class, it
14-
// should be constructed only once and shared among filters for better
15-
// performance.
16-
class FilterConfig : public Envoy::Logger::Loggable<Envoy::Logger::Id::filter> {
74+
// Initializes the method's request and response restrictions using the restrictions configured
75+
// in the proto config.
76+
absl::Status initializeMethodRestrictions(absl::string_view method_name,
77+
StringPairToMatchTreeMap& field_restrictions,
78+
const Map<std::string, RestrictionConfig>& restrictions,
79+
Server::Configuration::FactoryContext& context);
80+
81+
FilteringMode filtering_mode_;
82+
83+
// A map from {method_name, field_mask} to the respective match tree for request fields.
84+
StringPairToMatchTreeMap request_field_restrictions_;
85+
86+
// A map from {method_name, field_mask} to the respective match tree for response fields.
87+
StringPairToMatchTreeMap response_field_restrictions_;
88+
};
89+
90+
// A class to validate the input type specified for the unified matcher in the config.
91+
class MatcherInputValidatorVisitor : public Matcher::MatchTreeValidationVisitor<HttpMatchingData> {
1792
public:
18-
explicit FilterConfig(
19-
const envoy::extensions::filters::http::proto_api_scrubber::v3::ProtoApiScrubberConfig&);
93+
// Validates whether the input type for the matcher is in the list of supported input types.
94+
// Currently, only CEL input type (i.e., HttpAttributesCelMatchInput) is supported.
95+
absl::Status performDataInputValidation(const Matcher::DataInputFactory<HttpMatchingData>&,
96+
absl::string_view type_url) override {
97+
if (type_url == TypeUtil::descriptorFullNameToTypeUrl(
98+
HttpAttributesCelMatchInput::descriptor()->full_name())) {
99+
return absl::OkStatus();
100+
}
101+
102+
return absl::InvalidArgumentError(
103+
fmt::format("ProtoApiScrubber filter does not support matching on '{}'", type_url));
104+
}
20105
};
21106

22-
using FilterConfigSharedPtr = std::shared_ptr<const FilterConfig>;
107+
// Action class for the RemoveFieldAction.
108+
// RemoveFieldAction semantically denotes removing a field from request or response protobuf payload
109+
// based on the field_mask and corresponding the restriction config provided.
110+
class RemoveFieldAction : public Matcher::ActionBase<ProtoApiScrubberRemoveFieldAction> {};
111+
112+
// ActionFactory for the RemoveFieldAction.
113+
class RemoveFilterActionFactory : public Matcher::ActionFactory<ProtoApiScrubberRemoveFieldAction> {
114+
public:
115+
Matcher::ActionFactoryCb createActionFactoryCb(const Protobuf::Message&,
116+
ProtoApiScrubberRemoveFieldAction&,
117+
ProtobufMessage::ValidationVisitor&) override {
118+
return []() { return std::make_unique<RemoveFieldAction>(); };
119+
}
120+
121+
ProtobufTypes::MessagePtr createEmptyConfigProto() override {
122+
return std::make_unique<ProtoApiScrubberRemoveFieldAction>();
123+
}
124+
125+
std::string name() const override { return "removeFieldAction"; }
126+
};
23127

24128
} // namespace ProtoApiScrubber
25129
} // namespace HttpFilters
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
load(
2+
"//bazel:envoy_build_system.bzl",
3+
"envoy_cc_test",
4+
"envoy_package",
5+
)
6+
7+
licenses(["notice"]) # Apache 2
8+
9+
envoy_package()
10+
11+
envoy_cc_test(
12+
name = "filter_config_test",
13+
srcs = ["filter_config_test.cc"],
14+
data = [
15+
"//test/config/integration/certs",
16+
"//test/proto:apikeys_proto_descriptor",
17+
],
18+
rbe_pool = "6gig",
19+
deps = [
20+
"//source/common/matcher:matcher_lib",
21+
"//source/extensions/common/matcher:matcher_lib",
22+
"//source/extensions/filters/http/proto_api_scrubber:filter_config",
23+
"//source/extensions/matching/http/cel_input:cel_input_lib",
24+
"//source/extensions/matching/input_matchers/cel_matcher:config",
25+
"//test/mocks/http:http_mocks",
26+
"//test/mocks/runtime:runtime_mocks",
27+
"//test/mocks/server:factory_context_mocks",
28+
"//test/proto:apikeys_proto_cc_proto",
29+
"//test/test_common:environment_lib",
30+
"//test/test_common:utility_lib",
31+
"@com_github_cncf_xds//xds/type/matcher/v3:pkg_cc_proto",
32+
],
33+
)

0 commit comments

Comments
 (0)