fraiseql_auth/
security_init.rs1use serde_json::Value as JsonValue;
7use tracing::{debug, info, warn};
8
9use super::security_config::SecurityConfigFromSchema;
10use crate::{AuthError, error::Result};
11
12const MAX_SCHEMA_JSON_SIZE: usize = 10 * 1024 * 1024; const MAX_SECURITY_CONFIG_SIZE: usize = 100 * 1024; pub fn init_security_config(schema_json_str: &str) -> Result<SecurityConfigFromSchema> {
58 debug!("Parsing schema JSON for security configuration");
59
60 if schema_json_str.len() > MAX_SCHEMA_JSON_SIZE {
62 warn!(
63 "Schema JSON exceeds maximum size: {} > {} bytes",
64 schema_json_str.len(),
65 MAX_SCHEMA_JSON_SIZE
66 );
67 return Err(AuthError::ConfigError {
68 message: format!("Schema JSON exceeds maximum size of {} bytes", MAX_SCHEMA_JSON_SIZE),
69 });
70 }
71
72 let schema_json: JsonValue = serde_json::from_str(schema_json_str).map_err(|e| {
74 warn!("Failed to parse schema JSON: {e}");
75 AuthError::ConfigError {
76 message: format!("Invalid schema JSON: {e}"),
77 }
78 })?;
79
80 init_security_config_from_value(&schema_json)
81}
82
83fn init_security_config_from_value(schema_json: &JsonValue) -> Result<SecurityConfigFromSchema> {
103 debug!("Initializing security configuration from schema");
104
105 let security_value = schema_json.get("security").ok_or_else(|| {
107 warn!("No security configuration found in schema, using defaults");
108 AuthError::ConfigError {
109 message: "Missing security configuration in schema".to_string(),
110 }
111 })?;
112
113 let security_json_str = security_value.to_string();
115 if security_json_str.len() > MAX_SECURITY_CONFIG_SIZE {
116 warn!(
117 "Security configuration exceeds maximum size: {} > {} bytes",
118 security_json_str.len(),
119 MAX_SECURITY_CONFIG_SIZE
120 );
121 return Err(AuthError::ConfigError {
122 message: format!(
123 "Security configuration exceeds maximum size of {} bytes",
124 MAX_SECURITY_CONFIG_SIZE
125 ),
126 });
127 }
128
129 let mut config = SecurityConfigFromSchema::from_json(security_value).map_err(|e| {
131 warn!("Failed to parse security configuration: {e}");
132 AuthError::ConfigError {
133 message: format!("Invalid security configuration: {e}"),
134 }
135 })?;
136
137 info!("Security configuration loaded from schema");
138
139 config.apply_env_overrides();
141 debug!("Security environment variable overrides applied");
142
143 Ok(config)
144}
145
146pub fn init_default_security_config() -> SecurityConfigFromSchema {
155 info!("Initializing default security configuration");
156 let mut config = SecurityConfigFromSchema::default();
157 config.apply_env_overrides();
158 debug!("Default security configuration applied with environment overrides");
159 config
160}
161
162pub fn log_security_config(config: &SecurityConfigFromSchema) {
171 info!(
172 audit_logging_enabled = config.audit_logging.enabled,
173 audit_log_level = %config.audit_logging.log_level,
174 audit_async_logging = config.audit_logging.async_logging,
175 audit_buffer_size = config.audit_logging.buffer_size,
176 "Audit logging configuration"
177 );
178
179 info!(
180 error_sanitization_enabled = config.error_sanitization.enabled,
181 error_generic_messages = config.error_sanitization.generic_messages,
182 error_internal_logging = config.error_sanitization.internal_logging,
183 error_leak_sensitive = config.error_sanitization.leak_sensitive_details,
184 "Error sanitization configuration"
185 );
186
187 info!(
188 rate_limiting_enabled = config.rate_limiting.enabled,
189 auth_start_max = config.rate_limiting.auth_start_max_requests,
190 auth_callback_max = config.rate_limiting.auth_callback_max_requests,
191 auth_refresh_max = config.rate_limiting.auth_refresh_max_requests,
192 failed_login_max = config.rate_limiting.failed_login_max_requests,
193 "Rate limiting configuration"
194 );
195
196 info!(
197 state_encryption_enabled = config.state_encryption.enabled,
198 state_encryption_algorithm = %config.state_encryption.algorithm,
199 state_encryption_nonce_size = config.state_encryption.nonce_size,
200 state_encryption_key_size = config.state_encryption.key_size,
201 "State encryption configuration"
202 );
203}
204
205pub fn validate_security_config(config: &SecurityConfigFromSchema) -> Result<()> {
224 if config.error_sanitization.leak_sensitive_details {
226 warn!("SECURITY WARNING: leak_sensitive_details is enabled! This is a security risk.");
227 return Err(AuthError::ConfigError {
228 message: "leak_sensitive_details must be false in production".to_string(),
229 });
230 }
231
232 if config.rate_limiting.enabled {
234 if config.rate_limiting.auth_start_max_requests == 0 {
235 return Err(AuthError::ConfigError {
236 message: "auth_start_max_requests must be greater than 0".to_string(),
237 });
238 }
239 if config.rate_limiting.auth_start_window_secs == 0 {
240 return Err(AuthError::ConfigError {
241 message: "auth_start_window_secs must be greater than 0".to_string(),
242 });
243 }
244 }
245
246 if config.state_encryption.enabled && config.state_encryption.key_size != 32 {
248 warn!(
249 "State encryption key size is {} bytes, expected 32 bytes for ChaCha20-Poly1305",
250 config.state_encryption.key_size
251 );
252 }
253
254 info!("Security configuration validation passed");
255 Ok(())
256}
257
258#[allow(clippy::unwrap_used)] #[cfg(test)]
260mod tests {
261 #[allow(clippy::wildcard_imports)]
262 use super::*;
264
265 #[test]
266 fn test_init_default_security_config() {
267 let config = init_default_security_config();
268 assert!(config.audit_logging.enabled);
269 assert!(config.error_sanitization.enabled);
270 assert!(config.rate_limiting.enabled);
271 assert!(config.state_encryption.enabled);
272 }
273
274 #[test]
275 fn test_validate_security_config_success() {
276 let config = SecurityConfigFromSchema::default();
277 validate_security_config(&config)
278 .unwrap_or_else(|e| panic!("expected Ok for default security config: {e}"));
279 }
280
281 #[test]
282 fn test_validate_security_config_leak_sensitive_fails() {
283 let mut config = SecurityConfigFromSchema::default();
284 config.error_sanitization.leak_sensitive_details = true;
285 let result = validate_security_config(&config);
286 assert!(
287 matches!(result, Err(AuthError::ConfigError { .. })),
288 "expected ConfigError when leak_sensitive_details=true, got: {result:?}"
289 );
290 }
291
292 #[test]
293 fn test_log_security_config() {
294 let config = SecurityConfigFromSchema::default();
295 log_security_config(&config);
297 }
298
299 #[test]
300 fn test_init_security_config_from_json() {
301 let json = serde_json::json!({
302 "security": {
303 "auditLogging": {
304 "enabled": true,
305 "logLevel": "debug"
306 },
307 "rateLimiting": {
308 "enabled": true,
309 "authStart": {
310 "maxRequests": 200,
311 "windowSecs": 60
312 }
313 }
314 }
315 });
316
317 let cfg = init_security_config_from_value(&json)
318 .unwrap_or_else(|e| panic!("expected Ok for valid security JSON: {e}"));
319 assert_eq!(cfg.audit_logging.log_level, "debug");
320 assert_eq!(cfg.rate_limiting.auth_start_max_requests, 200);
321 }
322
323 #[test]
324 fn test_init_security_config_from_string() {
325 let json_str = r#"{
326 "security": {
327 "auditLogging": {
328 "enabled": true,
329 "logLevel": "info"
330 },
331 "errorSanitization": {
332 "enabled": true,
333 "genericMessages": true
334 }
335 }
336 }"#;
337
338 let cfg = init_security_config(json_str)
339 .unwrap_or_else(|e| panic!("expected Ok for valid security JSON string: {e}"));
340 assert_eq!(cfg.audit_logging.log_level, "info");
341 assert!(cfg.error_sanitization.generic_messages);
342 }
343
344 #[test]
345 fn test_init_security_config_missing_section() {
346 let json = serde_json::json!({});
347 let config = init_security_config_from_value(&json);
348 assert!(
350 matches!(config, Err(AuthError::ConfigError { .. })),
351 "expected ConfigError when security section is missing, got: {config:?}"
352 );
353 }
354}