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<()> {
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#[derive(Debug, Clone, Deserialize, Serialize)]
104#[serde(default, deny_unknown_fields)]
105pub struct RateLimitConfig {
106 pub enabled: bool,
108
109 pub auth_start_max_requests: u32,
111 pub auth_start_window_secs: u64,
113
114 pub auth_callback_max_requests: u32,
116 pub auth_callback_window_secs: u64,
118
119 pub auth_refresh_max_requests: u32,
121 pub auth_refresh_window_secs: u64,
123
124 pub auth_logout_max_requests: u32,
126 pub auth_logout_window_secs: u64,
128
129 pub failed_login_max_requests: u32,
131 pub failed_login_window_secs: u64,
133}
134
135impl Default for RateLimitConfig {
136 fn default() -> Self {
137 Self {
138 enabled: true,
139 auth_start_max_requests: 100,
140 auth_start_window_secs: 60,
141 auth_callback_max_requests: 50,
142 auth_callback_window_secs: 60,
143 auth_refresh_max_requests: 10,
144 auth_refresh_window_secs: 60,
145 auth_logout_max_requests: 20,
146 auth_logout_window_secs: 60,
147 failed_login_max_requests: 5,
148 failed_login_window_secs: 3600,
149 }
150 }
151}
152
153impl RateLimitConfig {
154 pub fn validate(&self) -> Result<()> {
156 for (name, window) in &[
157 ("auth_start_window_secs", self.auth_start_window_secs),
158 ("auth_callback_window_secs", self.auth_callback_window_secs),
159 ("auth_refresh_window_secs", self.auth_refresh_window_secs),
160 ("auth_logout_window_secs", self.auth_logout_window_secs),
161 ("failed_login_window_secs", self.failed_login_window_secs),
162 ] {
163 if *window == 0 {
164 anyhow::bail!("{name} must be positive");
165 }
166 }
167 Ok(())
168 }
169
170 pub fn to_json(&self) -> serde_json::Value {
172 serde_json::json!({
173 "enabled": self.enabled,
174 "authStart": {
175 "maxRequests": self.auth_start_max_requests,
176 "windowSecs": self.auth_start_window_secs,
177 },
178 "authCallback": {
179 "maxRequests": self.auth_callback_max_requests,
180 "windowSecs": self.auth_callback_window_secs,
181 },
182 "authRefresh": {
183 "maxRequests": self.auth_refresh_max_requests,
184 "windowSecs": self.auth_refresh_window_secs,
185 },
186 "authLogout": {
187 "maxRequests": self.auth_logout_max_requests,
188 "windowSecs": self.auth_logout_window_secs,
189 },
190 "failedLogin": {
191 "maxRequests": self.failed_login_max_requests,
192 "windowSecs": self.failed_login_window_secs,
193 },
194 })
195 }
196}
197
198#[derive(Debug, Clone, Deserialize, Serialize)]
200#[serde(default, deny_unknown_fields)]
201pub struct StateEncryptionConfig {
202 pub enabled: bool,
204 pub algorithm: String,
206 pub key_rotation_enabled: bool,
208 pub nonce_size: u32,
210 pub key_size: u32,
212}
213
214impl Default for StateEncryptionConfig {
215 fn default() -> Self {
216 Self {
217 enabled: true,
218 algorithm: "chacha20-poly1305".to_string(),
219 key_rotation_enabled: false,
220 nonce_size: 12,
221 key_size: 32,
222 }
223 }
224}
225
226impl StateEncryptionConfig {
227 pub fn validate(&self) -> Result<()> {
229 if ![16, 24, 32].contains(&self.key_size) {
230 anyhow::bail!("key_size must be 16, 24, or 32 bytes");
231 }
232 if self.nonce_size != 12 {
233 anyhow::bail!("nonce_size must be 12 bytes (96-bit)");
234 }
235 Ok(())
236 }
237
238 pub fn to_json(&self) -> serde_json::Value {
240 serde_json::json!({
241 "enabled": self.enabled,
242 "algorithm": self.algorithm,
243 "keyRotationEnabled": self.key_rotation_enabled,
244 "nonceSize": self.nonce_size,
245 "keySize": self.key_size,
246 })
247 }
248}
249
250#[derive(Debug, Clone, Deserialize, Serialize)]
252#[serde(default, deny_unknown_fields)]
253pub struct ConstantTimeConfig {
254 pub enabled: bool,
256 pub apply_to_jwt: bool,
258 pub apply_to_session_tokens: bool,
260 pub apply_to_csrf_tokens: bool,
262 pub apply_to_refresh_tokens: bool,
264}
265
266impl Default for ConstantTimeConfig {
267 fn default() -> Self {
268 Self {
269 enabled: true,
270 apply_to_jwt: true,
271 apply_to_session_tokens: true,
272 apply_to_csrf_tokens: true,
273 apply_to_refresh_tokens: true,
274 }
275 }
276}
277
278impl ConstantTimeConfig {
279 pub fn to_json(&self) -> serde_json::Value {
281 serde_json::json!({
282 "enabled": self.enabled,
283 "applyToJwt": self.apply_to_jwt,
284 "applyToSessionTokens": self.apply_to_session_tokens,
285 "applytoCsrfTokens": self.apply_to_csrf_tokens,
286 "applyToRefreshTokens": self.apply_to_refresh_tokens,
287 })
288 }
289}
290
291#[derive(Debug, Clone, Deserialize, Serialize)]
293#[serde(deny_unknown_fields)]
294pub struct RoleDefinitionConfig {
295 pub name: String,
297 #[serde(skip_serializing_if = "Option::is_none")]
299 pub description: Option<String>,
300 pub scopes: Vec<String>,
302}
303
304#[derive(Debug, Clone, Default, Deserialize, Serialize)]
306#[serde(default, deny_unknown_fields)]
307pub struct SecurityConfig {
308 #[serde(rename = "audit_logging")]
310 pub audit_logging: AuditLoggingConfig,
311 #[serde(rename = "error_sanitization")]
313 pub error_sanitization: ErrorSanitizationConfig,
314 #[serde(rename = "rate_limiting")]
316 pub rate_limiting: RateLimitConfig,
317 #[serde(rename = "state_encryption")]
319 pub state_encryption: StateEncryptionConfig,
320 #[serde(rename = "constant_time")]
322 pub constant_time: ConstantTimeConfig,
323 #[serde(default, skip_serializing_if = "Vec::is_empty")]
325 pub role_definitions: Vec<RoleDefinitionConfig>,
326 #[serde(skip_serializing_if = "Option::is_none")]
328 pub default_role: Option<String>,
329}
330
331impl SecurityConfig {
332 pub fn validate(&self) -> Result<()> {
334 self.error_sanitization.validate()?;
335 self.rate_limiting.validate()?;
336 self.state_encryption.validate()?;
337
338 for role in &self.role_definitions {
340 if role.name.is_empty() {
341 anyhow::bail!("Role name cannot be empty");
342 }
343 if role.scopes.is_empty() {
344 anyhow::bail!("Role '{}' must have at least one scope", role.name);
345 }
346 }
347
348 Ok(())
349 }
350
351 pub fn to_json(&self) -> serde_json::Value {
353 let mut json = serde_json::json!({
354 "auditLogging": self.audit_logging.to_json(),
355 "errorSanitization": self.error_sanitization.to_json(),
356 "rateLimiting": self.rate_limiting.to_json(),
357 "stateEncryption": self.state_encryption.to_json(),
358 "constantTime": self.constant_time.to_json(),
359 });
360
361 if !self.role_definitions.is_empty() {
363 json["roleDefinitions"] = serde_json::to_value(
364 self.role_definitions
365 .iter()
366 .map(|r| {
367 serde_json::json!({
368 "name": r.name,
369 "description": r.description,
370 "scopes": r.scopes,
371 })
372 })
373 .collect::<Vec<_>>(),
374 )
375 .unwrap_or_default();
376 }
377
378 if let Some(default_role) = &self.default_role {
380 json["defaultRole"] = serde_json::json!(default_role);
381 }
382
383 json
384 }
385}
386
387#[cfg(test)]
388mod tests {
389 use super::*;
390
391 #[test]
392 fn test_default_security_config() {
393 let config = SecurityConfig::default();
394 assert!(config.audit_logging.enabled);
395 assert!(config.error_sanitization.enabled);
396 assert!(config.rate_limiting.enabled);
397 assert!(config.state_encryption.enabled);
398 assert!(config.constant_time.enabled);
399 }
400
401 #[test]
402 fn test_error_sanitization_validation() {
403 let mut config = ErrorSanitizationConfig::default();
404 assert!(config.validate().is_ok());
405
406 config.leak_sensitive_details = true;
407 assert!(config.validate().is_err());
408 }
409
410 #[test]
411 fn test_rate_limiting_validation() {
412 let mut config = RateLimitConfig::default();
413 assert!(config.validate().is_ok());
414
415 config.auth_start_window_secs = 0;
416 assert!(config.validate().is_err());
417 }
418
419 #[test]
420 fn test_state_encryption_validation() {
421 let mut config = StateEncryptionConfig::default();
422 assert!(config.validate().is_ok());
423
424 config.key_size = 20;
425 assert!(config.validate().is_err());
426
427 config.key_size = 32;
428 config.nonce_size = 16;
429 assert!(config.validate().is_err());
430 }
431
432 #[test]
433 fn test_security_config_serialization() {
434 let config = SecurityConfig::default();
435 let json = config.to_json();
436 assert!(json["auditLogging"]["enabled"].is_boolean());
437 assert!(json["rateLimiting"]["authStart"]["maxRequests"].is_number());
438 assert!(json["stateEncryption"]["algorithm"].is_string());
439 }
440}