ricecoder_providers/
audit_log.rs

1//! Audit logging for security events
2//!
3//! This module provides audit logging functionality for tracking security-relevant events
4//! such as API key access, authentication attempts, and permission decisions.
5
6use serde::{Deserialize, Serialize};
7use std::fs::OpenOptions;
8use std::io::Write;
9use std::path::PathBuf;
10use std::sync::Mutex;
11use tracing::info;
12
13/// Audit event types
14#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
15pub enum AuditEventType {
16    /// API key accessed
17    ApiKeyAccessed,
18    /// API key rotated
19    ApiKeyRotated,
20    /// Authentication attempt
21    AuthenticationAttempt,
22    /// Authorization decision
23    AuthorizationDecision,
24    /// Configuration loaded
25    ConfigurationLoaded,
26    /// File accessed
27    FileAccessed,
28    /// File modified
29    FileModified,
30    /// Permission denied
31    PermissionDenied,
32    /// Rate limit exceeded
33    RateLimitExceeded,
34    /// Security error
35    SecurityError,
36}
37
38/// Audit log entry
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct AuditLogEntry {
41    /// Timestamp (ISO 8601 format)
42    pub timestamp: String,
43    /// Event type
44    pub event_type: AuditEventType,
45    /// Provider or component name
46    pub component: String,
47    /// User or service performing the action
48    pub actor: String,
49    /// Resource being accessed
50    pub resource: String,
51    /// Action result (success/failure)
52    pub result: String,
53    /// Additional details
54    pub details: String,
55}
56
57impl AuditLogEntry {
58    /// Create a new audit log entry
59    pub fn new(
60        event_type: AuditEventType,
61        component: &str,
62        actor: &str,
63        resource: &str,
64        result: &str,
65        details: &str,
66    ) -> Self {
67        let timestamp = chrono::Local::now().to_rfc3339();
68        Self {
69            timestamp,
70            event_type,
71            component: component.to_string(),
72            actor: actor.to_string(),
73            resource: resource.to_string(),
74            result: result.to_string(),
75            details: details.to_string(),
76        }
77    }
78
79    /// Convert to JSON string
80    pub fn to_json(&self) -> Result<String, serde_json::Error> {
81        serde_json::to_string(self)
82    }
83}
84
85/// Audit logger for recording security events
86pub struct AuditLogger {
87    /// Path to audit log file
88    log_path: PathBuf,
89    /// Lock for thread-safe file access
90    lock: Mutex<()>,
91}
92
93impl AuditLogger {
94    /// Create a new audit logger
95    pub fn new(log_path: PathBuf) -> Self {
96        Self {
97            log_path,
98            lock: Mutex::new(()),
99        }
100    }
101
102    /// Log an audit event
103    pub fn log(&self, entry: &AuditLogEntry) -> Result<(), Box<dyn std::error::Error>> {
104        let _guard = self.lock.lock().unwrap();
105
106        // Open file in append mode
107        let mut file = OpenOptions::new()
108            .create(true)
109            .append(true)
110            .open(&self.log_path)?;
111
112        // Write JSON entry
113        let json = entry.to_json()?;
114        writeln!(file, "{}", json)?;
115
116        // Also log to tracing
117        info!(
118            event_type = ?entry.event_type,
119            component = %entry.component,
120            actor = %entry.actor,
121            resource = %entry.resource,
122            result = %entry.result,
123            "Audit event logged"
124        );
125
126        Ok(())
127    }
128
129    /// Log API key access
130    pub fn log_api_key_access(
131        &self,
132        provider: &str,
133        actor: &str,
134        result: &str,
135    ) -> Result<(), Box<dyn std::error::Error>> {
136        let entry = AuditLogEntry::new(
137            AuditEventType::ApiKeyAccessed,
138            "providers",
139            actor,
140            provider,
141            result,
142            "API key accessed",
143        );
144        self.log(&entry)
145    }
146
147    /// Log API key rotation
148    pub fn log_api_key_rotation(
149        &self,
150        provider: &str,
151        actor: &str,
152        result: &str,
153    ) -> Result<(), Box<dyn std::error::Error>> {
154        let entry = AuditLogEntry::new(
155            AuditEventType::ApiKeyRotated,
156            "providers",
157            actor,
158            provider,
159            result,
160            "API key rotated",
161        );
162        self.log(&entry)
163    }
164
165    /// Log authentication attempt
166    pub fn log_authentication_attempt(
167        &self,
168        provider: &str,
169        actor: &str,
170        result: &str,
171        details: &str,
172    ) -> Result<(), Box<dyn std::error::Error>> {
173        let entry = AuditLogEntry::new(
174            AuditEventType::AuthenticationAttempt,
175            "providers",
176            actor,
177            provider,
178            result,
179            details,
180        );
181        self.log(&entry)
182    }
183
184    /// Log authorization decision
185    pub fn log_authorization_decision(
186        &self,
187        resource: &str,
188        actor: &str,
189        allowed: bool,
190        details: &str,
191    ) -> Result<(), Box<dyn std::error::Error>> {
192        let result = if allowed { "allowed" } else { "denied" };
193        let entry = AuditLogEntry::new(
194            AuditEventType::AuthorizationDecision,
195            "permissions",
196            actor,
197            resource,
198            result,
199            details,
200        );
201        self.log(&entry)
202    }
203
204    /// Log rate limit exceeded
205    pub fn log_rate_limit_exceeded(
206        &self,
207        provider: &str,
208        actor: &str,
209        details: &str,
210    ) -> Result<(), Box<dyn std::error::Error>> {
211        let entry = AuditLogEntry::new(
212            AuditEventType::RateLimitExceeded,
213            "providers",
214            actor,
215            provider,
216            "rate_limit_exceeded",
217            details,
218        );
219        self.log(&entry)
220    }
221
222    /// Log security error
223    pub fn log_security_error(
224        &self,
225        component: &str,
226        actor: &str,
227        resource: &str,
228        error: &str,
229    ) -> Result<(), Box<dyn std::error::Error>> {
230        let entry = AuditLogEntry::new(
231            AuditEventType::SecurityError,
232            component,
233            actor,
234            resource,
235            "error",
236            error,
237        );
238        self.log(&entry)
239    }
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245    use tempfile::TempDir;
246
247    #[test]
248    fn test_audit_log_entry_creation() {
249        let entry = AuditLogEntry::new(
250            AuditEventType::ApiKeyAccessed,
251            "providers",
252            "system",
253            "openai",
254            "success",
255            "API key accessed",
256        );
257
258        assert_eq!(entry.event_type, AuditEventType::ApiKeyAccessed);
259        assert_eq!(entry.component, "providers");
260        assert_eq!(entry.actor, "system");
261        assert_eq!(entry.resource, "openai");
262        assert_eq!(entry.result, "success");
263    }
264
265    #[test]
266    fn test_audit_log_entry_to_json() {
267        let entry = AuditLogEntry::new(
268            AuditEventType::ApiKeyAccessed,
269            "providers",
270            "system",
271            "openai",
272            "success",
273            "API key accessed",
274        );
275
276        let json = entry.to_json().unwrap();
277        assert!(json.contains("ApiKeyAccessed"));
278        assert!(json.contains("providers"));
279        assert!(json.contains("openai"));
280    }
281
282    #[test]
283    fn test_audit_logger_log() {
284        let temp_dir = TempDir::new().unwrap();
285        let log_path = temp_dir.path().join("audit.log");
286
287        let logger = AuditLogger::new(log_path.clone());
288        let entry = AuditLogEntry::new(
289            AuditEventType::ApiKeyAccessed,
290            "providers",
291            "system",
292            "openai",
293            "success",
294            "API key accessed",
295        );
296
297        let result = logger.log(&entry);
298        assert!(result.is_ok());
299
300        // Verify file was created and contains entry
301        let content = std::fs::read_to_string(&log_path).unwrap();
302        assert!(content.contains("ApiKeyAccessed"));
303    }
304
305    #[test]
306    fn test_audit_logger_log_api_key_access() {
307        let temp_dir = TempDir::new().unwrap();
308        let log_path = temp_dir.path().join("audit.log");
309
310        let logger = AuditLogger::new(log_path.clone());
311        let result = logger.log_api_key_access("openai", "system", "success");
312        assert!(result.is_ok());
313
314        let content = std::fs::read_to_string(&log_path).unwrap();
315        assert!(content.contains("ApiKeyAccessed"));
316        assert!(content.contains("openai"));
317    }
318
319    #[test]
320    fn test_audit_logger_log_authentication_attempt() {
321        let temp_dir = TempDir::new().unwrap();
322        let log_path = temp_dir.path().join("audit.log");
323
324        let logger = AuditLogger::new(log_path.clone());
325        let result = logger.log_authentication_attempt("openai", "system", "success", "Valid API key");
326        assert!(result.is_ok());
327
328        let content = std::fs::read_to_string(&log_path).unwrap();
329        assert!(content.contains("AuthenticationAttempt"));
330    }
331
332    #[test]
333    fn test_audit_logger_log_authorization_decision() {
334        let temp_dir = TempDir::new().unwrap();
335        let log_path = temp_dir.path().join("audit.log");
336
337        let logger = AuditLogger::new(log_path.clone());
338        let result = logger.log_authorization_decision("tool:read_file", "system", true, "Permission granted");
339        assert!(result.is_ok());
340
341        let content = std::fs::read_to_string(&log_path).unwrap();
342        assert!(content.contains("AuthorizationDecision"));
343        assert!(content.contains("allowed"));
344    }
345
346    #[test]
347    fn test_audit_logger_log_rate_limit_exceeded() {
348        let temp_dir = TempDir::new().unwrap();
349        let log_path = temp_dir.path().join("audit.log");
350
351        let logger = AuditLogger::new(log_path.clone());
352        let result = logger.log_rate_limit_exceeded("openai", "system", "Rate limit: 10 req/sec");
353        assert!(result.is_ok());
354
355        let content = std::fs::read_to_string(&log_path).unwrap();
356        assert!(content.contains("RateLimitExceeded"));
357    }
358
359    #[test]
360    fn test_audit_logger_multiple_entries() {
361        let temp_dir = TempDir::new().unwrap();
362        let log_path = temp_dir.path().join("audit.log");
363
364        let logger = AuditLogger::new(log_path.clone());
365
366        logger.log_api_key_access("openai", "system", "success").unwrap();
367        logger.log_api_key_access("anthropic", "system", "success").unwrap();
368        logger.log_authentication_attempt("openai", "system", "success", "Valid key").unwrap();
369
370        let content = std::fs::read_to_string(&log_path).unwrap();
371        let lines: Vec<&str> = content.lines().collect();
372        assert_eq!(lines.len(), 3);
373    }
374}