1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::fs::{File, OpenOptions};
6use std::io::{BufWriter, Write};
7use std::path::PathBuf;
8use std::sync::Mutex;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12#[serde(rename_all = "snake_case")]
13pub enum AuditEventType {
14 ToolAccess,
16 AgentAccess,
18 PermissionCheck,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
24#[serde(rename_all = "snake_case")]
25pub enum AuditOutcome {
26 Allowed,
28 Denied,
30 Error,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct AuditEvent {
37 pub timestamp: DateTime<Utc>,
39 pub user: String,
41 #[serde(skip_serializing_if = "Option::is_none")]
43 pub session_id: Option<String>,
44 pub event_type: AuditEventType,
46 pub resource: String,
48 pub outcome: AuditOutcome,
50 #[serde(skip_serializing_if = "Option::is_none")]
52 pub metadata: Option<serde_json::Value>,
53}
54
55impl AuditEvent {
56 pub fn tool_access(user: &str, tool_name: &str, outcome: AuditOutcome) -> Self {
58 Self {
59 timestamp: Utc::now(),
60 user: user.to_string(),
61 session_id: None,
62 event_type: AuditEventType::ToolAccess,
63 resource: tool_name.to_string(),
64 outcome,
65 metadata: None,
66 }
67 }
68
69 pub fn agent_access(user: &str, agent_name: &str, outcome: AuditOutcome) -> Self {
71 Self {
72 timestamp: Utc::now(),
73 user: user.to_string(),
74 session_id: None,
75 event_type: AuditEventType::AgentAccess,
76 resource: agent_name.to_string(),
77 outcome,
78 metadata: None,
79 }
80 }
81
82 pub fn with_session(mut self, session_id: impl Into<String>) -> Self {
84 self.session_id = Some(session_id.into());
85 self
86 }
87
88 pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
90 self.metadata = Some(metadata);
91 self
92 }
93}
94
95#[async_trait::async_trait]
97pub trait AuditSink: Send + Sync {
98 async fn log(&self, event: AuditEvent) -> Result<(), crate::AuthError>;
100}
101
102pub struct FileAuditSink {
104 writer: Mutex<BufWriter<File>>,
105 path: PathBuf,
106}
107
108impl FileAuditSink {
109 pub fn new(path: impl Into<PathBuf>) -> Result<Self, std::io::Error> {
111 let path = path.into();
112 let file = OpenOptions::new().create(true).append(true).open(&path)?;
113 let writer = Mutex::new(BufWriter::new(file));
114 Ok(Self { writer, path })
115 }
116
117 pub fn path(&self) -> &PathBuf {
119 &self.path
120 }
121}
122
123#[async_trait::async_trait]
124impl AuditSink for FileAuditSink {
125 async fn log(&self, event: AuditEvent) -> Result<(), crate::AuthError> {
126 let line = serde_json::to_string(&event)
127 .map_err(|e| crate::AuthError::AuditError(e.to_string()))?;
128
129 let mut writer = self.writer.lock().unwrap();
130 writeln!(writer, "{}", line)?;
131 writer.flush()?;
132
133 Ok(())
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 #[test]
142 fn test_audit_event_serialization() {
143 let event = AuditEvent::tool_access("alice", "search", AuditOutcome::Allowed);
144 let json = serde_json::to_string(&event).unwrap();
145 assert!(json.contains("\"user\":\"alice\""));
146 assert!(json.contains("\"resource\":\"search\""));
147 assert!(json.contains("\"outcome\":\"allowed\""));
148 }
149
150 #[test]
151 fn test_audit_event_with_session() {
152 let event = AuditEvent::tool_access("bob", "exec", AuditOutcome::Denied)
153 .with_session("session-123");
154 assert_eq!(event.session_id, Some("session-123".to_string()));
155 }
156}