use crate::error::{SecurityError, Result};
use crate::config::{AuditConfig, AuditLogLevel};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum AuditEventType {
Authentication,
Authorization,
UserManagement,
Session,
DataAccess,
Configuration,
Security,
System,
Custom(String),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub enum AuditSeverity {
Debug,
Info,
Warning,
Error,
Critical,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditEvent {
pub id: String,
pub timestamp: DateTime<Utc>,
pub event_type: AuditEventType,
pub severity: AuditSeverity,
pub user_id: Option<String>,
pub session_id: Option<String>,
pub ip_address: Option<String>,
pub user_agent: Option<String>,
pub resource: Option<String>,
pub action: Option<String>,
pub result: AuditResult,
pub metadata: HashMap<String, serde_json::Value>,
pub message: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum AuditResult {
Success,
Failure {
error_code: String,
error_message: String,
},
Denied {
reason: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditLogEntry {
pub event: AuditEvent,
pub retention_days: Option<u32>,
pub archived: bool,
}
pub struct AuditService {
config: AuditConfig,
events: Arc<RwLock<Vec<AuditLogEntry>>>,
}
impl AuditService {
pub fn new(config: AuditConfig) -> Self {
Self {
config,
events: Arc::new(RwLock::new(Vec::new())),
}
}
pub async fn log_event(&self, event: AuditEvent) -> Result<()> {
let retention_days = self.calculate_retention_days(&event.severity);
let entry = AuditLogEntry {
event: event.clone(),
retention_days,
archived: false,
};
let mut events = self.events.write().await;
events.push(entry);
self.enforce_memory_limits(&mut events).await;
self.log_to_outputs(&event).await?;
Ok(())
}
pub async fn log_authentication(
&self,
user_id: Option<&str>,
ip_address: Option<&str>,
user_agent: Option<&str>,
result: AuditResult,
message: &str,
) -> Result<()> {
let event = AuditEvent {
id: uuid::Uuid::new_v4().to_string(),
timestamp: Utc::now(),
event_type: AuditEventType::Authentication,
severity: match &result {
AuditResult::Success => AuditSeverity::Info,
AuditResult::Failure { .. } => AuditSeverity::Warning,
AuditResult::Denied { .. } => AuditSeverity::Error,
},
user_id: user_id.map(|s| s.to_string()),
session_id: None,
ip_address: ip_address.map(|s| s.to_string()),
user_agent: user_agent.map(|s| s.to_string()),
resource: None,
action: Some("authenticate".to_string()),
result,
metadata: HashMap::new(),
message: message.to_string(),
};
self.log_event(event).await
}
pub async fn log_authorization(
&self,
user_id: &str,
resource: &str,
action: &str,
result: AuditResult,
ip_address: Option<&str>,
) -> Result<()> {
let severity = match &result {
AuditResult::Success => AuditSeverity::Debug,
AuditResult::Failure { .. } => AuditSeverity::Warning,
AuditResult::Denied { .. } => AuditSeverity::Warning,
};
let event = AuditEvent {
id: uuid::Uuid::new_v4().to_string(),
timestamp: Utc::now(),
event_type: AuditEventType::Authorization,
severity,
user_id: Some(user_id.to_string()),
session_id: None,
ip_address: ip_address.map(|s| s.to_string()),
user_agent: None,
resource: Some(resource.to_string()),
action: Some(action.to_string()),
result,
metadata: HashMap::new(),
message: format!("Authorization check for {} on {}:{}", user_id, resource, action),
};
self.log_event(event).await
}
pub async fn log_data_access(
&self,
user_id: &str,
resource: &str,
action: &str,
result: AuditResult,
metadata: HashMap<String, serde_json::Value>,
) -> Result<()> {
let event = AuditEvent {
id: uuid::Uuid::new_v4().to_string(),
timestamp: Utc::now(),
event_type: AuditEventType::DataAccess,
severity: AuditSeverity::Info,
user_id: Some(user_id.to_string()),
session_id: None,
ip_address: None,
user_agent: None,
resource: Some(resource.to_string()),
action: Some(action.to_string()),
result,
metadata,
message: format!("Data access: {} performed {} on {}", user_id, action, resource),
};
self.log_event(event).await
}
pub async fn get_events(
&self,
start_time: Option<DateTime<Utc>>,
end_time: Option<DateTime<Utc>>,
event_type: Option<&AuditEventType>,
user_id: Option<&str>,
limit: Option<usize>,
) -> Result<Vec<AuditEvent>> {
let events = self.events.read().await;
let filtered_events: Vec<AuditEvent> = events
.iter()
.filter(|entry| {
if let Some(start) = start_time {
if entry.event.timestamp < start {
return false;
}
}
if let Some(end) = end_time {
if entry.event.timestamp > end {
return false;
}
}
if let Some(req_type) = event_type {
if std::mem::discriminant(&entry.event.event_type) != std::mem::discriminant(req_type) {
return false;
}
}
if let Some(req_user) = user_id {
if entry.event.user_id.as_ref() != Some(&req_user.to_string()) {
return false;
}
}
true
})
.map(|entry| entry.event.clone())
.take(limit.unwrap_or(usize::MAX))
.collect();
Ok(filtered_events)
}
pub async fn cleanup_old_events(&self) -> Result<usize> {
let mut events = self.events.write().await;
let now = Utc::now();
let initial_count = events.len();
events.retain(|entry| {
if let Some(retention_days) = entry.retention_days {
let max_age = chrono::Duration::days(retention_days as i64);
let cutoff_time = now - max_age;
entry.event.timestamp > cutoff_time
} else {
true }
});
let removed_count = initial_count - events.len();
Ok(removed_count)
}
pub async fn get_statistics(&self) -> Result<AuditStatistics> {
let events = self.events.read().await;
let mut stats = AuditStatistics::default();
for entry in events.iter() {
stats.total_events += 1;
match entry.event.severity {
AuditSeverity::Debug => stats.debug_events += 1,
AuditSeverity::Info => stats.info_events += 1,
AuditSeverity::Warning => stats.warning_events += 1,
AuditSeverity::Error => stats.error_events += 1,
AuditSeverity::Critical => stats.critical_events += 1,
}
match &entry.event.result {
AuditResult::Success => stats.successful_operations += 1,
AuditResult::Failure { .. } => stats.failed_operations += 1,
AuditResult::Denied { .. } => stats.denied_operations += 1,
}
}
Ok(stats)
}
fn calculate_retention_days(&self, _severity: &AuditSeverity) -> Option<u32> {
Some(self.config.retention_days as u32)
}
async fn enforce_memory_limits(&self, events: &mut Vec<AuditLogEntry>) {
if events.len() > self.config.max_entries_per_day {
let excess = events.len() - self.config.max_entries_per_day;
events.sort_by(|a, b| a.event.timestamp.cmp(&b.event.timestamp));
events.drain(0..excess);
}
}
async fn log_to_outputs(&self, event: &AuditEvent) -> Result<()> {
if !self.config.enabled {
return Ok(());
}
let should_log = match (&self.config.log_level, &event.severity) {
(AuditLogLevel::Debug, _) => true,
(AuditLogLevel::Info, AuditSeverity::Info | AuditSeverity::Warning | AuditSeverity::Error | AuditSeverity::Critical) => true,
(AuditLogLevel::Warn, AuditSeverity::Warning | AuditSeverity::Error | AuditSeverity::Critical) => true,
(AuditLogLevel::Error, AuditSeverity::Error | AuditSeverity::Critical) => true,
_ => false,
};
if !should_log {
return Ok(());
}
let level = match event.severity {
AuditSeverity::Debug => "DEBUG",
AuditSeverity::Info => "INFO",
AuditSeverity::Warning => "WARN",
AuditSeverity::Error => "ERROR",
AuditSeverity::Critical => "CRIT",
};
let result_str = match &event.result {
AuditResult::Success => "SUCCESS",
AuditResult::Failure { error_code, .. } => &format!("FAILURE({})", error_code),
AuditResult::Denied { reason } => &format!("DENIED({})", reason),
};
let user_id = if self.config.log_sensitive_data {
event.user_id.as_deref().unwrap_or("unknown")
} else {
event.user_id.as_ref().map(|_| "***").unwrap_or("unknown")
};
println!(
"[AUDIT {}] {} - {} - {} - {} - {}",
level,
event.timestamp.format("%Y-%m-%d %H:%M:%S"),
event.event_type.as_str(),
user_id,
result_str,
event.message
);
Ok(())
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AuditStatistics {
pub total_events: usize,
pub debug_events: usize,
pub info_events: usize,
pub warning_events: usize,
pub error_events: usize,
pub critical_events: usize,
pub successful_operations: usize,
pub failed_operations: usize,
pub denied_operations: usize,
}
impl AuditEventType {
pub fn as_str(&self) -> &str {
match self {
AuditEventType::Authentication => "authentication",
AuditEventType::Authorization => "authorization",
AuditEventType::UserManagement => "user_management",
AuditEventType::Session => "session",
AuditEventType::DataAccess => "data_access",
AuditEventType::Configuration => "configuration",
AuditEventType::Security => "security",
AuditEventType::System => "system",
AuditEventType::Custom(name) => name,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_audit_event_creation() {
let event = AuditEvent {
id: "test-id".to_string(),
timestamp: Utc::now(),
event_type: AuditEventType::Authentication,
severity: AuditSeverity::Info,
user_id: Some("user123".to_string()),
session_id: None,
ip_address: Some("192.168.1.1".to_string()),
user_agent: Some("TestAgent/1.0".to_string()),
resource: None,
action: Some("login".to_string()),
result: AuditResult::Success,
metadata: HashMap::new(),
message: "User logged in successfully".to_string(),
};
assert_eq!(event.id, "test-id");
assert_eq!(event.user_id, Some("user123".to_string()));
assert!(matches!(event.result, AuditResult::Success));
}
#[tokio::test]
async fn test_audit_service_logging() {
let config = AuditConfig::default();
let audit_service = AuditService::new(config);
audit_service
.log_authentication(
Some("user123"),
Some("192.168.1.1"),
Some("TestAgent/1.0"),
AuditResult::Success,
"User logged in successfully",
)
.await
.unwrap();
let stats = audit_service.get_statistics().await.unwrap();
assert_eq!(stats.total_events, 1);
assert_eq!(stats.successful_operations, 1);
}
#[tokio::test]
async fn test_audit_event_filtering() {
let config = AuditConfig::default();
let audit_service = AuditService::new(config);
audit_service
.log_authentication(
Some("user1"),
None,
None,
AuditResult::Success,
"User1 login",
)
.await
.unwrap();
audit_service
.log_authorization("user2", "resource1", "read", AuditResult::Success, None)
.await
.unwrap();
let user_events = audit_service
.get_events(None, None, None, Some("user1"), None)
.await
.unwrap();
assert_eq!(user_events.len(), 1);
assert_eq!(user_events[0].user_id, Some("user1".to_string()));
}
#[tokio::test]
async fn test_audit_cleanup() {
let mut config = AuditConfig::default();
config.retention_days = 0; let audit_service = AuditService::new(config);
audit_service
.log_event(AuditEvent {
id: "debug-event".to_string(),
timestamp: Utc::now(),
event_type: AuditEventType::System,
severity: AuditSeverity::Debug,
user_id: None,
session_id: None,
ip_address: None,
user_agent: None,
resource: None,
action: None,
result: AuditResult::Success,
metadata: HashMap::new(),
message: "Debug event".to_string(),
})
.await
.unwrap();
let removed_count = audit_service.cleanup_old_events().await.unwrap();
assert_eq!(removed_count, 1);
let stats = audit_service.get_statistics().await.unwrap();
assert_eq!(stats.total_events, 0);
}
}