Skip to main content

fraiseql_auth/
security_init.rs

1//! Security system initialization from compiled schema configuration
2//!
3//! Loads security configuration from schema.compiled.json and initializes
4//! all security subsystems with proper environment variable overrides.
5
6use serde_json::Value as JsonValue;
7use tracing::{debug, info, warn};
8
9use super::security_config::SecurityConfigFromSchema;
10use crate::{AuthError, error::Result};
11
12/// Maximum size for schema JSON to prevent DOS attacks
13/// 10 MB should be more than sufficient for any realistic schema
14const MAX_SCHEMA_JSON_SIZE: usize = 10 * 1024 * 1024; // 10 MB
15
16/// Maximum size for security configuration section
17/// Security config should be < 100 KB in realistic deployments
18const MAX_SECURITY_CONFIG_SIZE: usize = 100 * 1024; // 100 KB
19
20/// Initialize security configuration from compiled schema JSON string
21///
22/// Loads security settings from the schema.compiled.json and applies
23/// environment variable overrides. This function should be called during
24/// server startup after loading the compiled schema.
25///
26/// # SECURITY
27/// - Validates schema JSON size to prevent DOS attacks
28/// - Rejects JSON > 10 MB
29/// - Rejects security config section > 100 KB
30///
31/// # Arguments
32///
33/// * `schema_json_str` - The compiled schema as a JSON string
34///
35/// # Returns
36///
37/// Returns a configured `SecurityConfigFromSchema` with environment overrides applied
38///
39/// # Errors
40///
41/// Returns error if:
42/// - JSON size exceeds limits
43/// - JSON parsing fails
44/// - Security configuration section is invalid or missing required fields
45///
46/// # Example
47///
48/// ```no_run
49/// // Requires: compiled schema JSON string loaded from disk or a schema loader.
50/// # fn example() -> fraiseql_auth::error::Result<()> {
51/// use fraiseql_auth::security_init::init_security_config;
52/// let json_str = r#"{"security":{"auditLogging":{"enabled":true}}}"#;
53/// let security_config = init_security_config(json_str)?;
54/// # Ok(())
55/// # }
56/// ```
57pub fn init_security_config(schema_json_str: &str) -> Result<SecurityConfigFromSchema> {
58    debug!("Parsing schema JSON for security configuration");
59
60    // SECURITY: Check JSON size to prevent DOS from oversized payloads
61    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    // Parse JSON string to JsonValue
73    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
83/// Initialize security configuration from compiled schema JSON value
84///
85/// Internal function that works with parsed JsonValue. Use `init_security_config` for strings.
86///
87/// # SECURITY
88/// - Validates security configuration size to prevent DOS attacks
89/// - Rejects security config > 100 KB
90///
91/// # Arguments
92///
93/// * `schema_json` - The compiled schema as a JsonValue
94///
95/// # Returns
96///
97/// Returns a configured `SecurityConfigFromSchema` with environment overrides applied
98///
99/// # Errors
100///
101/// Returns error if security configuration section is invalid or missing required fields
102fn init_security_config_from_value(schema_json: &JsonValue) -> Result<SecurityConfigFromSchema> {
103    debug!("Initializing security configuration from schema");
104
105    // Extract security section from schema
106    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    // SECURITY: Check security config size to prevent DOS
114    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    // Parse security configuration from schema
130    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    // Apply environment variable overrides
140    config.apply_env_overrides();
141    debug!("Security environment variable overrides applied");
142
143    Ok(config)
144}
145
146/// Initialize security configuration with default values if schema doesn't have security config
147///
148/// This is useful for backward compatibility when the schema doesn't include
149/// a security section. It loads defaults and applies environment overrides.
150///
151/// # Returns
152///
153/// A default `SecurityConfigFromSchema` with environment overrides applied
154pub 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
162/// Log the active security configuration (sanitized for safe logging)
163///
164/// Outputs current security settings to logs, excluding sensitive values
165/// like encryption keys.
166///
167/// # Arguments
168///
169/// * `config` - The security configuration to log
170pub 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
205/// Verify security configuration consistency.
206///
207/// Performs validation checks to ensure the loaded security configuration
208/// doesn't have dangerous or conflicting settings.
209///
210/// # Arguments
211///
212/// * `config` - The security configuration to validate
213///
214/// # Returns
215///
216/// Returns `Ok(())` if configuration is valid.
217///
218/// # Errors
219///
220/// Returns [`AuthError::ConfigError`] if `leak_sensitive_details` is `true`,
221/// `auth_start_max_requests` is zero when rate limiting is enabled, or
222/// `auth_start_window_secs` is zero when rate limiting is enabled.
223pub fn validate_security_config(config: &SecurityConfigFromSchema) -> Result<()> {
224    // Check if sensitive data leaking is disabled (security requirement)
225    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    // Check rate limits are reasonable
233    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    // Check state encryption key size if enabled
247    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)] // Reason: test code, panics are acceptable
259#[cfg(test)]
260mod tests {
261    #[allow(clippy::wildcard_imports)]
262    // Reason: test module — wildcard keeps test boilerplate minimal
263    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        // Just verify the function doesn't panic
296        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        // Should return error because security section is required
349        assert!(
350            matches!(config, Err(AuthError::ConfigError { .. })),
351            "expected ConfigError when security section is missing, got: {config:?}"
352        );
353    }
354}