1use std::fmt;
2
3#[derive(Debug, Clone, PartialEq, Eq)]
5#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
6pub enum ValidationError {
7 MissingField { field: String, context: String },
9 InvalidValue {
11 field: String,
12 value: String,
13 reason: String,
14 },
15 LogicalError { message: String },
17 InvalidArn { arn: String, reason: String },
19 InvalidCondition {
21 operator: String,
22 key: String,
23 reason: String,
24 },
25 InvalidPrincipal { principal: String, reason: String },
27 InvalidAction { action: String, reason: String },
29 InvalidResource { resource: String, reason: String },
31 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
86pub type ValidationResult = Result<(), ValidationError>;
88
89#[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
132pub trait Validate {
135 fn validate(&self, context: &mut ValidationContext) -> ValidationResult;
141
142 fn is_valid(&self) -> bool {
144 let mut context = ValidationContext::new();
145 self.validate(&mut context).is_ok()
146 }
147
148 fn validate_result(&self) -> ValidationResult {
154 let mut context = ValidationContext::new();
155 self.validate(&mut context)
156 }
157}
158
159pub(crate) mod helpers {
161 use super::{ValidationContext, ValidationError, ValidationResult};
162 use crate::core::Arn;
163
164 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 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 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 pub fn validate_resource(resource: &str, _context: &ValidationContext) -> ValidationResult {
207 if resource == "*" {
208 return Ok(());
209 }
210
211 if resource.starts_with("arn:") {
213 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 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 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 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 let valid_policy = IAMPolicy::new()
316 .with_id("550e8400-e29b-41d4-a716-446655440000") .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 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 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 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 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 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}