fraiseql_auth/
security_config.rs1use std::env;
7
8use serde_json::Value as JsonValue;
9
10#[derive(Debug, Clone)]
12pub struct SecurityConfigFromSchema {
13 pub audit_logging: AuditLoggingSettings,
15 pub error_sanitization: ErrorSanitizationSettings,
17 pub rate_limiting: RateLimitingSettings,
19 pub state_encryption: StateEncryptionSettings,
21}
22
23#[derive(Debug, Clone)]
25pub struct AuditLoggingSettings {
26 pub enabled: bool,
28 pub log_level: String,
30 pub include_sensitive_data: bool,
33 pub async_logging: bool,
36 pub buffer_size: u32,
38 pub flush_interval_secs: u32,
40}
41
42#[derive(Debug, Clone)]
44pub struct ErrorSanitizationSettings {
45 pub enabled: bool,
48 pub generic_messages: bool,
50 pub internal_logging: bool,
52 pub leak_sensitive_details: bool,
56 pub user_facing_format: String,
58}
59
60#[derive(Debug, Clone)]
62pub struct RateLimitingSettings {
63 pub enabled: bool,
65 pub auth_start_max_requests: u32,
67 pub auth_start_window_secs: u64,
69 pub auth_callback_max_requests: u32,
71 pub auth_callback_window_secs: u64,
73 pub auth_refresh_max_requests: u32,
75 pub auth_refresh_window_secs: u64,
77 pub auth_logout_max_requests: u32,
79 pub auth_logout_window_secs: u64,
81 pub failed_login_max_requests: u32,
83 pub failed_login_window_secs: u64,
85}
86
87#[derive(Debug, Clone)]
89pub struct StateEncryptionSettings {
90 pub enabled: bool,
92 pub algorithm: String,
94 pub key_rotation_enabled: bool,
96 pub nonce_size: u32,
98 pub key_size: u32,
100}
101
102impl Default for SecurityConfigFromSchema {
103 fn default() -> Self {
104 Self {
105 audit_logging: AuditLoggingSettings {
106 enabled: true,
107 log_level: "info".to_string(),
108 include_sensitive_data: false,
109 async_logging: true,
110 buffer_size: 1000,
111 flush_interval_secs: 5,
112 },
113 error_sanitization: ErrorSanitizationSettings {
114 enabled: true,
115 generic_messages: true,
116 internal_logging: true,
117 leak_sensitive_details: false,
118 user_facing_format: "generic".to_string(),
119 },
120 rate_limiting: RateLimitingSettings {
121 enabled: true,
122 auth_start_max_requests: 100,
123 auth_start_window_secs: 60,
124 auth_callback_max_requests: 50,
125 auth_callback_window_secs: 60,
126 auth_refresh_max_requests: 10,
127 auth_refresh_window_secs: 60,
128 auth_logout_max_requests: 20,
129 auth_logout_window_secs: 60,
130 failed_login_max_requests: 5,
131 failed_login_window_secs: 3600,
132 },
133 state_encryption: StateEncryptionSettings {
134 enabled: true,
135 algorithm: "chacha20-poly1305".to_string(),
136 key_rotation_enabled: false,
137 nonce_size: 12,
138 key_size: 32,
139 },
140 }
141 }
142}
143
144impl SecurityConfigFromSchema {
145 pub fn from_json(value: &JsonValue) -> anyhow::Result<Self> {
151 let mut config = Self::default();
152
153 if let Some(audit) = value.get("auditLogging").and_then(|v| v.as_object()) {
154 config.audit_logging.enabled =
155 audit.get("enabled").and_then(|v| v.as_bool()).unwrap_or(true);
156 config.audit_logging.log_level =
157 audit.get("logLevel").and_then(|v| v.as_str()).unwrap_or("info").to_string();
158 config.audit_logging.include_sensitive_data =
159 audit.get("includeSensitiveData").and_then(|v| v.as_bool()).unwrap_or(false);
160 config.audit_logging.async_logging =
161 audit.get("asyncLogging").and_then(|v| v.as_bool()).unwrap_or(true);
162 #[allow(clippy::cast_possible_truncation)]
163 {
165 config.audit_logging.buffer_size =
166 audit.get("bufferSize").and_then(|v| v.as_u64()).unwrap_or(1000) as u32;
167 config.audit_logging.flush_interval_secs =
168 audit.get("flushIntervalSecs").and_then(|v| v.as_u64()).unwrap_or(5) as u32;
169 }
170 }
171
172 if let Some(error_san) = value.get("errorSanitization").and_then(|v| v.as_object()) {
173 config.error_sanitization.enabled =
174 error_san.get("enabled").and_then(|v| v.as_bool()).unwrap_or(true);
175 config.error_sanitization.generic_messages =
176 error_san.get("genericMessages").and_then(|v| v.as_bool()).unwrap_or(true);
177 config.error_sanitization.internal_logging =
178 error_san.get("internalLogging").and_then(|v| v.as_bool()).unwrap_or(true);
179 config.error_sanitization.leak_sensitive_details =
180 error_san.get("leakSensitiveDetails").and_then(|v| v.as_bool()).unwrap_or(false);
181 config.error_sanitization.user_facing_format = error_san
182 .get("userFacingFormat")
183 .and_then(|v| v.as_str())
184 .unwrap_or("generic")
185 .to_string();
186 }
187
188 if let Some(rate_limit) = value.get("rateLimiting").and_then(|v| v.as_object()) {
189 config.rate_limiting.enabled =
190 rate_limit.get("enabled").and_then(|v| v.as_bool()).unwrap_or(true);
191
192 #[allow(clippy::cast_possible_truncation)]
193 if let Some(auth_start) = rate_limit.get("authStart").and_then(|v| v.as_object()) {
195 config.rate_limiting.auth_start_max_requests =
196 auth_start.get("maxRequests").and_then(|v| v.as_u64()).unwrap_or(100) as u32;
197 config.rate_limiting.auth_start_window_secs =
198 auth_start.get("windowSecs").and_then(|v| v.as_u64()).unwrap_or(60);
199 }
200
201 #[allow(clippy::cast_possible_truncation)]
202 if let Some(auth_callback) = rate_limit.get("authCallback").and_then(|v| v.as_object())
204 {
205 config.rate_limiting.auth_callback_max_requests =
206 auth_callback.get("maxRequests").and_then(|v| v.as_u64()).unwrap_or(50) as u32;
207 config.rate_limiting.auth_callback_window_secs =
208 auth_callback.get("windowSecs").and_then(|v| v.as_u64()).unwrap_or(60);
209 }
210
211 #[allow(clippy::cast_possible_truncation)]
212 if let Some(auth_refresh) = rate_limit.get("authRefresh").and_then(|v| v.as_object()) {
214 config.rate_limiting.auth_refresh_max_requests =
215 auth_refresh.get("maxRequests").and_then(|v| v.as_u64()).unwrap_or(10) as u32;
216 config.rate_limiting.auth_refresh_window_secs =
217 auth_refresh.get("windowSecs").and_then(|v| v.as_u64()).unwrap_or(60);
218 }
219
220 #[allow(clippy::cast_possible_truncation)]
221 if let Some(auth_logout) = rate_limit.get("authLogout").and_then(|v| v.as_object()) {
223 config.rate_limiting.auth_logout_max_requests =
224 auth_logout.get("maxRequests").and_then(|v| v.as_u64()).unwrap_or(20) as u32;
225 config.rate_limiting.auth_logout_window_secs =
226 auth_logout.get("windowSecs").and_then(|v| v.as_u64()).unwrap_or(60);
227 }
228
229 #[allow(clippy::cast_possible_truncation)]
230 if let Some(failed_login) = rate_limit.get("failedLogin").and_then(|v| v.as_object()) {
232 config.rate_limiting.failed_login_max_requests =
233 failed_login.get("maxRequests").and_then(|v| v.as_u64()).unwrap_or(5) as u32;
234 config.rate_limiting.failed_login_window_secs =
235 failed_login.get("windowSecs").and_then(|v| v.as_u64()).unwrap_or(3600);
236 }
237 }
238
239 if let Some(state_enc) = value.get("stateEncryption").and_then(|v| v.as_object()) {
240 config.state_encryption.enabled =
241 state_enc.get("enabled").and_then(|v| v.as_bool()).unwrap_or(true);
242 config.state_encryption.algorithm = state_enc
243 .get("algorithm")
244 .and_then(|v| v.as_str())
245 .unwrap_or("chacha20-poly1305")
246 .to_string();
247 config.state_encryption.key_rotation_enabled =
248 state_enc.get("keyRotationEnabled").and_then(|v| v.as_bool()).unwrap_or(false);
249 #[allow(clippy::cast_possible_truncation)]
250 {
252 config.state_encryption.nonce_size =
253 state_enc.get("nonceSize").and_then(|v| v.as_u64()).unwrap_or(12) as u32;
254 config.state_encryption.key_size =
255 state_enc.get("keySize").and_then(|v| v.as_u64()).unwrap_or(32) as u32;
256 }
257 }
258
259 Ok(config)
260 }
261
262 pub fn apply_env_overrides(&mut self) {
264 if let Ok(level) = env::var("AUDIT_LOG_LEVEL") {
266 self.audit_logging.log_level = level;
267 }
268
269 if let Ok(val) = env::var("RATE_LIMIT_AUTH_START") {
271 if let Ok(n) = val.parse() {
272 self.rate_limiting.auth_start_max_requests = n;
273 }
274 }
275 if let Ok(val) = env::var("RATE_LIMIT_AUTH_CALLBACK") {
276 if let Ok(n) = val.parse() {
277 self.rate_limiting.auth_callback_max_requests = n;
278 }
279 }
280 if let Ok(val) = env::var("RATE_LIMIT_AUTH_REFRESH") {
281 if let Ok(n) = val.parse() {
282 self.rate_limiting.auth_refresh_max_requests = n;
283 }
284 }
285 if let Ok(val) = env::var("RATE_LIMIT_AUTH_LOGOUT") {
286 if let Ok(n) = val.parse() {
287 self.rate_limiting.auth_logout_max_requests = n;
288 }
289 }
290 if let Ok(val) = env::var("RATE_LIMIT_FAILED_LOGIN") {
291 if let Ok(n) = val.parse() {
292 self.rate_limiting.failed_login_max_requests = n;
293 }
294 }
295 }
296}
297
298#[cfg(test)]
299mod tests {
300 #[allow(clippy::wildcard_imports)]
301 use super::*;
303
304 #[test]
305 fn test_default_config() {
306 let config = SecurityConfigFromSchema::default();
307 assert!(config.audit_logging.enabled);
308 assert!(config.error_sanitization.enabled);
309 assert!(config.rate_limiting.enabled);
310 assert!(config.state_encryption.enabled);
311 }
312
313 #[test]
314 fn test_parse_from_json() {
315 let json = serde_json::json!({
316 "auditLogging": {
317 "enabled": true,
318 "logLevel": "debug",
319 "includeSensitiveData": false
320 },
321 "rateLimiting": {
322 "enabled": true,
323 "authStart": {
324 "maxRequests": 200,
325 "windowSecs": 60
326 }
327 }
328 });
329
330 let config = SecurityConfigFromSchema::from_json(&json).expect("Failed to parse");
331 assert_eq!(config.audit_logging.log_level, "debug");
332 assert_eq!(config.rate_limiting.auth_start_max_requests, 200);
333 }
334
335 #[test]
336 fn test_apply_env_overrides() {
337 let mut config = SecurityConfigFromSchema::default();
340 config.apply_env_overrides();
341 }
343}