iam_rs/evaluation/
engine.rs

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/// Result of policy evaluation
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16pub enum Decision {
17    /// Access is explicitly allowed
18    Allow,
19    /// Access is explicitly denied
20    Deny,
21    /// No applicable policy found (implicit deny)
22    NotApplicable,
23}
24
25/// Error types for policy evaluation
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub enum EvaluationError {
28    /// Invalid request context
29    InvalidContext(String),
30    /// Policy parsing or validation error
31    InvalidPolicy(String),
32    /// ARN format error during evaluation
33    InvalidArn(String),
34    /// Condition evaluation error
35    ConditionError(String),
36    /// Internal evaluation error
37    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/// Evaluation result with decision and metadata
55#[derive(Debug, Clone)]
56pub struct EvaluationResult {
57    /// The final decision
58    pub decision: Decision,
59    /// Statements that matched (for debugging/auditing)
60    pub matched_statements: Vec<StatementMatch>,
61    /// Evaluation context used
62    pub context: IAMRequest,
63}
64
65/// Information about a statement that matched during evaluation
66#[derive(Debug, Clone)]
67pub struct StatementMatch {
68    /// Statement ID if available
69    pub sid: Option<String>,
70    /// Effect of the statement
71    pub effect: Effect,
72    /// Whether all conditions were satisfied
73    pub conditions_satisfied: bool,
74    /// Reason for the match/non-match
75    pub reason: String,
76}
77
78/// Policy evaluation engine
79#[derive(Debug, Clone)]
80pub struct PolicyEvaluator {
81    /// Policies to evaluate
82    policies: Vec<IAMPolicy>,
83    /// Evaluation options
84    options: EvaluationOptions,
85}
86
87/// Options for policy evaluation
88#[derive(Debug, Clone)]
89pub struct EvaluationOptions {
90    /// Whether to continue evaluation after finding an explicit deny
91    pub stop_on_explicit_deny: bool,
92    /// Whether to collect detailed match information
93    pub collect_match_details: bool,
94    /// Maximum number of statements to evaluate (for safety)
95    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    /// Create a new policy evaluator
110    pub fn new() -> Self {
111        Self {
112            policies: Vec::new(),
113            options: EvaluationOptions::default(),
114        }
115    }
116
117    /// Create evaluator with policies
118    pub fn with_policies(policies: Vec<IAMPolicy>) -> Self {
119        Self {
120            policies,
121            options: EvaluationOptions::default(),
122        }
123    }
124
125    /// Add a policy to the evaluator
126    pub fn add_policy(&mut self, policy: IAMPolicy) {
127        self.policies.push(policy);
128    }
129
130    /// Set evaluation options
131    pub fn with_options(mut self, options: EvaluationOptions) -> Self {
132        self.options = options;
133        self
134    }
135
136    /// Evaluate an authorization request against all policies
137    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        // Evaluate each policy
144        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                // Check if this statement applies to the request
160                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        // Apply IAM evaluation logic: Explicit deny overrides everything,
187        // then explicit allow, then implicit deny
188        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    /// Evaluate a single statement against a request
204    fn evaluate_statement(
205        &self,
206        statement: &IAMStatement,
207        request: &IAMRequest,
208    ) -> Result<StatementMatch, EvaluationError> {
209        // Check if principal matches (for resource-based policies)
210        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        // Check if action matches
233        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        // Check if resource matches
256        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        // Check conditions
279        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        // All checks passed
291        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    /// Check if a principal matches the request principal
300    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                // Handle mapped principals (e.g., {"AWS": "arn:aws:iam::123456789012:user/test"})
309                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    /// Check if a principal string matches the request principal
334    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            // ARN-based principal matching
343            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    /// Check if an action matches the request action
354    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    /// Check if a resource matches the request resource
375    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                    // Use ARN matcher for resource patterns
386                    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    /// Evaluate condition block
405    fn evaluate_conditions(
406        &self,
407        condition_block: &ConditionBlock,
408        context: &Context,
409    ) -> Result<bool, EvaluationError> {
410        // All conditions in a block must be satisfied (AND logic)
411        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    /// Evaluate a single condition
422    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        // Get the context value for the key
430        let context_value = context.get(key);
431
432        match operator {
433            // String conditions
434            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            // Numeric conditions
449            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            // Date conditions
473            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            // Boolean conditions
493            Operator::Bool => self.evaluate_boolean_condition(context_value, value),
494
495            // Binary conditions
496            Operator::BinaryEquals => self.evaluate_binary_condition(context_value, value),
497
498            // IP address conditions
499            Operator::IpAddress => self.evaluate_ip_condition(context_value, value, true),
500            Operator::NotIpAddress => self.evaluate_ip_condition(context_value, value, false),
501
502            // ARN conditions
503            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            // Null check
515            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            // Set operators (for multivalued context)
526            Operator::ForAnyValueStringEquals
527            | Operator::ForAllValuesStringEquals
528            | Operator::ForAnyValueStringLike
529            | Operator::ForAllValuesStringLike => {
530                // TODO: Treat these as regular string conditions for now. Full implementation should handle set logic.
531                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    /// Helper for string condition evaluation
542    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), // Type mismatch
554            None => return Ok(false),    // Missing context
555        };
556
557        match condition_value {
558            serde_json::Value::String(s) => Ok(predicate(context_str, s)),
559            serde_json::Value::Array(arr) => {
560                // Any value in the array can match
561                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    /// Helper for numeric condition evaluation
577    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    /// Helper for date condition evaluation
636    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    /// Helper for boolean condition evaluation
669    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    /// Helper for binary condition evaluation
698    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                // Try to decode base64 string to bytes
706                BASE64_STANDARD.decode(s.as_bytes()).map_err(|_| {
707                    EvaluationError::ConditionError("Invalid base64 context value".to_string())
708                })?
709            }
710            Some(_) => return Ok(false), // Type mismatch
711            None => return Ok(false),    // Missing context
712        };
713
714        match condition_value {
715            serde_json::Value::String(s) => {
716                // Decode base64 condition value to bytes
717                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                // Any value in the array can match
724                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    /// Helper for IP address condition evaluation
746    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        // TODO: Simplified IP matching - real implementation would use IP parsing
753        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    /// Helper for ARN condition evaluation
759    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        // Use the same logic as string conditions for ARN comparison
769        self.evaluate_string_condition(context_value, condition_value, predicate)
770    }
771
772    /// Simple wildcard matching for actions and strings
773    fn wildcard_match(&self, text: &str, pattern: &str) -> bool {
774        // Use the ARN wildcard matching logic
775        crate::Arn::wildcard_match(text, pattern)
776    }
777}
778
779impl Default for PolicyEvaluator {
780    fn default() -> Self {
781        Self::new()
782    }
783}
784
785/// Convenience function for simple policy evaluation
786pub 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
795/// Convenience function for evaluating multiple policies
796pub 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}