Skip to main content

iam_rs/validation/
validator.rs

1use std::fmt;
2
3/// Validation error types for IAM policies
4#[derive(Debug, Clone, PartialEq, Eq)]
5#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
6pub enum ValidationError {
7    /// Empty or missing required field
8    MissingField { field: String, context: String },
9    /// Invalid field value
10    InvalidValue {
11        field: String,
12        value: String,
13        reason: String,
14    },
15    /// Logical inconsistency in policy
16    LogicalError { message: String },
17    /// ARN format error
18    InvalidArn { arn: String, reason: String },
19    /// Condition operator/value mismatch
20    InvalidCondition {
21        operator: String,
22        key: String,
23        reason: String,
24    },
25    /// Principal format error
26    InvalidPrincipal { principal: String, reason: String },
27    /// Action format error
28    InvalidAction { action: String, reason: String },
29    /// Resource format error
30    InvalidResource { resource: String, reason: String },
31    /// Multiple validation errors
32    Multiple(Vec<ValidationError>),
33}
34
35impl fmt::Display for ValidationError {
36    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37        match self {
38            ValidationError::MissingField { field, context } => {
39                write!(f, "Missing required field '{field}' in {context}")
40            }
41            ValidationError::InvalidValue {
42                field,
43                value,
44                reason,
45            } => {
46                write!(f, "Invalid value '{value}' for field '{field}': {reason}")
47            }
48            ValidationError::LogicalError { message } => {
49                write!(f, "Logical error: {message}")
50            }
51            ValidationError::InvalidArn { arn, reason } => {
52                write!(f, "Invalid ARN '{arn}': {reason}")
53            }
54            ValidationError::InvalidCondition {
55                operator,
56                key,
57                reason,
58            } => {
59                write!(
60                    f,
61                    "Invalid condition '{operator}' for key '{key}': {reason}"
62                )
63            }
64            ValidationError::InvalidPrincipal { principal, reason } => {
65                write!(f, "Invalid principal '{principal}': {reason}")
66            }
67            ValidationError::InvalidAction { action, reason } => {
68                write!(f, "Invalid action '{action}': {reason}")
69            }
70            ValidationError::InvalidResource { resource, reason } => {
71                write!(f, "Invalid resource '{resource}': {reason}")
72            }
73            ValidationError::Multiple(errors) => {
74                writeln!(f, "Multiple validation errors:")?;
75                for (i, error) in errors.iter().enumerate() {
76                    writeln!(f, "  {}: {error}", i + 1)?;
77                }
78                Ok(())
79            }
80        }
81    }
82}
83
84impl std::error::Error for ValidationError {}
85
86/// Result type for validation operations
87pub type ValidationResult = Result<(), ValidationError>;
88
89/// Validation context for tracking nested validation
90#[derive(Debug, Clone)]
91pub struct ValidationContext {
92    pub path: Vec<String>,
93}
94
95impl Default for ValidationContext {
96    fn default() -> Self {
97        Self::new()
98    }
99}
100
101impl ValidationContext {
102    #[must_use]
103    pub fn new() -> Self {
104        Self { path: Vec::new() }
105    }
106
107    pub fn push(&mut self, segment: &str) {
108        self.path.push(segment.to_string());
109    }
110
111    pub fn pop(&mut self) {
112        self.path.pop();
113    }
114
115    #[must_use]
116    pub fn current_path(&self) -> String {
117        if self.path.is_empty() {
118            "root".to_string()
119        } else {
120            self.path.join(".")
121        }
122    }
123
124    pub fn with_segment<T>(&mut self, segment: &str, f: impl FnOnce(&mut Self) -> T) -> T {
125        self.push(segment);
126        let result = f(self);
127        self.pop();
128        result
129    }
130}
131
132/// Trait for validating IAM policy components
133/// All validation is strict and enforces high quality standards
134pub trait Validate {
135    /// Validate the component within the given context
136    ///
137    /// # Errors
138    ///
139    /// Returns `ValidationError` if the component is invalid
140    fn validate(&self, context: &mut ValidationContext) -> ValidationResult;
141
142    /// Convenience method for basic validation
143    fn is_valid(&self) -> bool {
144        let mut context = ValidationContext::new();
145        self.validate(&mut context).is_ok()
146    }
147
148    /// Validate with detailed errors (same as regular validation)
149    ///
150    /// # Errors
151    ///
152    /// Returns `ValidationError` if the component is invalid
153    fn validate_result(&self) -> ValidationResult {
154        let mut context = ValidationContext::new();
155        self.validate(&mut context)
156    }
157}
158
159/// Helper functions for common validation patterns
160pub(crate) mod helpers {
161    use super::{ValidationContext, ValidationError, ValidationResult};
162    use crate::core::Arn;
163
164    /// Validate that a string is not empty
165    pub fn validate_non_empty(
166        value: &str,
167        field_name: &str,
168        context: &ValidationContext,
169    ) -> ValidationResult {
170        if value.is_empty() {
171            Err(ValidationError::MissingField {
172                field: field_name.to_string(),
173                context: context.current_path(),
174            })
175        } else {
176            Ok(())
177        }
178    }
179
180    /// Validate action format (service:action)
181    pub fn validate_action(action: &str, _context: &ValidationContext) -> ValidationResult {
182        if action == "*" {
183            return Ok(());
184        }
185
186        if action.contains(':') {
187            let parts: Vec<&str> = action.split(':').collect();
188            if parts.len() == 2 && !parts[0].is_empty() && !parts[1].is_empty() {
189                // Basic service:action format
190                Ok(())
191            } else {
192                Err(ValidationError::InvalidAction {
193                    action: action.to_string(),
194                    reason: "Action must be in format 'service:action'".to_string(),
195                })
196            }
197        } else {
198            Err(ValidationError::InvalidAction {
199                action: action.to_string(),
200                reason: "Action must contain a colon separator or be '*'".to_string(),
201            })
202        }
203    }
204
205    /// Validate resource ARN or wildcard
206    pub fn validate_resource(resource: &str, _context: &ValidationContext) -> ValidationResult {
207        if resource == "*" {
208            return Ok(());
209        }
210
211        // Resources should be ARNs, but may contain wildcards
212        if resource.starts_with("arn:") {
213            // Use lenient parsing for resources with wildcards
214            match Arn::parse(resource) {
215                Ok(_) => Ok(()),
216                Err(e) => Err(ValidationError::InvalidResource {
217                    resource: resource.to_string(),
218                    reason: e.to_string(),
219                }),
220            }
221        } else {
222            Err(ValidationError::InvalidResource {
223                resource: resource.to_string(),
224                reason: "Resource must be an ARN or '*'".to_string(),
225            })
226        }
227    }
228
229    /// Collect multiple validation errors
230    pub fn collect_errors(results: Vec<ValidationResult>) -> ValidationResult {
231        let errors: Vec<ValidationError> = results
232            .into_iter()
233            .filter_map(std::result::Result::err)
234            .collect();
235
236        if errors.is_empty() {
237            Ok(())
238        } else if errors.len() == 1 {
239            Err(errors.into_iter().next().unwrap())
240        } else {
241            Err(ValidationError::Multiple(errors))
242        }
243    }
244}
245
246#[cfg(test)]
247mod tests {
248    use crate::ConditionValue;
249
250    use super::*;
251
252    #[test]
253    fn test_validation_context() {
254        let mut context = ValidationContext::new();
255        assert_eq!(context.current_path(), "root");
256
257        context.push("policy");
258        context.push("statement");
259        assert_eq!(context.current_path(), "policy.statement");
260
261        context.pop();
262        assert_eq!(context.current_path(), "policy");
263    }
264
265    #[test]
266    fn test_validation_error_display() {
267        let error = ValidationError::MissingField {
268            field: "Effect".to_string(),
269            context: "statement".to_string(),
270        };
271        assert!(
272            error
273                .to_string()
274                .contains("Missing required field 'Effect'")
275        );
276
277        let multiple = ValidationError::Multiple(vec![
278            ValidationError::MissingField {
279                field: "Effect".to_string(),
280                context: "statement".to_string(),
281            },
282            ValidationError::InvalidValue {
283                field: "Action".to_string(),
284                value: "invalid".to_string(),
285                reason: "bad format".to_string(),
286            },
287        ]);
288        let display = multiple.to_string();
289        assert!(display.contains("Multiple validation errors"));
290        assert!(display.contains("Missing required field"));
291        assert!(display.contains("Invalid value"));
292    }
293
294    #[test]
295    fn test_helper_validations() {
296        let context = ValidationContext::new();
297
298        // Test action validation
299        assert!(helpers::validate_action("s3:GetObject", &context).is_ok());
300        assert!(helpers::validate_action("*", &context).is_ok());
301        assert!(helpers::validate_action("invalid-action", &context).is_err());
302
303        // Test resource validation
304        assert!(helpers::validate_resource("*", &context).is_ok());
305        assert!(helpers::validate_resource("arn:aws:s3:::bucket/*", &context).is_ok());
306        assert!(helpers::validate_resource("invalid-resource", &context).is_err());
307        assert!(helpers::validate_resource("arn:aws:s3:::bucket/object", &context).is_ok());
308    }
309
310    #[test]
311    fn test_policy_validation_integration() {
312        use crate::{IAMAction, IAMEffect, IAMPolicy, IAMResource, IAMStatement};
313
314        // Test valid policy with UUID-like ID
315        let valid_policy = IAMPolicy::new()
316            .with_id("550e8400-e29b-41d4-a716-446655440000") // UUID format
317            .add_statement(
318                IAMStatement::new(IAMEffect::Allow)
319                    .with_sid("ValidStatement")
320                    .with_action(IAMAction::Single("s3:GetObject".to_string()))
321                    .with_resource(IAMResource::Single("arn:aws:s3:::bucket/*".to_string())),
322            );
323        assert!(valid_policy.is_valid());
324
325        // Test invalid policy - missing action
326        let mut invalid_policy = IAMPolicy::new();
327        invalid_policy
328            .statement
329            .push(IAMStatement::new(IAMEffect::Allow));
330        assert!(!invalid_policy.is_valid());
331
332        // Test policy with validation errors
333        let complex_invalid_policy = IAMPolicy::new().add_statement(
334            IAMStatement::new(IAMEffect::Allow)
335                .with_action(IAMAction::Single("invalid-action".to_string()))
336                .with_resource(IAMResource::Single("invalid-resource".to_string())),
337        );
338
339        assert!(!complex_invalid_policy.is_valid());
340
341        let validation_result = complex_invalid_policy.validate_result();
342        assert!(validation_result.is_err());
343
344        let error = validation_result.unwrap_err();
345        assert!(
346            error.to_string().contains("Multiple validation errors")
347                || error.to_string().contains("Invalid")
348        );
349    }
350
351    #[test]
352    fn test_condition_validation_integration() {
353        use crate::{IAMAction, IAMEffect, IAMOperator, IAMResource, IAMStatement};
354
355        // Valid condition
356        let valid_statement = IAMStatement::new(IAMEffect::Allow)
357            .with_action(IAMAction::Single("s3:GetObject".to_string()))
358            .with_resource(IAMResource::Single("*".to_string()))
359            .with_condition(
360                IAMOperator::StringEquals,
361                "aws:username".to_string(),
362                ConditionValue::String("alice".to_string()),
363            );
364
365        assert!(valid_statement.is_valid());
366
367        // Invalid condition - numeric operator with string value
368        let invalid_condition_statement = IAMStatement::new(IAMEffect::Allow)
369            .with_action(IAMAction::Single("s3:GetObject".to_string()))
370            .with_resource(IAMResource::Single("*".to_string()))
371            .with_condition(
372                IAMOperator::NumericEquals,
373                "aws:RequestedRegion".to_string(),
374                ConditionValue::String("invalid-number".to_string()),
375            );
376
377        // Should fail validation due to type mismatch
378        assert!(!invalid_condition_statement.is_valid());
379        assert!(invalid_condition_statement.validate_result().is_err());
380    }
381
382    #[test]
383    fn test_collect_errors() {
384        let results = vec![
385            Ok(()),
386            Err(ValidationError::MissingField {
387                field: "test".to_string(),
388                context: "root".to_string(),
389            }),
390            Ok(()),
391            Err(ValidationError::InvalidValue {
392                field: "other".to_string(),
393                value: "bad".to_string(),
394                reason: "test".to_string(),
395            }),
396        ];
397
398        let result = helpers::collect_errors(results);
399        assert!(result.is_err());
400        match result.unwrap_err() {
401            ValidationError::Multiple(errors) => assert_eq!(errors.len(), 2),
402            _ => panic!("Expected Multiple error"),
403        }
404    }
405}