Skip to main content

fraiseql_server/
validation.rs

1//! GraphQL request validation module.
2//!
3//! Provides validation for GraphQL queries including:
4//! - Query depth validation (prevent deeply nested queries)
5//! - Query complexity scoring (prevent complex queries)
6//! - Variable type validation (ensure variable types match schema)
7
8use serde_json::Value as JsonValue;
9use thiserror::Error;
10
11/// Validation error types.
12#[derive(Debug, Error, Clone)]
13pub enum ValidationError {
14    /// Query exceeds maximum allowed depth.
15    #[error("Query exceeds maximum depth of {max_depth}: depth = {actual_depth}")]
16    QueryTooDeep {
17        /// Maximum allowed depth
18        max_depth:    usize,
19        /// Actual query depth
20        actual_depth: usize,
21    },
22
23    /// Query exceeds maximum complexity score.
24    #[error("Query exceeds maximum complexity of {max_complexity}: score = {actual_complexity}")]
25    QueryTooComplex {
26        /// Maximum allowed complexity
27        max_complexity:    usize,
28        /// Actual query complexity
29        actual_complexity: usize,
30    },
31
32    /// Invalid query variables.
33    #[error("Invalid variables: {0}")]
34    InvalidVariables(String),
35
36    /// Malformed GraphQL query.
37    #[error("Malformed GraphQL query: {0}")]
38    MalformedQuery(String),
39}
40
41/// GraphQL request validator.
42#[derive(Debug, Clone)]
43pub struct RequestValidator {
44    /// Maximum query depth allowed.
45    max_depth:           usize,
46    /// Maximum query complexity score allowed.
47    max_complexity:      usize,
48    /// Enable query depth validation.
49    validate_depth:      bool,
50    /// Enable query complexity validation.
51    validate_complexity: bool,
52}
53
54impl RequestValidator {
55    /// Create a new validator with default settings.
56    #[must_use]
57    pub fn new() -> Self {
58        Self::default()
59    }
60
61    /// Set maximum query depth.
62    #[must_use]
63    pub fn with_max_depth(mut self, max_depth: usize) -> Self {
64        self.max_depth = max_depth;
65        self
66    }
67
68    /// Set maximum query complexity.
69    #[must_use]
70    pub fn with_max_complexity(mut self, max_complexity: usize) -> Self {
71        self.max_complexity = max_complexity;
72        self
73    }
74
75    /// Enable/disable depth validation.
76    #[must_use]
77    pub fn with_depth_validation(mut self, enabled: bool) -> Self {
78        self.validate_depth = enabled;
79        self
80    }
81
82    /// Enable/disable complexity validation.
83    #[must_use]
84    pub fn with_complexity_validation(mut self, enabled: bool) -> Self {
85        self.validate_complexity = enabled;
86        self
87    }
88
89    /// Validate a GraphQL query string.
90    ///
91    /// # Errors
92    ///
93    /// Returns `ValidationError` if the query violates any validation rules.
94    pub fn validate_query(&self, query: &str) -> Result<(), ValidationError> {
95        // Validate query is not empty
96        if query.trim().is_empty() {
97            return Err(ValidationError::MalformedQuery("Empty query".to_string()));
98        }
99
100        // Check depth if enabled
101        if self.validate_depth {
102            let depth = self.calculate_depth(query);
103            if depth > self.max_depth {
104                return Err(ValidationError::QueryTooDeep {
105                    max_depth:    self.max_depth,
106                    actual_depth: depth,
107                });
108            }
109        }
110
111        // Check complexity if enabled
112        if self.validate_complexity {
113            let complexity = self.calculate_complexity(query);
114            if complexity > self.max_complexity {
115                return Err(ValidationError::QueryTooComplex {
116                    max_complexity:    self.max_complexity,
117                    actual_complexity: complexity,
118                });
119            }
120        }
121
122        Ok(())
123    }
124
125    /// Validate variables JSON.
126    ///
127    /// # Errors
128    ///
129    /// Returns `ValidationError` if variables are invalid.
130    pub fn validate_variables(&self, variables: Option<&JsonValue>) -> Result<(), ValidationError> {
131        if let Some(vars) = variables {
132            // Validate that variables is an object
133            if !vars.is_object() {
134                return Err(ValidationError::InvalidVariables(
135                    "Variables must be an object".to_string(),
136                ));
137            }
138
139            // Validate variable values are not null (optional - can be configured)
140            // For now, just ensure it's valid JSON which it already is
141        }
142
143        Ok(())
144    }
145
146    /// Calculate query depth (max nesting level).
147    fn calculate_depth(&self, query: &str) -> usize {
148        let mut max_depth: usize = 0;
149        let mut current_depth: usize = 0;
150        let mut in_string = false;
151        let mut escape_next = false;
152
153        for ch in query.chars() {
154            if escape_next {
155                escape_next = false;
156                continue;
157            }
158
159            if ch == '\\' && in_string {
160                escape_next = true;
161                continue;
162            }
163
164            if ch == '"' {
165                in_string = !in_string;
166                continue;
167            }
168
169            if in_string {
170                continue;
171            }
172
173            match ch {
174                '{' => {
175                    current_depth += 1;
176                    max_depth = max_depth.max(current_depth);
177                },
178                '}' => {
179                    current_depth = current_depth.saturating_sub(1);
180                },
181                _ => {},
182            }
183        }
184
185        max_depth
186    }
187
188    /// Calculate query complexity score (heuristic).
189    fn calculate_complexity(&self, query: &str) -> usize {
190        let mut complexity = 0;
191        let mut in_string = false;
192        let mut escape_next = false;
193
194        for ch in query.chars() {
195            if escape_next {
196                escape_next = false;
197                continue;
198            }
199
200            if ch == '\\' && in_string {
201                escape_next = true;
202                continue;
203            }
204
205            if ch == '"' {
206                in_string = !in_string;
207                continue;
208            }
209
210            if in_string {
211                continue;
212            }
213
214            match ch {
215                '{' => complexity += 1,
216                '[' => complexity += 2, // Array selections cost more
217                '(' => complexity += 1, // Arguments
218                _ => {},
219            }
220        }
221
222        complexity
223    }
224}
225
226impl Default for RequestValidator {
227    fn default() -> Self {
228        Self {
229            max_depth:           10,
230            max_complexity:      100,
231            validate_depth:      true,
232            validate_complexity: true,
233        }
234    }
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240
241    #[test]
242    fn test_empty_query_validation() {
243        let validator = RequestValidator::new();
244        assert!(validator.validate_query("").is_err());
245        assert!(validator.validate_query("   ").is_err());
246    }
247
248    #[test]
249    fn test_query_depth_validation() {
250        let validator = RequestValidator::new().with_max_depth(3);
251
252        // Shallow query should pass
253        let shallow = "{ user { id } }";
254        assert!(validator.validate_query(shallow).is_ok());
255
256        // Deep query should fail
257        let deep = "{ user { profile { settings { theme } } } }";
258        assert!(validator.validate_query(deep).is_err());
259    }
260
261    #[test]
262    fn test_query_complexity_validation() {
263        let validator = RequestValidator::new().with_max_complexity(5);
264
265        // Simple query should pass
266        let simple = "{ user { id name } }";
267        assert!(validator.validate_query(simple).is_ok());
268
269        // Complex query should fail (many nested fields and array selections)
270        let complex = "{ user [ id name email [ tags [ name ] ] profile { bio avatar [ url size ] settings { theme notifications } } ] }";
271        assert!(validator.validate_query(complex).is_err());
272    }
273
274    #[test]
275    fn test_variables_validation() {
276        let validator = RequestValidator::new();
277
278        // Valid variables object
279        let valid = serde_json::json!({"id": "123", "name": "John"});
280        assert!(validator.validate_variables(Some(&valid)).is_ok());
281
282        // No variables
283        assert!(validator.validate_variables(None).is_ok());
284
285        // Invalid: variables is not an object
286        let invalid = serde_json::json!([1, 2, 3]);
287        assert!(validator.validate_variables(Some(&invalid)).is_err());
288    }
289
290    #[test]
291    fn test_depth_calculation_with_strings() {
292        let validator = RequestValidator::new();
293
294        // Query with string containing braces should not affect depth
295        let query = r#"{ user { description: "Has { and }" } }"#;
296        let depth = validator.calculate_depth(query);
297        assert_eq!(depth, 2);
298    }
299
300    #[test]
301    fn test_disable_validation() {
302        let validator = RequestValidator::new()
303            .with_depth_validation(false)
304            .with_complexity_validation(false)
305            .with_max_depth(1)
306            .with_max_complexity(1);
307
308        // Even very deep query should pass when validation is disabled
309        let deep = "{ a { b { c { d { e { f } } } } } }";
310        assert!(validator.validate_query(deep).is_ok());
311    }
312}