diff --git a/api/envoy/extensions/filters/http/ext_authz/v3/ext_authz.proto b/api/envoy/extensions/filters/http/ext_authz/v3/ext_authz.proto index d3bca54baccc..f8f7e5738e7d 100644 --- a/api/envoy/extensions/filters/http/ext_authz/v3/ext_authz.proto +++ b/api/envoy/extensions/filters/http/ext_authz/v3/ext_authz.proto @@ -5,6 +5,7 @@ package envoy.extensions.filters.http.ext_authz.v3; import "envoy/config/common/mutation_rules/v3/mutation_rules.proto"; import "envoy/config/core/v3/base.proto"; import "envoy/config/core/v3/config_source.proto"; +import "envoy/config/core/v3/extension.proto"; import "envoy/config/core/v3/grpc_service.proto"; import "envoy/config/core/v3/http_uri.proto"; import "envoy/type/matcher/v3/metadata.proto"; @@ -30,7 +31,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // External Authorization :ref:`configuration overview `. // [#extension: envoy.filters.http.ext_authz] -// [#next-free-field: 30] +// [#next-free-field: 32] message ExtAuthz { option (udpa.annotations.versioning).previous_message_type = "envoy.config.filter.http.ext_authz.v3.ExtAuthz"; @@ -310,6 +311,11 @@ message ExtAuthz { // Field ``latency_us`` is exposed for CEL and logging when using gRPC or HTTP service. // Fields ``bytesSent`` and ``bytesReceived`` are exposed for CEL and logging only when using gRPC service. bool emit_filter_state_stats = 29; + + // Cache configuration to store response from external auth server. + optional envoy.config.core.v3.TypedExtensionConfig response_cache_config = 30; + // Tell cache whether to remember the whole response from authz server (response body, header modification, meta data, status) or just status. + optional bool response_cache_remember_body_headers = 31; } // Configuration for buffering the request data. diff --git a/source/common/resp_cache/BUILD b/source/common/resp_cache/BUILD new file mode 100644 index 000000000000..a0411186b19b --- /dev/null +++ b/source/common/resp_cache/BUILD @@ -0,0 +1,32 @@ +licenses(["notice"]) # Apache 2 + +proto_library( + name = "simple_cache_proto", + srcs = ["simple_cache.proto"], + deps = [ + "@com_google_protobuf//:descriptor_proto", + ], +) + +cc_proto_library( + name = "simple_cache_cc_proto", + deps = [":simple_cache_proto"], +) + +cc_library( + name = "resp_cache", + srcs = [ + # Add any .cc files if you have implementations in .cc files + ], + hdrs = [ + "resp_cache.h", + "resp_cache_factory.h", + "simple_cache.h", + ], + visibility = ["//visibility:public"], + deps = [ + ":simple_cache_cc_proto", + "@com_google_absl//absl/container:flat_hash_map", + "@com_google_absl//absl/types:optional", + ], +) diff --git a/source/common/resp_cache/resp_cache.h b/source/common/resp_cache/resp_cache.h new file mode 100644 index 000000000000..28162d8e3f64 --- /dev/null +++ b/source/common/resp_cache/resp_cache.h @@ -0,0 +1,39 @@ +#pragma once + +// Interface class for Response Cache + +#pragma once + +#include + +//#include "simple_cache.h" + +namespace Envoy { +namespace Common { +namespace RespCache { + +template class RespCache { +public: + virtual ~RespCache() = default; + + // Method to insert a value into the cache with a TTL + // ttl_seconds -1 means use the default TTL + virtual bool Insert(const Envoy::Http::RequestHeaderMap& headers, const T& value, + int ttl_seconds = -1) = 0; + + // Method to get a cached response based on the cache key + virtual absl::optional Get(const Envoy::Http::RequestHeaderMap& headers) = 0; + + // Method to erase a value from the cache + virtual bool Erase(const Envoy::Http::RequestHeaderMap& headers) = 0; + + // Public getter method for max cache size (number of objects that can be in cache) + virtual std::size_t getMaxCacheSize() const = 0; + + // Publig getter method for current cache size (number of objects in cache) + virtual size_t Size() const = 0; +}; + +} // namespace RespCache +} // namespace Common +} // namespace Envoy diff --git a/source/common/resp_cache/resp_cache_factory.h b/source/common/resp_cache/resp_cache_factory.h new file mode 100644 index 000000000000..c26b444d629f --- /dev/null +++ b/source/common/resp_cache/resp_cache_factory.h @@ -0,0 +1,40 @@ +#pragma once + +// This factory creates a ResponseCache object based on the configuration. + +#pragma once + +#include + +#include "source/common/protobuf/utility.h" + +#include "absl/status/statusor.h" +#include "resp_cache.h" +#include "simple_cache.h" + +namespace Envoy { +namespace Common { +namespace RespCache { + +enum class CacheType { Simple }; + +template +absl::StatusOr>> +createCache(CacheType type, const envoy::config::core::v3::TypedExtensionConfig& config, + Envoy::TimeSource& time_source) { + if (type == CacheType::Simple) { + envoy::common::resp_cache::v3::SimpleCacheConfig simple_cache_config; + if (!Envoy::MessageUtil::unpackTo(config.typed_config(), simple_cache_config).ok()) { + return absl::InvalidArgumentError("Invalid config type for SimpleCache"); + } + auto config_ptr = + std::make_shared(simple_cache_config); + return std::make_unique>(config_ptr, time_source); + } else { + return absl::InvalidArgumentError("Unsupported cache type"); + } +} + +} // namespace RespCache +} // namespace Common +} // namespace Envoy diff --git a/source/common/resp_cache/simple_cache.h b/source/common/resp_cache/simple_cache.h new file mode 100644 index 000000000000..3e26eb9d379a --- /dev/null +++ b/source/common/resp_cache/simple_cache.h @@ -0,0 +1,243 @@ +#pragma once + +// The file contains SimpleCache class, which implements RespCache interface. + +#pragma once + +#include // For std::shuffle +#include // For std::default_random_engine +#include + +#include "envoy/common/time.h" + +#include "source/common/common/logger.h" +#include "source/common/common/thread.h" +#include "source/common/resp_cache/simple_cache.pb.h" // For configuration in simple_cache.proto + +#include "absl/container/flat_hash_map.h" +#include "absl/types/optional.h" +#include "resp_cache.h" // For interface RespCache, which is implemented by SimpleCache. + +namespace Envoy { +namespace Common { +namespace RespCache { + +/** + A simple cache class with TTL. + It has a random subset eviction policy. This is memory efficient because it does not need to store + the order of elements. It restricts stored values to 16-bit unsigned integers, making it + memory efficient. + */ +template +class SimpleCache : public RespCache, public Logger::Loggable { +public: + SimpleCache(std::shared_ptr config, + Envoy::TimeSource& time_source) + : config_(std::move(config)), random_generator_(std::random_device{}()), + time_source_(time_source) {} + + ~SimpleCache() = default; + + // Public getter method for max_cache_size + std::size_t getMaxCacheSize() const override { return config_->max_cache_size(); } + + bool Insert(const Http::RequestHeaderMap& headers, const T& value, + int ttl_seconds = -1) override { + std::string key = makeCacheKey(headers); + if (key.empty()) { + return false; + } + auto expiration_time = CalculateExpirationTime(ttl_seconds); + CacheItem item(value, expiration_time); + return InsertInternal(key, std::move(item)); + } + + bool Erase(const Http::RequestHeaderMap& headers) override { + std::string key = makeCacheKey(headers); + if (key.empty()) { + return false; + } + Thread::LockGuard lock{mutex_}; + auto it = cache_items_map.find(key); + if (it != cache_items_map.end()) { + cache_items_map.erase(it); + return true; + } + return false; + } + + absl::optional Get(const Http::RequestHeaderMap& headers) override { + std::string key = makeCacheKey(headers); + if (key.empty()) { + return absl::nullopt; + } + Thread::LockGuard lock{mutex_}; + auto it = cache_items_map.find(key); + if (it != cache_items_map.end()) { + if (time_source_.monotonicTime() < it->second.expiration_time) { + ENVOY_LOG(debug, "Cache hit: key {}", key); + return it->second.value; + } else { + // Item has expired + ENVOY_LOG(debug, "Cache miss: key {} has expired", key); + cache_items_map.erase(it); + } + } + ENVOY_LOG(debug, "Cache miss: key {} not found", key); + return absl::nullopt; + } + + size_t Size() const override { + Thread::LockGuard lock{mutex_}; + return cache_items_map.size(); + } + +private: + // struct to define the cache item stored in the cache. + // It takes a template so that it can be only status code (to minimize memory footprint), + // or it can be the full response (to take advantage of full functionality of authz server). + struct CacheItem { + T value; + std::chrono::steady_clock::time_point expiration_time; + + CacheItem() = default; // default-constructed + + CacheItem(const T& val, std::chrono::steady_clock::time_point exp_time) + : value(val), expiration_time(exp_time) {} + }; + + // Make cache key from request header + std::string makeCacheKey(const Http::RequestHeaderMap& headers) const { + const auto header_entry = headers.get(Http::LowerCaseString(config_->cache_key_header())); + if (!header_entry.empty()) { + return std::string(header_entry[0]->value().getStringView()); + } + return ""; + } + + // Calculate expiration time with 5% jitter + std::chrono::steady_clock::time_point CalculateExpirationTime(int ttl_seconds) { + if (ttl_seconds == -1) { + ttl_seconds = config_->ttl_seconds(); + } + + std::uniform_real_distribution distribution(-0.05, 0.05); + double jitter_factor = distribution(random_generator_); + auto jittered_ttl = std::chrono::seconds(ttl_seconds) + + std::chrono::seconds(static_cast(ttl_seconds * jitter_factor)); + + return time_source_.monotonicTime() + jittered_ttl; + } + + bool InsertInternal(const std::string& key, CacheItem&& item) { + Thread::LockGuard lock{mutex_}; + auto it = cache_items_map.find(key); + if (it == cache_items_map.end()) { + if (cache_items_map.size() >= config_->max_cache_size()) { + Evict(); + } + cache_items_map[key] = std::move(item); + } else { + cache_items_map[key] = std::move(item); + } + ENVOY_LOG(debug, "Inserted cache key {}", key); + return true; + } + + // Eviction algorithm emulates LRU by: + // 1. Advance iterator on the cache randomly + // 2. Read 1000 items to decide average TTL + // 3. Look at some (by default, 1% of the whole cache) items, and delete items whose TTL are older + // than a fraction (by default, 50%) of the average. Eviction takes two configuration parameters: + // - Eviction candidate ratio: The fraction of the cache to read for potential eviction + // - Eviction threshold ratio: Evict items with TTL lower than X% of the average TTL + void Evict() { + // TODO: convert duration logging to log + // Start measuring real time + // auto start_real_time = std::chrono::high_resolution_clock::now(); + // Start measuring CPU time + // std::clock_t start_cpu_time = std::clock(); + + // Step 1: Advance pointer randomly + std::uniform_int_distribution distribution( + 0, (1.0 - config_->eviction_candidate_ratio()) * config_->max_cache_size()); + std::size_t advance_steps = distribution(random_generator_); + + auto it = cache_items_map.begin(); + for (std::size_t i = 0; i < advance_steps && it != cache_items_map.end(); ++i) { + ++it; + } + auto iterator_save = it; + + // Step 2: Read the next min(1000, num_remove_candidates) items and calculate the sum of TTLs + auto num_remove_candidates = + static_cast(config_->eviction_candidate_ratio() * config_->max_cache_size()); + auto current_time = time_source_.monotonicTime(); + std::size_t items_to_read = std::min(static_cast(1000), num_remove_candidates); + double sum_ttl = 0.0; + double min_ttl = std::numeric_limits::max(); + for (std::size_t i = 0; i < items_to_read && it != cache_items_map.end(); ++i, ++it) { + auto item_ttl = std::chrono::duration_cast( + it->second.expiration_time - current_time) + .count(); + sum_ttl += item_ttl; + min_ttl = std::min(min_ttl, static_cast(item_ttl)); + } + double average_ttl = sum_ttl / items_to_read; + + // Step 3: Evict items with TTL lower than a fraction of the average TTL + std::size_t removed = 0; + // Evict items with TTL lower than X% of the average TTL + auto eviction_threshold = std::chrono::milliseconds( + static_cast(min_ttl + (average_ttl - min_ttl) * config_->eviction_threshold_ratio())); + + // TODO: convert to log - std::cout << "Average TTL: " << average_ttl << " ms, Min TTL: " << + // min_ttl << " ms, Eviction threshold: " << eviction_threshold.count() << " ms" << std::endl; + + it = iterator_save; + for (std::size_t i = 0; i < num_remove_candidates && it != cache_items_map.end();) { + auto item_ttl = it->second.expiration_time - current_time; + if (item_ttl.count() < 0 || item_ttl < eviction_threshold) { + auto it_next = std::next(it); + cache_items_map.erase(it); + it = it_next; + removed++; + } else { + ++it; + ++i; + } + } + + // Stop measuring real time + // auto end_real_time = std::chrono::high_resolution_clock::now(); + // Stop measuring CPU time + // std::clock_t end_cpu_time = std::clock(); + + // Calculate elapsed real time in microseconds + // auto elapsed_real_time = std::chrono::duration_cast(end_real_time + // - start_real_time); + // Calculate elapsed CPU time + // double elapsed_cpu_time = double(end_cpu_time - start_cpu_time) * 1000000 / CLOCKS_PER_SEC; + + // Output the results + // std::cout << "Real time: " << elapsed_real_time.count() << " microseconds\n"; + // std::cout << "CPU time: " << elapsed_cpu_time << " microseconds\n"; + } + + absl::flat_hash_map cache_items_map; + + mutable Thread::MutexBasicLockable + mutex_; // Mark mutex_ as mutable to allow locking in const methods + // Note that this is a single mutex for the entire cache. This makes all Worker threads to + // synchronize on this mutex. This is not a problem. We implemented a cache which breaks up the + // mutex and flat_hash_map into 100 buckets. We then tested with 1,000 threads making requests at + // the same time. The result - the number of buckets (1 or 100) did not make any difference. + + std::shared_ptr config_; + std::default_random_engine random_generator_; // Random number generator + Envoy::TimeSource& time_source_; // Reference to TimeSource +}; + +} // namespace RespCache +} // namespace Common +} // namespace Envoy diff --git a/source/common/resp_cache/simple_cache.proto b/source/common/resp_cache/simple_cache.proto new file mode 100644 index 000000000000..58b1ddd691e0 --- /dev/null +++ b/source/common/resp_cache/simple_cache.proto @@ -0,0 +1,20 @@ +syntax = "proto3"; + +package envoy.common.resp_cache.v3; + +message SimpleCacheConfig { + // Name of the header to be used as cache key + string cache_key_header = 1; + + // The maximum number of items that can be stored in the cache. + uint32 max_cache_size = 2; + + // The time-to-live (TTL) for cache entries, in seconds. + uint32 ttl_seconds = 3; + + // The ratio of cache entries to consider for eviction when the cache is full. + double eviction_candidate_ratio = 4; + + // The threshold ratio for evicting cache entries. + double eviction_threshold_ratio = 5; +} diff --git a/source/extensions/filters/http/ext_authz/BUILD b/source/extensions/filters/http/ext_authz/BUILD index 02970d791e31..92f134914a09 100644 --- a/source/extensions/filters/http/ext_authz/BUILD +++ b/source/extensions/filters/http/ext_authz/BUILD @@ -15,7 +15,9 @@ envoy_extension_package() envoy_cc_library( name = "ext_authz", srcs = ["ext_authz.cc"], - hdrs = ["ext_authz.h"], + hdrs = [ + "ext_authz.h", + ], deps = [ "//envoy/http:codes_interface", "//envoy/stats:stats_macros", @@ -27,6 +29,7 @@ envoy_cc_library( "//source/common/common:minimal_logger_lib", "//source/common/http:codes_lib", "//source/common/http:utility_lib", + "//source/common/resp_cache", "//source/common/router:config_lib", "//source/common/runtime:runtime_protos_lib", "//source/extensions/filters/common/ext_authz:ext_authz_grpc_lib", diff --git a/source/extensions/filters/http/ext_authz/ext_authz.cc b/source/extensions/filters/http/ext_authz/ext_authz.cc index 7874c8a94b74..b05ab5f4c523 100644 --- a/source/extensions/filters/http/ext_authz/ext_authz.cc +++ b/source/extensions/filters/http/ext_authz/ext_authz.cc @@ -69,7 +69,6 @@ FilterConfig::FilterConfig(const envoy::extensions::filters::http::ext_authz::v3 failure_mode_allow_header_add_(config.failure_mode_allow_header_add()), clear_route_cache_(config.clear_route_cache()), max_request_bytes_(config.with_request_body().max_request_bytes()), - // `pack_as_bytes_` should be true when configured with the HTTP service because there is no // difference to where the body is written in http requests, and a value of false here will // cause non UTF-8 body content to be changed when it doesn't need to. @@ -119,12 +118,22 @@ FilterConfig::FilterConfig(const envoy::extensions::filters::http::ext_authz::v3 charge_cluster_response_stats_( PROTOBUF_GET_WRAPPED_OR_DEFAULT(config, charge_cluster_response_stats, true)), stats_(generateStats(stats_prefix, config.stat_prefix(), scope)), + // If response_cache_config section doesn't exist in the config, set response_cache_config_ to + // nullopt. + response_cache_config_(config.has_response_cache_config() + ? absl::make_optional(config.response_cache_config()) + : absl::nullopt), + response_cache_remember_body_headers_(config.response_cache_remember_body_headers()), ext_authz_ok_(pool_.add(createPoolStatName(config.stat_prefix(), "ok"))), ext_authz_denied_(pool_.add(createPoolStatName(config.stat_prefix(), "denied"))), ext_authz_error_(pool_.add(createPoolStatName(config.stat_prefix(), "error"))), ext_authz_invalid_(pool_.add(createPoolStatName(config.stat_prefix(), "invalid"))), ext_authz_failure_mode_allowed_( pool_.add(createPoolStatName(config.stat_prefix(), "failure_mode_allowed"))) { + + // Initialize the appropriate cache, out of two possible caches, based on configuration. + initializeCache(factory_context); + auto bootstrap = factory_context.bootstrap(); auto labels_key_it = bootstrap.node().metadata().fields().find(config.bootstrap_metadata_labels_key()); @@ -162,6 +171,38 @@ FilterConfig::FilterConfig(const envoy::extensions::filters::http::ext_authz::v3 } } +// Initialize the appropriate cache, out of two possible caches, based on configuration. +void FilterConfig::initializeCache(Server::Configuration::ServerFactoryContext& factory_context) { + // If response_cache_config section doesn't exist in the config, no need to create cache objects. + if (!response_cache_config_.has_value()) { + response_cache_status_ = nullptr; + response_cache_full_ = nullptr; + return; + } + + if (response_cache_remember_body_headers_) { + // Initialize the cache that stores full response (body, header modification, metadata, status). + auto cache_result = Envoy::Common::RespCache::createCache( + Envoy::Common::RespCache::CacheType::Simple, response_cache_config_.value(), + factory_context.timeSource()); + if (!cache_result.ok()) { + response_cache_full_ = nullptr; + } else { + response_cache_full_ = std::move(cache_result.value()); + } + } else { + // Initialize the cache that stores only status code. + auto cache_result = Envoy::Common::RespCache::createCache( + Envoy::Common::RespCache::CacheType::Simple, response_cache_config_.value(), + factory_context.timeSource()); + if (!cache_result.ok()) { + response_cache_status_ = nullptr; + } else { + response_cache_status_ = std::move(cache_result.value()); + } + } +} + void FilterConfigPerRoute::merge(const FilterConfigPerRoute& other) { // We only merge context extensions here, and leave boolean flags untouched since those flags are // not used from the merged config. @@ -278,6 +319,45 @@ Http::FilterHeadersStatus Filter::decodeHeaders(Http::RequestHeaderMap& headers, return Http::FilterHeadersStatus::Continue; } + if (config_->responseCacheConfig().has_value()) { +#ifdef CACHE_PERF_TEST + // Fill cache for testing + if (auth_header_str == "magic_fill_cache_for_testing") { + for (std::size_t i = 0; i < config_->responseCache().getMaxCacheSize() - 10; ++i) { + std::string test_key = "test_key_" + std::to_string(i); + config_->responseCache().Insert(test_key, 200); + } + } +#endif + if (config_->responseCacheRememberBodyHeaders()) { + auto cached_response = + config_->responseCache().Get(headers); + if (cached_response.has_value()) { + onCompleteSub(std::make_unique( + std::move(cached_response.value()))); + return Http::FilterHeadersStatus::StopIteration; + } + } else { + // Retrieve the HTTP status code from the cache + auto cached_status_code = config_->responseCache().Get(headers); + if (cached_status_code.has_value()) { + if (*cached_status_code >= 200 && *cached_status_code < 300) { + // Any 2xx response is a success: let the request proceed + return Http::FilterHeadersStatus::Continue; + } else { + // Non-2xx response: reject the request + decoder_callbacks_->streamInfo().setResponseFlag( + StreamInfo::CoreResponseFlag::UnauthorizedExternalService); + decoder_callbacks_->sendLocalReply( + static_cast(*cached_status_code), "Unauthorized", nullptr, absl::nullopt, + Filters::Common::ExtAuthz::ResponseCodeDetails::get().AuthzDenied); + + return Http::FilterHeadersStatus::StopIteration; + } + } + } + } + request_headers_ = &headers; const auto check_settings = per_route_flags.check_settings_; buffer_data_ = (config_->withRequestBody() || check_settings.has_with_request_body()) && @@ -467,12 +547,58 @@ CheckResult Filter::validateAndCheckDecoderHeaderMutation( return config_->checkDecoderHeaderMutation(operation, Http::LowerCaseString(key), value); } +std::string formatHeaders(const Filters::Common::ExtAuthz::UnsafeHeaderVector& headers) { + std::string formatted_headers; + for (const auto& header : headers) { + formatted_headers += fmt::format("{}: {}, ", header.first, header.second); + } + // Remove the trailing comma and space + if (!formatted_headers.empty()) { + formatted_headers.pop_back(); + formatted_headers.pop_back(); + } + return formatted_headers; +} + void Filter::onComplete(Filters::Common::ExtAuthz::ResponsePtr&& response) { + // Extract the actual HTTP status code + const int http_status_code = + static_cast(response->status_code); // Assuming this static cast is safe because + // http status code should be <= 0xffff + + // If response cache is configured, cache the response status code + if (config_->responseCacheConfig().has_value()) { + // If response_cache_remember_body_headers_ is set, remember the whole response + if (config_->responseCacheRememberBodyHeaders()) { + config_->responseCache().Insert(*request_headers_, + *response); + } else { + config_->responseCache().Insert(*request_headers_, + http_status_code); // Store the HTTP status code + } + } + + // Use std::move to cast the response to an rvalue reference, transferring ownership to + // onCompleteSub. This is necessary because onCompleteSub expects an rvalue reference + // (ResponsePtr&&). + onCompleteSub(std::move(response)); +} + +// This code, which handles response headers and metadata, were originally part of onComplete() +// function. It was moved into a separate method so that it can be invoked from onComplete() and +// onDecodeHeaders() in case of cache hit. +void Filter::onCompleteSub(Filters::Common::ExtAuthz::ResponsePtr&& response) { state_ = State::Complete; using Filters::Common::ExtAuthz::CheckStatus; Stats::StatName empty_stat_name; updateLoggingInfo(); + // ENVOY_STREAM_LOG(info, "ext_authz server response: status_code={}, headers_to_append={}, + // headers_to_set={}, headers_to_add={}, response_headers_to_add={}, response_headers_to_set={}, + // body={}", *decoder_callbacks_, static_cast(response->status_code), + // formatHeaders(response->headers_to_append), formatHeaders(response->headers_to_set), + // formatHeaders(response->headers_to_add), formatHeaders(response->response_headers_to_add), + // formatHeaders(response->response_headers_to_set), response->body); if (!response->dynamic_metadata.fields().empty()) { if (!config_->enableDynamicMetadataIngestion()) { diff --git a/source/extensions/filters/http/ext_authz/ext_authz.h b/source/extensions/filters/http/ext_authz/ext_authz.h index 237dda979e54..1b6b36819f1b 100644 --- a/source/extensions/filters/http/ext_authz/ext_authz.h +++ b/source/extensions/filters/http/ext_authz/ext_authz.h @@ -19,6 +19,9 @@ #include "source/common/common/utility.h" #include "source/common/http/codes.h" #include "source/common/http/header_map_impl.h" +#include "source/common/resp_cache/resp_cache.h" // For response cache +#include "source/common/resp_cache/resp_cache_factory.h" +#include "source/common/resp_cache/simple_cache.h" // For simple cache, which is a type of response cache #include "source/common/runtime/runtime_protos.h" #include "source/extensions/filters/common/ext_authz/check_request_utils.h" #include "source/extensions/filters/common/ext_authz/ext_authz.h" @@ -26,6 +29,8 @@ #include "source/extensions/filters/common/ext_authz/ext_authz_http_impl.h" #include "source/extensions/filters/common/mutation_rules/mutation_rules.h" +using Envoy::Common::RespCache::RespCache; + namespace Envoy { namespace Extensions { namespace HttpFilters { @@ -131,6 +136,28 @@ class FilterConfig { bool headersAsBytes() const { return encode_raw_headers_; } + // Return the right response cache, based on the template type. + template Envoy::Common::RespCache::RespCache& responseCache() { + if constexpr (std::is_same_v) { + return *response_cache_status_; + } else if constexpr (std::is_same_v) { + return *response_cache_full_; + } else { + throw std::runtime_error("Unsupported type"); + } + } + + /*const Envoy::Http::LowerCaseString& responseCacheHeaderName() const { + return response_cache_header_name_; + }*/ + + // Access to response_cache_config configuration parameters. + const absl::optional& responseCacheConfig() const { + return response_cache_config_; + } + + bool responseCacheRememberBodyHeaders() const { return response_cache_remember_body_headers_; } + Filters::Common::MutationRules::CheckResult checkDecoderHeaderMutation(const Filters::Common::MutationRules::CheckOperation& operation, const Http::LowerCaseString& key, absl::string_view value) const { @@ -271,6 +298,21 @@ class FilterConfig { Filters::Common::ExtAuthz::MatcherSharedPtr allowed_headers_matcher_; Filters::Common::ExtAuthz::MatcherSharedPtr disallowed_headers_matcher_; + // Private fields and methods for response cache + absl::optional response_cache_config_; + bool response_cache_remember_body_headers_; + + // Declare two caches. The response cache functionality has two modes: + // 1. Remember only the status code for a given request. This minimizes memory usage. + // 2. Remember the full response (headers, body, status) from external authorization server. + // This maximizes functionality. + // Depending on configuration (response_cache_remember_body_headers_), one of the two caches are + // used. + std::unique_ptr> response_cache_status_; + std::unique_ptr> + response_cache_full_; + void initializeCache(Server::Configuration::ServerFactoryContext& factory_context); + public: // TODO(nezdolik): deprecate cluster scope stats counters in favor of filter scope stats // (ExtAuthzFilterStats stats_). @@ -383,6 +425,7 @@ class Filter : public Logger::Loggable, void continueDecoding(); bool isBufferFull(uint64_t num_bytes_processing) const; void updateLoggingInfo(); + void onCompleteSub(Filters::Common::ExtAuthz::ResponsePtr&&); // This holds a set of flags defined in per-route configuration. struct PerRouteFlags {