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        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}