Skip to main content

oxirs_gql/
validation.rs

1//! GraphQL query validation and security features
2//!
3//! This module provides comprehensive validation for GraphQL queries, including
4//! security features like depth limiting, complexity analysis, and schema validation.
5
6use crate::ast::{
7    Definition, Document, Field, OperationDefinition, Selection, SelectionSet, Value,
8    VariableDefinition,
9};
10use crate::types::{GraphQLType, Schema};
11use anyhow::{anyhow, Result};
12use std::collections::{HashMap, HashSet};
13use std::time::Duration;
14
15/// Configuration for query validation and security
16#[derive(Debug, Clone)]
17pub struct ValidationConfig {
18    /// Maximum allowed query depth
19    pub max_depth: usize,
20    /// Maximum allowed query complexity score
21    pub max_complexity: usize,
22    /// Maximum number of aliases allowed
23    pub max_aliases: usize,
24    /// Maximum number of root fields
25    pub max_root_fields: usize,
26    /// Enable query timeout
27    pub query_timeout: Option<Duration>,
28    /// Disabled introspection queries
29    pub disable_introspection: bool,
30    /// Maximum number of fragments
31    pub max_fragments: usize,
32    /// Whitelist of allowed operation names
33    pub allowed_operations: Option<HashSet<String>>,
34    /// Blacklist of forbidden field names
35    pub forbidden_fields: HashSet<String>,
36    /// Enable cost analysis
37    pub enable_cost_analysis: bool,
38}
39
40impl Default for ValidationConfig {
41    fn default() -> Self {
42        Self {
43            max_depth: 10,
44            max_complexity: 1000,
45            max_aliases: 50,
46            max_root_fields: 20,
47            query_timeout: Some(Duration::from_secs(30)),
48            disable_introspection: false,
49            max_fragments: 50,
50            allowed_operations: None,
51            forbidden_fields: HashSet::new(),
52            enable_cost_analysis: true,
53        }
54    }
55}
56
57/// Result of query validation
58#[derive(Debug, Clone)]
59pub struct ValidationResult {
60    pub is_valid: bool,
61    pub errors: Vec<ValidationError>,
62    pub warnings: Vec<ValidationWarning>,
63    pub complexity_score: usize,
64    pub depth: usize,
65}
66
67impl Default for ValidationResult {
68    fn default() -> Self {
69        Self::new()
70    }
71}
72
73impl ValidationResult {
74    pub fn new() -> Self {
75        Self {
76            is_valid: true,
77            errors: Vec::new(),
78            warnings: Vec::new(),
79            complexity_score: 0,
80            depth: 0,
81        }
82    }
83
84    pub fn with_error(mut self, error: ValidationError) -> Self {
85        self.is_valid = false;
86        self.errors.push(error);
87        self
88    }
89
90    pub fn with_warning(mut self, warning: ValidationWarning) -> Self {
91        self.warnings.push(warning);
92        self
93    }
94
95    pub fn with_complexity(mut self, complexity: usize) -> Self {
96        self.complexity_score = complexity;
97        self
98    }
99
100    pub fn with_depth(mut self, depth: usize) -> Self {
101        self.depth = depth;
102        self
103    }
104}
105
106/// Validation error details
107#[derive(Debug, Clone)]
108pub struct ValidationError {
109    pub message: String,
110    pub path: Vec<String>,
111    pub rule: ValidationRule,
112}
113
114impl ValidationError {
115    pub fn new(message: String, rule: ValidationRule) -> Self {
116        Self {
117            message,
118            path: Vec::new(),
119            rule,
120        }
121    }
122
123    pub fn with_path(mut self, path: Vec<String>) -> Self {
124        self.path = path;
125        self
126    }
127}
128
129/// Validation warning details
130#[derive(Debug, Clone)]
131pub struct ValidationWarning {
132    pub message: String,
133    pub suggestion: Option<String>,
134}
135
136impl ValidationWarning {
137    pub fn new(message: String) -> Self {
138        Self {
139            message,
140            suggestion: None,
141        }
142    }
143
144    pub fn with_suggestion(mut self, suggestion: String) -> Self {
145        self.suggestion = Some(suggestion);
146        self
147    }
148}
149
150/// Types of validation rules
151#[derive(Debug, Clone, PartialEq)]
152pub enum ValidationRule {
153    MaxDepth,
154    MaxComplexity,
155    MaxAliases,
156    MaxRootFields,
157    MaxFragments,
158    FieldValidation,
159    TypeValidation,
160    VariableValidation,
161    FragmentValidation,
162    IntrospectionDisabled,
163    OperationNotAllowed,
164    ForbiddenField,
165    CostAnalysis,
166    /// GraphQL spec rule: see SpecRule for the specific rule
167    Spec(SpecRule),
168}
169
170/// GraphQL June 2018 specification validation rules
171#[derive(Debug, Clone, PartialEq)]
172pub enum SpecRule {
173    ExecutableDefinitions,
174    UniqueOperationNames,
175    LoneAnonymousOperation,
176    SingleFieldSubscriptions,
177    KnownTypeNames,
178    FragmentsOnCompositeTypes,
179    VariablesAreInputTypes,
180    LeafFieldSelections,
181    FieldsInSetCanMerge,
182    UniqueFragmentNames,
183    KnownFragmentNames,
184    NoUnusedFragments,
185    PossibleFragmentSpreads,
186    NoFragmentCycles,
187    UniqueVariableNames,
188    NoUndefinedVariables,
189    NoUnusedVariables,
190    KnownDirectives,
191    UniqueDirectivesPerLocation,
192    KnownArgumentNames,
193    UniqueArgumentNames,
194    ValuesOfCorrectType,
195    ProvidedRequiredArguments,
196    VariablesInAllowedPosition,
197    OverlappingFieldsCanBeMerged,
198}
199
200/// Main query validator
201pub struct QueryValidator {
202    config: ValidationConfig,
203    schema: Schema,
204}
205
206impl QueryValidator {
207    pub fn new(config: ValidationConfig, schema: Schema) -> Self {
208        Self { config, schema }
209    }
210
211    /// Validate a GraphQL document
212    pub fn validate(&self, document: &Document) -> Result<ValidationResult> {
213        let mut result = ValidationResult::new();
214        let mut validation_context = ValidationContext::new(&self.schema);
215
216        // Collect fragments first
217        for definition in &document.definitions {
218            if let Definition::Fragment(fragment) = definition {
219                validation_context.add_fragment(fragment.name.clone(), fragment.clone());
220            }
221        }
222
223        // Validate each operation
224        for definition in &document.definitions {
225            if let Definition::Operation(operation) = definition {
226                result = self.validate_operation(operation, &validation_context, result)?;
227            }
228        }
229
230        // Global validations
231        result = self.validate_fragments(&validation_context, result)?;
232        result = self.validate_global_limits(document, result)?;
233
234        Ok(result)
235    }
236
237    fn validate_operation(
238        &self,
239        operation: &OperationDefinition,
240        context: &ValidationContext,
241        mut result: ValidationResult,
242    ) -> Result<ValidationResult> {
243        // Check operation name whitelist
244        if let Some(ref allowed_ops) = self.config.allowed_operations {
245            if let Some(ref op_name) = operation.name {
246                if !allowed_ops.contains(op_name) {
247                    return Ok(result.with_error(ValidationError::new(
248                        format!("Operation '{op_name}' is not allowed"),
249                        ValidationRule::OperationNotAllowed,
250                    )));
251                }
252            }
253        }
254
255        // Validate variables
256        result = self.validate_variables(&operation.variable_definitions, context, result)?;
257
258        // Get root type based on operation type
259        let root_type_name = match operation.operation_type {
260            crate::ast::OperationType::Query => self
261                .schema
262                .query_type
263                .as_ref()
264                .ok_or_else(|| anyhow!("Schema has no query type"))?,
265            crate::ast::OperationType::Mutation => self
266                .schema
267                .mutation_type
268                .as_ref()
269                .ok_or_else(|| anyhow!("Schema has no mutation type"))?,
270            crate::ast::OperationType::Subscription => self
271                .schema
272                .subscription_type
273                .as_ref()
274                .ok_or_else(|| anyhow!("Schema has no subscription type"))?,
275        };
276
277        // Validate selection set
278        let (depth, complexity) = self.validate_selection_set(
279            &operation.selection_set,
280            root_type_name,
281            context,
282            0,
283            Vec::new(),
284        )?;
285
286        result.depth = depth.max(result.depth);
287        result.complexity_score += complexity;
288
289        // Check depth limit
290        if result.depth > self.config.max_depth {
291            let current_depth = result.depth;
292            result = result.with_error(ValidationError::new(
293                format!(
294                    "Query depth {} exceeds maximum allowed depth {}",
295                    current_depth, self.config.max_depth
296                ),
297                ValidationRule::MaxDepth,
298            ));
299        }
300
301        // Check complexity limit
302        if result.complexity_score > self.config.max_complexity {
303            let current_complexity = result.complexity_score;
304            result = result.with_error(ValidationError::new(
305                format!(
306                    "Query complexity {} exceeds maximum allowed complexity {}",
307                    current_complexity, self.config.max_complexity
308                ),
309                ValidationRule::MaxComplexity,
310            ));
311        }
312
313        Ok(result)
314    }
315
316    fn validate_selection_set(
317        &self,
318        selection_set: &SelectionSet,
319        parent_type_name: &str,
320        context: &ValidationContext,
321        current_depth: usize,
322        path: Vec<String>,
323    ) -> Result<(usize, usize)> {
324        let mut max_depth = current_depth;
325        let mut total_complexity = 0;
326        let mut alias_count = 0;
327
328        let parent_type = self
329            .schema
330            .get_type(parent_type_name)
331            .ok_or_else(|| anyhow!("Type '{}' not found in schema", parent_type_name))?;
332
333        for selection in &selection_set.selections {
334            match selection {
335                Selection::Field(field) => {
336                    // Check for aliases
337                    if field.alias.is_some() {
338                        alias_count += 1;
339                    }
340
341                    // Check forbidden fields
342                    if self.config.forbidden_fields.contains(&field.name) {
343                        return Err(anyhow!("Field '{}' is forbidden", field.name));
344                    }
345
346                    // Check introspection fields
347                    if self.config.disable_introspection && field.name.starts_with("__") {
348                        return Err(anyhow!("Introspection is disabled"));
349                    }
350
351                    // Validate field exists on type
352                    let field_type = self.get_field_type(parent_type, &field.name)?;
353
354                    let mut field_path = path.clone();
355                    field_path.push(field.alias.as_ref().unwrap_or(&field.name).clone());
356
357                    // Calculate field complexity
358                    let field_complexity = self.calculate_field_complexity(field);
359                    total_complexity += field_complexity;
360
361                    // Recurse into nested selection sets
362                    if let Some(ref nested_selection_set) = field.selection_set {
363                        let inner_type_name = self.get_inner_type_name(field_type);
364                        let (nested_depth, nested_complexity) = self.validate_selection_set(
365                            nested_selection_set,
366                            &inner_type_name,
367                            context,
368                            current_depth + 1,
369                            field_path,
370                        )?;
371                        max_depth = max_depth.max(nested_depth);
372                        total_complexity += nested_complexity;
373                    }
374                }
375                Selection::InlineFragment(inline_fragment) => {
376                    let fragment_type =
377                        if let Some(ref type_condition) = inline_fragment.type_condition {
378                            type_condition
379                        } else {
380                            parent_type_name
381                        };
382
383                    let (nested_depth, nested_complexity) = self.validate_selection_set(
384                        &inline_fragment.selection_set,
385                        fragment_type,
386                        context,
387                        current_depth,
388                        path.clone(),
389                    )?;
390                    max_depth = max_depth.max(nested_depth);
391                    total_complexity += nested_complexity;
392                }
393                Selection::FragmentSpread(fragment_spread) => {
394                    if let Some(fragment_def) = context.get_fragment(&fragment_spread.fragment_name)
395                    {
396                        let (nested_depth, nested_complexity) = self.validate_selection_set(
397                            &fragment_def.selection_set,
398                            &fragment_def.type_condition,
399                            context,
400                            current_depth,
401                            path.clone(),
402                        )?;
403                        max_depth = max_depth.max(nested_depth);
404                        total_complexity += nested_complexity;
405                    } else {
406                        return Err(anyhow!(
407                            "Fragment '{}' not found",
408                            fragment_spread.fragment_name
409                        ));
410                    }
411                }
412            }
413        }
414
415        // Check alias limit
416        if alias_count > self.config.max_aliases {
417            return Err(anyhow!(
418                "Too many aliases: {} exceeds limit {}",
419                alias_count,
420                self.config.max_aliases
421            ));
422        }
423
424        Ok((max_depth, total_complexity))
425    }
426
427    fn validate_variables(
428        &self,
429        variable_definitions: &[VariableDefinition],
430        _context: &ValidationContext,
431        mut result: ValidationResult,
432    ) -> Result<ValidationResult> {
433        for var_def in variable_definitions {
434            // Validate variable type exists in schema
435            if !self.type_exists(&var_def.type_) {
436                result = result.with_error(ValidationError::new(
437                    format!(
438                        "Variable type '{}' not found in schema",
439                        var_def.type_.name()
440                    ),
441                    ValidationRule::VariableValidation,
442                ));
443            }
444
445            // Validate default value compatibility
446            if let Some(ref default_value) = var_def.default_value {
447                if !self.is_value_compatible_with_type(default_value, &var_def.type_) {
448                    result = result.with_error(ValidationError::new(
449                        format!(
450                            "Default value for variable '{}' is not compatible with type '{}'",
451                            var_def.variable.name,
452                            var_def.type_.name()
453                        ),
454                        ValidationRule::VariableValidation,
455                    ));
456                }
457            }
458        }
459
460        Ok(result)
461    }
462
463    fn validate_fragments(
464        &self,
465        context: &ValidationContext,
466        mut result: ValidationResult,
467    ) -> Result<ValidationResult> {
468        if context.fragments.len() > self.config.max_fragments {
469            result = result.with_error(ValidationError::new(
470                format!(
471                    "Too many fragments: {} exceeds limit {}",
472                    context.fragments.len(),
473                    self.config.max_fragments
474                ),
475                ValidationRule::MaxFragments,
476            ));
477        }
478
479        // Validate fragment type conditions
480        for (fragment_name, fragment) in &context.fragments {
481            if !self.schema.types.contains_key(&fragment.type_condition) {
482                result = result.with_error(ValidationError::new(
483                    format!(
484                        "Fragment '{}' has unknown type condition '{}'",
485                        fragment_name, fragment.type_condition
486                    ),
487                    ValidationRule::FragmentValidation,
488                ));
489            }
490        }
491
492        Ok(result)
493    }
494
495    fn validate_global_limits(
496        &self,
497        document: &Document,
498        mut result: ValidationResult,
499    ) -> Result<ValidationResult> {
500        let mut root_field_count = 0;
501
502        for definition in &document.definitions {
503            if let Definition::Operation(operation) = definition {
504                root_field_count += operation.selection_set.selections.len();
505            }
506        }
507
508        if root_field_count > self.config.max_root_fields {
509            result = result.with_error(ValidationError::new(
510                format!(
511                    "Too many root fields: {} exceeds limit {}",
512                    root_field_count, self.config.max_root_fields
513                ),
514                ValidationRule::MaxRootFields,
515            ));
516        }
517
518        Ok(result)
519    }
520
521    fn get_field_type<'a>(
522        &self,
523        parent_type: &'a GraphQLType,
524        field_name: &str,
525    ) -> Result<&'a GraphQLType> {
526        match parent_type {
527            GraphQLType::Object(obj) => obj
528                .fields
529                .get(field_name)
530                .map(|field| &field.field_type)
531                .ok_or_else(|| {
532                    anyhow!(
533                        "Field '{}' not found on object type '{}'",
534                        field_name,
535                        obj.name
536                    )
537                }),
538            GraphQLType::Interface(iface) => iface
539                .fields
540                .get(field_name)
541                .map(|field| &field.field_type)
542                .ok_or_else(|| {
543                    anyhow!(
544                        "Field '{}' not found on interface type '{}'",
545                        field_name,
546                        iface.name
547                    )
548                }),
549            _ => Err(anyhow!(
550                "Cannot select field '{}' on non-composite type",
551                field_name
552            )),
553        }
554    }
555
556    #[allow(clippy::only_used_in_recursion)]
557    fn get_inner_type_name(&self, graphql_type: &GraphQLType) -> String {
558        match graphql_type {
559            GraphQLType::NonNull(inner) => self.get_inner_type_name(inner),
560            GraphQLType::List(inner) => self.get_inner_type_name(inner),
561            _ => graphql_type.name().to_string(),
562        }
563    }
564
565    fn calculate_field_complexity(&self, field: &Field) -> usize {
566        if !self.config.enable_cost_analysis {
567            return 1;
568        }
569
570        let mut complexity = 1;
571
572        // Add complexity for arguments
573        complexity += field.arguments.len();
574
575        // Add complexity for nested selections
576        if let Some(ref selection_set) = field.selection_set {
577            complexity += selection_set.selections.len();
578        }
579
580        // Special cases for expensive operations
581        match field.name.as_str() {
582            "sparql" => complexity *= 10, // Raw SPARQL queries are expensive
583            name if name.contains("search") => complexity *= 3,
584            name if name.contains("aggregate") => complexity *= 5,
585            _ => {}
586        }
587
588        complexity
589    }
590
591    fn type_exists(&self, ast_type: &crate::ast::Type) -> bool {
592        match ast_type {
593            crate::ast::Type::NamedType(name) => {
594                self.schema.types.contains_key(name)
595                    || matches!(name.as_str(), "String" | "Int" | "Float" | "Boolean" | "ID")
596            }
597            crate::ast::Type::ListType(inner) => self.type_exists(inner),
598            crate::ast::Type::NonNullType(inner) => self.type_exists(inner),
599        }
600    }
601
602    #[allow(clippy::only_used_in_recursion)]
603    fn is_value_compatible_with_type(&self, value: &Value, ast_type: &crate::ast::Type) -> bool {
604        match (value, ast_type) {
605            (Value::NullValue, crate::ast::Type::NonNullType(_)) => false,
606            (Value::NullValue, _) => true,
607            (Value::StringValue(_), crate::ast::Type::NamedType(name)) => {
608                matches!(name.as_str(), "String" | "ID")
609            }
610            (Value::IntValue(_), crate::ast::Type::NamedType(name)) => {
611                matches!(name.as_str(), "Int" | "ID")
612            }
613            (Value::FloatValue(_), crate::ast::Type::NamedType(name)) => {
614                matches!(name.as_str(), "Float")
615            }
616            (Value::BooleanValue(_), crate::ast::Type::NamedType(name)) => {
617                matches!(name.as_str(), "Boolean")
618            }
619            (Value::ListValue(list), crate::ast::Type::ListType(inner_type)) => list
620                .iter()
621                .all(|item| self.is_value_compatible_with_type(item, inner_type)),
622            (_, crate::ast::Type::NonNullType(inner)) => {
623                self.is_value_compatible_with_type(value, inner)
624            }
625            _ => false,
626        }
627    }
628}
629
630/// Context for validation operations
631struct ValidationContext {
632    #[allow(dead_code)]
633    schema: Schema,
634    fragments: HashMap<String, crate::ast::FragmentDefinition>,
635}
636
637impl ValidationContext {
638    fn new(schema: &Schema) -> Self {
639        Self {
640            schema: schema.clone(),
641            fragments: HashMap::new(),
642        }
643    }
644
645    fn add_fragment(&mut self, name: String, fragment: crate::ast::FragmentDefinition) {
646        self.fragments.insert(name, fragment);
647    }
648
649    fn get_fragment(&self, name: &str) -> Option<&crate::ast::FragmentDefinition> {
650        self.fragments.get(name)
651    }
652}
653
654/// Rate limiting for query validation
655#[derive(Debug, Clone)]
656pub struct RateLimitConfig {
657    /// Maximum queries per minute per client
658    pub max_queries_per_minute: usize,
659    /// Maximum complexity per minute per client
660    pub max_complexity_per_minute: usize,
661    /// Time window for rate limiting
662    pub window_duration: Duration,
663}
664
665impl Default for RateLimitConfig {
666    fn default() -> Self {
667        Self {
668            max_queries_per_minute: 60,
669            max_complexity_per_minute: 10000,
670            window_duration: Duration::from_secs(60),
671        }
672    }
673}
674
675#[cfg(test)]
676mod tests {
677    use super::*;
678    use crate::types::{BuiltinScalars, FieldType, ObjectType};
679
680    fn create_test_schema() -> Schema {
681        let mut schema = Schema::new();
682
683        let query_type = ObjectType::new("Query".to_string())
684            .with_field(
685                "hello".to_string(),
686                FieldType::new(
687                    "hello".to_string(),
688                    GraphQLType::Scalar(BuiltinScalars::string()),
689                ),
690            )
691            .with_field(
692                "__schema".to_string(),
693                FieldType::new(
694                    "__schema".to_string(),
695                    GraphQLType::Scalar(BuiltinScalars::string()),
696                ),
697            );
698
699        schema.add_type(GraphQLType::Object(query_type));
700        schema.set_query_type("Query".to_string());
701
702        schema
703    }
704
705    #[test]
706    fn test_validation_config_default() {
707        let config = ValidationConfig::default();
708        assert_eq!(config.max_depth, 10);
709        assert_eq!(config.max_complexity, 1000);
710        assert!(!config.disable_introspection);
711    }
712
713    #[test]
714    fn test_validation_result_creation() {
715        let result = ValidationResult::new()
716            .with_error(ValidationError::new(
717                "Test error".to_string(),
718                ValidationRule::MaxDepth,
719            ))
720            .with_warning(ValidationWarning::new("Test warning".to_string()))
721            .with_complexity(100)
722            .with_depth(5);
723
724        assert!(!result.is_valid);
725        assert_eq!(result.errors.len(), 1);
726        assert_eq!(result.warnings.len(), 1);
727        assert_eq!(result.complexity_score, 100);
728        assert_eq!(result.depth, 5);
729    }
730
731    #[test]
732    fn test_query_validator_creation() {
733        let config = ValidationConfig::default();
734        let schema = create_test_schema();
735        let validator = QueryValidator::new(config, schema);
736
737        // Validator should be created successfully
738        assert_eq!(validator.config.max_depth, 10);
739    }
740
741    #[test]
742    fn test_validation_error_with_path() {
743        let error = ValidationError::new("Test error".to_string(), ValidationRule::FieldValidation)
744            .with_path(vec![
745                "query".to_string(),
746                "user".to_string(),
747                "name".to_string(),
748            ]);
749
750        assert_eq!(error.message, "Test error");
751        assert_eq!(error.path, vec!["query", "user", "name"]);
752        assert_eq!(error.rule, ValidationRule::FieldValidation);
753    }
754
755    #[test]
756    fn test_validation_warning_with_suggestion() {
757        let warning = ValidationWarning::new("Performance warning".to_string())
758            .with_suggestion("Consider using pagination".to_string());
759
760        assert_eq!(warning.message, "Performance warning");
761        assert_eq!(
762            warning.suggestion,
763            Some("Consider using pagination".to_string())
764        );
765    }
766
767    #[test]
768    fn test_rate_limit_config_default() {
769        let config = RateLimitConfig::default();
770        assert_eq!(config.max_queries_per_minute, 60);
771        assert_eq!(config.max_complexity_per_minute, 10000);
772        assert_eq!(config.window_duration, Duration::from_secs(60));
773    }
774
775    #[test]
776    fn test_validation_context() {
777        let schema = create_test_schema();
778        let mut context = ValidationContext::new(&schema);
779
780        let fragment = crate::ast::FragmentDefinition {
781            name: "TestFragment".to_string(),
782            type_condition: "Query".to_string(),
783            selection_set: crate::ast::SelectionSet { selections: vec![] },
784            directives: vec![],
785        };
786
787        context.add_fragment("TestFragment".to_string(), fragment.clone());
788
789        let retrieved = context.get_fragment("TestFragment");
790        assert!(retrieved.is_some());
791        assert_eq!(retrieved.expect("should succeed").name, "TestFragment");
792    }
793}