1use crate::auth::{AuthMethod, Principal};
12use crate::authz::{Action, Resource};
13use chrono::{DateTime, Utc};
14use serde::{Deserialize, Serialize};
15use std::fs::{File, OpenOptions};
16use std::io::{BufWriter, Write};
17use std::path::{Path, PathBuf};
18use std::sync::{Arc, Mutex};
19use thiserror::Error;
20use tracing::warn;
21use uuid::Uuid;
22
23#[derive(Error, Debug)]
25pub enum AuditError {
26 #[error("IO error: {0}")]
27 Io(#[from] std::io::Error),
28
29 #[error("JSON serialization error: {0}")]
30 Json(#[from] serde_json::Error),
31
32 #[error("Audit log not configured")]
33 NotConfigured,
34}
35
36pub type AuditResult<T> = Result<T, AuditError>;
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
40#[serde(rename_all = "snake_case")]
41pub enum AuditEventType {
42 Authentication,
44 Authorization,
46 Admin,
48 SecurityViolation,
50 ConfigChange,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
56#[serde(rename_all = "lowercase")]
57pub enum AuditOutcome {
58 Success,
60 Failure,
62 Denied,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct AuditEvent {
69 pub id: String,
71
72 pub timestamp: DateTime<Utc>,
74
75 pub event_type: AuditEventType,
77
78 pub result: AuditOutcome,
80
81 #[serde(skip_serializing_if = "Option::is_none")]
83 pub principal: Option<PrincipalInfo>,
84
85 #[serde(skip_serializing_if = "Option::is_none")]
87 pub auth_method: Option<String>,
88
89 #[serde(skip_serializing_if = "Option::is_none")]
91 pub action: Option<String>,
92
93 #[serde(skip_serializing_if = "Option::is_none")]
95 pub resource: Option<String>,
96
97 #[serde(skip_serializing_if = "Option::is_none")]
99 pub error: Option<String>,
100
101 #[serde(skip_serializing_if = "Option::is_none")]
103 pub metadata: Option<serde_json::Value>,
104
105 #[serde(skip_serializing_if = "Option::is_none")]
107 pub source_ip: Option<String>,
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct PrincipalInfo {
113 pub id: String,
114 pub name: String,
115 #[serde(skip_serializing_if = "Option::is_none")]
116 pub role: Option<String>,
117}
118
119impl From<&Principal> for PrincipalInfo {
120 fn from(principal: &Principal) -> Self {
121 Self {
122 id: principal.id.clone(),
123 name: principal.name.clone(),
124 role: principal.get_attribute("role").cloned(),
125 }
126 }
127}
128
129pub struct AuditLogger {
131 writer: Arc<Mutex<Option<BufWriter<File>>>>,
132 log_path: Option<PathBuf>,
133}
134
135impl AuditLogger {
136 pub fn new(log_path: Option<PathBuf>) -> AuditResult<Self> {
138 let writer = if let Some(ref path) = log_path {
139 Some(Self::open_log_file(path)?)
140 } else {
141 None
142 };
143
144 Ok(Self {
145 writer: Arc::new(Mutex::new(writer)),
146 log_path,
147 })
148 }
149
150 fn open_log_file(path: &Path) -> AuditResult<BufWriter<File>> {
152 if let Some(parent) = path.parent() {
154 std::fs::create_dir_all(parent)?;
155 }
156
157 let file = OpenOptions::new().create(true).append(true).open(path)?;
158
159 Ok(BufWriter::new(file))
160 }
161
162 pub fn log(&self, event: AuditEvent) -> AuditResult<()> {
164 match event.result {
166 AuditOutcome::Success => {
167 tracing::info!(
168 event_id = %event.id,
169 event_type = ?event.event_type,
170 principal = ?event.principal,
171 action = ?event.action,
172 resource = ?event.resource,
173 "Audit: Success"
174 );
175 }
176 AuditOutcome::Failure => {
177 tracing::warn!(
178 event_id = %event.id,
179 event_type = ?event.event_type,
180 principal = ?event.principal,
181 error = ?event.error,
182 "Audit: Failure"
183 );
184 }
185 AuditOutcome::Denied => {
186 tracing::warn!(
187 event_id = %event.id,
188 event_type = ?event.event_type,
189 principal = ?event.principal,
190 action = ?event.action,
191 resource = ?event.resource,
192 "Audit: Denied"
193 );
194 }
195 }
196
197 if let Ok(mut writer_guard) = self.writer.lock() {
199 if let Some(ref mut writer) = *writer_guard {
200 let json = serde_json::to_string(&event)?;
201 writeln!(writer, "{}", json)?;
202 writer.flush()?;
203 }
204 }
205
206 Ok(())
207 }
208
209 pub fn log_auth_success(&self, principal: &Principal, source_ip: Option<String>) {
211 let event = AuditEvent {
212 id: Uuid::new_v4().to_string(),
213 timestamp: Utc::now(),
214 event_type: AuditEventType::Authentication,
215 result: AuditOutcome::Success,
216 principal: Some(principal.into()),
217 auth_method: Some(principal.auth_method.to_string()),
218 action: None,
219 resource: None,
220 error: None,
221 metadata: None,
222 source_ip,
223 };
224
225 if let Err(e) = self.log(event) {
226 warn!("Failed to log audit event: {}", e);
227 }
228 }
229
230 pub fn log_auth_failure(
232 &self,
233 auth_method: AuthMethod,
234 error: &str,
235 source_ip: Option<String>,
236 ) {
237 let event = AuditEvent {
238 id: Uuid::new_v4().to_string(),
239 timestamp: Utc::now(),
240 event_type: AuditEventType::Authentication,
241 result: AuditOutcome::Failure,
242 principal: None,
243 auth_method: Some(auth_method.to_string()),
244 action: None,
245 resource: None,
246 error: Some(error.to_string()),
247 metadata: None,
248 source_ip,
249 };
250
251 if let Err(e) = self.log(event) {
252 warn!("Failed to log audit event: {}", e);
253 }
254 }
255
256 pub fn log_authz_success(
258 &self,
259 principal: &Principal,
260 action: &Action,
261 resource: &Resource,
262 source_ip: Option<String>,
263 ) {
264 let event = AuditEvent {
265 id: Uuid::new_v4().to_string(),
266 timestamp: Utc::now(),
267 event_type: AuditEventType::Authorization,
268 result: AuditOutcome::Success,
269 principal: Some(principal.into()),
270 auth_method: None,
271 action: Some(format!("{:?}", action)),
272 resource: Some(format!("{:?}", resource)),
273 error: None,
274 metadata: None,
275 source_ip,
276 };
277
278 if let Err(e) = self.log(event) {
279 warn!("Failed to log audit event: {}", e);
280 }
281 }
282
283 pub fn log_authz_denied(
285 &self,
286 principal: &Principal,
287 action: &Action,
288 resource: &Resource,
289 reason: &str,
290 source_ip: Option<String>,
291 ) {
292 let event = AuditEvent {
293 id: Uuid::new_v4().to_string(),
294 timestamp: Utc::now(),
295 event_type: AuditEventType::Authorization,
296 result: AuditOutcome::Denied,
297 principal: Some(principal.into()),
298 auth_method: None,
299 action: Some(format!("{:?}", action)),
300 resource: Some(format!("{:?}", resource)),
301 error: Some(reason.to_string()),
302 metadata: None,
303 source_ip,
304 };
305
306 if let Err(e) = self.log(event) {
307 warn!("Failed to log audit event: {}", e);
308 }
309 }
310
311 pub fn log_admin_operation(
313 &self,
314 principal: &Principal,
315 operation: &str,
316 success: bool,
317 error: Option<String>,
318 source_ip: Option<String>,
319 ) {
320 let event = AuditEvent {
321 id: Uuid::new_v4().to_string(),
322 timestamp: Utc::now(),
323 event_type: AuditEventType::Admin,
324 result: if success {
325 AuditOutcome::Success
326 } else {
327 AuditOutcome::Failure
328 },
329 principal: Some(principal.into()),
330 auth_method: None,
331 action: Some(operation.to_string()),
332 resource: None,
333 error,
334 metadata: None,
335 source_ip,
336 };
337
338 if let Err(e) = self.log(event) {
339 warn!("Failed to log audit event: {}", e);
340 }
341 }
342
343 pub fn log_security_violation(
345 &self,
346 principal: Option<&Principal>,
347 violation: &str,
348 source_ip: Option<String>,
349 ) {
350 let event = AuditEvent {
351 id: Uuid::new_v4().to_string(),
352 timestamp: Utc::now(),
353 event_type: AuditEventType::SecurityViolation,
354 result: AuditOutcome::Denied,
355 principal: principal.map(|p| p.into()),
356 auth_method: None,
357 action: None,
358 resource: None,
359 error: Some(violation.to_string()),
360 metadata: None,
361 source_ip,
362 };
363
364 if let Err(e) = self.log(event) {
365 warn!("Failed to log audit event: {}", e);
366 }
367 }
368
369 pub fn log_config_change(
371 &self,
372 principal: &Principal,
373 change_description: &str,
374 source_ip: Option<String>,
375 ) {
376 let event = AuditEvent {
377 id: Uuid::new_v4().to_string(),
378 timestamp: Utc::now(),
379 event_type: AuditEventType::ConfigChange,
380 result: AuditOutcome::Success,
381 principal: Some(principal.into()),
382 auth_method: None,
383 action: Some(change_description.to_string()),
384 resource: None,
385 error: None,
386 metadata: None,
387 source_ip,
388 };
389
390 if let Err(e) = self.log(event) {
391 warn!("Failed to log audit event: {}", e);
392 }
393 }
394
395 pub fn is_configured(&self) -> bool {
397 self.log_path.is_some()
398 }
399
400 pub fn log_path(&self) -> Option<&Path> {
402 self.log_path.as_deref()
403 }
404}
405
406impl Default for AuditLogger {
407 fn default() -> Self {
408 Self {
409 writer: Arc::new(Mutex::new(None)),
410 log_path: None,
411 }
412 }
413}
414
415#[cfg(test)]
416mod tests {
417 use super::*;
418 use crate::auth::AuthMethod;
419 use std::env;
420
421 #[test]
422 fn test_audit_event_serialization() {
423 let event = AuditEvent {
424 id: "test-123".to_string(),
425 timestamp: Utc::now(),
426 event_type: AuditEventType::Authentication,
427 result: AuditOutcome::Success,
428 principal: Some(PrincipalInfo {
429 id: "user1".to_string(),
430 name: "Test User".to_string(),
431 role: Some("admin".to_string()),
432 }),
433 auth_method: Some("JWT".to_string()),
434 action: None,
435 resource: None,
436 error: None,
437 metadata: None,
438 source_ip: Some("192.168.1.1".to_string()),
439 };
440
441 let json = serde_json::to_string(&event).expect("Failed to serialize");
442 assert!(json.contains("test-123"));
443 assert!(json.contains("user1"));
444 }
445
446 #[test]
447 fn test_audit_logger_without_file() {
448 let logger = AuditLogger::new(None).expect("Failed to create logger");
449 assert!(!logger.is_configured());
450
451 let principal = Principal::new(
452 "user1".to_string(),
453 "Test User".to_string(),
454 AuthMethod::Jwt,
455 );
456
457 logger.log_auth_success(&principal, None);
459 }
460
461 #[test]
462 fn test_audit_logger_with_file() {
463 let temp_dir = env::temp_dir();
464 let log_path = temp_dir.join(format!("audit_test_{}.jsonl", Uuid::new_v4()));
465
466 let logger = AuditLogger::new(Some(log_path.clone())).expect("Failed to create logger");
467 assert!(logger.is_configured());
468
469 let principal = Principal::new(
470 "user1".to_string(),
471 "Test User".to_string(),
472 AuthMethod::Jwt,
473 );
474
475 logger.log_auth_success(&principal, Some("127.0.0.1".to_string()));
476
477 assert!(log_path.exists());
479
480 std::fs::remove_file(&log_path).ok();
482 }
483
484 #[test]
485 fn test_principal_info_conversion() {
486 let principal = Principal::new(
487 "user1".to_string(),
488 "Test User".to_string(),
489 AuthMethod::Jwt,
490 )
491 .with_attribute("role".to_string(), "admin".to_string());
492
493 let info: PrincipalInfo = (&principal).into();
494 assert_eq!(info.id, "user1");
495 assert_eq!(info.name, "Test User");
496 assert_eq!(info.role, Some("admin".to_string()));
497 }
498
499 #[test]
500 fn test_log_auth_failure() {
501 let logger = AuditLogger::new(None).expect("Failed to create logger");
502
503 logger.log_auth_failure(
504 AuthMethod::Jwt,
505 "Invalid token",
506 Some("192.168.1.1".to_string()),
507 );
508
509 }
511
512 #[test]
513 fn test_log_authz_denied() {
514 let logger = AuditLogger::new(None).expect("Failed to create logger");
515
516 let principal = Principal::new(
517 "user1".to_string(),
518 "Test User".to_string(),
519 AuthMethod::Jwt,
520 );
521
522 logger.log_authz_denied(
523 &principal,
524 &Action::Admin,
525 &Resource::Server,
526 "Insufficient permissions",
527 Some("192.168.1.1".to_string()),
528 );
529
530 }
532
533 #[test]
534 fn test_log_security_violation() {
535 let logger = AuditLogger::new(None).expect("Failed to create logger");
536
537 let principal = Principal::new(
538 "user1".to_string(),
539 "Test User".to_string(),
540 AuthMethod::Jwt,
541 );
542
543 logger.log_security_violation(
544 Some(&principal),
545 "Attempted SQL injection",
546 Some("192.168.1.1".to_string()),
547 );
548
549 }
551}