datasynth_server/rest/
audit.rs1use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(rename_all = "snake_case")]
17pub enum AuditOutcome {
18 Success,
20 Denied,
22 Error,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct AuditEvent {
29 pub timestamp: DateTime<Utc>,
31 pub request_id: String,
33 pub actor: String,
35 pub action: String,
37 pub resource: String,
39 pub outcome: AuditOutcome,
41 #[serde(skip_serializing_if = "Option::is_none")]
43 pub tenant_id: Option<String>,
44 #[serde(skip_serializing_if = "Option::is_none")]
46 pub ip_address: Option<String>,
47 #[serde(skip_serializing_if = "Option::is_none")]
49 pub user_agent: Option<String>,
50}
51
52pub trait AuditLogger: Send + Sync {
60 fn log_event(&self, event: &AuditEvent);
62}
63
64pub struct JsonAuditLogger;
69
70impl AuditLogger for JsonAuditLogger {
71 fn log_event(&self, event: &AuditEvent) {
72 match serde_json::to_string(event) {
74 Ok(json) => {
75 tracing::info!(
76 audit_event = %json,
77 actor = %event.actor,
78 action = %event.action,
79 outcome = ?event.outcome,
80 "audit"
81 );
82 }
83 Err(e) => {
84 tracing::warn!(
85 error = %e,
86 actor = %event.actor,
87 action = %event.action,
88 "Failed to serialize audit event"
89 );
90 }
91 }
92 }
93}
94
95pub struct NoopAuditLogger;
99
100impl AuditLogger for NoopAuditLogger {
101 fn log_event(&self, _event: &AuditEvent) {
102 }
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct AuditConfig {
113 #[serde(default)]
115 pub enabled: bool,
116
117 #[serde(default = "default_true")]
119 pub log_to_stdout: bool,
120}
121
122fn default_true() -> bool {
123 true
124}
125
126impl Default for AuditConfig {
127 fn default() -> Self {
128 Self {
129 enabled: false,
130 log_to_stdout: true,
131 }
132 }
133}
134
135#[cfg(test)]
140#[allow(clippy::unwrap_used)]
141mod tests {
142 use super::*;
143
144 #[test]
145 fn test_json_audit_logger_does_not_panic() {
146 let logger = JsonAuditLogger;
147 let event = AuditEvent {
148 timestamp: Utc::now(),
149 request_id: "req-001".to_string(),
150 actor: "user@example.com".to_string(),
151 action: "generate_data".to_string(),
152 resource: "/api/stream/start".to_string(),
153 outcome: AuditOutcome::Success,
154 tenant_id: Some("tenant-1".to_string()),
155 ip_address: Some("192.168.1.1".to_string()),
156 user_agent: Some("datasynth-cli/0.5.0".to_string()),
157 };
158 logger.log_event(&event);
160 }
161
162 #[test]
163 fn test_noop_audit_logger() {
164 let logger = NoopAuditLogger;
165 let event = AuditEvent {
166 timestamp: Utc::now(),
167 request_id: "req-002".to_string(),
168 actor: "anonymous".to_string(),
169 action: "view_metrics".to_string(),
170 resource: "/metrics".to_string(),
171 outcome: AuditOutcome::Denied,
172 tenant_id: None,
173 ip_address: None,
174 user_agent: None,
175 };
176 logger.log_event(&event);
178 }
179
180 #[test]
181 fn test_audit_event_serialization_roundtrip() {
182 let event = AuditEvent {
183 timestamp: Utc::now(),
184 request_id: "req-003".to_string(),
185 actor: "admin-key-ab12".to_string(),
186 action: "manage_config".to_string(),
187 resource: "/api/config".to_string(),
188 outcome: AuditOutcome::Error,
189 tenant_id: None,
190 ip_address: Some("10.0.0.1".to_string()),
191 user_agent: None,
192 };
193
194 let json = serde_json::to_string(&event).expect("should serialize");
195 let deserialized: AuditEvent = serde_json::from_str(&json).expect("should deserialize");
196
197 assert_eq!(deserialized.request_id, "req-003");
198 assert_eq!(deserialized.actor, "admin-key-ab12");
199 assert_eq!(deserialized.action, "manage_config");
200 assert_eq!(deserialized.outcome, AuditOutcome::Error);
201 assert!(deserialized.tenant_id.is_none());
202 assert_eq!(deserialized.ip_address, Some("10.0.0.1".to_string()));
203 assert!(deserialized.user_agent.is_none());
204 }
205
206 #[test]
207 fn test_audit_config_defaults() {
208 let config = AuditConfig::default();
209 assert!(!config.enabled);
210 assert!(config.log_to_stdout);
211 }
212
213 #[test]
214 fn test_audit_outcome_serialization() {
215 assert_eq!(
216 serde_json::to_string(&AuditOutcome::Success).unwrap(),
217 "\"success\""
218 );
219 assert_eq!(
220 serde_json::to_string(&AuditOutcome::Denied).unwrap(),
221 "\"denied\""
222 );
223 assert_eq!(
224 serde_json::to_string(&AuditOutcome::Error).unwrap(),
225 "\"error\""
226 );
227 }
228}