1use std::sync::{Arc, Mutex};
7
8use chrono::{DateTime, Utc};
9use serde::{Deserialize, Serialize};
10use tracing;
11
12#[derive(Debug, Clone, Copy, Default)]
14#[non_exhaustive]
15pub enum RedactionPolicy {
16 None,
18 #[default]
20 Conservative,
21 Aggressive,
23}
24
25#[derive(Debug, Clone)]
27pub struct ValidationAuditLoggerConfig {
28 pub enabled: bool,
30 pub capture_successful_validations: bool,
32 pub capture_query_strings: bool,
34 pub redaction_policy: RedactionPolicy,
36}
37
38impl Default for ValidationAuditLoggerConfig {
39 fn default() -> Self {
40 Self {
41 enabled: true,
42 capture_successful_validations: true,
43 capture_query_strings: true,
44 redaction_policy: RedactionPolicy::default(),
45 }
46 }
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct ValidationAuditEntry {
52 pub timestamp: DateTime<Utc>,
54 pub user_id: Option<String>,
56 pub tenant_id: Option<String>,
58 pub ip_address: String,
60 pub query_string: String,
62 pub mutation_name: Option<String>,
64 pub field: String,
66 pub validation_rule: String,
68 pub valid: bool,
70 pub failure_reason: Option<String>,
72 pub duration_us: u64,
74 pub execution_context: String,
76}
77
78#[derive(Clone)]
80pub struct ValidationAuditLogger {
81 config: Arc<ValidationAuditLoggerConfig>,
82 entries: Arc<Mutex<Vec<ValidationAuditEntry>>>,
83}
84
85impl ValidationAuditLogger {
86 pub fn new(config: ValidationAuditLoggerConfig) -> Self {
88 Self {
89 config: Arc::new(config),
90 entries: Arc::new(Mutex::new(Vec::new())),
91 }
92 }
93
94 pub fn log_entry(&self, entry: ValidationAuditEntry) {
96 if !self.config.enabled {
97 return;
98 }
99
100 if !entry.valid || self.config.capture_successful_validations {
102 match self.entries.lock() {
103 Ok(mut entries) => entries.push(entry),
104 Err(e) => {
105 tracing::error!(
106 error = ?e,
107 lost_entry = ?entry,
108 "CRITICAL: Audit log mutex poisoned, entry lost"
109 );
110 },
112 }
113 }
114 }
115
116 pub fn is_enabled(&self) -> bool {
118 self.config.enabled
119 }
120
121 pub fn get_entries(&self) -> Vec<ValidationAuditEntry> {
123 if let Ok(entries) = self.entries.lock() {
124 entries.clone()
125 } else {
126 Vec::new()
127 }
128 }
129
130 pub fn clear(&self) {
132 if let Ok(mut entries) = self.entries.lock() {
133 entries.clear();
134 }
135 }
136
137 pub fn entry_count(&self) -> usize {
139 if let Ok(entries) = self.entries.lock() {
140 entries.len()
141 } else {
142 0
143 }
144 }
145
146 pub fn entries_by_user(&self, user_id: &str) -> Vec<ValidationAuditEntry> {
148 if let Ok(entries) = self.entries.lock() {
149 entries
150 .iter()
151 .filter(|e| e.user_id.as_deref() == Some(user_id))
152 .cloned()
153 .collect()
154 } else {
155 Vec::new()
156 }
157 }
158
159 pub fn entries_by_tenant(&self, tenant_id: &str) -> Vec<ValidationAuditEntry> {
161 if let Ok(entries) = self.entries.lock() {
162 entries
163 .iter()
164 .filter(|e| e.tenant_id.as_deref() == Some(tenant_id))
165 .cloned()
166 .collect()
167 } else {
168 Vec::new()
169 }
170 }
171
172 pub fn entries_by_field(&self, field: &str) -> Vec<ValidationAuditEntry> {
174 if let Ok(entries) = self.entries.lock() {
175 entries.iter().filter(|e| e.field == field).cloned().collect()
176 } else {
177 Vec::new()
178 }
179 }
180
181 pub fn failure_count(&self) -> usize {
183 if let Ok(entries) = self.entries.lock() {
184 entries.iter().filter(|e| !e.valid).count()
185 } else {
186 0
187 }
188 }
189
190 pub fn config(&self) -> &ValidationAuditLoggerConfig {
192 &self.config
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 #[test]
201 fn test_redaction_policy_default() {
202 let policy = RedactionPolicy::default();
203 match policy {
204 RedactionPolicy::Conservative => {},
205 _ => panic!("Default should be Conservative"),
206 }
207 }
208
209 #[test]
210 fn test_config_default() {
211 let config = ValidationAuditLoggerConfig::default();
212 assert!(config.enabled);
213 assert!(config.capture_successful_validations);
214 assert!(config.capture_query_strings);
215 }
216
217 #[test]
218 fn test_logger_enabled_disabled() {
219 let config = ValidationAuditLoggerConfig {
220 enabled: false,
221 ..Default::default()
222 };
223
224 let logger = ValidationAuditLogger::new(config);
225 assert!(!logger.is_enabled());
226
227 let config2 = ValidationAuditLoggerConfig::default();
228 let logger2 = ValidationAuditLogger::new(config2);
229 assert!(logger2.is_enabled());
230 }
231
232 #[test]
233 fn test_logger_entry_logging() {
234 let config = ValidationAuditLoggerConfig::default();
235 let logger = ValidationAuditLogger::new(config);
236
237 let entry = ValidationAuditEntry {
238 timestamp: Utc::now(),
239 user_id: Some("user:1".to_string()),
240 tenant_id: Some("tenant:1".to_string()),
241 ip_address: "192.168.1.1".to_string(),
242 query_string: "{ user { id } }".to_string(),
243 mutation_name: None,
244 field: "email".to_string(),
245 validation_rule: "pattern".to_string(),
246 valid: false,
247 failure_reason: Some("Invalid format".to_string()),
248 duration_us: 100,
249 execution_context: "pattern_validator".to_string(),
250 };
251
252 logger.log_entry(entry);
253 assert_eq!(logger.entry_count(), 1);
254 }
255
256 #[test]
257 fn test_logger_filter_by_user() {
258 let config = ValidationAuditLoggerConfig::default();
259 let logger = ValidationAuditLogger::new(config);
260
261 let entry1 = ValidationAuditEntry {
262 timestamp: Utc::now(),
263 user_id: Some("user:1".to_string()),
264 tenant_id: None,
265 ip_address: "192.168.1.1".to_string(),
266 query_string: String::new(),
267 mutation_name: None,
268 field: "field1".to_string(),
269 validation_rule: "required".to_string(),
270 valid: false,
271 failure_reason: None,
272 duration_us: 0,
273 execution_context: "validator".to_string(),
274 };
275
276 let mut entry2 = entry1.clone();
277 entry2.user_id = Some("user:2".to_string());
278
279 logger.log_entry(entry1);
280 logger.log_entry(entry2);
281
282 let user1_entries = logger.entries_by_user("user:1");
283 assert_eq!(user1_entries.len(), 1);
284 }
285
286 #[test]
287 fn test_logger_failure_count() {
288 let config = ValidationAuditLoggerConfig::default();
289 let logger = ValidationAuditLogger::new(config);
290
291 let entry = ValidationAuditEntry {
292 timestamp: Utc::now(),
293 user_id: None,
294 tenant_id: None,
295 ip_address: "192.168.1.1".to_string(),
296 query_string: String::new(),
297 mutation_name: None,
298 field: "field".to_string(),
299 validation_rule: "pattern".to_string(),
300 valid: false,
301 failure_reason: Some("error".to_string()),
302 duration_us: 0,
303 execution_context: "validator".to_string(),
304 };
305
306 logger.log_entry(entry.clone());
307
308 let mut entry_success = entry;
309 entry_success.valid = true;
310 logger.log_entry(entry_success);
311
312 assert_eq!(logger.failure_count(), 1);
313 }
314}