1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
16#[serde(rename_all = "snake_case")]
17pub enum AdminActionType {
18 ConfigLatencyUpdated,
20 ConfigFaultsUpdated,
21 ConfigProxyUpdated,
22 ConfigTrafficShapingUpdated,
23 ConfigValidationUpdated,
24
25 ServerRestarted,
27 ServerShutdown,
28 ServerStatusChecked,
29
30 LogsCleared,
32 LogsExported,
33 LogsFiltered,
34
35 FixtureCreated,
37 FixtureUpdated,
38 FixtureDeleted,
39 FixtureBulkDeleted,
40 FixtureMoved,
41
42 RouteEnabled,
44 RouteDisabled,
45 RouteCreated,
46 RouteDeleted,
47 RouteUpdated,
48
49 ServiceEnabled,
51 ServiceDisabled,
52 ServiceConfigUpdated,
53
54 MetricsExported,
56 MetricsConfigUpdated,
57
58 UserCreated,
60 UserUpdated,
61 UserDeleted,
62 RoleChanged,
63 PermissionGranted,
64 PermissionRevoked,
65
66 SystemConfigBackedUp,
68 SystemConfigRestored,
69 SystemHealthChecked,
70
71 ApiKeyCreated,
73 ApiKeyDeleted,
74 ApiKeyRotated,
75 SecurityPolicyUpdated,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct AdminAuditLog {
81 pub id: Uuid,
83 pub timestamp: chrono::DateTime<Utc>,
85 pub action_type: AdminActionType,
87 pub user_id: Option<String>,
89 pub username: Option<String>,
91 pub ip_address: Option<String>,
93 pub user_agent: Option<String>,
95 pub description: String,
97 pub resource: Option<String>,
99 pub success: bool,
101 pub error_message: Option<String>,
103 pub metadata: Option<serde_json::Value>,
105}
106
107#[derive(Debug, Clone)]
109pub struct AuditLogStore {
110 logs: Arc<RwLock<Vec<AdminAuditLog>>>,
112 max_logs: usize,
114}
115
116impl Default for AuditLogStore {
117 fn default() -> Self {
118 Self::new(10000)
119 }
120}
121
122impl AuditLogStore {
123 pub fn new(max_logs: usize) -> Self {
125 Self {
126 logs: Arc::new(RwLock::new(Vec::new())),
127 max_logs,
128 }
129 }
130
131 pub async fn record(&self, log: AdminAuditLog) {
133 let mut logs = self.logs.write().await;
134 logs.push(log.clone());
135
136 if logs.len() > self.max_logs {
138 let remove_count = logs.len() - self.max_logs;
139 logs.drain(0..remove_count);
140 }
141
142 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 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 if let Some(action_type) = action_type {
176 filtered.retain(|log| log.action_type == action_type);
177 }
178
179 if let Some(user_id) = user_id {
181 filtered.retain(|log| log.user_id.as_deref() == Some(user_id));
182 }
183
184 filtered.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
186
187 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 pub async fn clear(&self) {
196 let mut logs = self.logs.write().await;
197 logs.clear();
198 }
199
200 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 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
230pub struct AuditLogStats {
231 pub total_actions: usize,
233 pub successful_actions: usize,
235 pub failed_actions: usize,
237 pub actions_by_type: HashMap<String, usize>,
239 pub most_recent_timestamp: Option<chrono::DateTime<Utc>>,
241}
242
243pub 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, username: None, ip_address: None, user_agent: None, description,
261 resource,
262 success,
263 error_message,
264 metadata,
265 }
266}
267
268static GLOBAL_AUDIT_STORE: std::sync::OnceLock<Arc<AuditLogStore>> = std::sync::OnceLock::new();
270
271pub 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
278pub 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 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 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 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 let logs = store.get_logs(None, None, Some(5), None).await;
498 assert_eq!(logs.len(), 5);
499
500 let logs = store.get_logs(None, None, Some(3), Some(2)).await;
502 assert_eq!(logs.len(), 3);
503
504 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 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 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 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 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 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 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 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 let store2 = store2.unwrap();
785
786 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 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 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 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}