diff --git a/source/extensions/filters/http/ext_proc/BUILD b/source/extensions/filters/http/ext_proc/BUILD index ee622f8cf3bc4..61ce28923d6f3 100644 --- a/source/extensions/filters/http/ext_proc/BUILD +++ b/source/extensions/filters/http/ext_proc/BUILD @@ -21,6 +21,7 @@ envoy_cc_library( ], tags = ["skip_on_windows"], deps = [ + ":allowed_override_modes_set_lib", ":client_lib", ":matching_utils_lib", ":mutation_utils_lib", @@ -47,6 +48,16 @@ envoy_cc_library( ], ) +envoy_cc_library( + name = "allowed_override_modes_set_lib", + hdrs = ["allowed_override_modes_set.h"], + tags = ["skip_on_windows"], + deps = [ + "@com_google_absl//absl/container:flat_hash_set", + "@envoy_api//envoy/extensions/filters/http/ext_proc/v3:pkg_cc_proto", + ], +) + envoy_cc_library( name = "client_lib", srcs = ["client_impl.cc"], @@ -66,6 +77,7 @@ envoy_cc_extension( hdrs = ["config.h"], tags = ["skip_on_windows"], deps = [ + ":allowed_override_modes_set_lib", ":client_lib", ":ext_proc", "//source/extensions/filters/http/common:factory_base_lib", diff --git a/source/extensions/filters/http/ext_proc/allowed_override_modes_set.h b/source/extensions/filters/http/ext_proc/allowed_override_modes_set.h new file mode 100644 index 0000000000000..d58d423df0844 --- /dev/null +++ b/source/extensions/filters/http/ext_proc/allowed_override_modes_set.h @@ -0,0 +1,100 @@ +#pragma once + +#include "envoy/extensions/filters/http/ext_proc/v3/processing_mode.pb.h" + +#include "absl/container/flat_hash_set.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace ExternalProcessing { + +/** + * A data-structure that holds the Allowed Override Mode set. + * Internally it converts each allowed-override-mode enums values to a single + * list. This saves some memory, and allows an O(1) lookup. + */ +class AllowedOverrideModesSet { +public: + /** + * Constructs an AllowedOverrideModesSet from a container of ProcessingMode protos. + * + * Each ProcessingMode in the input container is converted to an integer key + * and stored in an internal hash set for efficient lookup. + * The use of a template is to simplify passing any container that has + * envoy::extensions::filters::http::ext_proc::v3::ProcessingMode elements. + * + * @param modes The collection of ProcessingMode protos to initialize the set with. + */ + template explicit AllowedOverrideModesSet(const Container& modes) { + for (const auto& mode : modes) { + allowed_modes_.insert(processingModeToInt(mode)); + } + } + + /** + * Checks if a specific ProcessingMode is supported (i.e., present in the set). + * Note that the ``request_header_mode`` in the given mode will be ignored. + * + * @param mode The ProcessingMode object to check for support. + * @return True if the mode is supported, false otherwise. + */ + bool isModeSupported( + const envoy::extensions::filters::http::ext_proc::v3::ProcessingMode& mode) const { + // Convert the input ProcessingMode to its integer key representation and + // check if this key exists in the internal hash set. + return allowed_modes_.contains(processingModeToInt(mode)); + } + + /** + * Checks if the set of allowed override modes is empty. + * @return True if the set is empty, false otherwise. + */ + bool empty() const { return allowed_modes_.empty(); } + +private: + /** + * Converts a ProcessingMode proto to a unique integer key. + * + * This method maps each relevant field of the ProcessingMode (response_header_mode, + * request_body_mode, response_body_mode, request_trailer_mode, response_trailer_mode) + * to a single digit in a base-10 number, where each digit's value is the integral + * value of the corresponding enum. + * + * The `request_header_mode` field is explicitly ignored in this conversion + * (mapped to 0) to preserve existing filter behavior where this field is + * not considered during mode comparison. + * + * Example: + * response_header_mode = SEND (1) + * request_body_mode = BUFFERED (2) + * ... + * Results in a key like: ...21 (where 1 is for response_header_mode, 2 for request_body_mode) + * + * @param mode The ProcessingMode proto to convert. + * @return A uint32_t representing the unique integer key for the mode. + */ + static uint32_t + processingModeToInt(const envoy::extensions::filters::http::ext_proc::v3::ProcessingMode& mode) { + uint32_t key = 0; + // Field 1: request_header_mode (Index 1 -> 10^0). This is ignored! + // Field 2: response_header_mode (Index 2 -> 10^1) + key += static_cast(mode.response_header_mode()) * 10; + // Field 3: request_body_mode (Index 3 -> 10^2) + key += static_cast(mode.request_body_mode()) * 100; + // Field 4: response_body_mode (Index 4 -> 10^3) + key += static_cast(mode.response_body_mode()) * 1000; + // Field 5: request_trailer_mode (Index 5 -> 10^4) + key += static_cast(mode.request_trailer_mode()) * 10000; + // Field 6: response_trailer_mode (Index 6 -> 10^5) + key += static_cast(mode.response_trailer_mode()) * 100000; + return key; + } + + absl::flat_hash_set allowed_modes_; +}; + +} // namespace ExternalProcessing +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/ext_proc/ext_proc.cc b/source/extensions/filters/http/ext_proc/ext_proc.cc index 517ca33c2f6db..69c3590bed6a9 100644 --- a/source/extensions/filters/http/ext_proc/ext_proc.cc +++ b/source/extensions/filters/http/ext_proc/ext_proc.cc @@ -282,8 +282,7 @@ FilterConfig::FilterConfig(const ExternalProcessor& config, typed_cluster_metadata_forwarding_namespaces_( config.metadata_options().cluster_metadata_forwarding_namespaces().typed().begin(), config.metadata_options().cluster_metadata_forwarding_namespaces().typed().end()), - allowed_override_modes_(config.allowed_override_modes().begin(), - config.allowed_override_modes().end()), + allowed_override_modes_(config.allowed_override_modes()), expression_manager_(builder, context.localInfo(), config.request_attributes(), config.response_attributes()), processing_request_modifier_factory_cb_( @@ -1722,27 +1721,11 @@ void Filter::onReceiveMessage(std::unique_ptr&& r) { (config_->processingMode().request_body_mode() != ProcessingMode::FULL_DUPLEX_STREAMED) && (config_->processingMode().response_body_mode() != ProcessingMode::FULL_DUPLEX_STREAMED) && inHeaderProcessState() && response->has_mode_override()) { - bool mode_override_allowed = true; const auto mode_override = effectiveModeOverride(response->mode_override(), config_->processingMode()); - // First, check if mode override allow-list is configured - if (!config_->allowedOverrideModes().empty()) { - // Second, check if mode override from response is allowed. - mode_override_allowed = absl::c_any_of( - config_->allowedOverrideModes(), - [&mode_override]( - const envoy::extensions::filters::http::ext_proc::v3::ProcessingMode& other) { - // Ignore matching on request_header_mode as it's not applicable. - return mode_override.request_body_mode() == other.request_body_mode() && - mode_override.request_trailer_mode() == other.request_trailer_mode() && - mode_override.response_header_mode() == other.response_header_mode() && - mode_override.response_body_mode() == other.response_body_mode() && - mode_override.response_trailer_mode() == other.response_trailer_mode(); - }); - } - - if (mode_override_allowed) { + // Check if mode override allow-list is configured. + if (config_->isAllowedOverrideMode(mode_override)) { ENVOY_STREAM_LOG(debug, "Processing mode overridden by server for this request", *decoder_callbacks_); decoding_state_.setProcessingMode(mode_override); diff --git a/source/extensions/filters/http/ext_proc/ext_proc.h b/source/extensions/filters/http/ext_proc/ext_proc.h index 4d3081daf1af5..1c02c78372154 100644 --- a/source/extensions/filters/http/ext_proc/ext_proc.h +++ b/source/extensions/filters/http/ext_proc/ext_proc.h @@ -1,8 +1,5 @@ #pragma once -#include -#include -#include #include #include "envoy/config/core/v3/base.pb.h" @@ -25,6 +22,7 @@ #include "source/extensions/filters/common/ext_proc/client_base.h" #include "source/extensions/filters/common/mutation_rules/mutation_rules.h" #include "source/extensions/filters/http/common/pass_through_filter.h" +#include "source/extensions/filters/http/ext_proc/allowed_override_modes_set.h" #include "source/extensions/filters/http/ext_proc/client_impl.h" #include "source/extensions/filters/http/ext_proc/matching_utils.h" #include "source/extensions/filters/http/ext_proc/on_processing_response.h" @@ -270,9 +268,15 @@ class FilterConfig { return typed_cluster_metadata_forwarding_namespaces_; } - const std::vector& - allowedOverrideModes() const { - return allowed_override_modes_; + /* + * Returns true if there are no allowed_override_modes defined, or if defined + * then one of them matches the given input. + * + * @param mode the processing mode that needs to be explicitly defined. + */ + bool isAllowedOverrideMode( + const envoy::extensions::filters::http::ext_proc::v3::ProcessingMode& mode) const { + return allowed_override_modes_.empty() || allowed_override_modes_.isModeSupported(mode); } ThreadLocalStreamManager& threadLocalStreamManager() { @@ -346,8 +350,7 @@ class FilterConfig { const std::vector untyped_receiving_namespaces_; const std::vector untyped_cluster_metadata_forwarding_namespaces_; const std::vector typed_cluster_metadata_forwarding_namespaces_; - const std::vector - allowed_override_modes_; + const AllowedOverrideModesSet allowed_override_modes_; const ExpressionManager expression_manager_; const std::function()> diff --git a/test/extensions/filters/http/ext_proc/BUILD b/test/extensions/filters/http/ext_proc/BUILD index 1d1c557c89d32..82bc6712c3c18 100644 --- a/test/extensions/filters/http/ext_proc/BUILD +++ b/test/extensions/filters/http/ext_proc/BUILD @@ -773,3 +773,13 @@ envoy_extension_cc_test_library( "//source/extensions/tracers/common:factory_base_lib", ], ) + +envoy_extension_cc_test( + name = "allowed_override_modes_set_test", + size = "small", + srcs = ["allowed_override_modes_set_test.cc"], + extension_names = ["envoy.filters.http.ext_proc"], + deps = [ + "//source/extensions/filters/http/ext_proc:allowed_override_modes_set_lib", + ], +) diff --git a/test/extensions/filters/http/ext_proc/allowed_override_modes_set_test.cc b/test/extensions/filters/http/ext_proc/allowed_override_modes_set_test.cc new file mode 100644 index 0000000000000..eb35cc87ab9c7 --- /dev/null +++ b/test/extensions/filters/http/ext_proc/allowed_override_modes_set_test.cc @@ -0,0 +1,123 @@ +#include "source/extensions/filters/http/ext_proc/allowed_override_modes_set.h" + +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace ExternalProcessing { +namespace { + +using envoy::extensions::filters::http::ext_proc::v3::ProcessingMode; + +class AllowedOverrideModesSetTest : public testing::Test { +protected: + // Helper to create a ProcessingMode with specific settings for readability + ProcessingMode createMode(ProcessingMode::HeaderSendMode req_header, + ProcessingMode::HeaderSendMode resp_header, + ProcessingMode::BodySendMode req_body, + ProcessingMode::BodySendMode resp_body, + ProcessingMode::HeaderSendMode req_trailer, + ProcessingMode::HeaderSendMode resp_trailer) { + ProcessingMode pm; + pm.set_request_header_mode(req_header); + pm.set_response_header_mode(resp_header); + pm.set_request_body_mode(req_body); + pm.set_response_body_mode(resp_body); + pm.set_request_trailer_mode(req_trailer); + pm.set_response_trailer_mode(resp_trailer); + return pm; + } +}; + +// Verify exact matches work as expected. +TEST_F(AllowedOverrideModesSetTest, BasicExactMatch) { + const ProcessingMode allowed = + createMode(ProcessingMode::SEND, ProcessingMode::SKIP, ProcessingMode::BUFFERED, + ProcessingMode::NONE, ProcessingMode::SKIP, ProcessingMode::SEND); + + const std::vector config_modes = {allowed}; + const AllowedOverrideModesSet set(config_modes); + + EXPECT_TRUE(set.isModeSupported(allowed)); +} + +// Verify that 'request_header_mode' is IGNORED during comparison. +// The allowed mode has request_header_mode = SEND. +// The candidate mode has request_header_mode = SKIP. +// All other fields match. Expected result: Supported. +TEST_F(AllowedOverrideModesSetTest, IgnoresRequestHeaderMode) { + const ProcessingMode allowed = + createMode(ProcessingMode::SEND, // Value 1. + ProcessingMode::SKIP, ProcessingMode::BUFFERED, ProcessingMode::NONE, + ProcessingMode::SKIP, ProcessingMode::SEND); + + const ProcessingMode candidate = + createMode(ProcessingMode::SKIP, // Value 2 (Different!). + ProcessingMode::SKIP, ProcessingMode::BUFFERED, ProcessingMode::NONE, + ProcessingMode::SKIP, ProcessingMode::SEND); + + const std::vector config_modes = {allowed}; + const AllowedOverrideModesSet set(config_modes); + + EXPECT_TRUE(set.isModeSupported(candidate)); +} + +// Verify that differences in other fields (e.g. response_body_mode) result in rejection. +TEST_F(AllowedOverrideModesSetTest, EnforcesOtherFields) { + const ProcessingMode allowed = + createMode(ProcessingMode::SEND, ProcessingMode::SKIP, ProcessingMode::BUFFERED, + ProcessingMode::NONE, ProcessingMode::SKIP, ProcessingMode::SEND); + + // Candidate differs in response_body_mode (STREAMED vs NONE) + const ProcessingMode candidate = + createMode(ProcessingMode::SEND, ProcessingMode::SKIP, ProcessingMode::BUFFERED, + ProcessingMode::STREAMED, ProcessingMode::SKIP, ProcessingMode::SEND); + + const std::vector config_modes = {allowed}; + const AllowedOverrideModesSet set(config_modes); + + EXPECT_FALSE(set.isModeSupported(candidate)); +} + +// Verify behavior with multiple allowed modes. +TEST_F(AllowedOverrideModesSetTest, MultipleAllowedModes) { + const ProcessingMode mode1 = + createMode(ProcessingMode::SEND, ProcessingMode::SEND, ProcessingMode::NONE, + ProcessingMode::NONE, ProcessingMode::SKIP, ProcessingMode::SKIP); + + const ProcessingMode mode2 = + createMode(ProcessingMode::SKIP, ProcessingMode::SKIP, ProcessingMode::BUFFERED, + ProcessingMode::BUFFERED, ProcessingMode::SEND, ProcessingMode::SEND); + + const std::vector config_modes = {mode1, mode2}; + const AllowedOverrideModesSet set(config_modes); + + EXPECT_TRUE(set.isModeSupported(mode1)); + EXPECT_TRUE(set.isModeSupported(mode2)); + + // A mix of mode1 and mode2 should NOT be supported + const ProcessingMode mixed = + createMode(ProcessingMode::SEND, ProcessingMode::SEND, // Matches mode1 + ProcessingMode::BUFFERED, ProcessingMode::BUFFERED, // Matches mode2 + ProcessingMode::SKIP, ProcessingMode::SKIP); + EXPECT_FALSE(set.isModeSupported(mixed)); +} + +// Verify behavior with an empty set. +TEST_F(AllowedOverrideModesSetTest, EmptySetReturnsFalse) { + const std::vector config_modes = {}; // Empty + const AllowedOverrideModesSet set(config_modes); + + const ProcessingMode candidate = + createMode(ProcessingMode::SEND, ProcessingMode::SEND, ProcessingMode::NONE, + ProcessingMode::NONE, ProcessingMode::SKIP, ProcessingMode::SKIP); + + EXPECT_TRUE(set.empty()); + EXPECT_FALSE(set.isModeSupported(candidate)); +} +} // namespace +} // namespace ExternalProcessing +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy