Skip to main content

fraiseql_core/security/
query_validator.rs

1//! Query Validator
2//!
3//! This module provides query validation for GraphQL queries.
4//! It validates:
5//! - Query size (maximum bytes, O(1) check — no parsing required)
6//! - Query depth (maximum nesting levels) — via AST analysis
7//! - Query complexity (weighted scoring of fields) — via AST analysis
8//! - Alias count (alias amplification protection) — via AST analysis
9//!
10//! # Architecture
11//!
12//! The Query Validator acts as the third layer in the security middleware:
13//! ```text
14//! GraphQL Query String
15//!     ↓
16//! QueryValidator::validate()
17//!     ├─ Check 1: Validate query size (O(1) byte count)
18//!     ├─ Check 2: AST-based analysis (depth, complexity, alias count)
19//!     │           via `RequestValidator` from `graphql::complexity`
20//!     ├─ Check 3: Check query depth
21//!     ├─ Check 4: Check query complexity
22//!     └─ Check 5: Check alias count (alias amplification protection)
23//!     ↓
24//! Result<QueryMetrics> (validation passed or error)
25//! ```
26//!
27//! # Examples
28//!
29//! ```rust
30//! use fraiseql_core::security::{QueryValidator, QueryValidatorConfig};
31//!
32//! // Create validator with standard limits
33//! let config = QueryValidatorConfig {
34//!     max_depth: 10,
35//!     max_complexity: 1000,
36//!     max_size_bytes: 100_000,
37//!     max_aliases: 30,
38//! };
39//! let validator = QueryValidator::from_config(config);
40//!
41//! // Validate a query
42//! let query = "{ user { posts { comments { author { name } } } } }";
43//! let metrics = validator.validate(query).unwrap();
44//! println!("Query depth: {}", metrics.depth);
45//! println!("Query complexity: {}", metrics.complexity);
46//! println!("Query aliases: {}", metrics.alias_count);
47//! ```
48
49use serde::{Deserialize, Serialize};
50
51use crate::{
52    graphql::complexity::{ComplexityConfig, QueryMetrics, RequestValidator},
53    security::errors::{Result, SecurityError},
54};
55
56/// Query validation configuration
57///
58/// Defines limits for query depth, complexity, size, and alias count.
59#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
60pub struct QueryValidatorConfig {
61    /// Maximum nesting depth for queries
62    pub max_depth: usize,
63
64    /// Maximum complexity score for queries
65    pub max_complexity: usize,
66
67    /// Maximum query size in bytes
68    pub max_size_bytes: usize,
69
70    /// Maximum number of field aliases per query (alias amplification protection).
71    pub max_aliases: usize,
72}
73
74impl QueryValidatorConfig {
75    /// Create a permissive query validation configuration
76    ///
77    /// - Max depth: 20 levels
78    /// - Max complexity: 5000
79    /// - Max size: 1 MB
80    /// - Max aliases: 100
81    #[must_use]
82    pub const fn permissive() -> Self {
83        Self {
84            max_depth:      20,
85            max_complexity: 5000,
86            max_size_bytes: 1_000_000, // 1 MB
87            max_aliases:    100,
88        }
89    }
90
91    /// Create a standard query validation configuration
92    ///
93    /// - Max depth: 10 levels
94    /// - Max complexity: 1000
95    /// - Max size: 256 KB
96    /// - Max aliases: 30
97    #[must_use]
98    pub const fn standard() -> Self {
99        Self {
100            max_depth:      10,
101            max_complexity: 1000,
102            max_size_bytes: 256_000, // 256 KB
103            max_aliases:    30,
104        }
105    }
106
107    /// Create a strict query validation configuration
108    ///
109    /// - Max depth: 5 levels
110    /// - Max complexity: 500
111    /// - Max size: 64 KB (regulated environments)
112    /// - Max aliases: 10
113    #[must_use]
114    pub const fn strict() -> Self {
115        Self {
116            max_depth:      5,
117            max_complexity: 500,
118            max_size_bytes: 64_000, // 64 KB
119            max_aliases:    10,
120        }
121    }
122}
123
124/// Query Validator
125///
126/// Validates incoming GraphQL queries against security policies.
127/// Acts as the third layer in the security middleware pipeline.
128///
129/// Delegates AST-based analysis to [`RequestValidator`] from
130/// `graphql::complexity` — the single source of truth for depth,
131/// complexity, and alias-amplification logic.
132#[derive(Debug, Clone)]
133pub struct QueryValidator {
134    config: QueryValidatorConfig,
135}
136
137impl QueryValidator {
138    /// Create a new query validator from configuration
139    #[must_use]
140    pub const fn from_config(config: QueryValidatorConfig) -> Self {
141        Self { config }
142    }
143
144    /// Create validator with permissive settings
145    #[must_use]
146    pub const fn permissive() -> Self {
147        Self::from_config(QueryValidatorConfig::permissive())
148    }
149
150    /// Create validator with standard settings
151    #[must_use]
152    pub const fn standard() -> Self {
153        Self::from_config(QueryValidatorConfig::standard())
154    }
155
156    /// Create validator with strict settings
157    #[must_use]
158    pub const fn strict() -> Self {
159        Self::from_config(QueryValidatorConfig::strict())
160    }
161
162    /// Validate a GraphQL query, enforcing all configured limits.
163    ///
164    /// Performs checks in order:
165    /// 1. Query size (O(1) — no parsing)
166    /// 2. AST parse (rejects malformed GraphQL)
167    /// 3. Query depth
168    /// 4. Query complexity
169    /// 5. Alias count (alias amplification protection)
170    ///
171    /// Returns [`QueryMetrics`] if all checks pass, or the first
172    /// [`SecurityError`] encountered.
173    ///
174    /// # Errors
175    ///
176    /// Returns [`SecurityError::QueryTooLarge`] if the query exceeds `max_size_bytes`.
177    /// Returns [`SecurityError::MalformedQuery`] if GraphQL syntax is invalid.
178    /// Returns [`SecurityError::QueryTooDeep`] if nesting depth exceeds `max_depth`.
179    /// Returns [`SecurityError::QueryTooComplex`] if complexity exceeds `max_complexity`.
180    /// Returns [`SecurityError::TooManyAliases`] if alias count exceeds `max_aliases`.
181    pub fn validate(&self, query: &str) -> Result<QueryMetrics> {
182        // Check 1: Validate query size (O(1) — pre-parse)
183        let size_bytes = query.len();
184        if size_bytes > self.config.max_size_bytes {
185            return Err(SecurityError::QueryTooLarge {
186                size:     size_bytes,
187                max_size: self.config.max_size_bytes,
188            });
189        }
190
191        // Checks 2–5: AST-based analysis via RequestValidator
192        let rv = RequestValidator::from_config(&ComplexityConfig {
193            max_depth:      self.config.max_depth,
194            max_complexity: self.config.max_complexity,
195            max_aliases:    self.config.max_aliases,
196        });
197
198        let metrics =
199            rv.analyze(query).map_err(|e| SecurityError::MalformedQuery(e.to_string()))?;
200
201        // Check 3: Query depth
202        if metrics.depth > self.config.max_depth {
203            return Err(SecurityError::QueryTooDeep {
204                depth:     metrics.depth,
205                max_depth: self.config.max_depth,
206            });
207        }
208
209        // Check 4: Query complexity
210        if metrics.complexity > self.config.max_complexity {
211            return Err(SecurityError::QueryTooComplex {
212                complexity:     metrics.complexity,
213                max_complexity: self.config.max_complexity,
214            });
215        }
216
217        // Check 5: Alias count
218        if metrics.alias_count > self.config.max_aliases {
219            return Err(SecurityError::TooManyAliases {
220                alias_count: metrics.alias_count,
221                max_aliases: self.config.max_aliases,
222            });
223        }
224
225        Ok(metrics)
226    }
227
228    /// Get the underlying configuration
229    #[must_use]
230    pub const fn config(&self) -> &QueryValidatorConfig {
231        &self.config
232    }
233}
234
235#[cfg(test)]
236mod tests {
237    #![allow(clippy::unwrap_used)] // Reason: test code, panics are acceptable
238
239    use super::*;
240
241    // ============================================================================
242    // Check 1: Query Size Validation Tests
243    // ============================================================================
244
245    fn large_query(size: usize) -> String {
246        "{ ".to_string() + &"field ".repeat(size) + "}"
247    }
248
249    #[test]
250    fn test_query_size_within_limit() {
251        let validator = QueryValidator::standard();
252        validator
253            .validate("{ user { id name } }")
254            .unwrap_or_else(|e| panic!("expected Ok for small query: {e}"));
255    }
256
257    #[test]
258    fn test_query_size_exceeds_limit() {
259        let validator = QueryValidator::standard();
260        let q = large_query(100_000);
261        let result = validator.validate(&q);
262        assert!(matches!(result, Err(SecurityError::QueryTooLarge { .. })));
263    }
264
265    // ============================================================================
266    // Check 2: Malformed query
267    // ============================================================================
268
269    #[test]
270    fn test_malformed_query_returns_error() {
271        let validator = QueryValidator::standard();
272        let result = validator.validate("this is not graphql {{{}}}");
273        assert!(
274            matches!(result, Err(SecurityError::MalformedQuery(_))),
275            "malformed query must return MalformedQuery error, got {result:?}"
276        );
277    }
278
279    // ============================================================================
280    // Check 3: Query Depth Validation Tests
281    // ============================================================================
282
283    #[test]
284    fn test_valid_query_depth() {
285        let validator = QueryValidator::standard();
286        let metrics = validator
287            .validate("{ user { id name } }")
288            .unwrap_or_else(|e| panic!("expected Ok for shallow query: {e}"));
289        assert!(metrics.depth <= validator.config().max_depth);
290    }
291
292    #[test]
293    fn test_query_depth_exceeds_limit() {
294        let validator = QueryValidator::strict(); // max_depth = 5
295        // depth = 7 (a→b→c→d→e→f→g)
296        let deep = "{ a { b { c { d { e { f { g } } } } } } }";
297        let result = validator.validate(deep);
298        assert!(
299            matches!(result, Err(SecurityError::QueryTooDeep { .. })),
300            "depth-7 query must be rejected with strict (max=5), got {result:?}"
301        );
302    }
303
304    #[test]
305    fn test_very_deep_query_rejected() {
306        let validator = QueryValidator::strict(); // max_depth = 5
307        // depth = 8 (a→b→c→d→e→f→g→h)
308        let deep = "{ a { b { c { d { e { f { g { h } } } } } } } }";
309        let result = validator.validate(deep);
310        assert!(
311            matches!(result, Err(SecurityError::QueryTooDeep { .. })),
312            "depth-8 query must be rejected, got {result:?}"
313        );
314    }
315
316    // ============================================================================
317    // Check 4: Query Complexity Validation Tests
318    // ============================================================================
319
320    #[test]
321    fn test_valid_query_complexity() {
322        let validator = QueryValidator::standard();
323        let metrics = validator
324            .validate("{ user { id name } }")
325            .unwrap_or_else(|e| panic!("expected Ok for simple query: {e}"));
326        assert!(metrics.complexity <= validator.config().max_complexity);
327    }
328
329    #[test]
330    fn test_complexity_calculated() {
331        let validator = QueryValidator::standard();
332        let metrics = validator.validate("{ user { id } }").unwrap();
333        assert!(metrics.complexity > 0);
334    }
335
336    // ============================================================================
337    // Check 5: Alias amplification protection
338    // ============================================================================
339
340    #[test]
341    fn test_alias_amplification_rejected() {
342        let validator = QueryValidator::standard(); // max_aliases = 30
343        let aliases: String =
344            (0..31).map(|i| ["a", &i.to_string(), ": user { id } "].concat()).collect();
345        let query = format!("{{ {aliases} }}");
346        let result = validator.validate(&query);
347        assert!(
348            matches!(
349                result,
350                Err(SecurityError::TooManyAliases {
351                    alias_count: 31,
352                    max_aliases: 30,
353                })
354            ),
355            "31-alias query must be rejected with TooManyAliases, got {result:?}"
356        );
357    }
358
359    #[test]
360    fn test_alias_within_limit_allowed() {
361        let validator = QueryValidator::standard(); // max_aliases = 30
362        let aliases: String =
363            (0..5).map(|i| ["a", &i.to_string(), ": user { id } "].concat()).collect();
364        let query = format!("{{ {aliases} }}");
365        let result = validator.validate(&query);
366        assert!(result.is_ok(), "5 aliases should be allowed, got {result:?}");
367    }
368
369    // ============================================================================
370    // Configuration Tests
371    // ============================================================================
372
373    #[test]
374    fn test_permissive_config() {
375        let config = QueryValidatorConfig::permissive();
376        assert_eq!(config.max_depth, 20);
377        assert_eq!(config.max_complexity, 5000);
378        assert_eq!(config.max_size_bytes, 1_000_000);
379        assert_eq!(config.max_aliases, 100);
380    }
381
382    #[test]
383    fn test_standard_config() {
384        let config = QueryValidatorConfig::standard();
385        assert_eq!(config.max_depth, 10);
386        assert_eq!(config.max_complexity, 1000);
387        assert_eq!(config.max_size_bytes, 256_000);
388        assert_eq!(config.max_aliases, 30);
389    }
390
391    #[test]
392    fn test_strict_config() {
393        let config = QueryValidatorConfig::strict();
394        assert_eq!(config.max_depth, 5);
395        assert_eq!(config.max_complexity, 500);
396        assert_eq!(config.max_size_bytes, 64_000);
397        assert_eq!(config.max_aliases, 10);
398    }
399
400    #[test]
401    fn test_validator_helpers() {
402        let permissive = QueryValidator::permissive();
403        assert_eq!(permissive.config().max_depth, 20);
404
405        let standard = QueryValidator::standard();
406        assert_eq!(standard.config().max_depth, 10);
407
408        let strict = QueryValidator::strict();
409        assert_eq!(strict.config().max_depth, 5);
410    }
411
412    // ============================================================================
413    // Metrics Tests
414    // ============================================================================
415
416    #[test]
417    fn test_metrics_returned_on_valid_query() {
418        let validator = QueryValidator::standard();
419        let query = "{ user { id name } }";
420        let metrics = validator.validate(query).unwrap();
421        assert!(metrics.depth >= 2); // user → (id, name)
422        assert!(metrics.complexity > 0);
423        assert_eq!(metrics.alias_count, 0);
424    }
425
426    #[test]
427    fn test_alias_count_in_metrics() {
428        let validator = QueryValidator::standard();
429        let query = "{ a: user { id } b: user { id } }";
430        let metrics = validator.validate(query).unwrap();
431        assert_eq!(metrics.alias_count, 2);
432    }
433}