fraiseql_core/security/
query_validator.rs1use serde::{Deserialize, Serialize};
50
51use crate::{
52 graphql::complexity::{ComplexityConfig, QueryMetrics, RequestValidator},
53 security::errors::{Result, SecurityError},
54};
55
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
60pub struct QueryValidatorConfig {
61 pub max_depth: usize,
63
64 pub max_complexity: usize,
66
67 pub max_size_bytes: usize,
69
70 pub max_aliases: usize,
72}
73
74impl QueryValidatorConfig {
75 #[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, max_aliases: 100,
88 }
89 }
90
91 #[must_use]
98 pub const fn standard() -> Self {
99 Self {
100 max_depth: 10,
101 max_complexity: 1000,
102 max_size_bytes: 256_000, max_aliases: 30,
104 }
105 }
106
107 #[must_use]
114 pub const fn strict() -> Self {
115 Self {
116 max_depth: 5,
117 max_complexity: 500,
118 max_size_bytes: 64_000, max_aliases: 10,
120 }
121 }
122}
123
124#[derive(Debug, Clone)]
133pub struct QueryValidator {
134 config: QueryValidatorConfig,
135}
136
137impl QueryValidator {
138 #[must_use]
140 pub const fn from_config(config: QueryValidatorConfig) -> Self {
141 Self { config }
142 }
143
144 #[must_use]
146 pub const fn permissive() -> Self {
147 Self::from_config(QueryValidatorConfig::permissive())
148 }
149
150 #[must_use]
152 pub const fn standard() -> Self {
153 Self::from_config(QueryValidatorConfig::standard())
154 }
155
156 #[must_use]
158 pub const fn strict() -> Self {
159 Self::from_config(QueryValidatorConfig::strict())
160 }
161
162 pub fn validate(&self, query: &str) -> Result<QueryMetrics> {
182 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 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 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 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 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 #[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)] use super::*;
240
241 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 #[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 #[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(); 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(); 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 #[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 #[test]
341 fn test_alias_amplification_rejected() {
342 let validator = QueryValidator::standard(); 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(); 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 #[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 #[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); 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}