Skip to main content

fraiseql_server/auth/
security_config.rs

1//! Security configuration loading and initialization
2//!
3//! Loads security configuration from schema.compiled.json and initializes
4//! all security subsystems (audit logging, rate limiting, error sanitization, etc.)
5
6use std::env;
7
8use serde_json::Value as JsonValue;
9
10/// Security configuration loaded from schema.compiled.json
11#[derive(Debug, Clone)]
12pub struct SecurityConfigFromSchema {
13    /// Audit logging configuration
14    pub audit_logging:      AuditLoggingSettings,
15    /// Error sanitization configuration
16    pub error_sanitization: ErrorSanitizationSettings,
17    /// Rate limiting configuration
18    pub rate_limiting:      RateLimitingSettings,
19    /// State encryption configuration
20    pub state_encryption:   StateEncryptionSettings,
21}
22
23#[derive(Debug, Clone)]
24pub struct AuditLoggingSettings {
25    pub enabled:                bool,
26    pub log_level:              String,
27    pub include_sensitive_data: bool,
28    pub async_logging:          bool,
29    pub buffer_size:            u32,
30    pub flush_interval_secs:    u32,
31}
32
33#[derive(Debug, Clone)]
34pub struct ErrorSanitizationSettings {
35    pub enabled:                bool,
36    pub generic_messages:       bool,
37    pub internal_logging:       bool,
38    pub leak_sensitive_details: bool,
39    pub user_facing_format:     String,
40}
41
42#[derive(Debug, Clone)]
43pub struct RateLimitingSettings {
44    pub enabled:                    bool,
45    pub auth_start_max_requests:    u32,
46    pub auth_start_window_secs:     u64,
47    pub auth_callback_max_requests: u32,
48    pub auth_callback_window_secs:  u64,
49    pub auth_refresh_max_requests:  u32,
50    pub auth_refresh_window_secs:   u64,
51    pub auth_logout_max_requests:   u32,
52    pub auth_logout_window_secs:    u64,
53    pub failed_login_max_requests:  u32,
54    pub failed_login_window_secs:   u64,
55}
56
57#[derive(Debug, Clone)]
58pub struct StateEncryptionSettings {
59    pub enabled:              bool,
60    pub algorithm:            String,
61    pub key_rotation_enabled: bool,
62    pub nonce_size:           u32,
63    pub key_size:             u32,
64}
65
66impl Default for SecurityConfigFromSchema {
67    fn default() -> Self {
68        Self {
69            audit_logging:      AuditLoggingSettings {
70                enabled:                true,
71                log_level:              "info".to_string(),
72                include_sensitive_data: false,
73                async_logging:          true,
74                buffer_size:            1000,
75                flush_interval_secs:    5,
76            },
77            error_sanitization: ErrorSanitizationSettings {
78                enabled:                true,
79                generic_messages:       true,
80                internal_logging:       true,
81                leak_sensitive_details: false,
82                user_facing_format:     "generic".to_string(),
83            },
84            rate_limiting:      RateLimitingSettings {
85                enabled:                    true,
86                auth_start_max_requests:    100,
87                auth_start_window_secs:     60,
88                auth_callback_max_requests: 50,
89                auth_callback_window_secs:  60,
90                auth_refresh_max_requests:  10,
91                auth_refresh_window_secs:   60,
92                auth_logout_max_requests:   20,
93                auth_logout_window_secs:    60,
94                failed_login_max_requests:  5,
95                failed_login_window_secs:   3600,
96            },
97            state_encryption:   StateEncryptionSettings {
98                enabled:              true,
99                algorithm:            "chacha20-poly1305".to_string(),
100                key_rotation_enabled: false,
101                nonce_size:           12,
102                key_size:             32,
103            },
104        }
105    }
106}
107
108impl SecurityConfigFromSchema {
109    /// Parse security configuration from JSON (from schema.compiled.json)
110    pub fn from_json(value: &JsonValue) -> anyhow::Result<Self> {
111        let mut config = Self::default();
112
113        if let Some(audit) = value.get("auditLogging").and_then(|v| v.as_object()) {
114            config.audit_logging.enabled =
115                audit.get("enabled").and_then(|v| v.as_bool()).unwrap_or(true);
116            config.audit_logging.log_level =
117                audit.get("logLevel").and_then(|v| v.as_str()).unwrap_or("info").to_string();
118            config.audit_logging.include_sensitive_data =
119                audit.get("includeSensitiveData").and_then(|v| v.as_bool()).unwrap_or(false);
120            config.audit_logging.async_logging =
121                audit.get("asyncLogging").and_then(|v| v.as_bool()).unwrap_or(true);
122            config.audit_logging.buffer_size =
123                audit.get("bufferSize").and_then(|v| v.as_u64()).unwrap_or(1000) as u32;
124            config.audit_logging.flush_interval_secs =
125                audit.get("flushIntervalSecs").and_then(|v| v.as_u64()).unwrap_or(5) as u32;
126        }
127
128        if let Some(error_san) = value.get("errorSanitization").and_then(|v| v.as_object()) {
129            config.error_sanitization.enabled =
130                error_san.get("enabled").and_then(|v| v.as_bool()).unwrap_or(true);
131            config.error_sanitization.generic_messages =
132                error_san.get("genericMessages").and_then(|v| v.as_bool()).unwrap_or(true);
133            config.error_sanitization.internal_logging =
134                error_san.get("internalLogging").and_then(|v| v.as_bool()).unwrap_or(true);
135            config.error_sanitization.leak_sensitive_details =
136                error_san.get("leakSensitiveDetails").and_then(|v| v.as_bool()).unwrap_or(false);
137            config.error_sanitization.user_facing_format = error_san
138                .get("userFacingFormat")
139                .and_then(|v| v.as_str())
140                .unwrap_or("generic")
141                .to_string();
142        }
143
144        if let Some(rate_limit) = value.get("rateLimiting").and_then(|v| v.as_object()) {
145            config.rate_limiting.enabled =
146                rate_limit.get("enabled").and_then(|v| v.as_bool()).unwrap_or(true);
147
148            if let Some(auth_start) = rate_limit.get("authStart").and_then(|v| v.as_object()) {
149                config.rate_limiting.auth_start_max_requests =
150                    auth_start.get("maxRequests").and_then(|v| v.as_u64()).unwrap_or(100) as u32;
151                config.rate_limiting.auth_start_window_secs =
152                    auth_start.get("windowSecs").and_then(|v| v.as_u64()).unwrap_or(60);
153            }
154
155            if let Some(auth_callback) = rate_limit.get("authCallback").and_then(|v| v.as_object())
156            {
157                config.rate_limiting.auth_callback_max_requests =
158                    auth_callback.get("maxRequests").and_then(|v| v.as_u64()).unwrap_or(50) as u32;
159                config.rate_limiting.auth_callback_window_secs =
160                    auth_callback.get("windowSecs").and_then(|v| v.as_u64()).unwrap_or(60);
161            }
162
163            if let Some(auth_refresh) = rate_limit.get("authRefresh").and_then(|v| v.as_object()) {
164                config.rate_limiting.auth_refresh_max_requests =
165                    auth_refresh.get("maxRequests").and_then(|v| v.as_u64()).unwrap_or(10) as u32;
166                config.rate_limiting.auth_refresh_window_secs =
167                    auth_refresh.get("windowSecs").and_then(|v| v.as_u64()).unwrap_or(60);
168            }
169
170            if let Some(auth_logout) = rate_limit.get("authLogout").and_then(|v| v.as_object()) {
171                config.rate_limiting.auth_logout_max_requests =
172                    auth_logout.get("maxRequests").and_then(|v| v.as_u64()).unwrap_or(20) as u32;
173                config.rate_limiting.auth_logout_window_secs =
174                    auth_logout.get("windowSecs").and_then(|v| v.as_u64()).unwrap_or(60);
175            }
176
177            if let Some(failed_login) = rate_limit.get("failedLogin").and_then(|v| v.as_object()) {
178                config.rate_limiting.failed_login_max_requests =
179                    failed_login.get("maxRequests").and_then(|v| v.as_u64()).unwrap_or(5) as u32;
180                config.rate_limiting.failed_login_window_secs =
181                    failed_login.get("windowSecs").and_then(|v| v.as_u64()).unwrap_or(3600);
182            }
183        }
184
185        if let Some(state_enc) = value.get("stateEncryption").and_then(|v| v.as_object()) {
186            config.state_encryption.enabled =
187                state_enc.get("enabled").and_then(|v| v.as_bool()).unwrap_or(true);
188            config.state_encryption.algorithm = state_enc
189                .get("algorithm")
190                .and_then(|v| v.as_str())
191                .unwrap_or("chacha20-poly1305")
192                .to_string();
193            config.state_encryption.key_rotation_enabled =
194                state_enc.get("keyRotationEnabled").and_then(|v| v.as_bool()).unwrap_or(false);
195            config.state_encryption.nonce_size =
196                state_enc.get("nonceSize").and_then(|v| v.as_u64()).unwrap_or(12) as u32;
197            config.state_encryption.key_size =
198                state_enc.get("keySize").and_then(|v| v.as_u64()).unwrap_or(32) as u32;
199        }
200
201        Ok(config)
202    }
203
204    /// Apply environment variable overrides
205    pub fn apply_env_overrides(&mut self) {
206        // Audit logging
207        if let Ok(level) = env::var("AUDIT_LOG_LEVEL") {
208            self.audit_logging.log_level = level;
209        }
210
211        // Rate limiting
212        if let Ok(val) = env::var("RATE_LIMIT_AUTH_START") {
213            if let Ok(n) = val.parse() {
214                self.rate_limiting.auth_start_max_requests = n;
215            }
216        }
217        if let Ok(val) = env::var("RATE_LIMIT_AUTH_CALLBACK") {
218            if let Ok(n) = val.parse() {
219                self.rate_limiting.auth_callback_max_requests = n;
220            }
221        }
222        if let Ok(val) = env::var("RATE_LIMIT_AUTH_REFRESH") {
223            if let Ok(n) = val.parse() {
224                self.rate_limiting.auth_refresh_max_requests = n;
225            }
226        }
227        if let Ok(val) = env::var("RATE_LIMIT_AUTH_LOGOUT") {
228            if let Ok(n) = val.parse() {
229                self.rate_limiting.auth_logout_max_requests = n;
230            }
231        }
232        if let Ok(val) = env::var("RATE_LIMIT_FAILED_LOGIN") {
233            if let Ok(n) = val.parse() {
234                self.rate_limiting.failed_login_max_requests = n;
235            }
236        }
237    }
238}
239
240#[cfg(test)]
241mod tests {
242    use super::*;
243
244    #[test]
245    fn test_default_config() {
246        let config = SecurityConfigFromSchema::default();
247        assert!(config.audit_logging.enabled);
248        assert!(config.error_sanitization.enabled);
249        assert!(config.rate_limiting.enabled);
250        assert!(config.state_encryption.enabled);
251    }
252
253    #[test]
254    fn test_parse_from_json() {
255        let json = serde_json::json!({
256            "auditLogging": {
257                "enabled": true,
258                "logLevel": "debug",
259                "includeSensitiveData": false
260            },
261            "rateLimiting": {
262                "enabled": true,
263                "authStart": {
264                    "maxRequests": 200,
265                    "windowSecs": 60
266                }
267            }
268        });
269
270        let config = SecurityConfigFromSchema::from_json(&json).expect("Failed to parse");
271        assert_eq!(config.audit_logging.log_level, "debug");
272        assert_eq!(config.rate_limiting.auth_start_max_requests, 200);
273    }
274
275    #[test]
276    fn test_apply_env_overrides() {
277        // Note: This test would require setting env vars during test execution
278        // For now, we just verify the method works with defaults
279        let mut config = SecurityConfigFromSchema::default();
280        config.apply_env_overrides();
281        // No assertions needed, just verify it doesn't panic
282    }
283}