use crate::value::Value;
use std::collections::HashMap;
use std::fmt;
use std::sync::{Arc, Mutex};
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone, PartialEq)]
pub enum AuditEventType {
Access,
Modification,
ValidationFailure,
Reload,
Load,
Save,
}
#[derive(Debug, Clone, PartialEq, PartialOrd)]
pub enum AuditSeverity {
Info = 1,
Warning = 2,
Error = 3,
Critical = 4,
}
#[derive(Debug, Clone)]
pub struct AuditEvent {
pub id: String,
pub timestamp: SystemTime,
pub event_type: AuditEventType,
pub severity: AuditSeverity,
pub key: Option<String>,
pub old_value: Option<Value>,
pub new_value: Option<Value>,
pub user_context: Option<String>,
pub metadata: HashMap<String, String>,
pub error_message: Option<String>,
pub source: Option<String>,
}
impl AuditEvent {
pub fn new(event_type: AuditEventType, severity: AuditSeverity) -> Self {
Self {
id: generate_event_id(),
timestamp: SystemTime::now(),
event_type,
severity,
key: None,
old_value: None,
new_value: None,
user_context: None,
metadata: HashMap::new(),
error_message: None,
source: None,
}
}
pub fn with_key(mut self, key: impl Into<String>) -> Self {
self.key = Some(key.into());
self
}
pub fn with_old_value(mut self, value: Value) -> Self {
self.old_value = Some(value);
self
}
pub fn with_new_value(mut self, value: Value) -> Self {
self.new_value = Some(value);
self
}
pub fn with_user_context(mut self, context: impl Into<String>) -> Self {
self.user_context = Some(context.into());
self
}
pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
pub fn with_error(mut self, message: impl Into<String>) -> Self {
self.error_message = Some(message.into());
self
}
pub fn with_source(mut self, source: impl Into<String>) -> Self {
self.source = Some(source.into());
self
}
}
impl fmt::Display for AuditEvent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let timestamp_millis = self
.timestamp
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis();
write!(
f,
"[{}] {:?}:{:?} id={} key={} user={}",
timestamp_millis,
self.event_type,
self.severity,
self.id,
self.key.as_deref().unwrap_or("none"),
self.user_context.as_deref().unwrap_or("system")
)?;
if let Some(error) = &self.error_message {
write!(f, " error=\"{error}\"")?;
}
if let (Some(old), Some(new)) = (&self.old_value, &self.new_value) {
write!(f, " change=\"{old:?}\" -> \"{new:?}\"")?;
}
for (key, value) in &self.metadata {
write!(f, " {key}=\"{value}\"")?;
}
Ok(())
}
}
pub trait AuditSink: Send + Sync {
fn write_event(&self, event: &AuditEvent) -> Result<(), String>;
fn flush(&self) -> Result<(), String>;
}
pub struct ConsoleSink {
level_filter: AuditSeverity,
}
impl ConsoleSink {
pub fn new(min_level: AuditSeverity) -> Self {
Self {
level_filter: min_level,
}
}
}
impl AuditSink for ConsoleSink {
fn write_event(&self, event: &AuditEvent) -> Result<(), String> {
if event.severity >= self.level_filter {
println!("AUDIT: {event}");
}
Ok(())
}
fn flush(&self) -> Result<(), String> {
Ok(()) }
}
pub struct FileSink {
file_path: String,
level_filter: AuditSeverity,
}
impl FileSink {
pub fn new(file_path: impl Into<String>, min_level: AuditSeverity) -> Self {
Self {
file_path: file_path.into(),
level_filter: min_level,
}
}
}
impl AuditSink for FileSink {
fn write_event(&self, event: &AuditEvent) -> Result<(), String> {
if event.severity >= self.level_filter {
use std::fs::OpenOptions;
use std::io::Write;
let mut file = OpenOptions::new()
.create(true)
.append(true)
.open(&self.file_path)
.map_err(|e| format!("Failed to open audit log file: {e}"))?;
writeln!(file, "{event}").map_err(|e| format!("Failed to write to audit log: {e}"))?;
}
Ok(())
}
fn flush(&self) -> Result<(), String> {
Ok(())
}
}
pub struct AuditLogger {
sinks: Vec<Box<dyn AuditSink>>,
enabled: bool,
}
impl AuditLogger {
pub fn new() -> Self {
Self {
sinks: Vec::new(),
enabled: true,
}
}
pub fn add_sink(mut self, sink: Box<dyn AuditSink>) -> Self {
self.sinks.push(sink);
self
}
pub fn set_enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
pub fn log_event(&self, event: AuditEvent) {
if !self.enabled {
return;
}
for sink in &self.sinks {
if let Err(e) = sink.write_event(&event) {
eprintln!("Audit sink error: {e}");
}
}
}
pub fn log_access(&self, key: &str, user_context: Option<&str>) {
let event = AuditEvent::new(AuditEventType::Access, AuditSeverity::Info)
.with_key(key)
.with_metadata("operation", "get");
let event = if let Some(user) = user_context {
event.with_user_context(user)
} else {
event
};
self.log_event(event);
}
pub fn log_modification(
&self,
key: &str,
old_value: Option<&Value>,
new_value: &Value,
user_context: Option<&str>,
) {
let mut event = AuditEvent::new(AuditEventType::Modification, AuditSeverity::Warning)
.with_key(key)
.with_new_value(new_value.clone())
.with_metadata("operation", "set");
if let Some(old) = old_value {
event = event.with_old_value(old.clone());
}
if let Some(user) = user_context {
event = event.with_user_context(user);
}
self.log_event(event);
}
pub fn log_validation_failure(
&self,
key: &str,
error: &str,
value: &Value,
user_context: Option<&str>,
) {
let event = AuditEvent::new(AuditEventType::ValidationFailure, AuditSeverity::Error)
.with_key(key)
.with_new_value(value.clone())
.with_error(error)
.with_metadata("operation", "validate");
let event = if let Some(user) = user_context {
event.with_user_context(user)
} else {
event
};
self.log_event(event);
}
pub fn log_reload(&self, source: &str, success: bool, error: Option<&str>) {
let severity = if success {
AuditSeverity::Info
} else {
AuditSeverity::Error
};
let mut event = AuditEvent::new(AuditEventType::Reload, severity)
.with_source(source)
.with_metadata("operation", "reload");
if let Some(err) = error {
event = event.with_error(err);
}
self.log_event(event);
}
pub fn flush(&self) {
for sink in &self.sinks {
if let Err(e) = sink.flush() {
eprintln!("Audit sink flush error: {e}");
}
}
}
}
impl Default for AuditLogger {
fn default() -> Self {
Self::new()
}
}
static GLOBAL_AUDIT_LOGGER: Mutex<Option<Arc<AuditLogger>>> = Mutex::new(None);
pub fn init_audit_logger(logger: AuditLogger) {
if let Ok(mut global) = GLOBAL_AUDIT_LOGGER.lock() {
*global = Some(Arc::new(logger));
}
}
pub fn get_audit_logger() -> Option<Arc<AuditLogger>> {
GLOBAL_AUDIT_LOGGER
.lock()
.ok()
.and_then(|guard| guard.clone())
}
pub fn audit_log(event: AuditEvent) {
if let Some(logger) = get_audit_logger() {
logger.log_event(event);
}
}
fn generate_event_id() -> String {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(1);
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let counter = COUNTER.fetch_add(1, Ordering::Relaxed);
format!("{timestamp:x}-{counter:x}")
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::{Arc, Mutex};
struct TestSink {
events: Arc<Mutex<Vec<AuditEvent>>>,
}
impl TestSink {
fn new() -> (Self, Arc<Mutex<Vec<AuditEvent>>>) {
let events = Arc::new(Mutex::new(Vec::new()));
(
Self {
events: Arc::clone(&events),
},
events,
)
}
}
impl AuditSink for TestSink {
fn write_event(&self, event: &AuditEvent) -> Result<(), String> {
self.events.lock().unwrap().push(event.clone());
Ok(())
}
fn flush(&self) -> Result<(), String> {
Ok(())
}
}
#[test]
fn test_audit_event_creation() {
let event = AuditEvent::new(AuditEventType::Access, AuditSeverity::Info)
.with_key("test.key")
.with_user_context("test_user")
.with_metadata("operation", "get");
assert_eq!(event.event_type, AuditEventType::Access);
assert_eq!(event.severity, AuditSeverity::Info);
assert_eq!(event.key, Some("test.key".to_string()));
assert_eq!(event.user_context, Some("test_user".to_string()));
assert_eq!(event.metadata.get("operation"), Some(&"get".to_string()));
}
#[test]
fn test_audit_logger_basic() {
let (sink, events) = TestSink::new();
let logger = AuditLogger::new().add_sink(Box::new(sink));
logger.log_access("test.key", Some("test_user"));
logger.log_modification(
"test.key",
None,
&Value::String("new_value".to_string()),
Some("test_user"),
);
let events = events.lock().unwrap();
assert_eq!(events.len(), 2);
assert_eq!(events[0].event_type, AuditEventType::Access);
assert_eq!(events[0].key, Some("test.key".to_string()));
assert_eq!(events[1].event_type, AuditEventType::Modification);
assert_eq!(events[1].key, Some("test.key".to_string()));
}
#[test]
fn test_console_sink() {
let sink = ConsoleSink::new(AuditSeverity::Info);
let event =
AuditEvent::new(AuditEventType::Access, AuditSeverity::Info).with_key("test.key");
assert!(sink.write_event(&event).is_ok());
}
#[test]
fn test_event_display() {
let event = AuditEvent::new(AuditEventType::Modification, AuditSeverity::Warning)
.with_key("test.key")
.with_user_context("test_user")
.with_old_value(Value::String("old".to_string()))
.with_new_value(Value::String("new".to_string()))
.with_metadata("operation", "set");
let display = format!("{event}");
assert!(display.contains("Modification"));
assert!(display.contains("Warning"));
assert!(display.contains("test.key"));
assert!(display.contains("test_user"));
}
#[test]
fn test_severity_filtering() {
let (sink, events) = TestSink::new();
let logger = AuditLogger::new().add_sink(Box::new(sink));
logger.log_event(AuditEvent::new(AuditEventType::Access, AuditSeverity::Info));
logger.log_event(AuditEvent::new(
AuditEventType::ValidationFailure,
AuditSeverity::Error,
));
let events = events.lock().unwrap();
assert_eq!(events.len(), 2);
assert_eq!(events[0].severity, AuditSeverity::Info);
assert_eq!(events[1].severity, AuditSeverity::Error);
}
}