adk_auth/
audit.rs

1//! Audit logging for access control.
2
3use 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/// Type of audit event.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12#[serde(rename_all = "snake_case")]
13pub enum AuditEventType {
14    /// Tool access attempt.
15    ToolAccess,
16    /// Agent access attempt.
17    AgentAccess,
18    /// Permission check.
19    PermissionCheck,
20}
21
22/// Outcome of an audit event.
23#[derive(Debug, Clone, Serialize, Deserialize)]
24#[serde(rename_all = "snake_case")]
25pub enum AuditOutcome {
26    /// Access was allowed.
27    Allowed,
28    /// Access was denied.
29    Denied,
30    /// An error occurred.
31    Error,
32}
33
34/// An audit event.
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct AuditEvent {
37    /// Timestamp of the event.
38    pub timestamp: DateTime<Utc>,
39    /// User ID.
40    pub user: String,
41    /// Session ID (if available).
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub session_id: Option<String>,
44    /// Type of event.
45    pub event_type: AuditEventType,
46    /// Resource being accessed (tool name, agent name).
47    pub resource: String,
48    /// Outcome of the access attempt.
49    pub outcome: AuditOutcome,
50    /// Additional metadata.
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub metadata: Option<serde_json::Value>,
53}
54
55impl AuditEvent {
56    /// Create a new tool access event.
57    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    /// Create a new agent access event.
70    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    /// Set the session ID.
83    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    /// Set metadata.
89    pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
90        self.metadata = Some(metadata);
91        self
92    }
93}
94
95/// Trait for audit sinks.
96#[async_trait::async_trait]
97pub trait AuditSink: Send + Sync {
98    /// Log an audit event.
99    async fn log(&self, event: AuditEvent) -> Result<(), crate::AuthError>;
100}
101
102/// File-based audit sink that writes JSONL.
103pub struct FileAuditSink {
104    writer: Mutex<BufWriter<File>>,
105    path: PathBuf,
106}
107
108impl FileAuditSink {
109    /// Create a new file audit sink.
110    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    /// Get the path to the audit log file.
118    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}