1use anyhow::Result;
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Deserialize, Serialize)]
8#[serde(default, deny_unknown_fields)]
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, deny_unknown_fields)]
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<()> {
87 if self.leak_sensitive_details {
88 anyhow::bail!(
89 "leak_sensitive_details=true is a security risk! Never enable in production."
90 );
91 }
92 Ok(())
93 }
94
95 pub fn to_json(&self) -> serde_json::Value {
97 serde_json::json!({
98 "enabled": self.enabled,
99 "genericMessages": self.generic_messages,
100 "internalLogging": self.internal_logging,
101 "leakSensitiveDetails": self.leak_sensitive_details,
102 "userFacingFormat": self.user_facing_format,
103 })
104 }
105}
106
107#[derive(Debug, Clone, Deserialize, Serialize)]
109#[serde(default, deny_unknown_fields)]
110pub struct RateLimitConfig {
111 pub enabled: bool,
113
114 pub auth_start_max_requests: u32,
116 pub auth_start_window_secs: u64,
118
119 pub auth_callback_max_requests: u32,
121 pub auth_callback_window_secs: u64,
123
124 pub auth_refresh_max_requests: u32,
126 pub auth_refresh_window_secs: u64,
128
129 pub auth_logout_max_requests: u32,
131 pub auth_logout_window_secs: u64,
133
134 pub failed_login_max_requests: u32,
136 pub failed_login_window_secs: u64,
138}
139
140impl Default for RateLimitConfig {
141 fn default() -> Self {
142 Self {
143 enabled: true,
144 auth_start_max_requests: 100,
145 auth_start_window_secs: 60,
146 auth_callback_max_requests: 50,
147 auth_callback_window_secs: 60,
148 auth_refresh_max_requests: 10,
149 auth_refresh_window_secs: 60,
150 auth_logout_max_requests: 20,
151 auth_logout_window_secs: 60,
152 failed_login_max_requests: 5,
153 failed_login_window_secs: 3600,
154 }
155 }
156}
157
158impl RateLimitConfig {
159 pub fn validate(&self) -> Result<()> {
166 for (name, window) in &[
167 ("auth_start_window_secs", self.auth_start_window_secs),
168 ("auth_callback_window_secs", self.auth_callback_window_secs),
169 ("auth_refresh_window_secs", self.auth_refresh_window_secs),
170 ("auth_logout_window_secs", self.auth_logout_window_secs),
171 ("failed_login_window_secs", self.failed_login_window_secs),
172 ] {
173 if *window == 0 {
174 anyhow::bail!("{name} must be positive");
175 }
176 }
177 for (name, max_req) in &[
178 ("auth_start_max_requests", self.auth_start_max_requests),
179 ("auth_callback_max_requests", self.auth_callback_max_requests),
180 ("auth_refresh_max_requests", self.auth_refresh_max_requests),
181 ("auth_logout_max_requests", self.auth_logout_max_requests),
182 ("failed_login_max_requests", self.failed_login_max_requests),
183 ] {
184 if *max_req == 0 {
185 anyhow::bail!(
186 "{name} must be at least 1; \
187 setting it to 0 blocks all requests permanently"
188 );
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, deny_unknown_fields)]
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
250const SUPPORTED_ALGORITHMS: &[&str] = &["chacha20-poly1305", "aes-256-gcm"];
252
253impl StateEncryptionConfig {
254 pub fn validate(&self) -> Result<()> {
261 if !SUPPORTED_ALGORITHMS.contains(&self.algorithm.as_str()) {
262 anyhow::bail!(
263 "algorithm {:?} is not supported; must be one of: {}",
264 self.algorithm,
265 SUPPORTED_ALGORITHMS.join(", ")
266 );
267 }
268 if ![16, 24, 32].contains(&self.key_size) {
269 anyhow::bail!("key_size must be 16, 24, or 32 bytes");
270 }
271 if self.nonce_size != 12 {
272 anyhow::bail!("nonce_size must be 12 bytes (96-bit)");
273 }
274 Ok(())
275 }
276
277 pub fn to_json(&self) -> serde_json::Value {
279 serde_json::json!({
280 "enabled": self.enabled,
281 "algorithm": self.algorithm,
282 "keyRotationEnabled": self.key_rotation_enabled,
283 "nonceSize": self.nonce_size,
284 "keySize": self.key_size,
285 })
286 }
287}
288
289#[derive(Debug, Clone, Deserialize, Serialize)]
291#[serde(default, deny_unknown_fields)]
292pub struct ConstantTimeConfig {
293 pub enabled: bool,
295 pub apply_to_jwt: bool,
297 pub apply_to_session_tokens: bool,
299 pub apply_to_csrf_tokens: bool,
301 pub apply_to_refresh_tokens: bool,
303}
304
305impl Default for ConstantTimeConfig {
306 fn default() -> Self {
307 Self {
308 enabled: true,
309 apply_to_jwt: true,
310 apply_to_session_tokens: true,
311 apply_to_csrf_tokens: true,
312 apply_to_refresh_tokens: true,
313 }
314 }
315}
316
317impl ConstantTimeConfig {
318 pub fn to_json(&self) -> serde_json::Value {
320 serde_json::json!({
321 "enabled": self.enabled,
322 "applyToJwt": self.apply_to_jwt,
323 "applyToSessionTokens": self.apply_to_session_tokens,
324 "applytoCsrfTokens": self.apply_to_csrf_tokens,
325 "applyToRefreshTokens": self.apply_to_refresh_tokens,
326 })
327 }
328}
329
330#[derive(Debug, Clone, Deserialize, Serialize)]
332#[serde(deny_unknown_fields)]
333pub struct RoleDefinitionConfig {
334 pub name: String,
336 #[serde(skip_serializing_if = "Option::is_none")]
338 pub description: Option<String>,
339 pub scopes: Vec<String>,
341}
342
343#[derive(Debug, Clone, Default, Deserialize, Serialize)]
345#[serde(default, deny_unknown_fields)]
346pub struct SecurityConfig {
347 #[serde(rename = "audit_logging")]
349 pub audit_logging: AuditLoggingConfig,
350 #[serde(rename = "error_sanitization")]
352 pub error_sanitization: ErrorSanitizationConfig,
353 #[serde(rename = "rate_limiting")]
355 pub rate_limiting: RateLimitConfig,
356 #[serde(rename = "state_encryption")]
358 pub state_encryption: StateEncryptionConfig,
359 #[serde(rename = "constant_time")]
361 pub constant_time: ConstantTimeConfig,
362 #[serde(default, skip_serializing_if = "Vec::is_empty")]
364 pub role_definitions: Vec<RoleDefinitionConfig>,
365 #[serde(skip_serializing_if = "Option::is_none")]
367 pub default_role: Option<String>,
368}
369
370impl SecurityConfig {
371 pub fn validate(&self) -> Result<()> {
378 self.error_sanitization.validate()?;
379 self.rate_limiting.validate()?;
380 self.state_encryption.validate()?;
381
382 for role in &self.role_definitions {
384 if role.name.is_empty() {
385 anyhow::bail!("Role name cannot be empty");
386 }
387 if role.scopes.is_empty() {
388 anyhow::bail!("Role '{}' must have at least one scope", role.name);
389 }
390 }
391
392 Ok(())
393 }
394
395 pub fn to_json(&self) -> serde_json::Value {
397 let mut json = serde_json::json!({
398 "auditLogging": self.audit_logging.to_json(),
399 "errorSanitization": self.error_sanitization.to_json(),
400 "rateLimiting": self.rate_limiting.to_json(),
401 "stateEncryption": self.state_encryption.to_json(),
402 "constantTime": self.constant_time.to_json(),
403 });
404
405 if !self.role_definitions.is_empty() {
407 json["roleDefinitions"] = serde_json::to_value(
408 self.role_definitions
409 .iter()
410 .map(|r| {
411 serde_json::json!({
412 "name": r.name,
413 "description": r.description,
414 "scopes": r.scopes,
415 })
416 })
417 .collect::<Vec<_>>(),
418 )
419 .unwrap_or_default();
420 }
421
422 if let Some(default_role) = &self.default_role {
424 json["defaultRole"] = serde_json::json!(default_role);
425 }
426
427 json
428 }
429}
430
431#[cfg(test)]
432mod tests {
433 #![allow(clippy::unwrap_used)] #![allow(clippy::field_reassign_with_default)] use super::*;
437
438 #[test]
439 fn test_default_security_config() {
440 let config = SecurityConfig::default();
441 assert!(config.audit_logging.enabled);
442 assert!(config.error_sanitization.enabled);
443 assert!(config.rate_limiting.enabled);
444 assert!(config.state_encryption.enabled);
445 assert!(config.constant_time.enabled);
446 }
447
448 #[test]
449 fn test_error_sanitization_validation() {
450 let mut config = ErrorSanitizationConfig::default();
451 config
452 .validate()
453 .unwrap_or_else(|e| panic!("expected Ok for default config: {e}"));
454
455 config.leak_sensitive_details = true;
456 assert!(
457 config.validate().is_err(),
458 "expected Err when leak_sensitive_details=true, got Ok"
459 );
460 }
461
462 #[test]
463 fn test_rate_limiting_validation() {
464 let mut config = RateLimitConfig::default();
465 config
466 .validate()
467 .unwrap_or_else(|e| panic!("expected Ok for default config: {e}"));
468
469 config.auth_start_window_secs = 0;
470 assert!(config.validate().is_err(), "expected Err when auth_start_window_secs=0, got Ok");
471 }
472
473 #[test]
474 fn test_rate_limiting_zero_max_requests_rejected() {
475 let mut config = RateLimitConfig::default();
476 config.auth_start_max_requests = 0;
477 let err = config.validate().unwrap_err();
478 assert!(
479 err.to_string().contains("auth_start_max_requests"),
480 "error should name the field: {err}"
481 );
482 assert!(
483 err.to_string().contains("blocks all requests"),
484 "error should explain the impact: {err}"
485 );
486 }
487
488 #[test]
489 fn test_rate_limiting_one_max_requests_accepted() {
490 let mut config = RateLimitConfig::default();
491 config.auth_start_max_requests = 1;
492 config
493 .validate()
494 .unwrap_or_else(|e| panic!("expected Ok for max_requests=1: {e}"));
495 }
496
497 #[test]
498 fn test_rate_limiting_callback_zero_max_requests_rejected() {
499 let mut config = RateLimitConfig::default();
500 config.auth_callback_max_requests = 0;
501 assert!(
502 config.validate().is_err(),
503 "expected Err when auth_callback_max_requests=0, got Ok"
504 );
505 }
506
507 #[test]
508 fn test_state_encryption_validation() {
509 let mut config = StateEncryptionConfig::default();
510 config
511 .validate()
512 .unwrap_or_else(|e| panic!("expected Ok for default config: {e}"));
513
514 config.key_size = 20;
515 assert!(config.validate().is_err(), "expected Err when key_size=20, got Ok");
516
517 config.key_size = 32;
518 config.nonce_size = 16;
519 assert!(config.validate().is_err(), "expected Err when nonce_size=16, got Ok");
520 }
521
522 #[test]
523 fn test_state_encryption_unsupported_algorithm_rejected() {
524 let mut config = StateEncryptionConfig::default();
525 config.algorithm = "rot13".to_string();
526 let err = config.validate().unwrap_err();
527 assert!(err.to_string().contains("rot13"), "error should name the bad algorithm: {err}");
528 assert!(
529 err.to_string().contains("chacha20-poly1305"),
530 "error should list supported algorithms: {err}"
531 );
532 }
533
534 #[test]
535 fn test_state_encryption_aes_256_gcm_accepted() {
536 let mut config = StateEncryptionConfig::default();
537 config.algorithm = "aes-256-gcm".to_string();
538 config.validate().unwrap_or_else(|e| panic!("expected Ok for aes-256-gcm: {e}"));
539 }
540
541 #[test]
542 fn test_state_encryption_chacha20_poly1305_accepted() {
543 let config = StateEncryptionConfig::default();
544 config
546 .validate()
547 .unwrap_or_else(|e| panic!("expected Ok for default chacha20-poly1305: {e}"));
548 }
549
550 #[test]
551 fn test_security_config_serialization() {
552 let config = SecurityConfig::default();
553 let json = config.to_json();
554 assert!(json["auditLogging"]["enabled"].is_boolean());
555 assert!(json["rateLimiting"]["authStart"]["maxRequests"].is_number());
556 assert!(json["stateEncryption"]["algorithm"].is_string());
557 }
558}