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)]
140mod tests {
141 use super::*;
142
143 #[test]
144 fn test_json_audit_logger_does_not_panic() {
145 let logger = JsonAuditLogger;
146 let event = AuditEvent {
147 timestamp: Utc::now(),
148 request_id: "req-001".to_string(),
149 actor: "user@example.com".to_string(),
150 action: "generate_data".to_string(),
151 resource: "/api/stream/start".to_string(),
152 outcome: AuditOutcome::Success,
153 tenant_id: Some("tenant-1".to_string()),
154 ip_address: Some("192.168.1.1".to_string()),
155 user_agent: Some("datasynth-cli/0.5.0".to_string()),
156 };
157 logger.log_event(&event);
159 }
160
161 #[test]
162 fn test_noop_audit_logger() {
163 let logger = NoopAuditLogger;
164 let event = AuditEvent {
165 timestamp: Utc::now(),
166 request_id: "req-002".to_string(),
167 actor: "anonymous".to_string(),
168 action: "view_metrics".to_string(),
169 resource: "/metrics".to_string(),
170 outcome: AuditOutcome::Denied,
171 tenant_id: None,
172 ip_address: None,
173 user_agent: None,
174 };
175 logger.log_event(&event);
177 }
178
179 #[test]
180 fn test_audit_event_serialization_roundtrip() {
181 let event = AuditEvent {
182 timestamp: Utc::now(),
183 request_id: "req-003".to_string(),
184 actor: "admin-key-ab12".to_string(),
185 action: "manage_config".to_string(),
186 resource: "/api/config".to_string(),
187 outcome: AuditOutcome::Error,
188 tenant_id: None,
189 ip_address: Some("10.0.0.1".to_string()),
190 user_agent: None,
191 };
192
193 let json = serde_json::to_string(&event).expect("should serialize");
194 let deserialized: AuditEvent = serde_json::from_str(&json).expect("should deserialize");
195
196 assert_eq!(deserialized.request_id, "req-003");
197 assert_eq!(deserialized.actor, "admin-key-ab12");
198 assert_eq!(deserialized.action, "manage_config");
199 assert_eq!(deserialized.outcome, AuditOutcome::Error);
200 assert!(deserialized.tenant_id.is_none());
201 assert_eq!(deserialized.ip_address, Some("10.0.0.1".to_string()));
202 assert!(deserialized.user_agent.is_none());
203 }
204
205 #[test]
206 fn test_audit_config_defaults() {
207 let config = AuditConfig::default();
208 assert!(!config.enabled);
209 assert!(config.log_to_stdout);
210 }
211
212 #[test]
213 fn test_audit_outcome_serialization() {
214 assert_eq!(
215 serde_json::to_string(&AuditOutcome::Success).unwrap(),
216 "\"success\""
217 );
218 assert_eq!(
219 serde_json::to_string(&AuditOutcome::Denied).unwrap(),
220 "\"denied\""
221 );
222 assert_eq!(
223 serde_json::to_string(&AuditOutcome::Error).unwrap(),
224 "\"error\""
225 );
226 }
227}