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}