//! Condition evaluation for ABAC //! //! Evaluates condition expressions against the current authorization context. use std::net::IpAddr; use std::str::FromStr; use ipnetwork::IpNetwork; use iam_types::{Condition, ConditionExpr, Error, IamError, Result}; use crate::context::VariableContext; /// Evaluate a condition against the given context pub fn evaluate_condition(condition: &Condition, ctx: &VariableContext<'_>) -> Result { evaluate_expr(&condition.expression, ctx) } /// Evaluate a condition expression fn evaluate_expr(expr: &ConditionExpr, ctx: &VariableContext<'_>) -> Result { match expr { ConditionExpr::StringEquals { key, value } => { let actual = resolve_key(key, ctx); let expected = ctx.substitute(value); Ok(actual.as_deref() == Some(&expected)) } ConditionExpr::StringNotEquals { key, value } => { let actual = resolve_key(key, ctx); let expected = ctx.substitute(value); Ok(actual.as_deref() != Some(&expected)) } ConditionExpr::StringLike { key, pattern } => { let actual = require_key(key, ctx)?; let pattern = ctx.substitute(pattern); Ok(glob_match::glob_match(&pattern, &actual)) } ConditionExpr::StringNotLike { key, pattern } => { let actual = require_key(key, ctx)?; let pattern = ctx.substitute(pattern); Ok(!glob_match::glob_match(&pattern, &actual)) } ConditionExpr::NumericEquals { key, value } => { let actual = require_key(key, ctx)?; let actual_num: i64 = actual.parse().map_err(|_| { Error::Iam(IamError::InvalidCondition(format!( "Cannot parse '{}' as number for key '{}'", actual, key ))) })?; Ok(actual_num == *value) } ConditionExpr::NumericLessThan { key, value } => { let actual = require_key(key, ctx)?; let actual_num: i64 = actual.parse().map_err(|_| { Error::Iam(IamError::InvalidCondition(format!( "Cannot parse '{}' as number", actual ))) })?; Ok(actual_num < *value) } ConditionExpr::NumericLessThanEquals { key, value } => { let actual = require_key(key, ctx)?; let actual_num: i64 = actual.parse().map_err(|_| { Error::Iam(IamError::InvalidCondition(format!( "Cannot parse '{}' as number", actual ))) })?; Ok(actual_num <= *value) } ConditionExpr::NumericGreaterThan { key, value } => { let actual = require_key(key, ctx)?; let actual_num: i64 = actual.parse().map_err(|_| { Error::Iam(IamError::InvalidCondition(format!( "Cannot parse '{}' as number", actual ))) })?; Ok(actual_num > *value) } ConditionExpr::NumericGreaterThanEquals { key, value } => { let actual = require_key(key, ctx)?; let actual_num: i64 = actual.parse().map_err(|_| { Error::Iam(IamError::InvalidCondition(format!( "Cannot parse '{}' as number", actual ))) })?; Ok(actual_num >= *value) } ConditionExpr::IpAddress { key, cidr } => { let actual = require_key(key, ctx)?; let ip: IpAddr = actual.parse().map_err(|_| { Error::Iam(IamError::InvalidCondition(format!( "Cannot parse '{}' as IP address", actual ))) })?; let network: IpNetwork = cidr.parse().map_err(|_| { Error::Iam(IamError::InvalidCondition(format!( "Cannot parse '{}' as CIDR", cidr ))) })?; Ok(network.contains(ip)) } ConditionExpr::NotIpAddress { key, cidr } => { let actual = require_key(key, ctx)?; let ip: IpAddr = actual.parse().map_err(|_| { Error::Iam(IamError::InvalidCondition(format!( "Cannot parse '{}' as IP address", actual ))) })?; let network: IpNetwork = cidr.parse().map_err(|_| { Error::Iam(IamError::InvalidCondition(format!( "Cannot parse '{}' as CIDR", cidr ))) })?; Ok(!network.contains(ip)) } ConditionExpr::TimeBetween { start, end } => { let now = ctx.context.timestamp; // Parse start/end as either Unix timestamp or HH:MM format let start_ts = parse_time(start, now)?; let end_ts = parse_time(end, now)?; Ok(now >= start_ts && now <= end_ts) } ConditionExpr::Exists { key } => Ok(resolve_key(key, ctx).is_some()), ConditionExpr::StringEqualsAny { key, values } => { let actual = resolve_key(key, ctx); match actual { Some(actual_val) => { let substituted: Vec = values.iter().map(|v| ctx.substitute(v)).collect(); Ok(substituted.contains(&actual_val)) } None => Ok(false), } } ConditionExpr::Bool { key, value } => { let actual = require_key(key, ctx)?; let actual_bool = matches!(actual.to_lowercase().as_str(), "true" | "1" | "yes"); Ok(actual_bool == *value) } ConditionExpr::And(exprs) => { for e in exprs { if !evaluate_expr(e, ctx)? { return Ok(false); } } Ok(true) } ConditionExpr::Or(exprs) => { for e in exprs { if evaluate_expr(e, ctx)? { return Ok(true); } } Ok(false) } ConditionExpr::Not(inner) => Ok(!evaluate_expr(inner, ctx)?), } } /// Resolve a key to its value, returning Option fn resolve_key(key: &str, ctx: &VariableContext<'_>) -> Option { ctx.resolve(key) } /// Resolve a key, returning an error if not found fn require_key(key: &str, ctx: &VariableContext<'_>) -> Result { ctx.resolve(key).ok_or_else(|| { Error::Iam(IamError::InvalidCondition(format!( "Key not found: {}", key ))) }) } /// Parse time string to Unix timestamp fn parse_time(time_str: &str, reference_time: u64) -> Result { // Try parsing as Unix timestamp if let Ok(ts) = time_str.parse::() { return Ok(ts); } // Try parsing as HH:MM (relative to current day) if let Some((hours, minutes)) = time_str.split_once(':') { let hours: u64 = hours.parse().map_err(|_| { Error::Iam(IamError::InvalidCondition(format!( "Invalid time format: {}", time_str ))) })?; let minutes: u64 = minutes.parse().map_err(|_| { Error::Iam(IamError::InvalidCondition(format!( "Invalid time format: {}", time_str ))) })?; // Calculate seconds since midnight let time_of_day = hours * 3600 + minutes * 60; // Get current day start let day_start = (reference_time / 86400) * 86400; return Ok(day_start + time_of_day); } // Try parsing as ISO 8601 // For simplicity, just return an error for unsupported formats Err(Error::Iam(IamError::InvalidCondition(format!( "Unsupported time format: {}", time_str )))) } #[cfg(test)] mod tests { use super::*; use crate::context::AuthzContext; use iam_types::{Principal, Resource}; use std::net::{IpAddr, Ipv4Addr}; fn test_context<'a>( principal: &'a Principal, resource: &'a Resource, context: &'a AuthzContext, ) -> VariableContext<'a> { VariableContext::new(principal, resource, context) } #[test] fn test_string_equals() { let principal = Principal::new_user("alice", "Alice"); let resource = Resource::new("instance", "vm-1", "org-1", "proj-1").with_owner("alice"); let ctx = AuthzContext::new(); let var_ctx = test_context(&principal, &resource, &ctx); // Direct comparison let cond = Condition::string_equals("resource.owner", "alice"); assert!(evaluate_condition(&cond, &var_ctx).unwrap()); // Variable substitution let cond = Condition::string_equals("resource.owner", "${principal.id}"); assert!(evaluate_condition(&cond, &var_ctx).unwrap()); // Non-matching let cond = Condition::string_equals("resource.owner", "bob"); assert!(!evaluate_condition(&cond, &var_ctx).unwrap()); } #[test] fn test_string_like() { let principal = Principal::new_user("alice", "Alice"); let resource = Resource::new("instance", "vm-prod-001", "org-1", "proj-1"); let ctx = AuthzContext::new(); let var_ctx = test_context(&principal, &resource, &ctx); let cond = Condition::string_like("resource.id", "vm-prod-*"); assert!(evaluate_condition(&cond, &var_ctx).unwrap()); let cond = Condition::string_like("resource.id", "vm-dev-*"); assert!(!evaluate_condition(&cond, &var_ctx).unwrap()); } #[test] fn test_ip_address() { let principal = Principal::new_user("alice", "Alice"); let resource = Resource::new("instance", "vm-1", "org-1", "proj-1"); let ctx = AuthzContext::new().with_source_ip(IpAddr::V4(Ipv4Addr::new(10, 0, 1, 50))); let var_ctx = test_context(&principal, &resource, &ctx); let cond = Condition::ip_address("request.source_ip", "10.0.0.0/8"); assert!(evaluate_condition(&cond, &var_ctx).unwrap()); let cond = Condition::ip_address("request.source_ip", "192.168.0.0/16"); assert!(!evaluate_condition(&cond, &var_ctx).unwrap()); } #[test] fn test_and_condition() { let principal = Principal::new_user("alice", "Alice"); let resource = Resource::new("instance", "vm-1", "org-1", "proj-1").with_owner("alice"); let ctx = AuthzContext::new().with_source_ip(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1))); let var_ctx = test_context(&principal, &resource, &ctx); let cond = Condition::and(vec![ Condition::string_equals("resource.owner", "${principal.id}"), Condition::ip_address("request.source_ip", "10.0.0.0/8"), ]); assert!(evaluate_condition(&cond, &var_ctx).unwrap()); // One condition fails let cond = Condition::and(vec![ Condition::string_equals("resource.owner", "bob"), Condition::ip_address("request.source_ip", "10.0.0.0/8"), ]); assert!(!evaluate_condition(&cond, &var_ctx).unwrap()); } #[test] fn test_or_condition() { let principal = Principal::new_user("alice", "Alice"); let resource = Resource::new("instance", "vm-1", "org-1", "proj-1").with_owner("bob"); let ctx = AuthzContext::new(); let var_ctx = test_context(&principal, &resource, &ctx); let cond = Condition::or(vec![ Condition::string_equals("resource.owner", "${principal.id}"), // false Condition::string_equals("principal.id", "alice"), // true ]); assert!(evaluate_condition(&cond, &var_ctx).unwrap()); } #[test] fn test_not_condition() { let principal = Principal::new_user("alice", "Alice"); let resource = Resource::new("instance", "vm-1", "org-1", "proj-1"); let ctx = AuthzContext::new(); let var_ctx = test_context(&principal, &resource, &ctx); let cond = Condition::not(Condition::string_equals("principal.id", "bob")); assert!(evaluate_condition(&cond, &var_ctx).unwrap()); } }