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)]
347#[serde(rename_all = "snake_case")]
348pub enum TenancyModeConfig {
349 #[default]
351 None,
352 Row,
354 Schema,
356}
357
358#[derive(Debug, Clone, Deserialize, Serialize)]
360#[serde(default, deny_unknown_fields)]
361pub struct TenancyTomlConfig {
362 pub mode: TenancyModeConfig,
364 pub tenant_claim: String,
366}
367
368impl Default for TenancyTomlConfig {
369 fn default() -> Self {
370 Self {
371 mode: TenancyModeConfig::None,
372 tenant_claim: "tenant_id".to_string(),
373 }
374 }
375}
376
377impl TenancyTomlConfig {
378 pub fn validate(&self) -> Result<()> {
384 if !matches!(self.mode, TenancyModeConfig::None) && self.tenant_claim.is_empty() {
385 anyhow::bail!("tenancy.tenant_claim must not be empty when mode is not 'none'");
386 }
387 Ok(())
388 }
389
390 pub fn to_json(&self) -> serde_json::Value {
392 serde_json::json!({
393 "mode": match self.mode {
394 TenancyModeConfig::None => "none",
395 TenancyModeConfig::Row => "row",
396 TenancyModeConfig::Schema => "schema",
397 },
398 "tenantClaim": self.tenant_claim,
399 })
400 }
401}
402
403#[derive(Debug, Clone, Default, Deserialize, Serialize)]
405#[serde(default, deny_unknown_fields)]
406pub struct SecurityConfig {
407 #[serde(rename = "audit_logging")]
409 pub audit_logging: AuditLoggingConfig,
410 #[serde(rename = "error_sanitization")]
412 pub error_sanitization: ErrorSanitizationConfig,
413 #[serde(rename = "rate_limiting")]
415 pub rate_limiting: RateLimitConfig,
416 #[serde(rename = "state_encryption")]
418 pub state_encryption: StateEncryptionConfig,
419 #[serde(rename = "constant_time")]
421 pub constant_time: ConstantTimeConfig,
422 #[serde(default, skip_serializing_if = "Vec::is_empty")]
424 pub role_definitions: Vec<RoleDefinitionConfig>,
425 #[serde(skip_serializing_if = "Option::is_none")]
427 pub default_role: Option<String>,
428}
429
430impl SecurityConfig {
431 pub fn validate(&self) -> Result<()> {
438 self.error_sanitization.validate()?;
439 self.rate_limiting.validate()?;
440 self.state_encryption.validate()?;
441
442 for role in &self.role_definitions {
444 if role.name.is_empty() {
445 anyhow::bail!("Role name cannot be empty");
446 }
447 if role.scopes.is_empty() {
448 anyhow::bail!("Role '{}' must have at least one scope", role.name);
449 }
450 }
451
452 Ok(())
453 }
454
455 pub fn to_json(&self) -> serde_json::Value {
457 let mut json = serde_json::json!({
458 "auditLogging": self.audit_logging.to_json(),
459 "errorSanitization": self.error_sanitization.to_json(),
460 "rateLimiting": self.rate_limiting.to_json(),
461 "stateEncryption": self.state_encryption.to_json(),
462 "constantTime": self.constant_time.to_json(),
463 });
464
465 if !self.role_definitions.is_empty() {
467 json["roleDefinitions"] = serde_json::to_value(
468 self.role_definitions
469 .iter()
470 .map(|r| {
471 serde_json::json!({
472 "name": r.name,
473 "description": r.description,
474 "scopes": r.scopes,
475 })
476 })
477 .collect::<Vec<_>>(),
478 )
479 .unwrap_or_default();
480 }
481
482 if let Some(default_role) = &self.default_role {
484 json["defaultRole"] = serde_json::json!(default_role);
485 }
486
487 json
488 }
489}