mockforge_ui/
audit.rs

1//! Audit logging for Admin UI actions
2//!
3//! This module provides comprehensive audit logging for all administrative actions
4//! performed through the Admin UI, ensuring compliance and security monitoring.
5
6use chrono::Utc;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::sync::Arc;
10use tokio::sync::RwLock;
11use tracing::{info, warn};
12use uuid::Uuid;
13
14/// Admin action types that should be audited
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
16#[serde(rename_all = "snake_case")]
17pub enum AdminActionType {
18    // Configuration changes
19    ConfigLatencyUpdated,
20    ConfigFaultsUpdated,
21    ConfigProxyUpdated,
22    ConfigTrafficShapingUpdated,
23    ConfigValidationUpdated,
24
25    // Server management
26    ServerRestarted,
27    ServerShutdown,
28    ServerStatusChecked,
29
30    // Log management
31    LogsCleared,
32    LogsExported,
33    LogsFiltered,
34
35    // Fixture management
36    FixtureCreated,
37    FixtureUpdated,
38    FixtureDeleted,
39    FixtureBulkDeleted,
40    FixtureMoved,
41
42    // Route management
43    RouteEnabled,
44    RouteDisabled,
45    RouteCreated,
46    RouteDeleted,
47    RouteUpdated,
48
49    // Service management
50    ServiceEnabled,
51    ServiceDisabled,
52    ServiceConfigUpdated,
53
54    // Metrics and monitoring
55    MetricsExported,
56    MetricsConfigUpdated,
57
58    // User and access management
59    UserCreated,
60    UserUpdated,
61    UserDeleted,
62    RoleChanged,
63    PermissionGranted,
64    PermissionRevoked,
65
66    // System operations
67    SystemConfigBackedUp,
68    SystemConfigRestored,
69    SystemHealthChecked,
70
71    // Security operations
72    ApiKeyCreated,
73    ApiKeyDeleted,
74    ApiKeyRotated,
75    SecurityPolicyUpdated,
76}
77
78/// Audit log entry for admin actions
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct AdminAuditLog {
81    /// Unique audit log ID
82    pub id: Uuid,
83    /// Timestamp of the action
84    pub timestamp: chrono::DateTime<Utc>,
85    /// Type of action performed
86    pub action_type: AdminActionType,
87    /// User who performed the action (if authenticated)
88    pub user_id: Option<String>,
89    /// Username (if available)
90    pub username: Option<String>,
91    /// IP address of the requester
92    pub ip_address: Option<String>,
93    /// User agent string
94    pub user_agent: Option<String>,
95    /// Action description
96    pub description: String,
97    /// Resource affected (e.g., endpoint path, fixture ID)
98    pub resource: Option<String>,
99    /// Success status
100    pub success: bool,
101    /// Error message (if failed)
102    pub error_message: Option<String>,
103    /// Additional metadata
104    pub metadata: Option<serde_json::Value>,
105}
106
107/// Audit log storage
108#[derive(Debug, Clone)]
109pub struct AuditLogStore {
110    /// In-memory audit logs (max 10000 entries)
111    logs: Arc<RwLock<Vec<AdminAuditLog>>>,
112    /// Maximum number of logs to keep
113    max_logs: usize,
114}
115
116impl Default for AuditLogStore {
117    fn default() -> Self {
118        Self::new(10000)
119    }
120}
121
122impl AuditLogStore {
123    /// Create a new audit log store
124    pub fn new(max_logs: usize) -> Self {
125        Self {
126            logs: Arc::new(RwLock::new(Vec::new())),
127            max_logs,
128        }
129    }
130
131    /// Record an audit log entry
132    pub async fn record(&self, log: AdminAuditLog) {
133        let mut logs = self.logs.write().await;
134        logs.push(log.clone());
135
136        // Trim to max_logs if necessary
137        if logs.len() > self.max_logs {
138            let remove_count = logs.len() - self.max_logs;
139            logs.drain(0..remove_count);
140        }
141
142        // Log to tracing for external log aggregation
143        if log.success {
144            info!(
145                action = ?log.action_type,
146                user_id = ?log.user_id,
147                resource = ?log.resource,
148                "Admin action: {}",
149                log.description
150            );
151        } else {
152            warn!(
153                action = ?log.action_type,
154                user_id = ?log.user_id,
155                resource = ?log.resource,
156                error = ?log.error_message,
157                "Admin action failed: {}",
158                log.description
159            );
160        }
161    }
162
163    /// Get audit logs with optional filtering
164    pub async fn get_logs(
165        &self,
166        action_type: Option<AdminActionType>,
167        user_id: Option<&str>,
168        limit: Option<usize>,
169        offset: Option<usize>,
170    ) -> Vec<AdminAuditLog> {
171        let logs = self.logs.read().await;
172        let mut filtered: Vec<_> = logs.iter().cloned().collect();
173
174        // Filter by action type
175        if let Some(action_type) = action_type {
176            filtered.retain(|log| log.action_type == action_type);
177        }
178
179        // Filter by user ID
180        if let Some(user_id) = user_id {
181            filtered.retain(|log| log.user_id.as_deref() == Some(user_id));
182        }
183
184        // Sort by timestamp (newest first)
185        filtered.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
186
187        // Apply offset
188        let start = offset.unwrap_or(0);
189        let end = limit.map(|l| start + l).unwrap_or(filtered.len());
190
191        filtered.into_iter().skip(start).take(end - start).collect()
192    }
193
194    /// Clear all audit logs
195    pub async fn clear(&self) {
196        let mut logs = self.logs.write().await;
197        logs.clear();
198    }
199
200    /// Get statistics about audit logs
201    pub async fn get_stats(&self) -> AuditLogStats {
202        let logs = self.logs.read().await;
203
204        let total_actions = logs.len();
205        let successful_actions = logs.iter().filter(|log| log.success).count();
206        let failed_actions = total_actions - successful_actions;
207
208        // Count by action type
209        let mut actions_by_type: HashMap<String, usize> = HashMap::new();
210        for log in logs.iter() {
211            let key = format!("{:?}", log.action_type);
212            *actions_by_type.entry(key).or_insert(0) += 1;
213        }
214
215        // Get most recent action
216        let most_recent = logs.iter().max_by_key(|log| log.timestamp).cloned();
217
218        AuditLogStats {
219            total_actions,
220            successful_actions,
221            failed_actions,
222            actions_by_type,
223            most_recent_timestamp: most_recent.map(|log| log.timestamp),
224        }
225    }
226}
227
228/// Audit log statistics
229#[derive(Debug, Clone, Serialize, Deserialize)]
230pub struct AuditLogStats {
231    /// Total number of audit log entries
232    pub total_actions: usize,
233    /// Number of successful actions
234    pub successful_actions: usize,
235    /// Number of failed actions
236    pub failed_actions: usize,
237    /// Count of actions by type
238    pub actions_by_type: HashMap<String, usize>,
239    /// Timestamp of most recent action
240    pub most_recent_timestamp: Option<chrono::DateTime<Utc>>,
241}
242
243/// Helper function to create an audit log entry
244pub fn create_audit_log(
245    action_type: AdminActionType,
246    description: String,
247    resource: Option<String>,
248    success: bool,
249    error_message: Option<String>,
250    metadata: Option<serde_json::Value>,
251) -> AdminAuditLog {
252    AdminAuditLog {
253        id: Uuid::new_v4(),
254        timestamp: Utc::now(),
255        action_type,
256        user_id: None,    // Will be set by middleware
257        username: None,   // Will be set by middleware
258        ip_address: None, // Will be set by middleware
259        user_agent: None, // Will be set by middleware
260        description,
261        resource,
262        success,
263        error_message,
264        metadata,
265    }
266}
267
268/// Global audit log store instance
269static GLOBAL_AUDIT_STORE: std::sync::OnceLock<Arc<AuditLogStore>> = std::sync::OnceLock::new();
270
271/// Initialize the global audit log store
272pub fn init_global_audit_store(max_logs: usize) -> Arc<AuditLogStore> {
273    GLOBAL_AUDIT_STORE
274        .get_or_init(|| Arc::new(AuditLogStore::new(max_logs)))
275        .clone()
276}
277
278/// Get the global audit log store
279pub fn get_global_audit_store() -> Option<Arc<AuditLogStore>> {
280    GLOBAL_AUDIT_STORE.get().cloned()
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286
287    #[tokio::test]
288    async fn test_audit_log_store_creation() {
289        let store = AuditLogStore::new(100);
290        let stats = store.get_stats().await;
291        assert_eq!(stats.total_actions, 0);
292        assert_eq!(stats.successful_actions, 0);
293        assert_eq!(stats.failed_actions, 0);
294    }
295
296    #[tokio::test]
297    async fn test_audit_log_store_default() {
298        let store = AuditLogStore::default();
299        let stats = store.get_stats().await;
300        assert_eq!(stats.total_actions, 0);
301    }
302
303    #[tokio::test]
304    async fn test_record_audit_log() {
305        let store = AuditLogStore::new(100);
306
307        let log = create_audit_log(
308            AdminActionType::ConfigLatencyUpdated,
309            "Updated latency config".to_string(),
310            Some("/api/config/latency".to_string()),
311            true,
312            None,
313            None,
314        );
315
316        store.record(log).await;
317
318        let stats = store.get_stats().await;
319        assert_eq!(stats.total_actions, 1);
320        assert_eq!(stats.successful_actions, 1);
321        assert_eq!(stats.failed_actions, 0);
322    }
323
324    #[tokio::test]
325    async fn test_record_failed_audit_log() {
326        let store = AuditLogStore::new(100);
327
328        let log = create_audit_log(
329            AdminActionType::ConfigLatencyUpdated,
330            "Failed to update latency config".to_string(),
331            Some("/api/config/latency".to_string()),
332            false,
333            Some("Permission denied".to_string()),
334            None,
335        );
336
337        store.record(log).await;
338
339        let stats = store.get_stats().await;
340        assert_eq!(stats.total_actions, 1);
341        assert_eq!(stats.successful_actions, 0);
342        assert_eq!(stats.failed_actions, 1);
343    }
344
345    #[tokio::test]
346    async fn test_audit_log_with_metadata() {
347        let store = AuditLogStore::new(100);
348
349        let metadata = serde_json::json!({
350            "old_value": 100,
351            "new_value": 200,
352            "reason": "Performance optimization"
353        });
354
355        let log = create_audit_log(
356            AdminActionType::ConfigLatencyUpdated,
357            "Updated latency config".to_string(),
358            Some("/api/config/latency".to_string()),
359            true,
360            None,
361            Some(metadata.clone()),
362        );
363
364        store.record(log).await;
365
366        let logs = store.get_logs(None, None, None, None).await;
367        assert_eq!(logs.len(), 1);
368        assert_eq!(logs[0].metadata, Some(metadata));
369    }
370
371    #[tokio::test]
372    async fn test_max_logs_limit() {
373        let store = AuditLogStore::new(5);
374
375        // Add 10 logs
376        for i in 0..10 {
377            let log = create_audit_log(
378                AdminActionType::ConfigLatencyUpdated,
379                format!("Action {}", i),
380                None,
381                true,
382                None,
383                None,
384            );
385            store.record(log).await;
386        }
387
388        let logs = store.get_logs(None, None, None, None).await;
389        assert_eq!(logs.len(), 5, "Should only keep last 5 logs");
390    }
391
392    #[tokio::test]
393    async fn test_get_logs_filtering_by_action_type() {
394        let store = AuditLogStore::new(100);
395
396        // Add different action types
397        store
398            .record(create_audit_log(
399                AdminActionType::ConfigLatencyUpdated,
400                "Latency updated".to_string(),
401                None,
402                true,
403                None,
404                None,
405            ))
406            .await;
407
408        store
409            .record(create_audit_log(
410                AdminActionType::FixtureCreated,
411                "Fixture created".to_string(),
412                None,
413                true,
414                None,
415                None,
416            ))
417            .await;
418
419        store
420            .record(create_audit_log(
421                AdminActionType::ConfigLatencyUpdated,
422                "Latency updated again".to_string(),
423                None,
424                true,
425                None,
426                None,
427            ))
428            .await;
429
430        let logs = store
431            .get_logs(Some(AdminActionType::ConfigLatencyUpdated), None, None, None)
432            .await;
433        assert_eq!(logs.len(), 2);
434        assert!(logs.iter().all(|log| log.action_type == AdminActionType::ConfigLatencyUpdated));
435    }
436
437    #[tokio::test]
438    async fn test_get_logs_filtering_by_user() {
439        let store = AuditLogStore::new(100);
440
441        let mut log1 = create_audit_log(
442            AdminActionType::ConfigLatencyUpdated,
443            "Action by user1".to_string(),
444            None,
445            true,
446            None,
447            None,
448        );
449        log1.user_id = Some("user1".to_string());
450        store.record(log1).await;
451
452        let mut log2 = create_audit_log(
453            AdminActionType::FixtureCreated,
454            "Action by user2".to_string(),
455            None,
456            true,
457            None,
458            None,
459        );
460        log2.user_id = Some("user2".to_string());
461        store.record(log2).await;
462
463        let mut log3 = create_audit_log(
464            AdminActionType::ConfigFaultsUpdated,
465            "Action by user1".to_string(),
466            None,
467            true,
468            None,
469            None,
470        );
471        log3.user_id = Some("user1".to_string());
472        store.record(log3).await;
473
474        let logs = store.get_logs(None, Some("user1"), None, None).await;
475        assert_eq!(logs.len(), 2);
476        assert!(logs.iter().all(|log| log.user_id.as_deref() == Some("user1")));
477    }
478
479    #[tokio::test]
480    async fn test_get_logs_with_limit_and_offset() {
481        let store = AuditLogStore::new(100);
482
483        // Add 10 logs
484        for i in 0..10 {
485            let log = create_audit_log(
486                AdminActionType::ConfigLatencyUpdated,
487                format!("Action {}", i),
488                None,
489                true,
490                None,
491                None,
492            );
493            store.record(log).await;
494        }
495
496        // Get logs with limit
497        let logs = store.get_logs(None, None, Some(5), None).await;
498        assert_eq!(logs.len(), 5);
499
500        // Get logs with offset
501        let logs = store.get_logs(None, None, Some(3), Some(2)).await;
502        assert_eq!(logs.len(), 3);
503
504        // Get logs with offset beyond limit
505        let logs = store.get_logs(None, None, Some(5), Some(8)).await;
506        assert_eq!(logs.len(), 2);
507    }
508
509    #[tokio::test]
510    async fn test_logs_sorted_by_timestamp_newest_first() {
511        let store = AuditLogStore::new(100);
512
513        // Add logs with delays
514        for i in 0..5 {
515            let log = create_audit_log(
516                AdminActionType::ConfigLatencyUpdated,
517                format!("Action {}", i),
518                None,
519                true,
520                None,
521                None,
522            );
523            store.record(log).await;
524            tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
525        }
526
527        let logs = store.get_logs(None, None, None, None).await;
528        assert_eq!(logs.len(), 5);
529
530        // Verify newest first
531        for i in 0..logs.len() - 1 {
532            assert!(logs[i].timestamp >= logs[i + 1].timestamp);
533        }
534    }
535
536    #[tokio::test]
537    async fn test_clear_logs() {
538        let store = AuditLogStore::new(100);
539
540        // Add logs
541        for _ in 0..5 {
542            store
543                .record(create_audit_log(
544                    AdminActionType::ConfigLatencyUpdated,
545                    "Action".to_string(),
546                    None,
547                    true,
548                    None,
549                    None,
550                ))
551                .await;
552        }
553
554        let stats_before = store.get_stats().await;
555        assert_eq!(stats_before.total_actions, 5);
556
557        store.clear().await;
558
559        let stats_after = store.get_stats().await;
560        assert_eq!(stats_after.total_actions, 0);
561    }
562
563    #[tokio::test]
564    async fn test_audit_stats_actions_by_type() {
565        let store = AuditLogStore::new(100);
566
567        // Add various action types
568        store
569            .record(create_audit_log(
570                AdminActionType::ConfigLatencyUpdated,
571                "".to_string(),
572                None,
573                true,
574                None,
575                None,
576            ))
577            .await;
578        store
579            .record(create_audit_log(
580                AdminActionType::ConfigLatencyUpdated,
581                "".to_string(),
582                None,
583                true,
584                None,
585                None,
586            ))
587            .await;
588        store
589            .record(create_audit_log(
590                AdminActionType::FixtureCreated,
591                "".to_string(),
592                None,
593                true,
594                None,
595                None,
596            ))
597            .await;
598        store
599            .record(create_audit_log(
600                AdminActionType::RouteEnabled,
601                "".to_string(),
602                None,
603                true,
604                None,
605                None,
606            ))
607            .await;
608        store
609            .record(create_audit_log(
610                AdminActionType::FixtureCreated,
611                "".to_string(),
612                None,
613                true,
614                None,
615                None,
616            ))
617            .await;
618
619        let stats = store.get_stats().await;
620        assert_eq!(stats.total_actions, 5);
621        assert_eq!(stats.actions_by_type.get("ConfigLatencyUpdated"), Some(&2));
622        assert_eq!(stats.actions_by_type.get("FixtureCreated"), Some(&2));
623        assert_eq!(stats.actions_by_type.get("RouteEnabled"), Some(&1));
624    }
625
626    #[tokio::test]
627    async fn test_audit_stats_most_recent_timestamp() {
628        let store = AuditLogStore::new(100);
629
630        // Add first log
631        store
632            .record(create_audit_log(
633                AdminActionType::ConfigLatencyUpdated,
634                "First".to_string(),
635                None,
636                true,
637                None,
638                None,
639            ))
640            .await;
641
642        let stats1 = store.get_stats().await;
643        let first_timestamp = stats1.most_recent_timestamp.unwrap();
644
645        // Wait a bit and add another log
646        tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
647
648        store
649            .record(create_audit_log(
650                AdminActionType::FixtureCreated,
651                "Second".to_string(),
652                None,
653                true,
654                None,
655                None,
656            ))
657            .await;
658
659        let stats2 = store.get_stats().await;
660        let second_timestamp = stats2.most_recent_timestamp.unwrap();
661
662        assert!(second_timestamp > first_timestamp);
663    }
664
665    #[test]
666    fn test_create_audit_log_helper() {
667        let log = create_audit_log(
668            AdminActionType::UserCreated,
669            "Created new user".to_string(),
670            Some("users/123".to_string()),
671            true,
672            None,
673            Some(serde_json::json!({"username": "testuser"})),
674        );
675
676        assert_eq!(log.action_type, AdminActionType::UserCreated);
677        assert_eq!(log.description, "Created new user");
678        assert_eq!(log.resource, Some("users/123".to_string()));
679        assert!(log.success);
680        assert_eq!(log.error_message, None);
681        assert!(log.metadata.is_some());
682        assert_eq!(log.user_id, None);
683        assert_eq!(log.username, None);
684        assert_eq!(log.ip_address, None);
685        assert_eq!(log.user_agent, None);
686    }
687
688    #[test]
689    fn test_admin_action_type_serialization() {
690        let action = AdminActionType::ConfigLatencyUpdated;
691        let serialized = serde_json::to_string(&action).unwrap();
692        assert_eq!(serialized, "\"config_latency_updated\"");
693
694        let deserialized: AdminActionType = serde_json::from_str(&serialized).unwrap();
695        assert_eq!(deserialized, action);
696    }
697
698    #[test]
699    fn test_audit_log_serialization() {
700        let log = AdminAuditLog {
701            id: Uuid::new_v4(),
702            timestamp: Utc::now(),
703            action_type: AdminActionType::FixtureCreated,
704            user_id: Some("user123".to_string()),
705            username: Some("testuser".to_string()),
706            ip_address: Some("192.168.1.1".to_string()),
707            user_agent: Some("Mozilla/5.0".to_string()),
708            description: "Created fixture".to_string(),
709            resource: Some("/fixtures/test".to_string()),
710            success: true,
711            error_message: None,
712            metadata: Some(serde_json::json!({"key": "value"})),
713        };
714
715        let serialized = serde_json::to_string(&log).unwrap();
716        let deserialized: AdminAuditLog = serde_json::from_str(&serialized).unwrap();
717
718        assert_eq!(deserialized.id, log.id);
719        assert_eq!(deserialized.action_type, log.action_type);
720        assert_eq!(deserialized.user_id, log.user_id);
721        assert_eq!(deserialized.description, log.description);
722    }
723
724    #[test]
725    fn test_all_admin_action_types_covered() {
726        // Test that all action types can be serialized and deserialized
727        let actions = vec![
728            AdminActionType::ConfigLatencyUpdated,
729            AdminActionType::ConfigFaultsUpdated,
730            AdminActionType::ConfigProxyUpdated,
731            AdminActionType::ConfigTrafficShapingUpdated,
732            AdminActionType::ConfigValidationUpdated,
733            AdminActionType::ServerRestarted,
734            AdminActionType::ServerShutdown,
735            AdminActionType::ServerStatusChecked,
736            AdminActionType::LogsCleared,
737            AdminActionType::LogsExported,
738            AdminActionType::LogsFiltered,
739            AdminActionType::FixtureCreated,
740            AdminActionType::FixtureUpdated,
741            AdminActionType::FixtureDeleted,
742            AdminActionType::FixtureBulkDeleted,
743            AdminActionType::FixtureMoved,
744            AdminActionType::RouteEnabled,
745            AdminActionType::RouteDisabled,
746            AdminActionType::RouteCreated,
747            AdminActionType::RouteDeleted,
748            AdminActionType::RouteUpdated,
749            AdminActionType::ServiceEnabled,
750            AdminActionType::ServiceDisabled,
751            AdminActionType::ServiceConfigUpdated,
752            AdminActionType::MetricsExported,
753            AdminActionType::MetricsConfigUpdated,
754            AdminActionType::UserCreated,
755            AdminActionType::UserUpdated,
756            AdminActionType::UserDeleted,
757            AdminActionType::RoleChanged,
758            AdminActionType::PermissionGranted,
759            AdminActionType::PermissionRevoked,
760            AdminActionType::SystemConfigBackedUp,
761            AdminActionType::SystemConfigRestored,
762            AdminActionType::SystemHealthChecked,
763            AdminActionType::ApiKeyCreated,
764            AdminActionType::ApiKeyDeleted,
765            AdminActionType::ApiKeyRotated,
766            AdminActionType::SecurityPolicyUpdated,
767        ];
768
769        for action in actions {
770            let serialized = serde_json::to_string(&action).unwrap();
771            let deserialized: AdminActionType = serde_json::from_str(&serialized).unwrap();
772            assert_eq!(deserialized, action);
773        }
774    }
775
776    #[tokio::test]
777    async fn test_global_audit_store_initialization() {
778        let store1 = init_global_audit_store(100);
779        let store2 = get_global_audit_store();
780
781        assert!(store2.is_some());
782
783        // Both should point to the same store
784        let store2 = store2.unwrap();
785
786        // Add log via store1
787        store1
788            .record(create_audit_log(
789                AdminActionType::ConfigLatencyUpdated,
790                "Test".to_string(),
791                None,
792                true,
793                None,
794                None,
795            ))
796            .await;
797
798        // Should be visible via store2
799        let stats = store2.get_stats().await;
800        assert_eq!(stats.total_actions, 1);
801    }
802
803    #[tokio::test]
804    async fn test_concurrent_audit_log_writes() {
805        let store = Arc::new(AuditLogStore::new(1000));
806        let mut handles = vec![];
807
808        // Spawn multiple tasks writing logs concurrently
809        for i in 0..10 {
810            let store_clone = store.clone();
811            let handle = tokio::spawn(async move {
812                for j in 0..10 {
813                    let log = create_audit_log(
814                        AdminActionType::ConfigLatencyUpdated,
815                        format!("Task {} - Log {}", i, j),
816                        None,
817                        true,
818                        None,
819                        None,
820                    );
821                    store_clone.record(log).await;
822                }
823            });
824            handles.push(handle);
825        }
826
827        // Wait for all tasks to complete
828        for handle in handles {
829            handle.await.unwrap();
830        }
831
832        let stats = store.get_stats().await;
833        assert_eq!(stats.total_actions, 100);
834    }
835}