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}