Skip to main content

amaters_server/
audit.rs

1//! Audit logging module
2//!
3//! This module provides audit logging for security events:
4//! - Authentication attempts (success/failure)
5//! - Authorization decisions (allow/deny)
6//! - Administrative operations
7//! - Suspicious activities
8//!
9//! Audit logs are written in structured JSON format for easy parsing and analysis.
10
11use 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/// Audit errors
24#[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/// Audit event type
39#[derive(Debug, Clone, Serialize, Deserialize)]
40#[serde(rename_all = "snake_case")]
41pub enum AuditEventType {
42    /// Authentication attempt
43    Authentication,
44    /// Authorization check
45    Authorization,
46    /// Administrative operation
47    Admin,
48    /// Security violation
49    SecurityViolation,
50    /// Configuration change
51    ConfigChange,
52}
53
54/// Audit event result
55#[derive(Debug, Clone, Serialize, Deserialize)]
56#[serde(rename_all = "lowercase")]
57pub enum AuditOutcome {
58    /// Operation succeeded
59    Success,
60    /// Operation failed
61    Failure,
62    /// Operation denied by policy
63    Denied,
64}
65
66/// Audit event
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct AuditEvent {
69    /// Event ID
70    pub id: String,
71
72    /// Event timestamp
73    pub timestamp: DateTime<Utc>,
74
75    /// Event type
76    pub event_type: AuditEventType,
77
78    /// Event result
79    pub result: AuditOutcome,
80
81    /// User principal (if authenticated)
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub principal: Option<PrincipalInfo>,
84
85    /// Authentication method used
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub auth_method: Option<String>,
88
89    /// Action performed
90    #[serde(skip_serializing_if = "Option::is_none")]
91    pub action: Option<String>,
92
93    /// Resource accessed
94    #[serde(skip_serializing_if = "Option::is_none")]
95    pub resource: Option<String>,
96
97    /// Error message (if failed)
98    #[serde(skip_serializing_if = "Option::is_none")]
99    pub error: Option<String>,
100
101    /// Additional metadata
102    #[serde(skip_serializing_if = "Option::is_none")]
103    pub metadata: Option<serde_json::Value>,
104
105    /// Source IP address
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub source_ip: Option<String>,
108}
109
110/// Simplified principal info for audit logs
111#[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
129/// Audit logger
130pub struct AuditLogger {
131    writer: Arc<Mutex<Option<BufWriter<File>>>>,
132    log_path: Option<PathBuf>,
133}
134
135impl AuditLogger {
136    /// Create a new audit logger
137    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    /// Open or create the audit log file
151    fn open_log_file(path: &Path) -> AuditResult<BufWriter<File>> {
152        // Create parent directory if it doesn't exist
153        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    /// Log an audit event
163    pub fn log(&self, event: AuditEvent) -> AuditResult<()> {
164        // Always log to tracing
165        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        // Write to file if configured
198        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    /// Log successful authentication
210    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    /// Log failed authentication
231    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    /// Log successful authorization
257    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    /// Log authorization denial
284    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    /// Log administrative operation
312    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    /// Log security violation
344    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    /// Log configuration change
370    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    /// Check if audit logging is configured
396    pub fn is_configured(&self) -> bool {
397        self.log_path.is_some()
398    }
399
400    /// Get the audit log path
401    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        // Should not fail even without file
458        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        // Verify file was created
478        assert!(log_path.exists());
479
480        // Cleanup
481        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        // Should not panic
510    }
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        // Should not panic
531    }
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        // Should not panic
550    }
551}