Skip to main content

actix_security_core/http/security/
audit.rs

1//! Security Audit Logging system.
2//!
3//! Provides comprehensive logging of security-related events for compliance,
4//! debugging, and threat detection.
5//!
6//! # Spring Security Equivalent
7//! Similar to Spring Security's `AuthenticationEventPublisher` and
8//! `ApplicationEventPublisher` for security events.
9//!
10//! # Example
11//!
12//! ```ignore
13//! use actix_security::http::security::audit::{AuditLogger, SecurityEvent};
14//!
15//! // Create an audit logger
16//! let audit_logger = AuditLogger::new()
17//!     .with_handler(|event| {
18//!         println!("[AUDIT] {:?}", event);
19//!     });
20//!
21//! // Log events
22//! audit_logger.log(SecurityEvent::login_success("admin", "192.168.1.1"));
23//! ```
24
25use std::collections::HashMap;
26use std::fmt;
27use std::sync::Arc;
28use std::time::{SystemTime, UNIX_EPOCH};
29
30use tokio::sync::RwLock;
31
32/// Security event types for audit logging.
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub enum SecurityEventType {
35    // Authentication events
36    /// Successful login
37    AuthenticationSuccess,
38    /// Failed login attempt
39    AuthenticationFailure,
40    /// User logout
41    Logout,
42    /// Session created
43    SessionCreated,
44    /// Session destroyed
45    SessionDestroyed,
46    /// Session expired
47    SessionExpired,
48
49    // Authorization events
50    /// Access granted to resource
51    AccessGranted,
52    /// Access denied to resource
53    AccessDenied,
54    /// Insufficient permissions
55    InsufficientPermissions,
56
57    // Account events
58    /// Account locked due to failed attempts
59    AccountLocked,
60    /// Account unlocked
61    AccountUnlocked,
62    /// Password changed
63    PasswordChanged,
64    /// Password reset requested
65    PasswordResetRequested,
66
67    // Token events
68    /// Token generated
69    TokenGenerated,
70    /// Token refreshed
71    TokenRefreshed,
72    /// Token revoked
73    TokenRevoked,
74    /// Token expired
75    TokenExpired,
76    /// Invalid token used
77    InvalidToken,
78
79    // Rate limiting events
80    /// Rate limit exceeded
81    RateLimitExceeded,
82    /// Rate limit warning (approaching limit)
83    RateLimitWarning,
84
85    // CSRF events
86    /// CSRF validation failed
87    CsrfValidationFailed,
88    /// Missing CSRF token
89    CsrfTokenMissing,
90
91    // Suspicious activity
92    /// Potential brute force attack detected
93    BruteForceDetected,
94    /// Suspicious IP address
95    SuspiciousIp,
96    /// Multiple failed attempts from same source
97    MultipleFailures,
98
99    // Custom events
100    /// Custom security event
101    Custom(String),
102}
103
104impl fmt::Display for SecurityEventType {
105    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106        match self {
107            SecurityEventType::AuthenticationSuccess => write!(f, "AUTHENTICATION_SUCCESS"),
108            SecurityEventType::AuthenticationFailure => write!(f, "AUTHENTICATION_FAILURE"),
109            SecurityEventType::Logout => write!(f, "LOGOUT"),
110            SecurityEventType::SessionCreated => write!(f, "SESSION_CREATED"),
111            SecurityEventType::SessionDestroyed => write!(f, "SESSION_DESTROYED"),
112            SecurityEventType::SessionExpired => write!(f, "SESSION_EXPIRED"),
113            SecurityEventType::AccessGranted => write!(f, "ACCESS_GRANTED"),
114            SecurityEventType::AccessDenied => write!(f, "ACCESS_DENIED"),
115            SecurityEventType::InsufficientPermissions => write!(f, "INSUFFICIENT_PERMISSIONS"),
116            SecurityEventType::AccountLocked => write!(f, "ACCOUNT_LOCKED"),
117            SecurityEventType::AccountUnlocked => write!(f, "ACCOUNT_UNLOCKED"),
118            SecurityEventType::PasswordChanged => write!(f, "PASSWORD_CHANGED"),
119            SecurityEventType::PasswordResetRequested => write!(f, "PASSWORD_RESET_REQUESTED"),
120            SecurityEventType::TokenGenerated => write!(f, "TOKEN_GENERATED"),
121            SecurityEventType::TokenRefreshed => write!(f, "TOKEN_REFRESHED"),
122            SecurityEventType::TokenRevoked => write!(f, "TOKEN_REVOKED"),
123            SecurityEventType::TokenExpired => write!(f, "TOKEN_EXPIRED"),
124            SecurityEventType::InvalidToken => write!(f, "INVALID_TOKEN"),
125            SecurityEventType::RateLimitExceeded => write!(f, "RATE_LIMIT_EXCEEDED"),
126            SecurityEventType::RateLimitWarning => write!(f, "RATE_LIMIT_WARNING"),
127            SecurityEventType::CsrfValidationFailed => write!(f, "CSRF_VALIDATION_FAILED"),
128            SecurityEventType::CsrfTokenMissing => write!(f, "CSRF_TOKEN_MISSING"),
129            SecurityEventType::BruteForceDetected => write!(f, "BRUTE_FORCE_DETECTED"),
130            SecurityEventType::SuspiciousIp => write!(f, "SUSPICIOUS_IP"),
131            SecurityEventType::MultipleFailures => write!(f, "MULTIPLE_FAILURES"),
132            SecurityEventType::Custom(name) => write!(f, "CUSTOM_{}", name.to_uppercase()),
133        }
134    }
135}
136
137/// Severity level of security events.
138#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
139pub enum SecurityEventSeverity {
140    /// Informational (successful operations)
141    #[default]
142    Info,
143    /// Warning (potential issues)
144    Warning,
145    /// Error (failed operations)
146    Error,
147    /// Critical (security threats)
148    Critical,
149}
150
151impl fmt::Display for SecurityEventSeverity {
152    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
153        match self {
154            SecurityEventSeverity::Info => write!(f, "INFO"),
155            SecurityEventSeverity::Warning => write!(f, "WARNING"),
156            SecurityEventSeverity::Error => write!(f, "ERROR"),
157            SecurityEventSeverity::Critical => write!(f, "CRITICAL"),
158        }
159    }
160}
161
162impl SecurityEventType {
163    /// Get the default severity for this event type.
164    pub fn default_severity(&self) -> SecurityEventSeverity {
165        match self {
166            SecurityEventType::AuthenticationSuccess
167            | SecurityEventType::Logout
168            | SecurityEventType::SessionCreated
169            | SecurityEventType::AccessGranted
170            | SecurityEventType::TokenGenerated
171            | SecurityEventType::TokenRefreshed
172            | SecurityEventType::PasswordChanged => SecurityEventSeverity::Info,
173
174            SecurityEventType::SessionExpired
175            | SecurityEventType::TokenExpired
176            | SecurityEventType::RateLimitWarning => SecurityEventSeverity::Warning,
177
178            SecurityEventType::AuthenticationFailure
179            | SecurityEventType::SessionDestroyed
180            | SecurityEventType::AccessDenied
181            | SecurityEventType::InsufficientPermissions
182            | SecurityEventType::TokenRevoked
183            | SecurityEventType::InvalidToken
184            | SecurityEventType::RateLimitExceeded
185            | SecurityEventType::CsrfValidationFailed
186            | SecurityEventType::CsrfTokenMissing
187            | SecurityEventType::AccountLocked
188            | SecurityEventType::PasswordResetRequested => SecurityEventSeverity::Error,
189
190            SecurityEventType::BruteForceDetected
191            | SecurityEventType::SuspiciousIp
192            | SecurityEventType::MultipleFailures => SecurityEventSeverity::Critical,
193
194            SecurityEventType::AccountUnlocked | SecurityEventType::Custom(_) => {
195                SecurityEventSeverity::Info
196            }
197        }
198    }
199}
200
201/// A security audit event.
202#[derive(Debug, Clone)]
203pub struct SecurityEvent {
204    /// Unique event ID
205    pub id: String,
206    /// Event timestamp (Unix epoch milliseconds)
207    pub timestamp: u64,
208    /// Event type
209    pub event_type: SecurityEventType,
210    /// Event severity
211    pub severity: SecurityEventSeverity,
212    /// Username (if applicable)
213    pub username: Option<String>,
214    /// Source IP address
215    pub ip_address: Option<String>,
216    /// User agent
217    pub user_agent: Option<String>,
218    /// Request path
219    pub path: Option<String>,
220    /// HTTP method
221    pub method: Option<String>,
222    /// Session ID (if applicable)
223    pub session_id: Option<String>,
224    /// Additional details
225    pub details: HashMap<String, String>,
226    /// Error message (for failure events)
227    pub error: Option<String>,
228}
229
230impl SecurityEvent {
231    /// Create a new security event.
232    pub fn new(event_type: SecurityEventType) -> Self {
233        let now = SystemTime::now()
234            .duration_since(UNIX_EPOCH)
235            .unwrap_or_default()
236            .as_millis() as u64;
237
238        Self {
239            id: generate_event_id(),
240            timestamp: now,
241            severity: event_type.default_severity(),
242            event_type,
243            username: None,
244            ip_address: None,
245            user_agent: None,
246            path: None,
247            method: None,
248            session_id: None,
249            details: HashMap::new(),
250            error: None,
251        }
252    }
253
254    /// Set the username.
255    pub fn username(mut self, username: impl Into<String>) -> Self {
256        self.username = Some(username.into());
257        self
258    }
259
260    /// Set the IP address.
261    pub fn ip_address(mut self, ip: impl Into<String>) -> Self {
262        self.ip_address = Some(ip.into());
263        self
264    }
265
266    /// Set the user agent.
267    pub fn user_agent(mut self, ua: impl Into<String>) -> Self {
268        self.user_agent = Some(ua.into());
269        self
270    }
271
272    /// Set the request path.
273    pub fn path(mut self, path: impl Into<String>) -> Self {
274        self.path = Some(path.into());
275        self
276    }
277
278    /// Set the HTTP method.
279    pub fn method(mut self, method: impl Into<String>) -> Self {
280        self.method = Some(method.into());
281        self
282    }
283
284    /// Set the session ID.
285    pub fn session_id(mut self, id: impl Into<String>) -> Self {
286        self.session_id = Some(id.into());
287        self
288    }
289
290    /// Set the severity (overrides default).
291    pub fn severity(mut self, severity: SecurityEventSeverity) -> Self {
292        self.severity = severity;
293        self
294    }
295
296    /// Add a detail.
297    pub fn detail(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
298        self.details.insert(key.into(), value.into());
299        self
300    }
301
302    /// Set the error message.
303    pub fn error(mut self, error: impl Into<String>) -> Self {
304        self.error = Some(error.into());
305        self
306    }
307
308    // Convenience constructors
309
310    /// Create a login success event.
311    pub fn login_success(username: &str, ip: &str) -> Self {
312        Self::new(SecurityEventType::AuthenticationSuccess)
313            .username(username)
314            .ip_address(ip)
315    }
316
317    /// Create a login failure event.
318    pub fn login_failure(username: &str, ip: &str, reason: &str) -> Self {
319        Self::new(SecurityEventType::AuthenticationFailure)
320            .username(username)
321            .ip_address(ip)
322            .error(reason)
323    }
324
325    /// Create an access denied event.
326    pub fn access_denied(username: &str, path: &str, ip: &str) -> Self {
327        Self::new(SecurityEventType::AccessDenied)
328            .username(username)
329            .path(path)
330            .ip_address(ip)
331    }
332
333    /// Create a rate limit exceeded event.
334    pub fn rate_limit_exceeded(ip: &str, path: &str) -> Self {
335        Self::new(SecurityEventType::RateLimitExceeded)
336            .ip_address(ip)
337            .path(path)
338    }
339
340    /// Create an account locked event.
341    pub fn account_locked(username: &str, ip: &str, reason: &str) -> Self {
342        Self::new(SecurityEventType::AccountLocked)
343            .username(username)
344            .ip_address(ip)
345            .detail("reason", reason)
346    }
347
348    /// Create a brute force detected event.
349    pub fn brute_force_detected(ip: &str, attempts: u32) -> Self {
350        Self::new(SecurityEventType::BruteForceDetected)
351            .ip_address(ip)
352            .detail("attempts", attempts.to_string())
353    }
354
355    /// Format the event as a log line.
356    pub fn to_log_line(&self) -> String {
357        let mut parts = vec![
358            format!("[{}]", self.severity),
359            format!("[{}]", self.event_type),
360        ];
361
362        if let Some(ref username) = self.username {
363            parts.push(format!("user={}", username));
364        }
365        if let Some(ref ip) = self.ip_address {
366            parts.push(format!("ip={}", ip));
367        }
368        if let Some(ref path) = self.path {
369            parts.push(format!("path={}", path));
370        }
371        if let Some(ref error) = self.error {
372            parts.push(format!("error=\"{}\"", error));
373        }
374        for (k, v) in &self.details {
375            parts.push(format!("{}={}", k, v));
376        }
377
378        parts.join(" ")
379    }
380
381    /// Format the event as JSON.
382    pub fn to_json(&self) -> String {
383        serde_json::to_string(self).unwrap_or_else(|_| self.to_log_line())
384    }
385}
386
387impl serde::Serialize for SecurityEvent {
388    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
389    where
390        S: serde::Serializer,
391    {
392        use serde::ser::SerializeStruct;
393        let mut state = serializer.serialize_struct("SecurityEvent", 12)?;
394        state.serialize_field("id", &self.id)?;
395        state.serialize_field("timestamp", &self.timestamp)?;
396        state.serialize_field("event_type", &self.event_type.to_string())?;
397        state.serialize_field("severity", &self.severity.to_string())?;
398        state.serialize_field("username", &self.username)?;
399        state.serialize_field("ip_address", &self.ip_address)?;
400        state.serialize_field("user_agent", &self.user_agent)?;
401        state.serialize_field("path", &self.path)?;
402        state.serialize_field("method", &self.method)?;
403        state.serialize_field("session_id", &self.session_id)?;
404        state.serialize_field("details", &self.details)?;
405        state.serialize_field("error", &self.error)?;
406        state.end()
407    }
408}
409
410/// Generate a unique event ID.
411fn generate_event_id() -> String {
412    use rand::Rng;
413    let timestamp = SystemTime::now()
414        .duration_since(UNIX_EPOCH)
415        .unwrap_or_default()
416        .as_micros();
417    let random: u32 = rand::thread_rng().gen();
418    format!("{:x}-{:08x}", timestamp, random)
419}
420
421/// Trait for handling security events.
422pub trait SecurityEventHandler: Send + Sync {
423    /// Handle a security event.
424    fn handle(&self, event: &SecurityEvent);
425}
426
427/// Simple logging handler that prints to stdout.
428#[derive(Default)]
429pub struct StdoutHandler {
430    min_severity: SecurityEventSeverity,
431}
432
433impl StdoutHandler {
434    /// Create a new stdout handler.
435    pub fn new() -> Self {
436        Self::default()
437    }
438
439    /// Set minimum severity to log.
440    pub fn min_severity(mut self, severity: SecurityEventSeverity) -> Self {
441        self.min_severity = severity;
442        self
443    }
444}
445
446impl SecurityEventHandler for StdoutHandler {
447    fn handle(&self, event: &SecurityEvent) {
448        if event.severity >= self.min_severity {
449            println!("[SECURITY] {}", event.to_log_line());
450        }
451    }
452}
453
454/// Handler that calls a closure.
455pub struct ClosureHandler<F>
456where
457    F: Fn(&SecurityEvent) + Send + Sync,
458{
459    handler: F,
460}
461
462impl<F> ClosureHandler<F>
463where
464    F: Fn(&SecurityEvent) + Send + Sync,
465{
466    /// Create a new closure handler.
467    pub fn new(handler: F) -> Self {
468        Self { handler }
469    }
470}
471
472impl<F> SecurityEventHandler for ClosureHandler<F>
473where
474    F: Fn(&SecurityEvent) + Send + Sync,
475{
476    fn handle(&self, event: &SecurityEvent) {
477        (self.handler)(event);
478    }
479}
480
481/// In-memory event store for testing and debugging.
482#[derive(Clone)]
483pub struct InMemoryEventStore {
484    events: Arc<RwLock<Vec<SecurityEvent>>>,
485    max_events: usize,
486}
487
488impl Default for InMemoryEventStore {
489    fn default() -> Self {
490        Self::new()
491    }
492}
493
494impl InMemoryEventStore {
495    /// Create a new in-memory store.
496    pub fn new() -> Self {
497        Self {
498            events: Arc::new(RwLock::new(Vec::new())),
499            max_events: 10000,
500        }
501    }
502
503    /// Set maximum events to keep.
504    pub fn max_events(mut self, max: usize) -> Self {
505        self.max_events = max;
506        self
507    }
508
509    /// Get all stored events.
510    pub async fn get_events(&self) -> Vec<SecurityEvent> {
511        self.events.read().await.clone()
512    }
513
514    /// Get events filtered by type.
515    pub async fn get_events_by_type(&self, event_type: &SecurityEventType) -> Vec<SecurityEvent> {
516        self.events
517            .read()
518            .await
519            .iter()
520            .filter(|e| &e.event_type == event_type)
521            .cloned()
522            .collect()
523    }
524
525    /// Get events for a specific user.
526    pub async fn get_events_by_user(&self, username: &str) -> Vec<SecurityEvent> {
527        self.events
528            .read()
529            .await
530            .iter()
531            .filter(|e| e.username.as_deref() == Some(username))
532            .cloned()
533            .collect()
534    }
535
536    /// Clear all events.
537    pub async fn clear(&self) {
538        self.events.write().await.clear();
539    }
540}
541
542impl SecurityEventHandler for InMemoryEventStore {
543    fn handle(&self, event: &SecurityEvent) {
544        // Use blocking lock for sync trait implementation
545        let rt = tokio::runtime::Handle::try_current();
546        if let Ok(handle) = rt {
547            let events = Arc::clone(&self.events);
548            let event = event.clone();
549            let max = self.max_events;
550            handle.spawn(async move {
551                let mut guard = events.write().await;
552                guard.push(event);
553                if guard.len() > max {
554                    guard.remove(0);
555                }
556            });
557        }
558    }
559}
560
561/// The main audit logger.
562#[derive(Clone)]
563pub struct AuditLogger {
564    handlers: Arc<Vec<Arc<dyn SecurityEventHandler>>>,
565    enabled: bool,
566}
567
568impl Default for AuditLogger {
569    fn default() -> Self {
570        Self::new()
571    }
572}
573
574impl AuditLogger {
575    /// Create a new audit logger with no handlers.
576    pub fn new() -> Self {
577        Self {
578            handlers: Arc::new(Vec::new()),
579            enabled: true,
580        }
581    }
582
583    /// Create an audit logger with stdout logging.
584    pub fn with_stdout() -> Self {
585        Self::new().add_handler(StdoutHandler::new())
586    }
587
588    /// Add an event handler.
589    pub fn add_handler<H: SecurityEventHandler + 'static>(mut self, handler: H) -> Self {
590        let handlers = Arc::make_mut(&mut self.handlers);
591        handlers.push(Arc::new(handler));
592        self
593    }
594
595    /// Add a closure as event handler.
596    pub fn with_handler<F>(self, handler: F) -> Self
597    where
598        F: Fn(&SecurityEvent) + Send + Sync + 'static,
599    {
600        self.add_handler(ClosureHandler::new(handler))
601    }
602
603    /// Enable or disable the logger.
604    pub fn enabled(mut self, enabled: bool) -> Self {
605        self.enabled = enabled;
606        self
607    }
608
609    /// Log a security event.
610    pub fn log(&self, event: SecurityEvent) {
611        if !self.enabled {
612            return;
613        }
614
615        for handler in self.handlers.iter() {
616            handler.handle(&event);
617        }
618    }
619
620    /// Log a login success event.
621    pub fn log_login_success(&self, username: &str, ip: &str) {
622        self.log(SecurityEvent::login_success(username, ip));
623    }
624
625    /// Log a login failure event.
626    pub fn log_login_failure(&self, username: &str, ip: &str, reason: &str) {
627        self.log(SecurityEvent::login_failure(username, ip, reason));
628    }
629
630    /// Log an access denied event.
631    pub fn log_access_denied(&self, username: &str, path: &str, ip: &str) {
632        self.log(SecurityEvent::access_denied(username, path, ip));
633    }
634
635    /// Log a rate limit exceeded event.
636    pub fn log_rate_limit_exceeded(&self, ip: &str, path: &str) {
637        self.log(SecurityEvent::rate_limit_exceeded(ip, path));
638    }
639}
640
641/// Global audit logger instance.
642static GLOBAL_LOGGER: std::sync::OnceLock<AuditLogger> = std::sync::OnceLock::new();
643
644/// Initialize the global audit logger.
645pub fn init_global_logger(logger: AuditLogger) {
646    let _ = GLOBAL_LOGGER.set(logger);
647}
648
649/// Get the global audit logger.
650pub fn global_logger() -> &'static AuditLogger {
651    GLOBAL_LOGGER.get_or_init(AuditLogger::new)
652}
653
654/// Log a security event using the global logger.
655pub fn audit_log(event: SecurityEvent) {
656    global_logger().log(event);
657}
658
659#[cfg(test)]
660mod tests {
661    use super::*;
662
663    #[test]
664    fn test_event_creation() {
665        let event = SecurityEvent::login_success("admin", "192.168.1.1");
666        assert_eq!(event.event_type, SecurityEventType::AuthenticationSuccess);
667        assert_eq!(event.username, Some("admin".to_string()));
668        assert_eq!(event.ip_address, Some("192.168.1.1".to_string()));
669        assert_eq!(event.severity, SecurityEventSeverity::Info);
670    }
671
672    #[test]
673    fn test_event_builder() {
674        let event = SecurityEvent::new(SecurityEventType::AccessDenied)
675            .username("user1")
676            .ip_address("10.0.0.1")
677            .path("/admin")
678            .detail("reason", "missing role")
679            .error("Access denied");
680
681        assert_eq!(event.username, Some("user1".to_string()));
682        assert_eq!(event.path, Some("/admin".to_string()));
683        assert!(event.details.contains_key("reason"));
684        assert_eq!(event.error, Some("Access denied".to_string()));
685    }
686
687    #[test]
688    fn test_severity_ordering() {
689        assert!(SecurityEventSeverity::Info < SecurityEventSeverity::Warning);
690        assert!(SecurityEventSeverity::Warning < SecurityEventSeverity::Error);
691        assert!(SecurityEventSeverity::Error < SecurityEventSeverity::Critical);
692    }
693
694    #[test]
695    fn test_event_type_display() {
696        assert_eq!(
697            SecurityEventType::AuthenticationSuccess.to_string(),
698            "AUTHENTICATION_SUCCESS"
699        );
700        assert_eq!(
701            SecurityEventType::Custom("test".to_string()).to_string(),
702            "CUSTOM_TEST"
703        );
704    }
705
706    #[test]
707    fn test_log_line_format() {
708        let event = SecurityEvent::login_failure("admin", "192.168.1.1", "Invalid password");
709        let log_line = event.to_log_line();
710
711        assert!(log_line.contains("[ERROR]"));
712        assert!(log_line.contains("[AUTHENTICATION_FAILURE]"));
713        assert!(log_line.contains("user=admin"));
714        assert!(log_line.contains("ip=192.168.1.1"));
715    }
716
717    #[test]
718    fn test_default_severity() {
719        assert_eq!(
720            SecurityEventType::AuthenticationSuccess.default_severity(),
721            SecurityEventSeverity::Info
722        );
723        assert_eq!(
724            SecurityEventType::BruteForceDetected.default_severity(),
725            SecurityEventSeverity::Critical
726        );
727    }
728
729    #[test]
730    fn test_audit_logger_with_closure() {
731        use std::sync::atomic::{AtomicUsize, Ordering};
732        let counter = Arc::new(AtomicUsize::new(0));
733        let counter_clone = counter.clone();
734
735        let logger = AuditLogger::new().with_handler(move |_event| {
736            counter_clone.fetch_add(1, Ordering::SeqCst);
737        });
738
739        logger.log_login_success("admin", "127.0.0.1");
740        logger.log_login_failure("user", "127.0.0.1", "Bad password");
741
742        assert_eq!(counter.load(Ordering::SeqCst), 2);
743    }
744
745    #[test]
746    fn test_disabled_logger() {
747        use std::sync::atomic::{AtomicUsize, Ordering};
748        let counter = Arc::new(AtomicUsize::new(0));
749        let counter_clone = counter.clone();
750
751        let logger = AuditLogger::new()
752            .with_handler(move |_event| {
753                counter_clone.fetch_add(1, Ordering::SeqCst);
754            })
755            .enabled(false);
756
757        logger.log_login_success("admin", "127.0.0.1");
758
759        assert_eq!(counter.load(Ordering::SeqCst), 0);
760    }
761}