1use std::fmt;
2
3#[derive(Debug, Clone, PartialEq, Eq)]
5pub enum ValidationError {
6 MissingField { field: String, context: String },
8 InvalidValue {
10 field: String,
11 value: String,
12 reason: String,
13 },
14 LogicalError { message: String },
16 InvalidArn { arn: String, reason: String },
18 InvalidCondition {
20 operator: String,
21 key: String,
22 reason: String,
23 },
24 InvalidPrincipal { principal: String, reason: String },
26 InvalidAction { action: String, reason: String },
28 InvalidResource { resource: String, reason: String },
30 Multiple(Vec<ValidationError>),
32}
33
34impl fmt::Display for ValidationError {
35 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36 match self {
37 ValidationError::MissingField { field, context } => {
38 write!(f, "Missing required field '{}' in {}", field, context)
39 }
40 ValidationError::InvalidValue {
41 field,
42 value,
43 reason,
44 } => {
45 write!(
46 f,
47 "Invalid value '{}' for field '{}': {}",
48 value, field, reason
49 )
50 }
51 ValidationError::LogicalError { message } => {
52 write!(f, "Logical error: {}", message)
53 }
54 ValidationError::InvalidArn { arn, reason } => {
55 write!(f, "Invalid ARN '{}': {}", arn, reason)
56 }
57 ValidationError::InvalidCondition {
58 operator,
59 key,
60 reason,
61 } => {
62 write!(
63 f,
64 "Invalid condition '{}' for key '{}': {}",
65 operator, key, reason
66 )
67 }
68 ValidationError::InvalidPrincipal { principal, reason } => {
69 write!(f, "Invalid principal '{}': {}", principal, reason)
70 }
71 ValidationError::InvalidAction { action, reason } => {
72 write!(f, "Invalid action '{}': {}", action, reason)
73 }
74 ValidationError::InvalidResource { resource, reason } => {
75 write!(f, "Invalid resource '{}': {}", resource, reason)
76 }
77 ValidationError::Multiple(errors) => {
78 write!(f, "Multiple validation errors:\n")?;
79 for (i, error) in errors.iter().enumerate() {
80 write!(f, " {}: {}\n", i + 1, error)?;
81 }
82 Ok(())
83 }
84 }
85 }
86}
87
88impl std::error::Error for ValidationError {}
89
90pub type ValidationResult = Result<(), ValidationError>;
92
93#[derive(Debug, Clone)]
95pub struct ValidationContext {
96 pub path: Vec<String>,
97}
98
99impl ValidationContext {
100 pub fn new() -> Self {
101 Self { path: Vec::new() }
102 }
103
104 pub fn push(&mut self, segment: &str) {
105 self.path.push(segment.to_string());
106 }
107
108 pub fn pop(&mut self) {
109 self.path.pop();
110 }
111
112 pub fn current_path(&self) -> String {
113 if self.path.is_empty() {
114 "root".to_string()
115 } else {
116 self.path.join(".")
117 }
118 }
119
120 pub fn with_segment<T>(&mut self, segment: &str, f: impl FnOnce(&mut Self) -> T) -> T {
121 self.push(segment);
122 let result = f(self);
123 self.pop();
124 result
125 }
126}
127
128pub trait Validate {
131 fn validate(&self, context: &mut ValidationContext) -> ValidationResult;
132
133 fn is_valid(&self) -> bool {
135 let mut context = ValidationContext::new();
136 self.validate(&mut context).is_ok()
137 }
138
139 fn validate_result(&self) -> ValidationResult {
141 let mut context = ValidationContext::new();
142 self.validate(&mut context)
143 }
144}
145
146pub(crate) mod helpers {
148 use super::*;
149 use crate::core::Arn;
150
151 pub fn validate_non_empty(
153 value: &str,
154 field_name: &str,
155 context: &ValidationContext,
156 ) -> ValidationResult {
157 if value.is_empty() {
158 Err(ValidationError::MissingField {
159 field: field_name.to_string(),
160 context: context.current_path(),
161 })
162 } else {
163 Ok(())
164 }
165 }
166
167 pub fn validate_arn(arn: &str, _context: &ValidationContext) -> ValidationResult {
169 match Arn::parse(arn) {
170 Ok(parsed_arn) => {
171 if !parsed_arn.is_valid() {
172 Err(ValidationError::InvalidArn {
173 arn: arn.to_string(),
174 reason: "ARN format is valid but does not conform to AWS standards"
175 .to_string(),
176 })
177 } else {
178 Ok(())
179 }
180 }
181 Err(e) => Err(ValidationError::InvalidArn {
182 arn: arn.to_string(),
183 reason: e.to_string(),
184 }),
185 }
186 }
187
188 pub fn validate_action(action: &str, _context: &ValidationContext) -> ValidationResult {
190 if action == "*" {
191 return Ok(());
192 }
193
194 if action.contains(':') {
195 let parts: Vec<&str> = action.split(':').collect();
196 if parts.len() == 2 && !parts[0].is_empty() && !parts[1].is_empty() {
197 Ok(())
199 } else {
200 Err(ValidationError::InvalidAction {
201 action: action.to_string(),
202 reason: "Action must be in format 'service:action'".to_string(),
203 })
204 }
205 } else {
206 Err(ValidationError::InvalidAction {
207 action: action.to_string(),
208 reason: "Action must contain a colon separator or be '*'".to_string(),
209 })
210 }
211 }
212
213 pub fn validate_principal(principal: &str, _context: &ValidationContext) -> ValidationResult {
215 if principal == "*" || principal == "AWS" || principal == "Federated" {
216 return Ok(());
217 }
218
219 if principal.starts_with("arn:") {
221 return validate_arn(principal, _context);
222 }
223
224 if principal.len() == 12 && principal.chars().all(|c| c.is_ascii_digit()) {
226 return Ok(());
227 }
228
229 if principal.contains('.')
231 && (principal.ends_with(".amazonaws.com")
232 || principal.ends_with(".amazonaws.com.cn")
233 || principal.ends_with(".api.aws")
234 || principal.ends_with(".internal"))
235 {
236 return Ok(());
237 }
238
239 if principal.starts_with("https://") || principal.starts_with("http://") {
241 return Ok(());
242 }
243
244 if principal.starts_with("arn:aws:iam::") && principal.contains(":saml-provider/") {
246 return Ok(());
247 }
248
249 Err(ValidationError::InvalidPrincipal {
251 principal: principal.to_string(),
252 reason:
253 "Principal must be an ARN, account ID, service principal, URL, or special value"
254 .to_string(),
255 })
256 }
257
258 pub fn validate_resource(resource: &str, _context: &ValidationContext) -> ValidationResult {
260 if resource == "*" {
261 return Ok(());
262 }
263
264 if resource.starts_with("arn:") {
266 match Arn::parse(resource) {
268 Ok(_) => Ok(()),
269 Err(e) => Err(ValidationError::InvalidResource {
270 resource: resource.to_string(),
271 reason: e.to_string(),
272 }),
273 }
274 } else {
275 Err(ValidationError::InvalidResource {
276 resource: resource.to_string(),
277 reason: "Resource must be an ARN or '*'".to_string(),
278 })
279 }
280 }
281
282 pub fn collect_errors(results: Vec<ValidationResult>) -> ValidationResult {
284 let errors: Vec<ValidationError> = results.into_iter().filter_map(|r| r.err()).collect();
285
286 if errors.is_empty() {
287 Ok(())
288 } else if errors.len() == 1 {
289 Err(errors.into_iter().next().unwrap())
290 } else {
291 Err(ValidationError::Multiple(errors))
292 }
293 }
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299
300 #[test]
301 fn test_validation_context() {
302 let mut context = ValidationContext::new();
303 assert_eq!(context.current_path(), "root");
304
305 context.push("policy");
306 context.push("statement");
307 assert_eq!(context.current_path(), "policy.statement");
308
309 context.pop();
310 assert_eq!(context.current_path(), "policy");
311 }
312
313 #[test]
314 fn test_validation_error_display() {
315 let error = ValidationError::MissingField {
316 field: "Effect".to_string(),
317 context: "statement".to_string(),
318 };
319 assert!(
320 error
321 .to_string()
322 .contains("Missing required field 'Effect'")
323 );
324
325 let multiple = ValidationError::Multiple(vec![
326 ValidationError::MissingField {
327 field: "Effect".to_string(),
328 context: "statement".to_string(),
329 },
330 ValidationError::InvalidValue {
331 field: "Action".to_string(),
332 value: "invalid".to_string(),
333 reason: "bad format".to_string(),
334 },
335 ]);
336 let display = multiple.to_string();
337 assert!(display.contains("Multiple validation errors"));
338 assert!(display.contains("Missing required field"));
339 assert!(display.contains("Invalid value"));
340 }
341
342 #[test]
343 fn test_helper_validations() {
344 let context = ValidationContext::new();
345
346 assert!(helpers::validate_arn("arn:aws:s3:::bucket/object", &context).is_ok());
348 assert!(helpers::validate_arn("invalid-arn", &context).is_err());
349
350 assert!(helpers::validate_action("s3:GetObject", &context).is_ok());
352 assert!(helpers::validate_action("*", &context).is_ok());
353 assert!(helpers::validate_action("invalid-action", &context).is_err());
354
355 assert!(helpers::validate_principal("*", &context).is_ok());
357 assert!(helpers::validate_principal("123456789012", &context).is_ok());
358 assert!(
359 helpers::validate_principal("arn:aws:iam::123456789012:user/test", &context).is_ok()
360 );
361 assert!(
362 helpers::validate_principal("arn:aws:iam::123456789012:role/test", &context).is_ok()
363 );
364 assert!(helpers::validate_principal("asset.controlpanel.internal", &context).is_ok());
365 assert!(helpers::validate_principal("invalid", &context).is_err());
366 assert!(helpers::validate_principal("https://example.com", &context).is_ok());
367 assert!(helpers::validate_principal("http://example.com", &context).is_ok());
368 assert!(
369 helpers::validate_principal(
370 "arn:aws:iam::123456789012:saml-provider/TestProvider",
371 &context
372 )
373 .is_ok()
374 );
375
376 assert!(helpers::validate_resource("*", &context).is_ok());
378 assert!(helpers::validate_resource("arn:aws:s3:::bucket/*", &context).is_ok());
379 assert!(helpers::validate_resource("invalid-resource", &context).is_err());
380 assert!(helpers::validate_resource("arn:aws:s3:::bucket/object", &context).is_ok());
381 }
382
383 #[test]
384 fn test_policy_validation_integration() {
385 use crate::{Action, Effect, IAMPolicy, IAMStatement, Resource};
386
387 let valid_policy = IAMPolicy::new()
389 .with_id("550e8400-e29b-41d4-a716-446655440000") .add_statement(
391 IAMStatement::new(Effect::Allow)
392 .with_sid("ValidStatement")
393 .with_action(Action::Single("s3:GetObject".to_string()))
394 .with_resource(Resource::Single("arn:aws:s3:::bucket/*".to_string())),
395 );
396 assert!(valid_policy.is_valid());
397
398 let mut invalid_policy = IAMPolicy::new();
400 invalid_policy
401 .statement
402 .push(IAMStatement::new(Effect::Allow));
403 assert!(!invalid_policy.is_valid());
404
405 let complex_invalid_policy = IAMPolicy::new().add_statement(
407 IAMStatement::new(Effect::Allow)
408 .with_action(Action::Single("invalid-action".to_string()))
409 .with_resource(Resource::Single("invalid-resource".to_string())),
410 );
411
412 assert!(!complex_invalid_policy.is_valid());
413
414 let validation_result = complex_invalid_policy.validate_result();
415 assert!(validation_result.is_err());
416
417 let error = validation_result.unwrap_err();
418 assert!(
419 error.to_string().contains("Multiple validation errors")
420 || error.to_string().contains("Invalid")
421 );
422 }
423
424 #[test]
425 fn test_condition_validation_integration() {
426 use crate::{Action, Effect, IAMStatement, Operator, Resource};
427 use serde_json::json;
428
429 let valid_statement = IAMStatement::new(Effect::Allow)
431 .with_action(Action::Single("s3:GetObject".to_string()))
432 .with_resource(Resource::Single("*".to_string()))
433 .with_condition(
434 Operator::StringEquals,
435 "aws:username".to_string(),
436 json!("alice"),
437 );
438
439 assert!(valid_statement.is_valid());
440
441 let invalid_condition_statement = IAMStatement::new(Effect::Allow)
443 .with_action(Action::Single("s3:GetObject".to_string()))
444 .with_resource(Resource::Single("*".to_string()))
445 .with_condition(
446 Operator::NumericEquals,
447 "aws:RequestedRegion".to_string(),
448 json!("invalid-number"),
449 );
450
451 assert!(!invalid_condition_statement.is_valid());
453 assert!(invalid_condition_statement.validate_result().is_err());
454 }
455
456 #[test]
457 fn test_collect_errors() {
458 let results = vec![
459 Ok(()),
460 Err(ValidationError::MissingField {
461 field: "test".to_string(),
462 context: "root".to_string(),
463 }),
464 Ok(()),
465 Err(ValidationError::InvalidValue {
466 field: "other".to_string(),
467 value: "bad".to_string(),
468 reason: "test".to_string(),
469 }),
470 ];
471
472 let result = helpers::collect_errors(results);
473 assert!(result.is_err());
474 match result.unwrap_err() {
475 ValidationError::Multiple(errors) => assert_eq!(errors.len(), 2),
476 _ => panic!("Expected Multiple error"),
477 }
478 }
479}