Skip to main content

fraiseql_core/audit/
mod.rs

1//! Audit logging infrastructure
2//!
3//! Provides audit event structures and backend trait for multi-backend audit logging.
4//!
5//! # Architecture
6//!
7//! Audit events capture security-relevant operations:
8//! - User authentication and authorization
9//! - Data mutations (create, update, delete)
10//! - Administrative actions
11//! - Configuration changes
12//!
13//! Multiple backends support different deployments:
14//! - File: JSON lines to local files
15//! - PostgreSQL: Relational storage with indexing
16//! - Syslog: Centralized logging infrastructure
17//!
18//! # Example
19//!
20//! ```ignore
21//! use fraiseql_core::audit::AuditEvent;
22//!
23//! let event = AuditEvent::new_user_action(
24//!     "user123",
25//!     "alice",
26//!     "192.168.1.1",
27//!     "users",
28//!     "create",
29//!     "success",
30//! );
31//!
32//! // Log to backend
33//! backend.log_event(event).await?;
34//! ```
35
36use chrono::Utc;
37use serde::{Deserialize, Serialize};
38use serde_json::Value as JsonValue;
39use uuid::Uuid;
40
41/// Audit event representing a security-relevant operation.
42///
43/// Captures detailed information about user actions, system events,
44/// and data mutations for compliance and security auditing.
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct AuditEvent {
47    /// Unique event identifier (UUID)
48    pub id: String,
49
50    /// ISO 8601 timestamp of the event
51    pub timestamp: String,
52
53    /// Event type (e.g., "user_login", "data_modification", "access_denied")
54    pub event_type: String,
55
56    /// User ID (None for system events)
57    pub user_id: String,
58
59    /// Username for human readability
60    pub username: String,
61
62    /// IP address of the request origin
63    pub ip_address: String,
64
65    /// Resource type affected (e.g., "users", "posts", "admin_config")
66    pub resource_type: String,
67
68    /// Resource ID (None for bulk operations or system events)
69    pub resource_id: Option<String>,
70
71    /// Action performed (e.g., "create", "update", "delete", "read")
72    pub action: String,
73
74    /// State before modification (None for read operations)
75    pub before_state: Option<JsonValue>,
76
77    /// State after modification (None for deletions or reads)
78    pub after_state: Option<JsonValue>,
79
80    /// Event status: "success", "failure", or "denied"
81    pub status: String,
82
83    /// Error message if status is "failure" or "denied"
84    pub error_message: Option<String>,
85
86    /// Tenant ID for multi-tenant deployments
87    pub tenant_id: Option<String>,
88
89    /// Additional context as JSON (user_agent, correlation_id, etc.)
90    pub metadata: JsonValue,
91}
92
93impl AuditEvent {
94    /// Create a new audit event for a user action.
95    ///
96    /// # Arguments
97    ///
98    /// * `user_id` - User performing the action
99    /// * `username` - User's name for readability
100    /// * `ip_address` - Request origin IP
101    /// * `resource_type` - Type of resource affected
102    /// * `action` - Action performed
103    /// * `status` - Result status (success/failure/denied)
104    #[must_use]
105    pub fn new_user_action(
106        user_id: impl Into<String>,
107        username: impl Into<String>,
108        ip_address: impl Into<String>,
109        resource_type: impl Into<String>,
110        action: impl Into<String>,
111        status: impl Into<String>,
112    ) -> Self {
113        let resource_type_str = resource_type.into();
114        let action_str = action.into();
115
116        Self {
117            id:            Uuid::new_v4().to_string(),
118            timestamp:     Utc::now().to_rfc3339(),
119            event_type:    format!(
120                "{}_{}",
121                resource_type_str.to_lowercase(),
122                action_str.to_lowercase()
123            ),
124            user_id:       user_id.into(),
125            username:      username.into(),
126            ip_address:    ip_address.into(),
127            resource_type: resource_type_str,
128            resource_id:   None,
129            action:        action_str,
130            before_state:  None,
131            after_state:   None,
132            status:        status.into(),
133            error_message: None,
134            tenant_id:     None,
135            metadata:      JsonValue::Object(serde_json::Map::new()),
136        }
137    }
138
139    /// Add resource ID to the event.
140    #[must_use]
141    pub fn with_resource_id(mut self, id: impl Into<String>) -> Self {
142        self.resource_id = Some(id.into());
143        self
144    }
145
146    /// Add before state to track modifications.
147    #[must_use]
148    pub fn with_before_state(mut self, state: JsonValue) -> Self {
149        self.before_state = Some(state);
150        self
151    }
152
153    /// Add after state to track modifications.
154    #[must_use]
155    pub fn with_after_state(mut self, state: JsonValue) -> Self {
156        self.after_state = Some(state);
157        self
158    }
159
160    /// Add error message for failed operations.
161    #[must_use]
162    pub fn with_error(mut self, message: impl Into<String>) -> Self {
163        self.error_message = Some(message.into());
164        self
165    }
166
167    /// Set tenant ID for multi-tenant tracking.
168    #[must_use]
169    pub fn with_tenant_id(mut self, tenant_id: impl Into<String>) -> Self {
170        self.tenant_id = Some(tenant_id.into());
171        self
172    }
173
174    /// Add metadata (user agent, correlation ID, etc.).
175    #[must_use]
176    pub fn with_metadata(mut self, key: impl Into<String>, value: JsonValue) -> Self {
177        if let JsonValue::Object(ref mut map) = self.metadata {
178            map.insert(key.into(), value);
179        }
180        self
181    }
182
183    /// Validate the audit event.
184    pub fn validate(&self) -> AuditResult<()> {
185        // Validate required fields
186        if self.user_id.is_empty() {
187            return Err(AuditError::ValidationError("user_id cannot be empty".to_string()));
188        }
189
190        // Validate status is one of allowed values
191        match self.status.as_str() {
192            "success" | "failure" | "denied" => {},
193            _ => {
194                return Err(AuditError::ValidationError(format!(
195                    "Invalid status: {}",
196                    self.status
197                )));
198            },
199        }
200
201        // Validate that status=failure has error_message
202        if self.status == "failure" && self.error_message.is_none() {
203            return Err(AuditError::ValidationError(
204                "failure status requires error_message".to_string(),
205            ));
206        }
207
208        Ok(())
209    }
210}
211
212/// Result type for audit operations
213pub type AuditResult<T> = Result<T, AuditError>;
214
215/// Error type for audit operations
216#[derive(Debug, thiserror::Error)]
217pub enum AuditError {
218    /// File I/O error
219    #[error("File error: {0}")]
220    FileError(String),
221
222    /// Database error
223    #[error("Database error: {0}")]
224    DatabaseError(String),
225
226    /// Network error
227    #[error("Network error: {0}")]
228    NetworkError(String),
229
230    /// Validation error
231    #[error("Validation error: {0}")]
232    ValidationError(String),
233
234    /// Serialization error
235    #[error("Serialization error: {0}")]
236    SerializationError(String),
237}
238
239/// Audit backend trait - implement for each storage backend.
240#[async_trait::async_trait]
241pub trait AuditBackend: Send + Sync {
242    /// Log an audit event to this backend.
243    async fn log_event(&self, event: AuditEvent) -> AuditResult<()>;
244
245    /// Query audit events from this backend.
246    async fn query_events(&self, filters: AuditQueryFilters) -> AuditResult<Vec<AuditEvent>>;
247}
248
249/// Filters for querying audit events
250#[derive(Debug, Clone)]
251pub struct AuditQueryFilters {
252    /// Filter by event type
253    pub event_type: Option<String>,
254
255    /// Filter by user ID
256    pub user_id: Option<String>,
257
258    /// Filter by resource type
259    pub resource_type: Option<String>,
260
261    /// Filter by status
262    pub status: Option<String>,
263
264    /// Filter by tenant ID
265    pub tenant_id: Option<String>,
266
267    /// Start time (ISO 8601)
268    pub start_time: Option<String>,
269
270    /// End time (ISO 8601)
271    pub end_time: Option<String>,
272
273    /// Maximum number of results
274    pub limit: Option<usize>,
275
276    /// Offset for pagination
277    pub offset: Option<usize>,
278}
279
280impl Default for AuditQueryFilters {
281    fn default() -> Self {
282        Self {
283            event_type:    None,
284            user_id:       None,
285            resource_type: None,
286            status:        None,
287            tenant_id:     None,
288            start_time:    None,
289            end_time:      None,
290            limit:         Some(100),
291            offset:        None,
292        }
293    }
294}
295
296/// File-based audit backend
297pub mod file_backend;
298
299/// PostgreSQL audit backend
300pub mod postgres_backend;
301
302/// Syslog audit backend
303pub mod syslog_backend;
304
305// Re-export backends for convenience
306pub use file_backend::FileAuditBackend;
307pub use postgres_backend::PostgresAuditBackend;
308pub use syslog_backend::SyslogAuditBackend;
309
310#[cfg(test)]
311mod tests;
312
313#[cfg(test)]
314mod file_backend_tests;
315
316#[cfg(test)]
317mod postgres_backend_tests;
318
319#[cfg(test)]
320mod syslog_backend_tests;