Skip to main content

fraiseql_core/security/
validation_audit.rs

1//! Validation-specific audit logging with tenant isolation and PII redaction.
2//!
3//! Provides audit trail tracking for all validation decisions, including
4//! field name, validation rule applied, success/failure, and execution context.
5
6use std::sync::{Arc, Mutex};
7
8use chrono::{DateTime, Utc};
9use serde::{Deserialize, Serialize};
10use tracing;
11
12/// Redaction policy for sensitive fields in audit logs
13#[derive(Debug, Clone, Copy, Default)]
14#[non_exhaustive]
15pub enum RedactionPolicy {
16    /// No redaction - log everything
17    None,
18    /// Conservative redaction - redact passwords, tokens, etc.
19    #[default]
20    Conservative,
21    /// Aggressive redaction - redact most user-related data
22    Aggressive,
23}
24
25/// Configuration for validation audit logging
26#[derive(Debug, Clone)]
27pub struct ValidationAuditLoggerConfig {
28    /// Enable validation audit logging
29    pub enabled: bool,
30    /// Capture successful validation entries (not just failures)
31    pub capture_successful_validations: bool,
32    /// Include the GraphQL query/mutation string in logs
33    pub capture_query_strings: bool,
34    /// Redaction policy for sensitive data
35    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/// A single validation audit entry
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct ValidationAuditEntry {
52    /// Timestamp of the validation check
53    pub timestamp:         DateTime<Utc>,
54    /// User ID from authentication context
55    pub user_id:           Option<String>,
56    /// Tenant ID for multi-tenancy isolation
57    pub tenant_id:         Option<String>,
58    /// Client IP address
59    pub ip_address:        String,
60    /// GraphQL query or mutation string (may be redacted)
61    pub query_string:      String,
62    /// Name of the mutation (if applicable)
63    pub mutation_name:     Option<String>,
64    /// Field name that was validated
65    pub field:             String,
66    /// Validation rule that was applied
67    pub validation_rule:   String,
68    /// Whether the validation passed
69    pub valid:             bool,
70    /// Reason for failure (if applicable)
71    pub failure_reason:    Option<String>,
72    /// Duration of validation in microseconds
73    pub duration_us:       u64,
74    /// Type of validator executed (e.g., "`pattern_validator`", "`async_validator`")
75    pub execution_context: String,
76}
77
78/// Validation audit logger for recording validation decisions
79#[derive(Clone)]
80pub struct ValidationAuditLogger {
81    config:  Arc<ValidationAuditLoggerConfig>,
82    entries: Arc<Mutex<Vec<ValidationAuditEntry>>>,
83}
84
85impl ValidationAuditLogger {
86    /// Create a new validation audit logger with the given configuration
87    #[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    /// Log a validation audit entry
96    pub fn log_entry(&self, entry: ValidationAuditEntry) {
97        if !self.config.enabled {
98            return;
99        }
100
101        // Only log failures or successful entries if configured to capture successes
102        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                    // In production, this should trigger an alert/metric
112                },
113            }
114        }
115    }
116
117    /// Check if audit logging is enabled
118    #[must_use]
119    pub fn is_enabled(&self) -> bool {
120        self.config.enabled
121    }
122
123    /// Get all logged entries (for testing/compliance export)
124    #[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    /// Clear all logged entries
134    pub fn clear(&self) {
135        if let Ok(mut entries) = self.entries.lock() {
136            entries.clear();
137        }
138    }
139
140    /// Get count of logged entries
141    #[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    /// Filter entries by user ID
151    #[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    /// Filter entries by tenant ID
165    #[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    /// Filter entries by field name
179    #[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    /// Count validation failures
189    #[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    /// Get configuration reference
199    #[must_use]
200    pub fn config(&self) -> &ValidationAuditLoggerConfig {
201        &self.config
202    }
203}