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