You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
proxysql/include/MySQL_Passthrough_Auth_Cache.h

209 lines
8.1 KiB

#ifndef PROXYSQL_MYSQL_PASSTHROUGH_AUTH_CACHE_H
#define PROXYSQL_MYSQL_PASSTHROUGH_AUTH_CACHE_H
#include <pthread.h>
#include <atomic>
#include <cstddef>
#include <cstdint>
#include <deque>
#include <string>
#include <unordered_map>
#include <vector>
/**
* @brief Forward declaration of re2::RE2 to keep this header light.
*
* The full re2/re2.h pulls in a large set of headers and dependencies; we
* only need a pointer to RE2 in the class state, so a forward declaration
* suffices. The translation unit that owns the compiled regex
* (lib/MySQL_Passthrough_Auth_Cache.cpp) includes the full header.
*/
namespace re2 { class RE2; }
#ifdef DEBUG
#define MYSQL_PASSTHROUGH_AUTH_CACHE_DEB "_DEBUG"
#else
#define MYSQL_PASSTHROUGH_AUTH_CACHE_DEB ""
#endif
#define MYSQL_PASSTHROUGH_AUTH_CACHE_VERSION "0.1.0000" MYSQL_PASSTHROUGH_AUTH_CACHE_DEB
struct passthrough_entry_view {
std::string username;
uint64_t learned_at_us;
int hostgroup_probed;
};
class MySQL_Passthrough_Auth_Cache {
private:
struct entry_t {
std::string cleartext_password;
uint64_t learned_at_us;
int hostgroup_probed;
};
mutable pthread_rwlock_t lock;
std::unordered_map<std::string, entry_t> entries;
std::atomic<int> inflight_probes;
/**
* @brief Atomic counters for operational observability (spec §7.4 follow-up).
*
* Exposed via @c stats_mysql_passthrough_auth_metrics. Each
* counter increments at exactly one well-defined point in
* @c handler_again___status_AUTHENTICATING_BACKEND_FOR_CLIENT
* (see the corresponding @c bump_* methods below). Monotonic
* since process start; reset only by process restart.
*
* Naming mirrors the existing @c stats_mysql_global pattern of
* "what happened" snake-case-counters; no special suffixes.
*/
std::atomic<uint64_t> stat_probes_attempted;
std::atomic<uint64_t> stat_probes_ok;
std::atomic<uint64_t> stat_probes_failed_credentials;
std::atomic<uint64_t> stat_probes_failed_transport;
std::atomic<uint64_t> stat_lockouts_user;
std::atomic<uint64_t> stat_lockouts_ip;
std::atomic<uint64_t> stat_inflight_cap_rejects;
std::atomic<uint64_t> stat_cache_hits;
std::atomic<uint64_t> stat_cache_invalidations;
// Sliding-window failure counters (spec §7.2). Per-username and
// per-source-IP. Mutated only behind failure_lock — a separate
// mutex from `lock` since these are write-mostly and accessed on
// every probe.
mutable pthread_mutex_t failure_lock;
mutable std::unordered_map<std::string, std::deque<uint64_t>> failures_by_user;
mutable std::unordered_map<std::string, std::deque<uint64_t>> failures_by_ip;
/**
* @brief Compiled-regex cache for the username allowlist (spec §7.1).
*
* Compiling an re2::RE2 is cheap (single-digit microseconds) but
* is paid every time a candidate connect is checked. Cache the
* last-seen pattern string alongside its compiled form so that as
* long as @c mysql-passthrough_auth_username_pattern is unchanged
* we hit the compiled form. A pattern change (admin SET, reload)
* triggers a re-compile under the write lock.
*
* @c pattern_lock is a pthread_rwlock so the COMMON case
* (steady-state pattern, every probe takes the read lock for
* FullMatch) doesn't serialize through a single mutex. The write
* lock is taken only when the pattern STRING changes, which
* happens on admin SET / LOAD MYSQL VARIABLES TO RUNTIME and is
* effectively rare. re2::RE2::FullMatch is documented as
* thread-safe on a const RE2 instance, so concurrent readers
* are fine.
*
* Holds a raw pointer (forward-declared above) rather than
* unique_ptr so we don't need to drag re2/re2.h into this header.
*/
mutable pthread_rwlock_t pattern_lock;
mutable std::string compiled_pattern_str;
mutable re2::RE2 *compiled_pattern;
public:
MySQL_Passthrough_Auth_Cache();
~MySQL_Passthrough_Auth_Cache();
// Look up a cached credential. Returns true on hit (and populates
// out_cleartext); false on miss. If ttl_s > 0 and the entry is older
// than ttl_s, the entry is evicted and a miss is returned.
bool lookup(const std::string& username, std::string& out_cleartext, uint32_t ttl_s);
// Insert or replace a cached credential.
void insert(const std::string& username, const std::string& cleartext, int hostgroup_probed);
// Evict a single entry. Returns true if the entry was present.
bool evict(const std::string& username);
// Remove every entry.
void clear();
// Number of entries currently held.
size_t size() const;
// Snapshot of entries (without password) for stats / observability.
std::vector<passthrough_entry_view> snapshot() const;
// Global in-flight probe counter (spec §7.3). Sessions wishing to
// start a backend probe call try_acquire_inflight with the current
// configured cap; on true they MUST pair with release_inflight when
// the probe completes (success or failure). On false the session
// must reject the auth with a generic ERR.
bool try_acquire_inflight(int max_inflight);
void release_inflight();
int inflight() const;
// Sliding-window failure counters (spec §7.2). Sessions check
// would_lockout before probing; on probe failure, record a
// failure. window_s defines the sliding window in seconds; older
// timestamps are dropped lazily on check.
bool would_lockout_user(const std::string& username, int max_failures, uint32_t window_s) const;
bool would_lockout_ip(const std::string& ip, int max_failures, uint32_t window_s) const;
/**
* @brief Record a probe failure against (username, ip) deques.
*
* @param max_keys Operator-tunable cap on the size of each
* failure map (failures_by_user, failures_by_ip).
* Driven by @c mysql-passthrough_auth_failure_map_cap.
* When the map exceeds the cap, evict_oldest
* reclaims an entry (defense-in-depth against
* username/IP churn that would otherwise grow
* the maps unbounded).
*/
void record_failure(const std::string& username, const std::string& ip, int max_keys);
/**
* @brief Observability counters (B7 follow-up).
*
* Each @c bump_* method increments the corresponding atomic at
* the single call site documented in @c MySQL_Session.cpp. The
* @c metrics_snapshot helper returns the current values for the
* @c stats_mysql_passthrough_auth_metrics virtual table.
*/
void bump_probes_attempted();
void bump_probes_ok();
void bump_probes_failed_credentials();
void bump_probes_failed_transport();
void bump_lockouts_user();
void bump_lockouts_ip();
void bump_inflight_cap_rejects();
void bump_cache_hits();
void bump_cache_invalidations();
/**
* @brief Snapshot of metric counters + current-state gauges.
*
* Returns a vector of (name, value) pairs ordered for stable JSON /
* stats-table output. Values are read with relaxed memory ordering
* since stats are advisory, not synchronizing.
*/
struct metric_kv {
std::string name;
uint64_t value;
};
std::vector<metric_kv> metrics_snapshot() const;
/**
* @brief Check whether @p username matches the configured allowlist
* regex (spec §7.1, mysql-passthrough_auth_username_pattern).
*
* @param username Frontend user attempting pass-through.
* @param pattern Regex string from the global variable. Empty means
* "allow every username" (back-compat default).
* @return @c true when the pattern is empty or @p username FullMatches
* the compiled regex; @c false when the regex is set and
* either fails to compile or the username doesn't match.
*
* The compiled regex is cached on the class behind @c pattern_lock;
* a pattern-string change triggers a re-compile on the next call.
* Match semantics are RE2 FullMatch (the entire username must
* match), matching how query rules use re2 elsewhere in ProxySQL.
* A regex that fails to compile is treated as a deny-all -- the
* fail-safe direction for a security gate.
*/
bool username_allowed(const std::string& username, const std::string& pattern);
void print_version();
};
#endif // PROXYSQL_MYSQL_PASSTHROUGH_AUTH_CACHE_H