fraiseql_server/middleware/rate_limit/config.rs
1//! Rate limit configuration types.
2
3use serde::{Deserialize, Serialize};
4
5/// Minimal mirror of the `[security.rate_limiting]` TOML section, deserialized
6/// from the compiled schema's `security.rate_limiting` JSON key.
7#[derive(Debug, Clone, Deserialize, Default)]
8#[serde(default)]
9pub struct RateLimitingSecurityConfig {
10 /// Enable rate limiting.
11 pub enabled: bool,
12 /// Global request rate cap (requests per second, per IP).
13 pub requests_per_second: u32,
14 /// Burst allowance above the steady-state rate.
15 pub burst_size: u32,
16 /// Auth initiation endpoint — max requests per window.
17 pub auth_start_max_requests: u32,
18 /// Auth initiation window in seconds.
19 pub auth_start_window_secs: u64,
20 /// OAuth callback endpoint — max requests per window.
21 pub auth_callback_max_requests: u32,
22 /// OAuth callback window in seconds.
23 pub auth_callback_window_secs: u64,
24 /// Token refresh endpoint — max requests per window.
25 pub auth_refresh_max_requests: u32,
26 /// Token refresh window in seconds.
27 pub auth_refresh_window_secs: u64,
28 /// Per-authenticated-user request rate in requests/second.
29 /// Defaults to 10× `requests_per_second` if not set.
30 #[serde(default)]
31 pub requests_per_second_per_user: Option<u32>,
32 /// Redis URL for distributed rate limiting (not yet implemented).
33 pub redis_url: Option<String>,
34 /// Trust `X-Real-IP` / `X-Forwarded-For` headers for the client IP.
35 ///
36 /// Enable only when FraiseQL is deployed behind a trusted reverse proxy
37 /// (e.g. nginx, Cloudflare, AWS ALB) that sets these headers. Enabling
38 /// without a trusted proxy allows clients to spoof their IP address.
39 #[serde(default)]
40 pub trust_proxy_headers: bool,
41
42 /// CIDR ranges trusted as proxy IPs (e.g. `["10.0.0.0/8", "172.16.0.0/12"]`).
43 ///
44 /// When set and `trust_proxy_headers = true`, X-Forwarded-For is only honoured
45 /// when the direct connection IP falls within one of these CIDR ranges.
46 /// Requests arriving from outside these ranges use the connection IP directly,
47 /// preventing clients from spoofing their address by setting X-Forwarded-For.
48 ///
49 /// When `None` and `trust_proxy_headers = true`, all proxy IPs are trusted
50 /// (less secure — a startup warning is emitted).
51 #[serde(default)]
52 pub trusted_proxy_cidrs: Option<Vec<String>>,
53}
54
55/// Rate limiting configuration (token-bucket algorithm).
56///
57/// Enforces request-per-second limits per IP/user across all GraphQL
58/// operations. This is the canonical rate limiter for request throttling.
59///
60/// Distinct from `fraiseql_auth::AuthRateLimitConfig`, which uses a
61/// sliding-window algorithm for auth endpoint brute-force protection.
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct RateLimitConfig {
64 /// Enable rate limiting
65 pub enabled: bool,
66
67 /// Requests per second per IP
68 pub rps_per_ip: u32,
69
70 /// Requests per second per user (if authenticated)
71 pub rps_per_user: u32,
72
73 /// Burst capacity (maximum tokens to accumulate)
74 pub burst_size: u32,
75
76 /// Cleanup interval in seconds (remove stale entries)
77 pub cleanup_interval_secs: u64,
78
79 /// Trust `X-Real-IP` / `X-Forwarded-For` headers for client IP extraction.
80 ///
81 /// Must only be enabled when behind a trusted reverse proxy.
82 pub trust_proxy_headers: bool,
83
84 /// Parsed CIDR ranges trusted as proxy IPs.
85 ///
86 /// When non-empty, X-Forwarded-For is only trusted if the direct connection IP
87 /// falls within one of these ranges. An empty `Vec` with `trust_proxy_headers = true`
88 /// means all direct IPs are treated as trusted proxies (less secure).
89 pub trusted_proxy_cidrs: Vec<ipnet::IpNet>,
90}
91
92impl Default for RateLimitConfig {
93 fn default() -> Self {
94 Self {
95 enabled: true,
96 rps_per_ip: 100, // 100 req/sec per IP
97 rps_per_user: 1000, // 1000 req/sec per user
98 burst_size: 500, // Allow bursts up to 500 requests
99 cleanup_interval_secs: 300, // Clean up every 5 minutes
100 trust_proxy_headers: false,
101 trusted_proxy_cidrs: Vec::new(),
102 }
103 }
104}
105
106impl RateLimitConfig {
107 /// Build from the `[security.rate_limiting]` config embedded in the compiled schema.
108 ///
109 /// Maps `requests_per_second` → `rps_per_ip` and `burst_size` directly.
110 /// `rps_per_user` uses the explicit `requests_per_second_per_user` value when set,
111 /// or defaults to 10× `requests_per_second`.
112 ///
113 /// The default 10× multiplier reflects that authenticated users are identifiable
114 /// (abuse is traceable) and include service accounts with higher call rates.
115 /// Operators can override with `requests_per_second_per_user` in `fraiseql.toml`.
116 pub fn from_security_config(sec: &RateLimitingSecurityConfig) -> Self {
117 let trusted_proxy_cidrs = sec
118 .trusted_proxy_cidrs
119 .as_deref()
120 .unwrap_or(&[])
121 .iter()
122 .filter_map(|s| {
123 s.parse::<ipnet::IpNet>()
124 .map_err(|e| {
125 tracing::warn!(cidr = %s, error = %e, "Invalid trusted_proxy_cidr — skipping");
126 })
127 .ok()
128 })
129 .collect();
130
131 Self {
132 enabled: sec.enabled,
133 rps_per_ip: sec.requests_per_second,
134 rps_per_user: sec
135 .requests_per_second_per_user
136 .unwrap_or_else(|| sec.requests_per_second.saturating_mul(10)),
137 burst_size: sec.burst_size,
138 cleanup_interval_secs: 300,
139 trust_proxy_headers: sec.trust_proxy_headers,
140 trusted_proxy_cidrs,
141 }
142 }
143}
144
145/// Result returned by all `check_*` rate-limit methods.
146///
147/// Carries the allow/deny decision, the approximate remaining token count
148/// (used for the `X-RateLimit-Remaining` response header), and the
149/// recommended `Retry-After` interval in seconds (0 when the request was
150/// allowed).
151#[derive(Debug, Clone)]
152pub struct CheckResult {
153 /// Whether the request should be allowed.
154 pub allowed: bool,
155 /// Tokens remaining in the bucket after this request (≥ 0).
156 pub remaining: f64,
157 /// Seconds the client should wait before retrying (0 when allowed).
158 pub retry_after_secs: u32,
159}
160
161impl CheckResult {
162 pub(super) const fn allow(remaining: f64) -> Self {
163 Self {
164 allowed: true,
165 remaining,
166 retry_after_secs: 0,
167 }
168 }
169
170 pub(super) const fn deny(retry_after_secs: u32) -> Self {
171 Self {
172 allowed: false,
173 remaining: 0.0,
174 retry_after_secs,
175 }
176 }
177}