1use super::{
2 context::{Context, ContextValue},
3 matcher::ArnMatcher,
4 request::IAMRequest,
5};
6use crate::{
7 core::{Action, Effect, Operator, Principal, Resource},
8 policy::{ConditionBlock, IAMPolicy, IAMStatement},
9};
10use base64::prelude::*;
11use chrono::{DateTime, Utc};
12use serde::{Deserialize, Serialize};
13
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16pub enum Decision {
17 Allow,
19 Deny,
21 NotApplicable,
23}
24
25#[derive(Debug, Clone, PartialEq, Eq)]
27pub enum EvaluationError {
28 InvalidContext(String),
30 InvalidPolicy(String),
32 InvalidArn(String),
34 ConditionError(String),
36 InternalError(String),
38}
39
40impl std::fmt::Display for EvaluationError {
41 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42 match self {
43 EvaluationError::InvalidContext(msg) => write!(f, "Invalid context: {}", msg),
44 EvaluationError::InvalidPolicy(msg) => write!(f, "Invalid policy: {}", msg),
45 EvaluationError::InvalidArn(msg) => write!(f, "Invalid ARN: {}", msg),
46 EvaluationError::ConditionError(msg) => write!(f, "Condition error: {}", msg),
47 EvaluationError::InternalError(msg) => write!(f, "Internal error: {}", msg),
48 }
49 }
50}
51
52impl std::error::Error for EvaluationError {}
53
54#[derive(Debug, Clone)]
56pub struct EvaluationResult {
57 pub decision: Decision,
59 pub matched_statements: Vec<StatementMatch>,
61 pub context: IAMRequest,
63}
64
65#[derive(Debug, Clone)]
67pub struct StatementMatch {
68 pub sid: Option<String>,
70 pub effect: Effect,
72 pub conditions_satisfied: bool,
74 pub reason: String,
76}
77
78#[derive(Debug, Clone)]
80pub struct PolicyEvaluator {
81 policies: Vec<IAMPolicy>,
83 options: EvaluationOptions,
85}
86
87#[derive(Debug, Clone)]
89pub struct EvaluationOptions {
90 pub stop_on_explicit_deny: bool,
92 pub collect_match_details: bool,
94 pub max_statements: usize,
96}
97
98impl Default for EvaluationOptions {
99 fn default() -> Self {
100 Self {
101 stop_on_explicit_deny: true,
102 collect_match_details: false,
103 max_statements: 1000,
104 }
105 }
106}
107
108impl PolicyEvaluator {
109 pub fn new() -> Self {
111 Self {
112 policies: Vec::new(),
113 options: EvaluationOptions::default(),
114 }
115 }
116
117 pub fn with_policies(policies: Vec<IAMPolicy>) -> Self {
119 Self {
120 policies,
121 options: EvaluationOptions::default(),
122 }
123 }
124
125 pub fn add_policy(&mut self, policy: IAMPolicy) {
127 self.policies.push(policy);
128 }
129
130 pub fn with_options(mut self, options: EvaluationOptions) -> Self {
132 self.options = options;
133 self
134 }
135
136 pub fn evaluate(&self, request: &IAMRequest) -> Result<EvaluationResult, EvaluationError> {
138 let mut matched_statements = Vec::new();
139 let mut has_explicit_allow = false;
140 let mut has_explicit_deny = false;
141 let mut statement_count = 0;
142
143 for policy in &self.policies {
145 for statement in &policy.statement {
146 statement_count += 1;
147 if statement_count > self.options.max_statements {
148 return Err(EvaluationError::InternalError(
149 "Maximum statement evaluation limit exceeded".to_string(),
150 ));
151 }
152
153 let statement_result = self.evaluate_statement(statement, request)?;
154
155 if self.options.collect_match_details {
156 matched_statements.push(statement_result.clone());
157 }
158
159 if statement_result.conditions_satisfied {
161 match statement.effect {
162 Effect::Allow => {
163 has_explicit_allow = true;
164 if self.options.collect_match_details {
165 matched_statements.push(statement_result);
166 }
167 }
168 Effect::Deny => {
169 has_explicit_deny = true;
170 if self.options.collect_match_details {
171 matched_statements.push(statement_result);
172 }
173 if self.options.stop_on_explicit_deny {
174 return Ok(EvaluationResult {
175 decision: Decision::Deny,
176 matched_statements,
177 context: request.clone(),
178 });
179 }
180 }
181 }
182 }
183 }
184 }
185
186 let decision = if has_explicit_deny {
189 Decision::Deny
190 } else if has_explicit_allow {
191 Decision::Allow
192 } else {
193 Decision::NotApplicable
194 };
195
196 Ok(EvaluationResult {
197 decision,
198 matched_statements,
199 context: request.clone(),
200 })
201 }
202
203 fn evaluate_statement(
205 &self,
206 statement: &IAMStatement,
207 request: &IAMRequest,
208 ) -> Result<StatementMatch, EvaluationError> {
209 if let Some(ref principal) = statement.principal {
211 if !self.principal_matches(principal, &request.principal)? {
212 return Ok(StatementMatch {
213 sid: statement.sid.clone(),
214 effect: statement.effect,
215 conditions_satisfied: false,
216 reason: "Principal does not match".to_string(),
217 });
218 }
219 }
220
221 if let Some(ref not_principal) = statement.not_principal {
222 if self.principal_matches(not_principal, &request.principal)? {
223 return Ok(StatementMatch {
224 sid: statement.sid.clone(),
225 effect: statement.effect,
226 conditions_satisfied: false,
227 reason: "Principal matches NotPrincipal exclusion".to_string(),
228 });
229 }
230 }
231
232 let action_matches = if let Some(ref action) = statement.action {
234 self.action_matches(action, &request.action)?
235 } else if let Some(ref not_action) = statement.not_action {
236 !self.action_matches(not_action, &request.action)?
237 } else {
238 return Ok(StatementMatch {
239 sid: statement.sid.clone(),
240 effect: statement.effect,
241 conditions_satisfied: false,
242 reason: "No action or not_action specified".to_string(),
243 });
244 };
245
246 if !action_matches {
247 return Ok(StatementMatch {
248 sid: statement.sid.clone(),
249 effect: statement.effect,
250 conditions_satisfied: false,
251 reason: "Action does not match".to_string(),
252 });
253 }
254
255 let resource_matches = if let Some(ref resource) = statement.resource {
257 self.resource_matches(resource, &request.resource)?
258 } else if let Some(ref not_resource) = statement.not_resource {
259 !self.resource_matches(not_resource, &request.resource)?
260 } else {
261 return Ok(StatementMatch {
262 sid: statement.sid.clone(),
263 effect: statement.effect,
264 conditions_satisfied: false,
265 reason: "No resource or not_resource specified".to_string(),
266 });
267 };
268
269 if !resource_matches {
270 return Ok(StatementMatch {
271 sid: statement.sid.clone(),
272 effect: statement.effect,
273 conditions_satisfied: false,
274 reason: "Resource does not match".to_string(),
275 });
276 }
277
278 if let Some(ref condition_block) = statement.condition {
280 if !self.evaluate_conditions(condition_block, &request.context)? {
281 return Ok(StatementMatch {
282 sid: statement.sid.clone(),
283 effect: statement.effect,
284 conditions_satisfied: false,
285 reason: "Conditions not satisfied".to_string(),
286 });
287 }
288 }
289
290 Ok(StatementMatch {
292 sid: statement.sid.clone(),
293 effect: statement.effect,
294 conditions_satisfied: true,
295 reason: "Statement fully matched".to_string(),
296 })
297 }
298
299 fn principal_matches(
301 &self,
302 principal: &Principal,
303 request_principal: &str,
304 ) -> Result<bool, EvaluationError> {
305 match principal {
306 Principal::Wildcard => Ok(true),
307 Principal::Mapped(map) => {
308 for values in map.values() {
310 match values {
311 serde_json::Value::String(s) => {
312 if self.principal_string_matches(s, request_principal)? {
313 return Ok(true);
314 }
315 }
316 serde_json::Value::Array(arr) => {
317 for val in arr {
318 if let serde_json::Value::String(s) = val {
319 if self.principal_string_matches(s, request_principal)? {
320 return Ok(true);
321 }
322 }
323 }
324 }
325 _ => {}
326 }
327 }
328 Ok(false)
329 }
330 }
331 }
332
333 fn principal_string_matches(
335 &self,
336 principal_str: &str,
337 request_principal: &str,
338 ) -> Result<bool, EvaluationError> {
339 if principal_str == "*" || principal_str == request_principal {
340 Ok(true)
341 } else if principal_str.starts_with("arn:") {
342 let matcher = ArnMatcher::from_pattern(principal_str)
344 .map_err(|e| EvaluationError::InvalidArn(e.to_string()))?;
345 matcher
346 .matches(request_principal)
347 .map_err(|e| EvaluationError::InvalidArn(e.to_string()))
348 } else {
349 Ok(false)
350 }
351 }
352
353 fn action_matches(
355 &self,
356 action: &Action,
357 request_action: &str,
358 ) -> Result<bool, EvaluationError> {
359 match action {
360 Action::Single(a) => {
361 Ok(a == "*" || a == request_action || self.wildcard_match(request_action, a))
362 }
363 Action::Multiple(actions) => {
364 for a in actions {
365 if a == "*" || a == request_action || self.wildcard_match(request_action, a) {
366 return Ok(true);
367 }
368 }
369 Ok(false)
370 }
371 }
372 }
373
374 fn resource_matches(
376 &self,
377 resource: &Resource,
378 request_resource: &str,
379 ) -> Result<bool, EvaluationError> {
380 match resource {
381 Resource::Single(r) => {
382 if r == "*" || r == request_resource {
383 Ok(true)
384 } else {
385 let matcher = ArnMatcher::from_pattern(r)
387 .map_err(|e| EvaluationError::InvalidArn(e.to_string()))?;
388 matcher
389 .matches(request_resource)
390 .map_err(|e| EvaluationError::InvalidArn(e.to_string()))
391 }
392 }
393 Resource::Multiple(resources) => {
394 for r in resources {
395 if self.resource_matches(&Resource::Single(r.clone()), request_resource)? {
396 return Ok(true);
397 }
398 }
399 Ok(false)
400 }
401 }
402 }
403
404 fn evaluate_conditions(
406 &self,
407 condition_block: &ConditionBlock,
408 context: &Context,
409 ) -> Result<bool, EvaluationError> {
410 for (operator, condition_map) in &condition_block.conditions {
412 for (key, value) in condition_map {
413 if !self.evaluate_single_condition(operator, key, value, context)? {
414 return Ok(false);
415 }
416 }
417 }
418 Ok(true)
419 }
420
421 fn evaluate_single_condition(
423 &self,
424 operator: &Operator,
425 key: &str,
426 value: &serde_json::Value,
427 context: &Context,
428 ) -> Result<bool, EvaluationError> {
429 let context_value = context.get(key);
431
432 match operator {
433 Operator::StringEquals => {
435 self.evaluate_string_condition(context_value, value, |a, b| a == b)
436 }
437 Operator::StringNotEquals => {
438 self.evaluate_string_condition(context_value, value, |a, b| a != b)
439 }
440 Operator::StringLike => self
441 .evaluate_string_condition(context_value, value, |a, b| self.wildcard_match(a, b)),
442 Operator::StringNotLike => {
443 self.evaluate_string_condition(context_value, value, |a, b| {
444 !self.wildcard_match(a, b)
445 })
446 }
447
448 Operator::NumericEquals => {
450 self.evaluate_numeric_condition(context_value, value, |a, b| {
451 (a - b).abs() < f64::EPSILON
452 })
453 }
454 Operator::NumericNotEquals => {
455 self.evaluate_numeric_condition(context_value, value, |a, b| {
456 (a - b).abs() >= f64::EPSILON
457 })
458 }
459 Operator::NumericLessThan => {
460 self.evaluate_numeric_condition(context_value, value, |a, b| a < b)
461 }
462 Operator::NumericLessThanEquals => {
463 self.evaluate_numeric_condition(context_value, value, |a, b| a <= b)
464 }
465 Operator::NumericGreaterThan => {
466 self.evaluate_numeric_condition(context_value, value, |a, b| a > b)
467 }
468 Operator::NumericGreaterThanEquals => {
469 self.evaluate_numeric_condition(context_value, value, |a, b| a >= b)
470 }
471
472 Operator::DateEquals => {
474 self.evaluate_date_condition(context_value, value, |a, b| a == b)
475 }
476 Operator::DateNotEquals => {
477 self.evaluate_date_condition(context_value, value, |a, b| a != b)
478 }
479 Operator::DateLessThan => {
480 self.evaluate_date_condition(context_value, value, |a, b| a < b)
481 }
482 Operator::DateLessThanEquals => {
483 self.evaluate_date_condition(context_value, value, |a, b| a <= b)
484 }
485 Operator::DateGreaterThan => {
486 self.evaluate_date_condition(context_value, value, |a, b| a > b)
487 }
488 Operator::DateGreaterThanEquals => {
489 self.evaluate_date_condition(context_value, value, |a, b| a >= b)
490 }
491
492 Operator::Bool => self.evaluate_boolean_condition(context_value, value),
494
495 Operator::BinaryEquals => self.evaluate_binary_condition(context_value, value),
497
498 Operator::IpAddress => self.evaluate_ip_condition(context_value, value, true),
500 Operator::NotIpAddress => self.evaluate_ip_condition(context_value, value, false),
501
502 Operator::ArnEquals => self.evaluate_arn_condition(context_value, value, |a, b| a == b),
504 Operator::ArnNotEquals => {
505 self.evaluate_arn_condition(context_value, value, |a, b| a != b)
506 }
507 Operator::ArnLike => {
508 self.evaluate_arn_condition(context_value, value, |a, b| self.wildcard_match(a, b))
509 }
510 Operator::ArnNotLike => {
511 self.evaluate_arn_condition(context_value, value, |a, b| !self.wildcard_match(a, b))
512 }
513
514 Operator::Null => match value {
516 serde_json::Value::Bool(should_be_null) => {
517 let is_null = context_value.is_none();
518 Ok(is_null == *should_be_null)
519 }
520 _ => Err(EvaluationError::ConditionError(
521 "Null operator requires boolean value".to_string(),
522 )),
523 },
524
525 Operator::ForAnyValueStringEquals
527 | Operator::ForAllValuesStringEquals
528 | Operator::ForAnyValueStringLike
529 | Operator::ForAllValuesStringLike => {
530 self.evaluate_string_condition(context_value, value, |a, b| a == b)
532 }
533
534 _ => Err(EvaluationError::ConditionError(format!(
535 "Unsupported operator: {:?}",
536 operator
537 ))),
538 }
539 }
540
541 fn evaluate_string_condition<F>(
543 &self,
544 context_value: Option<&ContextValue>,
545 condition_value: &serde_json::Value,
546 predicate: F,
547 ) -> Result<bool, EvaluationError>
548 where
549 F: Fn(&str, &str) -> bool,
550 {
551 let context_str = match context_value {
552 Some(ContextValue::String(s)) => s,
553 Some(_) => return Ok(false), None => return Ok(false), };
556
557 match condition_value {
558 serde_json::Value::String(s) => Ok(predicate(context_str, s)),
559 serde_json::Value::Array(arr) => {
560 for val in arr {
562 if let serde_json::Value::String(s) = val {
563 if predicate(context_str, s) {
564 return Ok(true);
565 }
566 }
567 }
568 Ok(false)
569 }
570 _ => Err(EvaluationError::ConditionError(
571 "String condition requires string value".to_string(),
572 )),
573 }
574 }
575
576 fn evaluate_numeric_condition<F>(
578 &self,
579 context_value: Option<&ContextValue>,
580 condition_value: &serde_json::Value,
581 predicate: F,
582 ) -> Result<bool, EvaluationError>
583 where
584 F: Fn(f64, f64) -> bool,
585 {
586 let context_num = match context_value {
587 Some(ContextValue::Number(n)) => *n,
588 Some(ContextValue::String(s)) => s.parse::<f64>().map_err(|_| {
589 EvaluationError::ConditionError("Invalid numeric context value".to_string())
590 })?,
591 Some(_) => return Ok(false),
592 None => return Ok(false),
593 };
594
595 match condition_value {
596 serde_json::Value::Number(n) => {
597 let val = n.as_f64().ok_or_else(|| {
598 EvaluationError::ConditionError("Invalid numeric condition value".to_string())
599 })?;
600 Ok(predicate(context_num, val))
601 }
602 serde_json::Value::String(s) => {
603 let val = s.parse::<f64>().map_err(|_| {
604 EvaluationError::ConditionError("Invalid numeric condition value".to_string())
605 })?;
606 Ok(predicate(context_num, val))
607 }
608 serde_json::Value::Array(arr) => {
609 for val in arr {
610 let num_val = match val {
611 serde_json::Value::Number(n) => n.as_f64().ok_or_else(|| {
612 EvaluationError::ConditionError(
613 "Invalid numeric value in array".to_string(),
614 )
615 })?,
616 serde_json::Value::String(s) => s.parse::<f64>().map_err(|_| {
617 EvaluationError::ConditionError(
618 "Invalid numeric value in array".to_string(),
619 )
620 })?,
621 _ => continue,
622 };
623 if predicate(context_num, num_val) {
624 return Ok(true);
625 }
626 }
627 Ok(false)
628 }
629 _ => Err(EvaluationError::ConditionError(
630 "Numeric condition requires numeric value".to_string(),
631 )),
632 }
633 }
634
635 fn evaluate_date_condition<F>(
637 &self,
638 context_value: Option<&ContextValue>,
639 condition_value: &serde_json::Value,
640 predicate: F,
641 ) -> Result<bool, EvaluationError>
642 where
643 F: Fn(DateTime<Utc>, DateTime<Utc>) -> bool,
644 {
645 let context_date = match context_value {
646 Some(ContextValue::DateTime(dt)) => *dt,
647 Some(ContextValue::String(s)) => DateTime::parse_from_rfc3339(s)
648 .map_err(|_| EvaluationError::ConditionError("Invalid date format".to_string()))?
649 .with_timezone(&Utc),
650 Some(_) => return Ok(false),
651 None => return Ok(false),
652 };
653
654 let condition_date = match condition_value {
655 serde_json::Value::String(s) => DateTime::parse_from_rfc3339(s)
656 .map_err(|_| EvaluationError::ConditionError("Invalid date format".to_string()))?
657 .with_timezone(&Utc),
658 _ => {
659 return Err(EvaluationError::ConditionError(
660 "Date condition requires string value".to_string(),
661 ));
662 }
663 };
664
665 Ok(predicate(context_date, condition_date))
666 }
667
668 fn evaluate_boolean_condition(
670 &self,
671 context_value: Option<&ContextValue>,
672 condition_value: &serde_json::Value,
673 ) -> Result<bool, EvaluationError> {
674 let context_bool = match context_value {
675 Some(ContextValue::Boolean(b)) => *b,
676 Some(ContextValue::String(s)) => s.parse::<bool>().map_err(|_| {
677 EvaluationError::ConditionError("Invalid boolean context value".to_string())
678 })?,
679 Some(_) => return Ok(false),
680 None => return Ok(false),
681 };
682
683 match condition_value {
684 serde_json::Value::Bool(b) => Ok(context_bool == *b),
685 serde_json::Value::String(s) => {
686 let condition_bool = s.parse::<bool>().map_err(|_| {
687 EvaluationError::ConditionError("Invalid boolean condition value".to_string())
688 })?;
689 Ok(context_bool == condition_bool)
690 }
691 _ => Err(EvaluationError::ConditionError(
692 "Boolean condition requires boolean value".to_string(),
693 )),
694 }
695 }
696
697 fn evaluate_binary_condition(
699 &self,
700 context_value: Option<&ContextValue>,
701 condition_value: &serde_json::Value,
702 ) -> Result<bool, EvaluationError> {
703 let context_bytes = match context_value {
704 Some(ContextValue::String(s)) => {
705 BASE64_STANDARD.decode(s.as_bytes()).map_err(|_| {
707 EvaluationError::ConditionError("Invalid base64 context value".to_string())
708 })?
709 }
710 Some(_) => return Ok(false), None => return Ok(false), };
713
714 match condition_value {
715 serde_json::Value::String(s) => {
716 let condition_bytes = BASE64_STANDARD.decode(s.as_bytes()).map_err(|_| {
718 EvaluationError::ConditionError("Invalid base64 condition value".to_string())
719 })?;
720 Ok(context_bytes == condition_bytes)
721 }
722 serde_json::Value::Array(arr) => {
723 for val in arr {
725 if let serde_json::Value::String(s) = val {
726 let condition_bytes =
727 BASE64_STANDARD.decode(s.as_bytes()).map_err(|_| {
728 EvaluationError::ConditionError(
729 "Invalid base64 value in array".to_string(),
730 )
731 })?;
732 if context_bytes == condition_bytes {
733 return Ok(true);
734 }
735 }
736 }
737 Ok(false)
738 }
739 _ => Err(EvaluationError::ConditionError(
740 "Binary condition requires string value".to_string(),
741 )),
742 }
743 }
744
745 fn evaluate_ip_condition(
747 &self,
748 context_value: Option<&ContextValue>,
749 condition_value: &serde_json::Value,
750 should_match: bool,
751 ) -> Result<bool, EvaluationError> {
752 let result =
754 self.evaluate_string_condition(context_value, condition_value, |a, b| a == b)?;
755 Ok(if should_match { result } else { !result })
756 }
757
758 fn evaluate_arn_condition<F>(
760 &self,
761 context_value: Option<&ContextValue>,
762 condition_value: &serde_json::Value,
763 predicate: F,
764 ) -> Result<bool, EvaluationError>
765 where
766 F: Fn(&str, &str) -> bool,
767 {
768 self.evaluate_string_condition(context_value, condition_value, predicate)
770 }
771
772 fn wildcard_match(&self, text: &str, pattern: &str) -> bool {
774 crate::Arn::wildcard_match(text, pattern)
776 }
777}
778
779impl Default for PolicyEvaluator {
780 fn default() -> Self {
781 Self::new()
782 }
783}
784
785pub fn evaluate_policy(
787 policy: &IAMPolicy,
788 request: &IAMRequest,
789) -> Result<Decision, EvaluationError> {
790 let evaluator = PolicyEvaluator::with_policies(vec![policy.clone()]);
791 let result = evaluator.evaluate(request)?;
792 Ok(result.decision)
793}
794
795pub fn evaluate_policies(
797 policies: &[IAMPolicy],
798 request: &IAMRequest,
799) -> Result<Decision, EvaluationError> {
800 let evaluator = PolicyEvaluator::with_policies(policies.to_vec());
801 let result = evaluator.evaluate(request)?;
802 Ok(result.decision)
803}
804
805#[cfg(test)]
806mod tests {
807 use super::*;
808 use crate::{Action, Effect, IAMStatement, Resource};
809 use serde_json::json;
810
811 #[test]
812 fn test_simple_allow_policy() {
813 let policy = IAMPolicy::new().add_statement(
814 IAMStatement::new(Effect::Allow)
815 .with_action(Action::Single("s3:GetObject".to_string()))
816 .with_resource(Resource::Single("arn:aws:s3:::my-bucket/*".to_string())),
817 );
818
819 let request = IAMRequest::new(
820 "arn:aws:iam::123456789012:user/test",
821 "s3:GetObject",
822 "arn:aws:s3:::my-bucket/file.txt",
823 );
824
825 let result = evaluate_policy(&policy, &request).unwrap();
826 assert_eq!(result, Decision::Allow);
827 }
828
829 #[test]
830 fn test_simple_deny_policy() {
831 let policy = IAMPolicy::new().add_statement(
832 IAMStatement::new(Effect::Deny)
833 .with_action(Action::Single("s3:DeleteObject".to_string()))
834 .with_resource(Resource::Single("arn:aws:s3:::my-bucket/*".to_string())),
835 );
836
837 let request = IAMRequest::new(
838 "arn:aws:iam::123456789012:user/test",
839 "s3:DeleteObject",
840 "arn:aws:s3:::my-bucket/file.txt",
841 );
842
843 let result = evaluate_policy(&policy, &request).unwrap();
844 assert_eq!(result, Decision::Deny);
845 }
846
847 #[test]
848 fn test_not_applicable_policy() {
849 let policy = IAMPolicy::new().add_statement(
850 IAMStatement::new(Effect::Allow)
851 .with_action(Action::Single("s3:GetObject".to_string()))
852 .with_resource(Resource::Single("arn:aws:s3:::other-bucket/*".to_string())),
853 );
854
855 let request = IAMRequest::new(
856 "arn:aws:iam::123456789012:user/test",
857 "s3:GetObject",
858 "arn:aws:s3:::my-bucket/file.txt",
859 );
860
861 let result = evaluate_policy(&policy, &request).unwrap();
862 assert_eq!(result, Decision::NotApplicable);
863 }
864
865 #[test]
866 fn test_wildcard_action_matching() {
867 let policy = IAMPolicy::new().add_statement(
868 IAMStatement::new(Effect::Allow)
869 .with_action(Action::Single("s3:*".to_string()))
870 .with_resource(Resource::Single("arn:aws:s3:::my-bucket/*".to_string())),
871 );
872
873 let request = IAMRequest::new(
874 "arn:aws:iam::123456789012:user/test",
875 "s3:GetObject",
876 "arn:aws:s3:::my-bucket/file.txt",
877 );
878
879 let result = evaluate_policy(&policy, &request).unwrap();
880 assert_eq!(result, Decision::Allow);
881 }
882
883 #[test]
884 fn test_condition_evaluation() {
885 use crate::Operator;
886
887 let mut context = Context::new();
888 context.insert(
889 "aws:userid".to_string(),
890 ContextValue::String("test-user".to_string()),
891 );
892
893 let policy = IAMPolicy::new().add_statement(
894 IAMStatement::new(Effect::Allow)
895 .with_action(Action::Single("s3:GetObject".to_string()))
896 .with_resource(Resource::Single("arn:aws:s3:::my-bucket/*".to_string()))
897 .with_condition(
898 Operator::StringEquals,
899 "aws:userid".to_string(),
900 json!("test-user"),
901 ),
902 );
903
904 let request = IAMRequest::new_with_context(
905 "arn:aws:iam::123456789012:user/test",
906 "s3:GetObject",
907 "arn:aws:s3:::my-bucket/file.txt",
908 context,
909 );
910
911 let result = evaluate_policy(&policy, &request).unwrap();
912 assert_eq!(result, Decision::Allow);
913 }
914
915 #[test]
916 fn test_condition_evaluation_failure() {
917 use crate::Operator;
918
919 let mut context = Context::new();
920 context.insert(
921 "aws:userid".to_string(),
922 ContextValue::String("other-user".to_string()),
923 );
924
925 let policy = IAMPolicy::new().add_statement(
926 IAMStatement::new(Effect::Allow)
927 .with_action(Action::Single("s3:GetObject".to_string()))
928 .with_resource(Resource::Single("arn:aws:s3:::my-bucket/*".to_string()))
929 .with_condition(
930 Operator::StringEquals,
931 "aws:userid".to_string(),
932 json!("test-user"),
933 ),
934 );
935
936 let request = IAMRequest::new_with_context(
937 "arn:aws:iam::123456789012:user/test",
938 "s3:GetObject",
939 "arn:aws:s3:::my-bucket/file.txt",
940 context,
941 );
942
943 let result = evaluate_policy(&policy, &request).unwrap();
944 assert_eq!(result, Decision::NotApplicable);
945 }
946
947 #[test]
948 fn test_explicit_deny_overrides_allow() {
949 let policies = vec![
950 IAMPolicy::new().add_statement(
951 IAMStatement::new(Effect::Allow)
952 .with_action(Action::Single("s3:*".to_string()))
953 .with_resource(Resource::Single("*".to_string())),
954 ),
955 IAMPolicy::new().add_statement(
956 IAMStatement::new(Effect::Deny)
957 .with_action(Action::Single("s3:DeleteObject".to_string()))
958 .with_resource(Resource::Single(
959 "arn:aws:s3:::protected-bucket/*".to_string(),
960 )),
961 ),
962 ];
963
964 let request = IAMRequest::new(
965 "arn:aws:iam::123456789012:user/test",
966 "s3:DeleteObject",
967 "arn:aws:s3:::protected-bucket/file.txt",
968 );
969
970 let result = evaluate_policies(&policies, &request).unwrap();
971 assert_eq!(result, Decision::Deny);
972 }
973
974 #[test]
975 fn test_numeric_condition() {
976 let mut context = Context::new();
977 context.insert("aws:RequestedRegion".to_string(), ContextValue::Number(5.0));
978
979 let policy = IAMPolicy::new().add_statement(
980 IAMStatement::new(Effect::Allow)
981 .with_action(Action::Single("s3:GetObject".to_string()))
982 .with_resource(Resource::Single("*".to_string()))
983 .with_condition(
984 Operator::NumericLessThan,
985 "aws:RequestedRegion".to_string(),
986 json!(10),
987 ),
988 );
989
990 let request = IAMRequest::new_with_context(
991 "arn:aws:iam::123456789012:user/test",
992 "s3:GetObject",
993 "arn:aws:s3:::my-bucket/file.txt",
994 context,
995 );
996
997 let result = evaluate_policy(&policy, &request).unwrap();
998 assert_eq!(result, Decision::Allow);
999 }
1000
1001 #[test]
1002 fn test_evaluator_with_options() {
1003 let policy = IAMPolicy::new().add_statement(
1004 IAMStatement::new(Effect::Allow)
1005 .with_sid("AllowS3Read")
1006 .with_action(Action::Single("s3:GetObject".to_string()))
1007 .with_resource(Resource::Single("arn:aws:s3:::my-bucket/*".to_string())),
1008 );
1009
1010 let request = IAMRequest::new(
1011 "arn:aws:iam::123456789012:user/test",
1012 "s3:GetObject",
1013 "arn:aws:s3:::my-bucket/file.txt",
1014 );
1015
1016 let evaluator =
1017 PolicyEvaluator::with_policies(vec![policy]).with_options(EvaluationOptions {
1018 collect_match_details: true,
1019 ..Default::default()
1020 });
1021
1022 let result = evaluator.evaluate(&request).unwrap();
1023 assert_eq!(result.decision, Decision::Allow);
1024 assert!(!result.matched_statements.is_empty());
1025 assert_eq!(
1026 result.matched_statements[0].sid,
1027 Some("AllowS3Read".to_string())
1028 );
1029 }
1030}