Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ext_authz cache #37953

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion api/envoy/extensions/filters/http/ext_authz/v3/ext_authz.proto
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE;
// External Authorization :ref:`configuration overview <config_http_filters_ext_authz>`.
// [#extension: envoy.filters.http.ext_authz]

// [#next-free-field: 30]
// [#next-free-field: 33]
message ExtAuthz {
option (udpa.annotations.versioning).previous_message_type =
"envoy.config.filter.http.ext_authz.v3.ExtAuthz";
Expand Down Expand Up @@ -310,6 +310,14 @@ 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;

// Configuration parameters for ext_authz response cache
uint32 response_cache_max_size = 30;

uint32 response_cache_ttl = 31;

// Array of header names. ext_authz will use the first header that appears in the HTTP request as the cache key.
repeated string response_cache_header_names = 32;
}

// Configuration for buffering the request data.
Expand Down
5 changes: 4 additions & 1 deletion source/extensions/filters/http/ext_authz/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ envoy_extension_package()
envoy_cc_library(
name = "ext_authz",
srcs = ["ext_authz.cc"],
hdrs = ["ext_authz.h"],
hdrs = [
"ext_authz.h",
"fifo_cache.h",
],
deps = [
"//envoy/http:codes_interface",
"//envoy/stats:stats_macros",
Expand Down
70 changes: 69 additions & 1 deletion source/extensions/filters/http/ext_authz/ext_authz.cc
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,20 @@ void fillMetadataContext(const std::vector<const MetadataProto*>& source_metadat
}
}

// Utility function to find the first set auth header, based on the candidate headers in
// configuration
absl::optional<absl::string_view>
get_first_auth_header(const Http::RequestHeaderMap& headers,
const std::vector<std::string>& header_names) {
for (const auto& header_name : header_names) {
const auto header = headers.get(Http::LowerCaseString(header_name));
if (!header.empty()) {
return header[0]->value().getStringView();
}
}
return absl::nullopt;
}

} // namespace

FilterConfig::FilterConfig(const envoy::extensions::filters::http::ext_authz::v3::ExtAuthz& config,
Expand All @@ -69,7 +83,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.
Expand Down Expand Up @@ -119,6 +132,14 @@ 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)),
response_cache_max_size_(
config.response_cache_max_size() != 0 ? config.response_cache_max_size() : 100),
response_cache_ttl_(config.response_cache_ttl() != 0 ? config.response_cache_ttl() : 10),
response_cache_header_names_(
config.response_cache_header_names().begin(),
config.response_cache_header_names().end()), // Initialize header names
response_cache_(response_cache_max_size_, response_cache_ttl_,
factory_context.timeSource()), // response cache
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"))),
Expand Down Expand Up @@ -278,6 +299,38 @@ Http::FilterHeadersStatus Filter::decodeHeaders(Http::RequestHeaderMap& headers,
return Http::FilterHeadersStatus::Continue;
}

// Try reading response from the response cache
const auto auth_header = get_first_auth_header(headers, config_->responseCacheHeaderNames());

if (auth_header.has_value()) {
const std::string auth_header_str(auth_header.value());
// Retrieve the HTTP status code from the cache
auto cached_status_code = config_->responseCache().Get(auth_header_str.c_str());
if (cached_status_code.has_value()) {
ENVOY_STREAM_LOG(info, "Cache HIT for token {}: HTTP status {}", *decoder_callbacks_,
auth_header_str, *cached_status_code);

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<Http::Code>(*cached_status_code), "Unauthorized", nullptr, absl::nullopt,
Filters::Common::ExtAuthz::ResponseCodeDetails::get().AuthzDenied);

return Http::FilterHeadersStatus::StopIteration;
}
} else {
ENVOY_STREAM_LOG(info, "Cache miss for auth token {}. Will call external authz.",
*decoder_callbacks_, auth_header_str);
}
} else {
ENVOY_STREAM_LOG(info, "Cannot check cache because auth_header is empty", *decoder_callbacks_);
}

