fraiseql_core/audit/
mod.rs1use chrono::Utc;
37use serde::{Deserialize, Serialize};
38use serde_json::Value as JsonValue;
39use uuid::Uuid;
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct AuditEvent {
47 pub id: String,
49
50 pub timestamp: String,
52
53 pub event_type: String,
55
56 pub user_id: String,
58
59 pub username: String,
61
62 pub ip_address: String,
64
65 pub resource_type: String,
67
68 pub resource_id: Option<String>,
70
71 pub action: String,
73
74 pub before_state: Option<JsonValue>,
76
77 pub after_state: Option<JsonValue>,
79
80 pub status: String,
82
83 pub error_message: Option<String>,
85
86 pub tenant_id: Option<String>,
88
89 pub metadata: JsonValue,
91}
92
93impl AuditEvent {
94 #[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 #[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 #[must_use]
148 pub fn with_before_state(mut self, state: JsonValue) -> Self {
149 self.before_state = Some(state);
150 self
151 }
152
153 #[must_use]
155 pub fn with_after_state(mut self, state: JsonValue) -> Self {
156 self.after_state = Some(state);
157 self
158 }
159
160 #[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 #[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 #[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 pub fn validate(&self) -> AuditResult<()> {
185 if self.user_id.is_empty() {
187 return Err(AuditError::ValidationError("user_id cannot be empty".to_string()));
188 }
189
190 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 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
212pub type AuditResult<T> = Result<T, AuditError>;
214
215#[derive(Debug, thiserror::Error)]
217pub enum AuditError {
218 #[error("File error: {0}")]
220 FileError(String),
221
222 #[error("Database error: {0}")]
224 DatabaseError(String),
225
226 #[error("Network error: {0}")]
228 NetworkError(String),
229
230 #[error("Validation error: {0}")]
232 ValidationError(String),
233
234 #[error("Serialization error: {0}")]
236 SerializationError(String),
237}
238
239#[async_trait::async_trait]
241pub trait AuditBackend: Send + Sync {
242 async fn log_event(&self, event: AuditEvent) -> AuditResult<()>;
244
245 async fn query_events(&self, filters: AuditQueryFilters) -> AuditResult<Vec<AuditEvent>>;
247}
248
249#[derive(Debug, Clone)]
251pub struct AuditQueryFilters {
252 pub event_type: Option<String>,
254
255 pub user_id: Option<String>,
257
258 pub resource_type: Option<String>,
260
261 pub status: Option<String>,
263
264 pub tenant_id: Option<String>,
266
267 pub start_time: Option<String>,
269
270 pub end_time: Option<String>,
272
273 pub limit: Option<usize>,
275
276 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
296pub mod file_backend;
298
299pub mod postgres_backend;
301
302pub mod syslog_backend;
304
305pub 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;