Skip to main content

fraiseql_core/validation/
rate_limiting.rs

1//! Validation-specific rate limiting with per-dimension tracking.
2//!
3//! Provides rate limiting for different types of validation errors:
4//! - `validation_errors`: General field-level validation failures
5//! - `depth_errors`: Query depth limit violations
6//! - `complexity_errors`: Query complexity limit violations
7//! - `malformed_errors`: Malformed query/input errors
8//! - `async_validation_errors`: Async validator failures
9
10use std::{
11    num::NonZeroUsize,
12    sync::{Arc, Mutex},
13};
14
15use lru::LruCache;
16
17use crate::{
18    error::FraiseQLError,
19    utils::clock::{Clock, SystemClock},
20};
21
22/// Maximum number of distinct keys (IPs, user IDs) tracked per rate-limiter
23/// dimension. Prevents unbounded memory growth from IP rotation attacks.
24const MAX_RATE_LIMITER_ENTRIES: usize = 100_000;
25
26/// Rate limit configuration for a single dimension
27#[derive(Debug, Clone)]
28pub struct RateLimitDimension {
29    /// Maximum number of errors allowed in the window
30    pub max_requests: u32,
31    /// Window duration in seconds
32    pub window_secs:  u64,
33}
34
35impl RateLimitDimension {
36    const fn is_rate_limited(&self) -> bool {
37        self.max_requests == 0
38    }
39}
40
41/// Configuration for validation-specific rate limiting
42#[derive(Debug, Clone)]
43pub struct ValidationRateLimitingConfig {
44    /// Enable validation rate limiting
45    pub enabled: bool,
46    /// Validation errors limit
47    pub validation_errors_max_requests: u32,
48    /// Validation errors window in seconds
49    pub validation_errors_window_secs: u64,
50    /// Depth errors limit
51    pub depth_errors_max_requests: u32,
52    /// Depth errors window in seconds
53    pub depth_errors_window_secs: u64,
54    /// Complexity errors limit
55    pub complexity_errors_max_requests: u32,
56    /// Complexity errors window in seconds
57    pub complexity_errors_window_secs: u64,
58    /// Malformed errors limit
59    pub malformed_errors_max_requests: u32,
60    /// Malformed errors window in seconds
61    pub malformed_errors_window_secs: u64,
62    /// Async validation errors limit
63    pub async_validation_errors_max_requests: u32,
64    /// Async validation errors window in seconds
65    pub async_validation_errors_window_secs: u64,
66}
67
68impl Default for ValidationRateLimitingConfig {
69    fn default() -> Self {
70        Self {
71            enabled: true,
72            validation_errors_max_requests: 100,
73            validation_errors_window_secs: 60,
74            depth_errors_max_requests: 50,
75            depth_errors_window_secs: 60,
76            complexity_errors_max_requests: 30,
77            complexity_errors_window_secs: 60,
78            malformed_errors_max_requests: 40,
79            malformed_errors_window_secs: 60,
80            async_validation_errors_max_requests: 60,
81            async_validation_errors_window_secs: 60,
82        }
83    }
84}
85
86impl ValidationRateLimitingConfig {
87    /// Returns a builder for `ValidationRateLimitingConfig`.
88    #[must_use = "builder does nothing until .build() is called"]
89    pub fn builder() -> ValidationRateLimitingConfigBuilder {
90        ValidationRateLimitingConfigBuilder::default()
91    }
92}
93
94/// Builder for [`ValidationRateLimitingConfig`].
95#[derive(Debug, Default)]
96pub struct ValidationRateLimitingConfigBuilder {
97    inner: ValidationRateLimitingConfig,
98}
99
100impl ValidationRateLimitingConfigBuilder {
101    /// Sets whether validation rate limiting is enabled.
102    #[must_use = "builder method returns modified builder"]
103    pub const fn enabled(mut self, enabled: bool) -> Self {
104        self.inner.enabled = enabled;
105        self
106    }
107
108    /// Sets the max requests and window for validation errors.
109    #[must_use = "builder method returns modified builder"]
110    pub const fn validation_errors(mut self, max_requests: u32, window_secs: u64) -> Self {
111        self.inner.validation_errors_max_requests = max_requests;
112        self.inner.validation_errors_window_secs = window_secs;
113        self
114    }
115
116    /// Sets the max requests and window for depth errors.
117    #[must_use = "builder method returns modified builder"]
118    pub const fn depth_errors(mut self, max_requests: u32, window_secs: u64) -> Self {
119        self.inner.depth_errors_max_requests = max_requests;
120        self.inner.depth_errors_window_secs = window_secs;
121        self
122    }
123
124    /// Sets the max requests and window for complexity errors.
125    #[must_use = "builder method returns modified builder"]
126    pub const fn complexity_errors(mut self, max_requests: u32, window_secs: u64) -> Self {
127        self.inner.complexity_errors_max_requests = max_requests;
128        self.inner.complexity_errors_window_secs = window_secs;
129        self
130    }
131
132    /// Sets the max requests and window for malformed errors.
133    #[must_use = "builder method returns modified builder"]
134    pub const fn malformed_errors(mut self, max_requests: u32, window_secs: u64) -> Self {
135        self.inner.malformed_errors_max_requests = max_requests;
136        self.inner.malformed_errors_window_secs = window_secs;
137        self
138    }
139
140    /// Sets the max requests and window for async validation errors.
141    #[must_use = "builder method returns modified builder"]
142    pub const fn async_validation_errors(mut self, max_requests: u32, window_secs: u64) -> Self {
143        self.inner.async_validation_errors_max_requests = max_requests;
144        self.inner.async_validation_errors_window_secs = window_secs;
145        self
146    }
147
148    /// Builds the [`ValidationRateLimitingConfig`].
149    #[must_use = "building a config that is not used has no effect"]
150    pub const fn build(self) -> ValidationRateLimitingConfig {
151        self.inner
152    }
153}
154
155/// Request record for tracking
156#[derive(Debug, Clone)]
157struct RequestRecord {
158    /// Number of errors in current window
159    count:        u32,
160    /// Unix timestamp of window start
161    window_start: u64,
162}
163
164/// Single dimension rate limiter
165pub(crate) struct DimensionRateLimiter {
166    records:   Arc<Mutex<LruCache<String, RequestRecord>>>,
167    dimension: RateLimitDimension,
168    clock:     Arc<dyn Clock>,
169}
170
171impl DimensionRateLimiter {
172    #[cfg(test)]
173    pub(crate) fn new(max_requests: u32, window_secs: u64) -> Self {
174        Self::new_with_clock(max_requests, window_secs, Arc::new(SystemClock))
175    }
176
177    pub(crate) fn new_with_clock(
178        max_requests: u32,
179        window_secs: u64,
180        clock: Arc<dyn Clock>,
181    ) -> Self {
182        #[allow(clippy::expect_used)]
183        // Reason: MAX_RATE_LIMITER_ENTRIES is a non-zero compile-time constant
184        let cap = NonZeroUsize::new(MAX_RATE_LIMITER_ENTRIES)
185            .expect("MAX_RATE_LIMITER_ENTRIES must be > 0");
186        Self {
187            records: Arc::new(Mutex::new(LruCache::new(cap))),
188            dimension: RateLimitDimension {
189                max_requests,
190                window_secs,
191            },
192            clock,
193        }
194    }
195
196    pub(crate) fn check(&self, key: &str) -> Result<(), FraiseQLError> {
197        if self.dimension.is_rate_limited() {
198            return Ok(());
199        }
200
201        let mut records = self.records.lock().expect("records mutex poisoned");
202        let now = self.clock.now_secs();
203
204        // `get_or_insert` promotes the entry to most-recently-used, evicting the
205        // least-recently-used entry when the cache is at capacity.
206        let record = records.get_or_insert_mut(key.to_string(), || RequestRecord {
207            count:        0,
208            window_start: now,
209        });
210
211        // Check if window has expired
212        if now >= record.window_start + self.dimension.window_secs {
213            // Reset window
214            record.count = 1;
215            record.window_start = now;
216            Ok(())
217        } else if record.count < self.dimension.max_requests {
218            // Request allowed
219            record.count += 1;
220            Ok(())
221        } else {
222            // Rate limited
223            Err(FraiseQLError::RateLimited {
224                message:          "Rate limit exceeded for validation errors".to_string(),
225                retry_after_secs: self.dimension.window_secs,
226            })
227        }
228    }
229
230    pub(crate) fn clear(&self) {
231        let mut records = self.records.lock().expect("records mutex poisoned");
232        records.clear();
233    }
234}
235
236impl Clone for DimensionRateLimiter {
237    fn clone(&self) -> Self {
238        Self {
239            records:   Arc::clone(&self.records),
240            dimension: self.dimension.clone(),
241            clock:     Arc::clone(&self.clock),
242        }
243    }
244}
245
246/// Validation-specific rate limiter with per-dimension tracking
247#[derive(Clone)]
248#[allow(clippy::module_name_repetitions, clippy::struct_field_names)] // Reason: RateLimiting prefix provides clarity at call sites
249pub struct ValidationRateLimiter {
250    validation_errors:       DimensionRateLimiter,
251    depth_errors:            DimensionRateLimiter,
252    complexity_errors:       DimensionRateLimiter,
253    malformed_errors:        DimensionRateLimiter,
254    async_validation_errors: DimensionRateLimiter,
255}
256
257impl ValidationRateLimiter {
258    /// Create a new validation rate limiter with the given configuration.
259    #[must_use]
260    pub fn new(config: &ValidationRateLimitingConfig) -> Self {
261        Self::new_with_clock(config, Arc::new(SystemClock))
262    }
263
264    /// Create a validation rate limiter with a custom clock (for testing).
265    pub fn new_with_clock(config: &ValidationRateLimitingConfig, clock: Arc<dyn Clock>) -> Self {
266        Self {
267            validation_errors:       DimensionRateLimiter::new_with_clock(
268                config.validation_errors_max_requests,
269                config.validation_errors_window_secs,
270                Arc::clone(&clock),
271            ),
272            depth_errors:            DimensionRateLimiter::new_with_clock(
273                config.depth_errors_max_requests,
274                config.depth_errors_window_secs,
275                Arc::clone(&clock),
276            ),
277            complexity_errors:       DimensionRateLimiter::new_with_clock(
278                config.complexity_errors_max_requests,
279                config.complexity_errors_window_secs,
280                Arc::clone(&clock),
281            ),
282            malformed_errors:        DimensionRateLimiter::new_with_clock(
283                config.malformed_errors_max_requests,
284                config.malformed_errors_window_secs,
285                Arc::clone(&clock),
286            ),
287            async_validation_errors: DimensionRateLimiter::new_with_clock(
288                config.async_validation_errors_max_requests,
289                config.async_validation_errors_window_secs,
290                clock,
291            ),
292        }
293    }
294
295    /// Check rate limit for validation errors.
296    ///
297    /// # Errors
298    ///
299    /// Returns [`FraiseQLError::RateLimited`] if the key has exceeded the
300    /// validation-errors rate limit within the configured window.
301    pub fn check_validation_errors(&self, key: &str) -> Result<(), FraiseQLError> {
302        self.validation_errors.check(key)
303    }
304
305    /// Check rate limit for depth errors.
306    ///
307    /// # Errors
308    ///
309    /// Returns [`FraiseQLError::RateLimited`] if the key has exceeded the
310    /// depth-errors rate limit within the configured window.
311    pub fn check_depth_errors(&self, key: &str) -> Result<(), FraiseQLError> {
312        self.depth_errors.check(key)
313    }
314
315    /// Check rate limit for complexity errors.
316    ///
317    /// # Errors
318    ///
319    /// Returns [`FraiseQLError::RateLimited`] if the key has exceeded the
320    /// complexity-errors rate limit within the configured window.
321    pub fn check_complexity_errors(&self, key: &str) -> Result<(), FraiseQLError> {
322        self.complexity_errors.check(key)
323    }
324
325    /// Check rate limit for malformed errors.
326    ///
327    /// # Errors
328    ///
329    /// Returns [`FraiseQLError::RateLimited`] if the key has exceeded the
330    /// malformed-errors rate limit within the configured window.
331    pub fn check_malformed_errors(&self, key: &str) -> Result<(), FraiseQLError> {
332        self.malformed_errors.check(key)
333    }
334
335    /// Check rate limit for async validation errors.
336    ///
337    /// # Errors
338    ///
339    /// Returns [`FraiseQLError::RateLimited`] if the key has exceeded the
340    /// async-validation-errors rate limit within the configured window.
341    pub fn check_async_validation_errors(&self, key: &str) -> Result<(), FraiseQLError> {
342        self.async_validation_errors.check(key)
343    }
344
345    /// Clear all rate limiter state
346    pub fn clear(&self) {
347        self.validation_errors.clear();
348        self.depth_errors.clear();
349        self.complexity_errors.clear();
350        self.malformed_errors.clear();
351        self.async_validation_errors.clear();
352    }
353}