Skip to main content

fraiseql_core/validation/
input_processor.rs

1//! Input processor for GraphQL variables with ID policy validation
2//!
3//! This module provides utilities to validate GraphQL input variables,
4//! particularly ID fields, according to the configured ID policy.
5//!
6//! **SECURITY CRITICAL**: Input validation is a critical security layer that
7//! prevents invalid data from propagating through the GraphQL pipeline.
8
9use std::collections::HashSet;
10
11use serde_json::{Map, Value};
12
13use super::id_policy::{IDPolicy, validate_id};
14
15/// Configuration for input processing
16#[derive(Debug, Clone)]
17pub struct InputProcessingConfig {
18    /// ID policy to enforce for ID fields
19    pub id_policy: IDPolicy,
20
21    /// Enable ID validation on all inputs (recommended)
22    pub validate_ids: bool,
23
24    /// List of field names known to be ID types
25    /// (in a real implementation, this would come from the schema)
26    pub id_field_names: HashSet<String>,
27}
28
29impl Default for InputProcessingConfig {
30    fn default() -> Self {
31        Self {
32            id_policy:      IDPolicy::default(),
33            validate_ids:   true,
34            id_field_names: Self::default_id_field_names(),
35        }
36    }
37}
38
39impl InputProcessingConfig {
40    /// Get default set of common ID field names
41    fn default_id_field_names() -> HashSet<String> {
42        [
43            "id",
44            "userId",
45            "user_id",
46            "postId",
47            "post_id",
48            "commentId",
49            "comment_id",
50            "authorId",
51            "author_id",
52            "ownerId",
53            "owner_id",
54            "creatorId",
55            "creator_id",
56            "tenantId",
57            "tenant_id",
58        ]
59        .iter()
60        .map(|s| (*s).to_string())
61        .collect()
62    }
63
64    /// Add a custom ID field name to validation
65    pub fn add_id_field(&mut self, field_name: String) {
66        self.id_field_names.insert(field_name);
67    }
68
69    /// Create a configuration for strict UUID validation
70    #[must_use]
71    pub fn strict_uuid() -> Self {
72        Self {
73            id_policy:      IDPolicy::UUID,
74            validate_ids:   true,
75            id_field_names: Self::default_id_field_names(),
76        }
77    }
78
79    /// Create a configuration for opaque IDs (GraphQL spec compliant)
80    #[must_use]
81    pub fn opaque() -> Self {
82        Self {
83            id_policy:      IDPolicy::OPAQUE,
84            validate_ids:   false, // No validation needed for opaque
85            id_field_names: Self::default_id_field_names(),
86        }
87    }
88}
89
90/// Process and validate GraphQL input variables
91///
92/// **SECURITY CRITICAL**: This validates all ID fields in input variables
93/// according to the configured ID policy.
94///
95/// # Arguments
96///
97/// * `variables` - GraphQL operation variables (JSON object)
98/// * `config` - Input processing configuration
99///
100/// # Returns
101///
102/// `Ok(processed_variables)` with validated data, or
103/// `Err(ProcessingError)` if validation fails
104///
105/// # Errors
106///
107/// Returns `ProcessingError` if any ID field fails validation according to the configured policy.
108///
109/// # Examples
110///
111/// ```
112/// use fraiseql_core::validation::{InputProcessingConfig, process_variables};
113/// use serde_json::json;
114///
115/// let config = InputProcessingConfig::strict_uuid();
116/// let variables = json!({"userId": "550e8400-e29b-41d4-a716-446655440000"});
117///
118/// match process_variables(&variables, &config) {
119///     Ok(processed) => { /* Use processed variables */ },
120///     Err(_e) => { /* Handle validation error */ },
121/// }
122/// ```
123pub fn process_variables(
124    variables: &Value,
125    config: &InputProcessingConfig,
126) -> Result<Value, ProcessingError> {
127    if !config.validate_ids {
128        return Ok(variables.clone());
129    }
130
131    match variables {
132        Value::Object(obj) => {
133            let mut result = Map::new();
134
135            for (key, value) in obj {
136                let processed_value = process_value(value, config, key)?;
137                result.insert(key.clone(), processed_value);
138            }
139
140            Ok(Value::Object(result))
141        },
142        Value::Null => Ok(Value::Null),
143        other => Ok(other.clone()),
144    }
145}
146
147/// Process a single JSON value, validating ID fields
148fn process_value(
149    value: &Value,
150    config: &InputProcessingConfig,
151    field_name: &str,
152) -> Result<Value, ProcessingError> {
153    match value {
154        // Validate ID string fields
155        // Extract base field name (before array indices like [0])
156        Value::String(s)
157            if {
158                let base_field = field_name.split('[').next().unwrap_or(field_name);
159                config.id_field_names.contains(base_field)
160            } =>
161        {
162            validate_id(s, config.id_policy).map_err(|e| ProcessingError {
163                field_path: field_name.to_string(),
164                reason:     format!("Invalid ID value: {e}"),
165            })?;
166            Ok(Value::String(s.clone()))
167        },
168
169        // Recursively process nested objects
170        Value::Object(obj) => {
171            let mut result = Map::new();
172
173            for (key, nested_value) in obj {
174                let processed = process_value(nested_value, config, key)?;
175                result.insert(key.clone(), processed);
176            }
177
178            Ok(Value::Object(result))
179        },
180
181        // Process array items
182        Value::Array(arr) => {
183            let processed_items: Result<Vec<_>, _> = arr
184                .iter()
185                .enumerate()
186                .map(|(idx, item)| {
187                    let array_field = format!("{field_name}[{idx}]");
188                    process_value(item, config, &array_field)
189                })
190                .collect();
191
192            Ok(Value::Array(processed_items?))
193        },
194
195        // Pass through other values unchanged
196        other => Ok(other.clone()),
197    }
198}
199
200/// Error type for input processing failures
201#[derive(Debug, Clone, PartialEq, Eq)]
202pub struct ProcessingError {
203    /// The field path where the error occurred
204    pub field_path: String,
205    /// The reason for the error
206    pub reason:     String,
207}
208
209impl std::fmt::Display for ProcessingError {
210    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
211        write!(f, "Error in field '{}': {}", self.field_path, self.reason)
212    }
213}
214
215impl std::error::Error for ProcessingError {}