1use super::{context::Context, matcher::ArnMatcher, request::IAMRequest};
2use crate::{
3 Arn, Validate,
4 core::{IAMAction, IAMEffect, IAMResource, Principal, PrincipalId},
5 evaluation::{
6 operator_eval::{evaluate_condition, wildcard_match},
7 variable::interpolate_variables,
8 },
9 policy::{ConditionBlock, IAMPolicy, IAMStatement},
10};
11use serde::{Deserialize, Serialize};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
15#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
16pub enum Decision {
17 Allow,
19 Deny,
21 NotApplicable,
23}
24
25impl std::fmt::Display for Decision {
26 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27 serde_json::to_string(self)
28 .map_err(|_| std::fmt::Error)?
29 .trim_matches('"')
30 .fmt(f)
31 }
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
36#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
37pub enum EvaluationError {
38 InvalidRequest(String),
40 InvalidPolicy(String),
42 InvalidArn(String),
44 InvalidVariable(String),
46 ConditionError(String),
48 InternalError(String),
50}
51
52impl std::fmt::Display for EvaluationError {
53 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 match self {
55 EvaluationError::InvalidRequest(msg) => write!(f, "Invalid request: {msg}"),
56 EvaluationError::InvalidPolicy(msg) => write!(f, "Invalid policy: {msg}"),
57 EvaluationError::InvalidArn(msg) => write!(f, "Invalid ARN: {msg}"),
58 EvaluationError::InvalidVariable(msg) => write!(f, "Invalid variable: {msg}"),
59 EvaluationError::ConditionError(msg) => write!(f, "Condition error: {msg}"),
60 EvaluationError::InternalError(msg) => write!(f, "Internal error: {msg}"),
61 }
62 }
63}
64
65impl std::error::Error for EvaluationError {}
66
67#[derive(Debug, Clone, PartialEq, Serialize)]
69#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
70pub struct EvaluationResult {
71 pub decision: Decision,
73 pub matched_statements: Vec<StatementMatch>,
75 pub context: IAMRequest,
77}
78
79#[derive(Debug, Clone, PartialEq, Serialize)]
81#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
82pub struct StatementMatch {
83 pub sid: Option<String>,
85 pub effect: IAMEffect,
87 pub conditions_satisfied: bool,
89 pub reason: String,
91}
92
93#[derive(Debug, Clone)]
95#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
96pub struct PolicyEvaluator {
97 policies: Vec<IAMPolicy>,
99 options: EvaluationOptions,
101}
102
103#[derive(Debug, Clone)]
105#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
106pub struct EvaluationOptions {
107 pub stop_on_explicit_deny: bool,
109 pub collect_match_details: bool,
111 pub max_statements: usize,
113 pub ignore_resource_constraints: bool,
115}
116
117impl Default for EvaluationOptions {
118 fn default() -> Self {
119 Self {
120 stop_on_explicit_deny: true,
121 collect_match_details: false,
122 max_statements: 1000,
123 ignore_resource_constraints: false,
124 }
125 }
126}
127
128impl PolicyEvaluator {
129 #[must_use]
131 pub fn new() -> Self {
132 Self {
133 policies: Vec::new(),
134 options: EvaluationOptions::default(),
135 }
136 }
137
138 #[must_use]
140 pub fn with_policies(policies: Vec<IAMPolicy>) -> Self {
141 Self {
142 policies,
143 options: EvaluationOptions::default(),
144 }
145 }
146
147 pub fn add_policy(&mut self, policy: IAMPolicy) {
149 self.policies.push(policy);
150 }
151
152 #[must_use]
154 pub fn with_options(mut self, options: EvaluationOptions) -> Self {
155 self.options = options;
156 self
157 }
158
159 pub fn evaluate(&self, request: &IAMRequest) -> Result<EvaluationResult, EvaluationError> {
170 if !request.principal.is_single() {
171 return Err(EvaluationError::InvalidRequest(
172 "Request principal must be a single entity".to_string(),
173 ));
174 }
175 if !request.principal.is_valid() {
176 return Err(EvaluationError::InvalidRequest(
177 "Invalid principal".to_string(),
178 ));
179 }
180 if request.action.is_empty() {
181 return Err(EvaluationError::InvalidRequest(
182 "Action cannot be empty".to_string(),
183 ));
184 }
185 if !request.resource.is_valid() && !self.options.ignore_resource_constraints {
186 return Err(EvaluationError::InvalidRequest(
187 "Invalid resource ARN".to_string(),
188 ));
189 }
190
191 let mut matched_statements = Vec::new();
192 let mut has_explicit_allow = false;
193 let mut has_explicit_deny = false;
194 let mut statement_count = 0;
195
196 for policy in &self.policies {
198 for statement in &policy.statement {
199 statement_count += 1;
200 if statement_count > self.options.max_statements {
201 return Err(EvaluationError::InternalError(
202 "Maximum statement evaluation limit exceeded".to_string(),
203 ));
204 }
205
206 let statement_result = Self::evaluate_statement(statement, request, &self.options)?;
207
208 if self.options.collect_match_details {
209 matched_statements.push(statement_result.clone());
210 }
211
212 if statement_result.conditions_satisfied {
214 match statement.effect {
215 IAMEffect::Allow => {
216 has_explicit_allow = true;
217 }
218 IAMEffect::Deny => {
219 has_explicit_deny = true;
220 if self.options.stop_on_explicit_deny {
221 return Ok(EvaluationResult {
222 decision: Decision::Deny,
223 matched_statements,
224 context: request.clone(),
225 });
226 }
227 }
228 }
229 }
230 }
231 }
232
233 let decision = if has_explicit_deny {
236 Decision::Deny
237 } else if has_explicit_allow {
238 Decision::Allow
239 } else {
240 Decision::NotApplicable
241 };
242
243 Ok(EvaluationResult {
244 decision,
245 matched_statements,
246 context: request.clone(),
247 })
248 }
249
250 fn evaluate_statement(
252 statement: &IAMStatement,
253 request: &IAMRequest,
254 options: &EvaluationOptions,
255 ) -> Result<StatementMatch, EvaluationError> {
256 if let Some(ref principal) = statement.principal
258 && !Self::principal_matches(principal, &request.principal)?
259 {
260 return Ok(StatementMatch {
261 sid: statement.sid.clone(),
262 effect: statement.effect,
263 conditions_satisfied: false,
264 reason: "Principal does not match".to_string(),
265 });
266 }
267
268 if let Some(ref not_principal) = statement.not_principal
269 && Self::principal_matches(not_principal, &request.principal)?
270 {
271 return Ok(StatementMatch {
272 sid: statement.sid.clone(),
273 effect: statement.effect,
274 conditions_satisfied: false,
275 reason: "Principal matches NotPrincipal exclusion".to_string(),
276 });
277 }
278
279 let action_matches = if let Some(ref action) = statement.action {
281 Self::action_matches(action, &request.action)
282 } else if let Some(ref not_action) = statement.not_action {
283 !Self::action_matches(not_action, &request.action)
284 } else {
285 return Ok(StatementMatch {
286 sid: statement.sid.clone(),
287 effect: statement.effect,
288 conditions_satisfied: false,
289 reason: "No action or not_action specified".to_string(),
290 });
291 };
292
293 if !action_matches {
294 return Ok(StatementMatch {
295 sid: statement.sid.clone(),
296 effect: statement.effect,
297 conditions_satisfied: false,
298 reason: "Action does not match".to_string(),
299 });
300 }
301
302 let resource_matches = if options.ignore_resource_constraints {
304 true
305 } else if let Some(ref resource) = statement.resource {
306 Self::resource_matches(resource, &request.resource, &request.context)?
307 } else if let Some(ref not_resource) = statement.not_resource {
308 !Self::resource_matches(not_resource, &request.resource, &request.context)?
309 } else {
310 return Ok(StatementMatch {
311 sid: statement.sid.clone(),
312 effect: statement.effect,
313 conditions_satisfied: false,
314 reason: "No resource or not_resource specified".to_string(),
315 });
316 };
317
318 if !resource_matches {
319 return Ok(StatementMatch {
320 sid: statement.sid.clone(),
321 effect: statement.effect,
322 conditions_satisfied: false,
323 reason: "Resource does not match".to_string(),
324 });
325 }
326
327 if let Some(ref condition_block) = statement.condition
329 && !Self::evaluate_conditions(condition_block, &request.context)?
330 {
331 return Ok(StatementMatch {
332 sid: statement.sid.clone(),
333 effect: statement.effect,
334 conditions_satisfied: false,
335 reason: "Conditions not satisfied".to_string(),
336 });
337 }
338
339 Ok(StatementMatch {
341 sid: statement.sid.clone(),
342 effect: statement.effect,
343 conditions_satisfied: true,
344 reason: "Statement fully matched".to_string(),
345 })
346 }
347
348 fn principal_matches(
350 principal: &Principal,
351 request_principal: &Principal,
352 ) -> Result<bool, EvaluationError> {
353 if !request_principal.is_single() {
354 return Err(EvaluationError::InvalidRequest(
355 "Request principal must be a single entity".to_string(),
356 ));
357 }
358
359 match (principal, request_principal) {
360 (Principal::Wildcard, _) | (_, Principal::Wildcard) => Ok(true),
362
363 (
367 Principal::Aws(principal_id),
368 Principal::Aws(PrincipalId::String(request_principal_id)),
369 ) => Self::principal_id_matches(principal_id, request_principal_id, |id| {
370 if id == "*" || id == request_principal_id {
374 return Ok(true);
375 }
376 if id.len() == 12 && id.chars().all(|c| c.is_ascii_digit()) {
378 let root_arn = format!("arn:aws:iam::{id}:root");
380 if request_principal_id == id || request_principal_id.as_str() == root_arn {
381 return Ok(true);
382 }
383 }
384 if id.starts_with("arn:") {
386 return Self::principal_string_matches(id, request_principal_id);
387 }
388 Ok(false)
389 }),
390
391 (
395 Principal::Federated(principal_id),
396 Principal::Federated(PrincipalId::String(request_principal_id)),
397 ) => Self::principal_id_matches(principal_id, request_principal_id, |id| {
398 if id == request_principal_id {
401 return Ok(true);
402 }
403 if request_principal_id.starts_with(id) {
405 return Ok(true);
406 }
407 Ok(false)
408 }),
409
410 (
414 Principal::Service(principal_id),
415 Principal::Service(PrincipalId::String(request_principal_id)),
416 ) => Self::principal_id_matches(principal_id, request_principal_id, |id| {
417 if id == request_principal_id {
420 return Ok(true);
421 }
422 Ok(false)
423 }),
424
425 (
429 Principal::CanonicalUser(principal_id),
430 Principal::CanonicalUser(PrincipalId::String(request_principal_id)),
431 ) => Self::principal_id_matches(principal_id, request_principal_id, |id| {
432 if id == request_principal_id {
434 return Ok(true);
435 }
436 Ok(false)
437 }),
438 _ => {
439 Ok(false)
441 }
442 }
443 }
444
445 fn principal_id_matches<F>(
447 principal_id: &PrincipalId,
448 _request_principal: &str,
449 matcher: F,
450 ) -> Result<bool, EvaluationError>
451 where
452 F: Fn(&str) -> Result<bool, EvaluationError>,
453 {
454 match principal_id {
455 PrincipalId::String(id) => matcher(id),
456 PrincipalId::Array(ids) => {
457 for id in ids {
459 if matcher(id)? {
460 return Ok(true);
461 }
462 }
463 Ok(false)
464 }
465 }
466 }
467
468 fn principal_string_matches(
470 principal_str: &str,
471 request_principal: &str,
472 ) -> Result<bool, EvaluationError> {
473 if principal_str == "*" || principal_str == request_principal {
474 Ok(true)
475 } else if principal_str.starts_with("arn:") {
476 let matcher = ArnMatcher::from_pattern(principal_str)
478 .map_err(|e| EvaluationError::InvalidArn(e.to_string()))?;
479 matcher
480 .matches(&Arn::parse(request_principal).unwrap())
481 .map_err(|e| EvaluationError::InvalidArn(e.to_string()))
482 } else {
483 Ok(false)
484 }
485 }
486
487 fn action_matches(action: &IAMAction, request_action: &str) -> bool {
489 match action {
490 IAMAction::Single(a) => {
491 a == "*" || a == request_action || wildcard_match(request_action, a)
492 }
493 IAMAction::Multiple(actions) => {
494 for a in actions {
495 if a == "*" || a == request_action || wildcard_match(request_action, a) {
496 return true;
497 }
498 }
499 false
500 }
501 }
502 }
503
504 fn resource_matches(
506 resource: &IAMResource,
507 request_resource: &Arn,
508 context: &Context,
509 ) -> Result<bool, EvaluationError> {
510 match resource {
511 IAMResource::Single(r) => {
512 if r == "*" {
513 Ok(true)
514 } else {
515 let interpolated = interpolate_variables(r, context)?;
517
518 let matcher = ArnMatcher::from_pattern(&interpolated)
520 .map_err(|e| EvaluationError::InvalidArn(e.to_string()))?;
521 matcher
522 .matches(request_resource)
523 .map_err(|e| EvaluationError::InvalidArn(e.to_string()))
524 }
525 }
526 IAMResource::Multiple(resources) => {
527 for r in resources {
528 if Self::resource_matches(
529 &IAMResource::Single(r.clone()),
530 request_resource,
531 context,
532 )? {
533 return Ok(true);
534 }
535 }
536 Ok(false)
537 }
538 }
539 }
540
541 fn evaluate_conditions(
543 condition_block: &ConditionBlock,
544 context: &Context,
545 ) -> Result<bool, EvaluationError> {
546 for (operator, condition_map) in &condition_block.conditions {
548 for (key, value) in condition_map {
549 if !evaluate_condition(context, operator, key, &value.to_json_value())? {
550 return Ok(false);
551 }
552 }
553 }
554 Ok(true)
555 }
556}
557
558impl Default for PolicyEvaluator {
559 fn default() -> Self {
560 Self::new()
561 }
562}
563
564pub fn evaluate_policy(
574 policy: &IAMPolicy,
575 request: &IAMRequest,
576) -> Result<Decision, EvaluationError> {
577 let evaluator = PolicyEvaluator::with_policies(vec![policy.clone()]);
578 let result = evaluator.evaluate(request)?;
579 Ok(result.decision)
580}
581
582pub fn evaluate_policies(
592 policies: &[IAMPolicy],
593 request: &IAMRequest,
594) -> Result<Decision, EvaluationError> {
595 let evaluator = PolicyEvaluator::with_policies(policies.to_vec());
596 let result = evaluator.evaluate(request)?;
597 Ok(result.decision)
598}
599
600#[cfg(test)]
601mod tests {
602 use super::*;
603 use crate::{
604 Arn, ConditionValue, ContextValue, IAMAction, IAMEffect, IAMOperator, IAMResource,
605 IAMStatement,
606 };
607
608 #[test]
609 fn test_simple_allow_policy() {
610 let policy = IAMPolicy::new().add_statement(
611 IAMStatement::new(IAMEffect::Allow)
612 .with_action(IAMAction::Single("s3:GetObject".to_string()))
613 .with_resource(IAMResource::Single("arn:aws:s3:::my-bucket/*".to_string())),
614 );
615
616 let request = IAMRequest::new(
617 Principal::Aws(PrincipalId::String(
618 "arn:aws:iam::123456789012:user/test".into(),
619 )),
620 "s3:GetObject",
621 Arn::parse("arn:aws:s3:::my-bucket/file.txt").unwrap(),
622 );
623
624 let result = evaluate_policy(&policy, &request).unwrap();
625 assert_eq!(result, Decision::Allow);
626 }
627
628 #[test]
629 fn test_simple_deny_policy() {
630 let policy = IAMPolicy::new().add_statement(
631 IAMStatement::new(IAMEffect::Deny)
632 .with_action(IAMAction::Single("s3:DeleteObject".to_string()))
633 .with_resource(IAMResource::Single("arn:aws:s3:::my-bucket/*".to_string())),
634 );
635
636 let request = IAMRequest::new(
637 Principal::Aws(PrincipalId::String(
638 "arn:aws:iam::123456789012:user/test".into(),
639 )),
640 "s3:DeleteObject",
641 Arn::parse("arn:aws:s3:::my-bucket/file.txt").unwrap(),
642 );
643
644 let result = evaluate_policy(&policy, &request).unwrap();
645 assert_eq!(result, Decision::Deny);
646 }
647
648 #[test]
649 fn test_not_applicable_policy() {
650 let policy = IAMPolicy::new().add_statement(
651 IAMStatement::new(IAMEffect::Allow)
652 .with_action(IAMAction::Single("s3:GetObject".to_string()))
653 .with_resource(IAMResource::Single(
654 "arn:aws:s3:::other-bucket/*".to_string(),
655 )),
656 );
657
658 let request = IAMRequest::new(
659 Principal::Aws(PrincipalId::String(
660 "arn:aws:iam::123456789012:user/test".into(),
661 )),
662 "s3:GetObject",
663 Arn::parse("arn:aws:s3:::my-bucket/file.txt").unwrap(),
664 );
665
666 let result = evaluate_policy(&policy, &request).unwrap();
667 assert_eq!(result, Decision::NotApplicable);
668 }
669
670 #[test]
671 fn test_wildcard_action_matching() {
672 let policy = IAMPolicy::new().add_statement(
673 IAMStatement::new(IAMEffect::Allow)
674 .with_action(IAMAction::Single("s3:*".to_string()))
675 .with_resource(IAMResource::Single("arn:aws:s3:::my-bucket/*".to_string())),
676 );
677
678 let request = IAMRequest::new(
679 Principal::Aws(PrincipalId::String(
680 "arn:aws:iam::123456789012:user/test".into(),
681 )),
682 "s3:GetObject",
683 Arn::parse("arn:aws:s3:::my-bucket/file.txt").unwrap(),
684 );
685
686 let result = evaluate_policy(&policy, &request).unwrap();
687 assert_eq!(result, Decision::Allow);
688 }
689
690 #[test]
691 fn test_condition_evaluation() {
692 use crate::IAMOperator;
693
694 let mut context = Context::new();
695 context.insert(
696 "aws:userid".to_string(),
697 ContextValue::String("test-user".to_string()),
698 );
699
700 let policy = IAMPolicy::new().add_statement(
701 IAMStatement::new(IAMEffect::Allow)
702 .with_action(IAMAction::Single("s3:GetObject".to_string()))
703 .with_resource(IAMResource::Single("arn:aws:s3:::my-bucket/*".to_string()))
704 .with_condition(
705 IAMOperator::StringEquals,
706 "aws:userid".to_string(),
707 ConditionValue::String("test-user".to_string()),
708 ),
709 );
710
711 let request = IAMRequest::new_with_context(
712 Principal::Aws(PrincipalId::String(
713 "arn:aws:iam::123456789012:user/test".into(),
714 )),
715 "s3:GetObject",
716 Arn::parse("arn:aws:s3:::my-bucket/file.txt").unwrap(),
717 context,
718 );
719
720 let result = evaluate_policy(&policy, &request).unwrap();
721 assert_eq!(result, Decision::Allow);
722 }
723
724 #[test]
725 fn test_condition_evaluation_failure() {
726 use crate::IAMOperator;
727
728 let mut context = Context::new();
729 context.insert(
730 "aws:userid".to_string(),
731 ContextValue::String("other-user".to_string()),
732 );
733
734 let policy = IAMPolicy::new().add_statement(
735 IAMStatement::new(IAMEffect::Allow)
736 .with_action(IAMAction::Single("s3:GetObject".to_string()))
737 .with_resource(IAMResource::Single("arn:aws:s3:::my-bucket/*".to_string()))
738 .with_condition(
739 IAMOperator::StringEquals,
740 "aws:userid".to_string(),
741 ConditionValue::String("test-user".to_string()),
742 ),
743 );
744
745 let request = IAMRequest::new_with_context(
746 Principal::Aws(PrincipalId::String(
747 "arn:aws:iam::123456789012:user/test".into(),
748 )),
749 "s3:GetObject",
750 Arn::parse("arn:aws:s3:::my-bucket/file.txt").unwrap(),
751 context,
752 );
753
754 let result = evaluate_policy(&policy, &request).unwrap();
755 assert_eq!(result, Decision::NotApplicable);
756 }
757
758 #[test]
759 fn test_explicit_deny_overrides_allow() {
760 let policies = vec![
761 IAMPolicy::new().add_statement(
762 IAMStatement::new(IAMEffect::Allow)
763 .with_action(IAMAction::Single("s3:*".to_string()))
764 .with_resource(IAMResource::Single("*".to_string())),
765 ),
766 IAMPolicy::new().add_statement(
767 IAMStatement::new(IAMEffect::Deny)
768 .with_action(IAMAction::Single("s3:DeleteObject".to_string()))
769 .with_resource(IAMResource::Single(
770 "arn:aws:s3:::protected-bucket/*".to_string(),
771 )),
772 ),
773 ];
774
775 let request = IAMRequest::new(
776 Principal::Aws(PrincipalId::String(
777 "arn:aws:iam::123456789012:user/test".into(),
778 )),
779 "s3:DeleteObject",
780 Arn::parse("arn:aws:s3:::protected-bucket/file.txt").unwrap(),
781 );
782
783 let result = evaluate_policies(&policies, &request).unwrap();
784 assert_eq!(result, Decision::Deny);
785 }
786
787 #[test]
788 fn test_numeric_condition() {
789 let mut context = Context::new();
790 context.insert("aws:RequestedRegion".to_string(), ContextValue::Number(5.0));
791
792 let policy = IAMPolicy::new().add_statement(
793 IAMStatement::new(IAMEffect::Allow)
794 .with_action(IAMAction::Single("s3:GetObject".to_string()))
795 .with_resource(IAMResource::Single("*".to_string()))
796 .with_condition(
797 IAMOperator::NumericLessThan,
798 "aws:RequestedRegion".to_string(),
799 ConditionValue::Number(10),
800 ),
801 );
802
803 let request = IAMRequest::new_with_context(
804 Principal::Aws(PrincipalId::String(
805 "arn:aws:iam::123456789012:user/test".into(),
806 )),
807 "s3:GetObject",
808 Arn::parse("arn:aws:s3:::my-bucket/file.txt").unwrap(),
809 context,
810 );
811
812 let result = evaluate_policy(&policy, &request).unwrap();
813 assert_eq!(result, Decision::Allow);
814 }
815
816 #[test]
817 fn test_evaluator_with_options() {
818 let policy = IAMPolicy::new().add_statement(
819 IAMStatement::new(IAMEffect::Allow)
820 .with_sid("AllowS3Read")
821 .with_action(IAMAction::Single("s3:GetObject".to_string()))
822 .with_resource(IAMResource::Single("arn:aws:s3:::my-bucket/*".to_string())),
823 );
824
825 let request = IAMRequest::new(
826 Principal::Aws(PrincipalId::String(
827 "arn:aws:iam::123456789012:user/test".into(),
828 )),
829 "s3:GetObject",
830 Arn::parse("arn:aws:s3:::my-bucket/file.txt").unwrap(),
831 );
832
833 let evaluator =
834 PolicyEvaluator::with_policies(vec![policy]).with_options(EvaluationOptions {
835 collect_match_details: true,
836 ..Default::default()
837 });
838
839 let result = evaluator.evaluate(&request).unwrap();
840 assert_eq!(result.decision, Decision::Allow);
841 assert!(!result.matched_statements.is_empty());
842 assert_eq!(
843 result.matched_statements[0].sid,
844 Some("AllowS3Read".to_string())
845 );
846 }
847
848 #[derive(Debug, Clone, Serialize, Deserialize)]
849 struct TestCase {
850 result: Decision,
851 request: IAMRequest,
852 policy: IAMPolicy,
853 }
854
855 #[test]
856 fn test_requests_testset() {
857 let request_dir = "tests/requests";
859 let mut request_files = std::fs::read_dir(request_dir)
860 .unwrap_or_else(|e| panic!("Failed to read requests directory '{request_dir}': {e}"))
861 .filter_map(|entry| {
862 let entry = entry.ok()?;
863 let path = entry.path();
864 if path.extension()? == "json" {
865 Some(path)
866 } else {
867 None
868 }
869 })
870 .collect::<Vec<_>>();
871
872 assert!(
874 !request_files.is_empty(),
875 "No request JSON files found in {request_dir}/"
876 );
877
878 request_files.sort_by_key(|p| {
881 p.file_name()
882 .and_then(|n| n.to_str())
883 .map(|s| s.split('.').next().unwrap().parse::<u32>().unwrap())
884 .map(|n| format!("{n:010}"))
885 });
886
887 println!(
888 "Testing {} request files from {}/",
889 request_files.len(),
890 request_dir
891 );
892
893 for (index, request_file) in request_files.iter().enumerate() {
894 let filename = request_file
895 .file_name()
896 .and_then(|n| n.to_str())
897 .unwrap_or("unknown");
898
899 println!("Testing request #{}: {} ... ", index + 1, filename);
900
901 let json_content = std::fs::read_to_string(request_file).unwrap_or_else(|e| {
903 panic!("Failed to read file '{}': {}", request_file.display(), e)
904 });
905
906 let test: TestCase = serde_json::from_str(&json_content).unwrap_or_else(|e| {
908 panic!(
909 "Failed to parse JSON from file '{}': {:?}",
910 request_file.display(),
911 e
912 )
913 });
914
915 let result = evaluate_policy(&test.policy, &test.request).unwrap();
917 assert_eq!(result, test.result);
918 }
919 }
920}