1use anyhow::Result;
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Deserialize, Serialize)]
8#[serde(default)]
9pub struct AuditLoggingConfig {
10 pub enabled: bool,
12 pub log_level: String,
14 pub include_sensitive_data: bool,
16 pub async_logging: bool,
18 pub buffer_size: u32,
20 pub flush_interval_secs: u32,
22}
23
24impl Default for AuditLoggingConfig {
25 fn default() -> Self {
26 Self {
27 enabled: true,
28 log_level: "info".to_string(),
29 include_sensitive_data: false,
30 async_logging: true,
31 buffer_size: 1000,
32 flush_interval_secs: 5,
33 }
34 }
35}
36
37impl AuditLoggingConfig {
38 pub fn to_json(&self) -> serde_json::Value {
40 serde_json::json!({
41 "enabled": self.enabled,
42 "logLevel": self.log_level,
43 "includeSensitiveData": self.include_sensitive_data,
44 "asyncLogging": self.async_logging,
45 "bufferSize": self.buffer_size,
46 "flushIntervalSecs": self.flush_interval_secs,
47 })
48 }
49}
50
51#[derive(Debug, Clone, Deserialize, Serialize)]
53#[serde(default)]
54pub struct ErrorSanitizationConfig {
55 pub enabled: bool,
57 pub generic_messages: bool,
59 pub internal_logging: bool,
61 pub leak_sensitive_details: bool,
63 pub user_facing_format: String,
65}
66
67impl Default for ErrorSanitizationConfig {
68 fn default() -> Self {
69 Self {
70 enabled: true,
71 generic_messages: true,
72 internal_logging: true,
73 leak_sensitive_details: false,
74 user_facing_format: "generic".to_string(),
75 }
76 }
77}
78
79impl ErrorSanitizationConfig {
80 pub fn validate(&self) -> Result<()> {
82 if self.leak_sensitive_details {
83 anyhow::bail!(
84 "leak_sensitive_details=true is a security risk! Never enable in production."
85 );
86 }
87 Ok(())
88 }
89
90 pub fn to_json(&self) -> serde_json::Value {
92 serde_json::json!({
93 "enabled": self.enabled,
94 "genericMessages": self.generic_messages,
95 "internalLogging": self.internal_logging,
96 "leakSensitiveDetails": self.leak_sensitive_details,
97 "userFacingFormat": self.user_facing_format,
98 })
99 }
100}
101
102#[allow(dead_code)]
107#[derive(Debug, Clone, Deserialize, Serialize)]
108pub struct RateLimitingPerEndpoint {
109 pub max_requests: u32,
111 pub window_secs: u64,
113}
114
115#[allow(dead_code)]
116impl RateLimitingPerEndpoint {
117 pub fn to_json(&self) -> serde_json::Value {
119 serde_json::json!({
120 "maxRequests": self.max_requests,
121 "windowSecs": self.window_secs,
122 })
123 }
124}
125
126#[derive(Debug, Clone, Deserialize, Serialize)]
128#[serde(default)]
129pub struct RateLimitConfig {
130 pub enabled: bool,
132
133 pub auth_start_max_requests: u32,
135 pub auth_start_window_secs: u64,
137
138 pub auth_callback_max_requests: u32,
140 pub auth_callback_window_secs: u64,
142
143 pub auth_refresh_max_requests: u32,
145 pub auth_refresh_window_secs: u64,
147
148 pub auth_logout_max_requests: u32,
150 pub auth_logout_window_secs: u64,
152
153 pub failed_login_max_requests: u32,
155 pub failed_login_window_secs: u64,
157}
158
159impl Default for RateLimitConfig {
160 fn default() -> Self {
161 Self {
162 enabled: true,
163 auth_start_max_requests: 100,
164 auth_start_window_secs: 60,
165 auth_callback_max_requests: 50,
166 auth_callback_window_secs: 60,
167 auth_refresh_max_requests: 10,
168 auth_refresh_window_secs: 60,
169 auth_logout_max_requests: 20,
170 auth_logout_window_secs: 60,
171 failed_login_max_requests: 5,
172 failed_login_window_secs: 3600,
173 }
174 }
175}
176
177impl RateLimitConfig {
178 pub fn validate(&self) -> Result<()> {
180 for (name, window) in &[
181 ("auth_start_window_secs", self.auth_start_window_secs),
182 ("auth_callback_window_secs", self.auth_callback_window_secs),
183 ("auth_refresh_window_secs", self.auth_refresh_window_secs),
184 ("auth_logout_window_secs", self.auth_logout_window_secs),
185 ("failed_login_window_secs", self.failed_login_window_secs),
186 ] {
187 if *window == 0 {
188 anyhow::bail!("{name} must be positive");
189 }
190 }
191 Ok(())
192 }
193
194 pub fn to_json(&self) -> serde_json::Value {
196 serde_json::json!({
197 "enabled": self.enabled,
198 "authStart": {
199 "maxRequests": self.auth_start_max_requests,
200 "windowSecs": self.auth_start_window_secs,
201 },
202 "authCallback": {
203 "maxRequests": self.auth_callback_max_requests,
204 "windowSecs": self.auth_callback_window_secs,
205 },
206 "authRefresh": {
207 "maxRequests": self.auth_refresh_max_requests,
208 "windowSecs": self.auth_refresh_window_secs,
209 },
210 "authLogout": {
211 "maxRequests": self.auth_logout_max_requests,
212 "windowSecs": self.auth_logout_window_secs,
213 },
214 "failedLogin": {
215 "maxRequests": self.failed_login_max_requests,
216 "windowSecs": self.failed_login_window_secs,
217 },
218 })
219 }
220}
221
222#[derive(Debug, Clone, Deserialize, Serialize)]
224#[serde(default)]
225pub struct StateEncryptionConfig {
226 pub enabled: bool,
228 pub algorithm: String,
230 pub key_rotation_enabled: bool,
232 pub nonce_size: u32,
234 pub key_size: u32,
236}
237
238impl Default for StateEncryptionConfig {
239 fn default() -> Self {
240 Self {
241 enabled: true,
242 algorithm: "chacha20-poly1305".to_string(),
243 key_rotation_enabled: false,
244 nonce_size: 12,
245 key_size: 32,
246 }
247 }
248}
249
250impl StateEncryptionConfig {
251 pub fn validate(&self) -> Result<()> {
253 if ![16, 24, 32].contains(&self.key_size) {
254 anyhow::bail!("key_size must be 16, 24, or 32 bytes");
255 }
256 if self.nonce_size != 12 {
257 anyhow::bail!("nonce_size must be 12 bytes (96-bit)");
258 }
259 Ok(())
260 }
261
262 pub fn to_json(&self) -> serde_json::Value {
264 serde_json::json!({
265 "enabled": self.enabled,
266 "algorithm": self.algorithm,
267 "keyRotationEnabled": self.key_rotation_enabled,
268 "nonceSize": self.nonce_size,
269 "keySize": self.key_size,
270 })
271 }
272}
273
274#[derive(Debug, Clone, Deserialize, Serialize)]
276#[serde(default)]
277pub struct ConstantTimeConfig {
278 pub enabled: bool,
280 pub apply_to_jwt: bool,
282 pub apply_to_session_tokens: bool,
284 pub apply_to_csrf_tokens: bool,
286 pub apply_to_refresh_tokens: bool,
288}
289
290impl Default for ConstantTimeConfig {
291 fn default() -> Self {
292 Self {
293 enabled: true,
294 apply_to_jwt: true,
295 apply_to_session_tokens: true,
296 apply_to_csrf_tokens: true,
297 apply_to_refresh_tokens: true,
298 }
299 }
300}
301
302impl ConstantTimeConfig {
303 pub fn to_json(&self) -> serde_json::Value {
305 serde_json::json!({
306 "enabled": self.enabled,
307 "applyToJwt": self.apply_to_jwt,
308 "applyToSessionTokens": self.apply_to_session_tokens,
309 "applytoCsrfTokens": self.apply_to_csrf_tokens,
310 "applyToRefreshTokens": self.apply_to_refresh_tokens,
311 })
312 }
313}
314
315#[derive(Debug, Clone, Deserialize, Serialize)]
317pub struct RoleDefinitionConfig {
318 pub name: String,
320 #[serde(skip_serializing_if = "Option::is_none")]
322 pub description: Option<String>,
323 pub scopes: Vec<String>,
325}
326
327impl RoleDefinitionConfig {
328 #[allow(dead_code)]
331 pub fn to_core_role_definition(&self) -> fraiseql_core::schema::RoleDefinition {
332 fraiseql_core::schema::RoleDefinition {
333 name: self.name.clone(),
334 description: self.description.clone(),
335 scopes: self.scopes.clone(),
336 }
337 }
338}
339
340#[derive(Debug, Clone, Default, Deserialize, Serialize)]
342#[serde(default)]
343pub struct SecurityConfig {
344 #[serde(rename = "audit_logging")]
346 pub audit_logging: AuditLoggingConfig,
347 #[serde(rename = "error_sanitization")]
349 pub error_sanitization: ErrorSanitizationConfig,
350 #[serde(rename = "rate_limiting")]
352 pub rate_limiting: RateLimitConfig,
353 #[serde(rename = "state_encryption")]
355 pub state_encryption: StateEncryptionConfig,
356 #[serde(rename = "constant_time")]
358 pub constant_time: ConstantTimeConfig,
359 #[serde(default, skip_serializing_if = "Vec::is_empty")]
361 pub role_definitions: Vec<RoleDefinitionConfig>,
362 #[serde(skip_serializing_if = "Option::is_none")]
364 pub default_role: Option<String>,
365}
366
367impl SecurityConfig {
368 pub fn validate(&self) -> Result<()> {
370 self.error_sanitization.validate()?;
371 self.rate_limiting.validate()?;
372 self.state_encryption.validate()?;
373
374 for role in &self.role_definitions {
376 if role.name.is_empty() {
377 anyhow::bail!("Role name cannot be empty");
378 }
379 if role.scopes.is_empty() {
380 anyhow::bail!("Role '{}' must have at least one scope", role.name);
381 }
382 }
383
384 Ok(())
385 }
386
387 #[allow(dead_code)]
390 pub fn find_role(&self, name: &str) -> Option<&RoleDefinitionConfig> {
391 self.role_definitions.iter().find(|r| r.name == name)
392 }
393
394 #[allow(dead_code)]
397 pub fn get_role_scopes(&self, role_name: &str) -> Vec<String> {
398 self.find_role(role_name).map(|role| role.scopes.clone()).unwrap_or_default()
399 }
400
401 pub fn to_json(&self) -> serde_json::Value {
403 let mut json = serde_json::json!({
404 "auditLogging": self.audit_logging.to_json(),
405 "errorSanitization": self.error_sanitization.to_json(),
406 "rateLimiting": self.rate_limiting.to_json(),
407 "stateEncryption": self.state_encryption.to_json(),
408 "constantTime": self.constant_time.to_json(),
409 });
410
411 if !self.role_definitions.is_empty() {
413 json["roleDefinitions"] = serde_json::to_value(
414 self.role_definitions
415 .iter()
416 .map(|r| {
417 serde_json::json!({
418 "name": r.name,
419 "description": r.description,
420 "scopes": r.scopes,
421 })
422 })
423 .collect::<Vec<_>>(),
424 )
425 .unwrap_or_default();
426 }
427
428 if let Some(default_role) = &self.default_role {
430 json["defaultRole"] = serde_json::json!(default_role);
431 }
432
433 json
434 }
435}
436
437#[cfg(test)]
438mod tests {
439 use super::*;
440
441 #[test]
442 fn test_default_security_config() {
443 let config = SecurityConfig::default();
444 assert!(config.audit_logging.enabled);
445 assert!(config.error_sanitization.enabled);
446 assert!(config.rate_limiting.enabled);
447 assert!(config.state_encryption.enabled);
448 assert!(config.constant_time.enabled);
449 }
450
451 #[test]
452 fn test_error_sanitization_validation() {
453 let mut config = ErrorSanitizationConfig::default();
454 assert!(config.validate().is_ok());
455
456 config.leak_sensitive_details = true;
457 assert!(config.validate().is_err());
458 }
459
460 #[test]
461 fn test_rate_limiting_validation() {
462 let mut config = RateLimitConfig::default();
463 assert!(config.validate().is_ok());
464
465 config.auth_start_window_secs = 0;
466 assert!(config.validate().is_err());
467 }
468
469 #[test]
470 fn test_state_encryption_validation() {
471 let mut config = StateEncryptionConfig::default();
472 assert!(config.validate().is_ok());
473
474 config.key_size = 20;
475 assert!(config.validate().is_err());
476
477 config.key_size = 32;
478 config.nonce_size = 16;
479 assert!(config.validate().is_err());
480 }
481
482 #[test]
483 fn test_security_config_serialization() {
484 let config = SecurityConfig::default();
485 let json = config.to_json();
486 assert!(json["auditLogging"]["enabled"].is_boolean());
487 assert!(json["rateLimiting"]["authStart"]["maxRequests"].is_number());
488 assert!(json["stateEncryption"]["algorithm"].is_string());
489 }
490}