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 self.validate_with_variables(query, None)
183 }
184
185 /// Validate a GraphQL query with the request's variables available for
186 /// pagination-argument resolution.
187 ///
188 /// Identical to [`validate`](Self::validate), except a variable-valued
189 /// pagination argument (`first: $n`) is scored at its resolved value
190 /// rather than at the fail-closed ceiling (#869). Callers that have the
191 /// request variables must use this entry point, or legitimate
192 /// variable-paginated queries are over-scored and rejected.
193 ///
194 /// # Errors
195 ///
196 /// Same as [`validate`](Self::validate).
197 pub fn validate_with_variables(
198 &self,
199 query: &str,
200 variables: Option<&serde_json::Value>,
201 ) -> Result<QueryMetrics> {
202 // Check 1: Validate query size (O(1) — pre-parse)
203 let size_bytes = query.len();
204 if size_bytes > self.config.max_size_bytes {
205 return Err(SecurityError::QueryTooLarge {
206 size: size_bytes,
207 max_size: self.config.max_size_bytes,
208 });
209 }
210
211 // Checks 2–5: AST-based analysis via RequestValidator
212 let rv = RequestValidator::from_config(&ComplexityConfig {
213 max_depth: self.config.max_depth,
214 max_complexity: self.config.max_complexity,
215 max_aliases: self.config.max_aliases,
216 });
217
218 let metrics = rv
219 .analyze_with_variables(query, variables)
220 .map_err(|e| SecurityError::MalformedQuery(e.to_string()))?;
221
222 // Check 3: Query depth
223 if metrics.depth > self.config.max_depth {
224 return Err(SecurityError::QueryTooDeep {
225 depth: metrics.depth,
226 max_depth: self.config.max_depth,
227 });
228 }
229
230 // Check 4: Query complexity
231 if metrics.complexity > self.config.max_complexity {
232 return Err(SecurityError::QueryTooComplex {
233 complexity: metrics.complexity,
234 max_complexity: self.config.max_complexity,
235 });
236 }
237
238 // Check 5: Alias count
239 if metrics.alias_count > self.config.max_aliases {
240 return Err(SecurityError::TooManyAliases {
241 alias_count: metrics.alias_count,
242 max_aliases: self.config.max_aliases,
243 });
244 }
245
246 Ok(metrics)
247 }
248
249 /// Get the underlying configuration
250 #[must_use]
251 pub const fn config(&self) -> &QueryValidatorConfig {
252 &self.config
253 }
254}