//! Audit event types //! //! Defines structured audit events for IAM operations. use std::collections::HashMap; use std::net::IpAddr; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; use iam_types::Scope; /// Audit event #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AuditEvent { /// Unique event ID pub id: String, /// Event timestamp pub timestamp: DateTime, /// Event kind pub kind: AuditEventKind, /// Principal ID (if known) pub principal_id: Option, /// Source IP address pub source_ip: Option, /// Request ID (for correlation) pub request_id: Option, /// Session ID pub session_id: Option, /// Additional metadata pub metadata: HashMap, } /// Kind of audit event #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum AuditEventKind { /// Authentication event Authn(AuthnEventData), /// Authorization event Authz(AuthzEventData), /// Policy change event Policy(PolicyEventData), /// Token event (issue, revoke, refresh) Token(TokenEventData), /// Administrative action Admin(AdminEventData), } /// Authentication event data #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AuthnEventData { /// Whether authentication succeeded pub success: bool, /// Authentication method used (jwt, mtls, api_key, etc.) pub method: String, /// Failure reason (if failed) pub failure_reason: Option, /// Token issuer (if JWT) pub issuer: Option, /// Client certificate subject (if mTLS) pub cert_subject: Option, } /// Authorization event data #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AuthzEventData { /// Whether authorization was granted pub allowed: bool, /// Action being authorized pub action: String, /// Resource being accessed pub resource_kind: String, pub resource_id: String, /// Scope of the request pub scope: Scope, /// Matching policy ID (if allowed) pub policy_id: Option, /// Denial reason (if denied) pub denial_reason: Option, /// Roles evaluated pub roles_evaluated: Vec, } /// Policy change event data #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PolicyEventData { /// Type of change pub change_type: PolicyChangeType, /// Policy ID pub policy_id: String, /// Policy name pub policy_name: Option, /// Scope affected pub scope: Scope, /// Actor who made the change pub actor_id: String, } /// Type of policy change #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum PolicyChangeType { Created, Updated, Deleted, Attached, Detached, } /// Token event data #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TokenEventData { /// Type of token event pub event_type: TokenEventType, /// Token ID or session ID pub token_id: Option, /// Token TTL (if issued) pub ttl_seconds: Option, /// Scope of the token pub scope: Option, } /// Type of token event #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum TokenEventType { Issued, Refreshed, Revoked, Expired, } /// Administrative action event data #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AdminEventData { /// Type of admin action pub action: String, /// Target entity type pub target_type: String, /// Target entity ID pub target_id: String, /// Details of the action pub details: Option, } impl AuditEvent { /// Create a new audit event pub fn new(kind: AuditEventKind) -> Self { Self { id: Uuid::new_v4().to_string(), timestamp: Utc::now(), kind, principal_id: None, source_ip: None, request_id: None, session_id: None, metadata: HashMap::new(), } } /// Create an authentication success event pub fn authn_success(principal_id: &str, method: &str) -> Self { Self::new(AuditEventKind::Authn(AuthnEventData { success: true, method: method.to_string(), failure_reason: None, issuer: None, cert_subject: None, })) .with_principal(principal_id) } /// Create an authentication failure event pub fn authn_failure(method: &str, reason: &str) -> Self { Self::new(AuditEventKind::Authn(AuthnEventData { success: false, method: method.to_string(), failure_reason: Some(reason.to_string()), issuer: None, cert_subject: None, })) } /// Create an authorization allowed event pub fn authz_allowed( principal_id: &str, action: &str, resource_kind: &str, resource_id: &str, scope: Scope, ) -> Self { Self::new(AuditEventKind::Authz(AuthzEventData { allowed: true, action: action.to_string(), resource_kind: resource_kind.to_string(), resource_id: resource_id.to_string(), scope, policy_id: None, denial_reason: None, roles_evaluated: Vec::new(), })) .with_principal(principal_id) } /// Create an authorization denied event pub fn authz_denied( principal_id: &str, action: &str, resource_kind: &str, resource_id: &str, scope: Scope, reason: &str, ) -> Self { Self::new(AuditEventKind::Authz(AuthzEventData { allowed: false, action: action.to_string(), resource_kind: resource_kind.to_string(), resource_id: resource_id.to_string(), scope, policy_id: None, denial_reason: Some(reason.to_string()), roles_evaluated: Vec::new(), })) .with_principal(principal_id) } /// Create a token issued event pub fn token_issued( principal_id: &str, token_id: &str, ttl_seconds: u64, scope: Scope, ) -> Self { Self::new(AuditEventKind::Token(TokenEventData { event_type: TokenEventType::Issued, token_id: Some(token_id.to_string()), ttl_seconds: Some(ttl_seconds), scope: Some(scope), })) .with_principal(principal_id) } /// Create a token revoked event pub fn token_revoked(principal_id: &str, token_id: &str) -> Self { Self::new(AuditEventKind::Token(TokenEventData { event_type: TokenEventType::Revoked, token_id: Some(token_id.to_string()), ttl_seconds: None, scope: None, })) .with_principal(principal_id) } /// Create a policy created event pub fn policy_created( actor_id: &str, policy_id: &str, policy_name: &str, scope: Scope, ) -> Self { Self::new(AuditEventKind::Policy(PolicyEventData { change_type: PolicyChangeType::Created, policy_id: policy_id.to_string(), policy_name: Some(policy_name.to_string()), scope, actor_id: actor_id.to_string(), })) .with_principal(actor_id) } /// Create a policy deleted event pub fn policy_deleted(actor_id: &str, policy_id: &str, scope: Scope) -> Self { Self::new(AuditEventKind::Policy(PolicyEventData { change_type: PolicyChangeType::Deleted, policy_id: policy_id.to_string(), policy_name: None, scope, actor_id: actor_id.to_string(), })) .with_principal(actor_id) } /// Set principal ID pub fn with_principal(mut self, principal_id: &str) -> Self { self.principal_id = Some(principal_id.to_string()); self } /// Set source IP pub fn with_source_ip(mut self, ip: IpAddr) -> Self { self.source_ip = Some(ip); self } /// Set request ID pub fn with_request_id(mut self, request_id: &str) -> Self { self.request_id = Some(request_id.to_string()); self } /// Set session ID pub fn with_session_id(mut self, session_id: &str) -> Self { self.session_id = Some(session_id.to_string()); self } /// Add metadata pub fn with_metadata(mut self, key: &str, value: &str) -> Self { self.metadata.insert(key.to_string(), value.to_string()); self } /// Serialize to JSON pub fn to_json(&self) -> Result { serde_json::to_string(self) } /// Serialize to pretty JSON pub fn to_json_pretty(&self) -> Result { serde_json::to_string_pretty(self) } } #[cfg(test)] mod tests { use super::*; use std::net::Ipv4Addr; #[test] fn test_authn_success_event() { let event = AuditEvent::authn_success("alice", "jwt") .with_source_ip(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1))) .with_request_id("req-123"); assert_eq!(event.principal_id, Some("alice".to_string())); assert!(matches!(event.kind, AuditEventKind::Authn(ref data) if data.success)); let json = event.to_json().unwrap(); assert!(json.contains("alice")); assert!(json.contains("jwt")); } #[test] fn test_authn_failure_event() { let event = AuditEvent::authn_failure("jwt", "Token expired"); assert!(event.principal_id.is_none()); match &event.kind { AuditEventKind::Authn(data) => { assert!(!data.success); assert_eq!(data.failure_reason, Some("Token expired".to_string())); } _ => panic!("Expected Authn event"), } } #[test] fn test_authz_allowed_event() { let event = AuditEvent::authz_allowed( "alice", "read", "instance", "vm-1", Scope::project("proj-1", "org-1"), ); assert_eq!(event.principal_id, Some("alice".to_string())); match &event.kind { AuditEventKind::Authz(data) => { assert!(data.allowed); assert_eq!(data.action, "read"); } _ => panic!("Expected Authz event"), } } #[test] fn test_authz_denied_event() { let event = AuditEvent::authz_denied( "bob", "delete", "instance", "vm-1", Scope::project("proj-1", "org-1"), "No matching policy", ); match &event.kind { AuditEventKind::Authz(data) => { assert!(!data.allowed); assert_eq!(data.denial_reason, Some("No matching policy".to_string())); } _ => panic!("Expected Authz event"), } } #[test] fn test_token_issued_event() { let event = AuditEvent::token_issued("alice", "session-123", 3600, Scope::System); match &event.kind { AuditEventKind::Token(data) => { assert!(matches!(data.event_type, TokenEventType::Issued)); assert_eq!(data.ttl_seconds, Some(3600)); } _ => panic!("Expected Token event"), } } #[test] fn test_policy_created_event() { let event = AuditEvent::policy_created( "admin", "policy-1", "AllowReadInstances", Scope::org("org-1"), ); match &event.kind { AuditEventKind::Policy(data) => { assert!(matches!(data.change_type, PolicyChangeType::Created)); assert_eq!(data.policy_id, "policy-1"); } _ => panic!("Expected Policy event"), } } #[test] fn test_event_serialization() { let event = AuditEvent::authn_success("alice", "jwt").with_metadata("user_agent", "curl/7.68.0"); let json = event.to_json().unwrap(); let parsed: AuditEvent = serde_json::from_str(&json).unwrap(); assert_eq!(parsed.principal_id, event.principal_id); assert_eq!( parsed.metadata.get("user_agent"), Some(&"curl/7.68.0".to_string()) ); } }