fraiseql_core/security/
validation_audit.rs1use 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 #[must_use]
88 pub fn new(config: ValidationAuditLoggerConfig) -> Self {
89 Self {
90 config: Arc::new(config),
91 entries: Arc::new(Mutex::new(Vec::new())),
92 }
93 }
94
95 pub fn log_entry(&self, entry: ValidationAuditEntry) {
97 if !self.config.enabled {
98 return;
99 }
100
101 if !entry.valid || self.config.capture_successful_validations {
103 match self.entries.lock() {
104 Ok(mut entries) => entries.push(entry),
105 Err(e) => {
106 tracing::error!(
107 error = ?e,
108 lost_entry = ?entry,
109 "CRITICAL: Audit log mutex poisoned, entry lost"
110 );
111 },
113 }
114 }
115 }
116
117 #[must_use]
119 pub fn is_enabled(&self) -> bool {
120 self.config.enabled
121 }
122
123 #[must_use]
125 pub fn get_entries(&self) -> Vec<ValidationAuditEntry> {
126 if let Ok(entries) = self.entries.lock() {
127 entries.clone()
128 } else {
129 Vec::new()
130 }
131 }
132
133 pub fn clear(&self) {
135 if let Ok(mut entries) = self.entries.lock() {
136 entries.clear();
137 }
138 }
139
140 #[must_use]
142 pub fn entry_count(&self) -> usize {
143 if let Ok(entries) = self.entries.lock() {
144 entries.len()
145 } else {
146 0
147 }
148 }
149
150 #[must_use]
152 pub fn entries_by_user(&self, user_id: &str) -> Vec<ValidationAuditEntry> {
153 if let Ok(entries) = self.entries.lock() {
154 entries
155 .iter()
156 .filter(|e| e.user_id.as_deref() == Some(user_id))
157 .cloned()
158 .collect()
159 } else {
160 Vec::new()
161 }
162 }
163
164 #[must_use]
166 pub fn entries_by_tenant(&self, tenant_id: &str) -> Vec<ValidationAuditEntry> {
167 if let Ok(entries) = self.entries.lock() {
168 entries
169 .iter()
170 .filter(|e| e.tenant_id.as_deref() == Some(tenant_id))
171 .cloned()
172 .collect()
173 } else {
174 Vec::new()
175 }
176 }
177
178 #[must_use]
180 pub fn entries_by_field(&self, field: &str) -> Vec<ValidationAuditEntry> {
181 if let Ok(entries) = self.entries.lock() {
182 entries.iter().filter(|e| e.field == field).cloned().collect()
183 } else {
184 Vec::new()
185 }
186 }
187
188 #[must_use]
190 pub fn failure_count(&self) -> usize {
191 if let Ok(entries) = self.entries.lock() {
192 entries.iter().filter(|e| !e.valid).count()
193 } else {
194 0
195 }
196 }
197
198 #[must_use]
200 pub fn config(&self) -> &ValidationAuditLoggerConfig {
201 &self.config
202 }
203}