//! Authorization context //! //! Provides context for policy evaluation including principal, resource, and request metadata. use std::collections::HashMap; use std::net::IpAddr; use iam_types::{Principal, Resource}; /// Context for authorization evaluation #[derive(Debug, Clone)] pub struct AuthzContext { /// Source IP address of the request pub source_ip: Option, /// Request timestamp (Unix seconds) pub timestamp: u64, /// Request metadata pub metadata: HashMap, /// HTTP method (if applicable) pub http_method: Option, /// Request path (if applicable) pub request_path: Option, } impl AuthzContext { /// Create a new context with the current timestamp pub fn new() -> Self { let timestamp = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_secs(); Self { source_ip: None, timestamp, metadata: HashMap::new(), http_method: None, request_path: None, } } /// Set the source IP pub fn with_source_ip(mut self, ip: IpAddr) -> Self { self.source_ip = Some(ip); self } /// Set the timestamp pub fn with_timestamp(mut self, timestamp: u64) -> Self { self.timestamp = timestamp; self } /// Add metadata pub fn with_metadata(mut self, key: impl Into, value: impl Into) -> Self { self.metadata.insert(key.into(), value.into()); self } /// Set HTTP method pub fn with_http_method(mut self, method: impl Into) -> Self { self.http_method = Some(method.into()); self } /// Set request path pub fn with_request_path(mut self, path: impl Into) -> Self { self.request_path = Some(path.into()); self } /// Get a value for condition evaluation pub fn get_value(&self, key: &str) -> Option { match key { "request.source_ip" => self.source_ip.map(|ip| ip.to_string()), "request.time" => Some(self.timestamp.to_string()), "request.method" => self.http_method.clone(), "request.path" => self.request_path.clone(), key if key.starts_with("request.metadata.") => { let meta_key = &key["request.metadata.".len()..]; self.metadata.get(meta_key).cloned() } _ => None, } } } impl Default for AuthzContext { fn default() -> Self { Self::new() } } /// Variable context for condition evaluation /// Combines principal, resource, and request context pub struct VariableContext<'a> { pub principal: &'a Principal, pub resource: &'a Resource, pub context: &'a AuthzContext, } impl<'a> VariableContext<'a> { /// Create a new variable context pub fn new( principal: &'a Principal, resource: &'a Resource, context: &'a AuthzContext, ) -> Self { Self { principal, resource, context, } } /// Resolve a variable key to its value pub fn resolve(&self, key: &str) -> Option { if let Some(prop) = key.strip_prefix("principal.") { self.resolve_principal(prop) } else if let Some(prop) = key.strip_prefix("resource.") { self.resource.get_property(prop) } else if key.starts_with("request.") { self.context.get_value(key) } else { None } } fn resolve_principal(&self, prop: &str) -> Option { match prop { "id" => Some(self.principal.id.clone()), "kind" => Some(self.principal.kind.to_string()), "name" => Some(self.principal.name.clone()), "org_id" => self.principal.org_id.clone(), "project_id" => self.principal.project_id.clone(), "node_id" => self.principal.node_id.clone(), "email" => self.principal.email.clone(), key if key.starts_with("metadata.") => { let meta_key = &key["metadata.".len()..]; self.principal.metadata.get(meta_key).cloned() } _ => None, } } /// Substitute variables in a string (${var} syntax) pub fn substitute(&self, template: &str) -> String { let mut result = template.to_string(); let mut start = 0; while let Some(var_start) = result[start..].find("${") { let absolute_start = start + var_start; if let Some(var_end) = result[absolute_start..].find('}') { let absolute_end = absolute_start + var_end; let var_name = &result[absolute_start + 2..absolute_end]; if let Some(value) = self.resolve(var_name) { result = format!( "{}{}{}", &result[..absolute_start], value, &result[absolute_end + 1..] ); start = absolute_start + value.len(); } else { // Variable not found, keep as is start = absolute_end + 1; } } else { break; } } result } } #[cfg(test)] mod tests { use super::*; use std::net::Ipv4Addr; #[test] fn test_context_values() { let ctx = AuthzContext::new() .with_source_ip(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1))) .with_metadata("tenant", "acme"); assert_eq!(ctx.get_value("request.source_ip"), Some("10.0.0.1".into())); assert_eq!( ctx.get_value("request.metadata.tenant"), Some("acme".into()) ); } #[test] fn test_variable_substitution() { let principal = Principal::new_user("alice", "Alice Smith"); let resource = Resource::new("instance", "vm-123", "org-1", "proj-1").with_owner("alice"); let context = AuthzContext::new(); let var_ctx = VariableContext::new(&principal, &resource, &context); // Test simple substitution assert_eq!(var_ctx.substitute("user-${principal.id}"), "user-alice"); // Test multiple substitutions assert_eq!( var_ctx.substitute("${resource.kind}/${resource.id}"), "instance/vm-123" ); // Test unknown variable (kept as is) assert_eq!(var_ctx.substitute("${unknown.var}"), "${unknown.var}"); } }