request_headers_ = &headers;
const auto check_settings = per_route_flags.check_settings_;
buffer_data_ = (config_->withRequestBody() || check_settings.has_with_request_body()) &&
Expand Down Expand Up @@ -472,6 +525,21 @@ void Filter::onComplete(Filters::Common::ExtAuthz::ResponsePtr&& response) {
using Filters::Common::ExtAuthz::CheckStatus;
Stats::StatName empty_stat_name;

// Extract the actual HTTP status code
const int http_status_code =
static_cast<uint16_t>(response->status_code); // Assuming this static cast is safe because
// http status code should be <= 0xffff

// If auth header exists, cache the response status code
const auto auth_header =
get_first_auth_header(*request_headers_, config_->responseCacheHeaderNames());
if (auth_header.has_value()) {
const std::string auth_header_str(auth_header.value());
ENVOY_LOG(info, "Caching response: {} with HTTP status: {}", auth_header_str, http_status_code);
config_->responseCache().Insert(auth_header_str.c_str(),
http_status_code); // Store the HTTP status code
}

updateLoggingInfo();

if (!response->dynamic_metadata.fields().empty()) {
Expand Down
16 changes: 16 additions & 0 deletions source/extensions/filters/http/ext_authz/ext_authz.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@
#include "source/extensions/filters/common/ext_authz/ext_authz_http_impl.h"
#include "source/extensions/filters/common/mutation_rules/mutation_rules.h"

// For response cache
#include "source/extensions/filters/http/ext_authz/fifo_cache.h"

namespace Envoy {
namespace Extensions {
namespace HttpFilters {
Expand Down Expand Up @@ -131,6 +134,12 @@ class FilterConfig {

bool headersAsBytes() const { return encode_raw_headers_; }

FIFOEvictionCache& responseCache() { return response_cache_; }

const std::vector<std::string>& responseCacheHeaderNames() const {
return response_cache_header_names_;
}

Filters::Common::MutationRules::CheckResult
checkDecoderHeaderMutation(const Filters::Common::MutationRules::CheckOperation& operation,
const Http::LowerCaseString& key, absl::string_view value) const {
Expand Down Expand Up @@ -271,6 +280,13 @@ class FilterConfig {
Filters::Common::ExtAuthz::MatcherSharedPtr allowed_headers_matcher_;
Filters::Common::ExtAuthz::MatcherSharedPtr disallowed_headers_matcher_;

// Fields for response cache configuration
uint32_t response_cache_max_size_;
uint32_t response_cache_ttl_;
std::vector<std::string> response_cache_header_names_; // New field for header names
// Response cache
FIFOEvictionCache response_cache_;

public:
// TODO(nezdolik): deprecate cluster scope stats counters in favor of filter scope stats
// (ExtAuthzFilterStats stats_).
Expand Down
150 changes: 150 additions & 0 deletions source/extensions/filters/http/ext_authz/fifo_cache.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
#pragma once

#include <algorithm> // For std::shuffle
#include <random> // For std::default_random_engine
#include <vector>

#include "envoy/common/time.h"

#include "source/common/common/thread.h"

#include "absl/container/flat_hash_map.h"
#include "absl/types/optional.h"

namespace Envoy {
namespace Extensions {
namespace HttpFilters {
namespace ExtAuthz {

/**
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.
*/
class FIFOEvictionCache {
public:
// By default, TTL will be 10 seconds.
FIFOEvictionCache(std::size_t max_size, int default_ttl_seconds, Envoy::TimeSource& time_source)
: max_cache_size(max_size), default_ttl_seconds(default_ttl_seconds),
time_source_(time_source) {}

~FIFOEvictionCache() {
for (auto& pair : cache_items_map) {
free(const_cast<char*>(pair.first));
}
}

bool Insert(const char* key, uint16_t value, int ttl_seconds = -1) {
Thread::LockGuard lock{mutex_};
const char* c_key = strdup(key);
if (ttl_seconds == -1) {
ttl_seconds = default_ttl_seconds;
}
auto expiration_time = time_source_.monotonicTime() + std::chrono::seconds(ttl_seconds);
CacheItem item = {value, expiration_time};
auto it = cache_items_map.find(c_key);
if (it == cache_items_map.end()) {
if (cache_items_map.size() >= max_cache_size) {
Evict();
}
cache_items_map[c_key] = item;
} else {
cache_items_map[c_key] = item;
}
return true;
}

bool Erase(const char* key) {
Thread::LockGuard lock{mutex_};
auto it = cache_items_map.find(key);
if (it != cache_items_map.end()) {
free(const_cast<char*>(it->first));
cache_items_map.erase(it);
return true;
}
return false;
}

absl::optional<uint16_t> Get(const char* key) {
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) {
return it->second.value;
} else {
// Item has expired
free(const_cast<char*>(it->first));
cache_items_map.erase(it);
}
}
return absl::nullopt;
}

size_t Size() const {
Thread::LockGuard lock{mutex_};
return cache_items_map.size();
}

private:
struct CacheItem {
uint16_t value;
std::chrono::steady_clock::time_point expiration_time;
};

void Evict() {
if (cache_items_map.size() > 0) {
// Select a random subset of items
std::vector<const char*> keys;
for (const auto& pair : cache_items_map) {
keys.push_back(pair.first);
}
std::default_random_engine rng(std::random_device{}());
std::shuffle(keys.begin(), keys.end(), rng);

// Sort the subset by TTL
std::sort(keys.begin(), keys.begin() + std::min(keys.size(), size_t(10)),
[this](const char* lhs, const char* rhs) {
return cache_items_map[lhs].expiration_time <
cache_items_map[rhs].expiration_time;
});

// Evict the items with the nearest TTL
for (size_t i = 0; i < std::min(keys.size(), size_t(3)); ++i) {
auto it = cache_items_map.find(keys[i]);
if (it != cache_items_map.end()) {
free(const_cast<char*>(it->first));
cache_items_map.erase(it);
}
}
}
}

struct CharPtrHash {
std::size_t operator()(const char* str) const {
std::size_t hash = 0;
while (*str) {
hash = hash * 101 + *str++;
}
return hash;
}
};

struct CharPtrEqual {
bool operator()(const char* lhs, const char* rhs) const { return std::strcmp(lhs, rhs) == 0; }
};

absl::flat_hash_map<const char*, CacheItem, CharPtrHash, CharPtrEqual> cache_items_map;

mutable Thread::MutexBasicLockable
mutex_; // Mark mutex_ as mutable to allow locking in const methods

std::size_t max_cache_size;
int default_ttl_seconds;
Envoy::TimeSource& time_source_; // Reference to TimeSource
};

} // namespace ExtAuthz
} // namespace HttpFilters
} // namespace Extensions
} // namespace Envoy