fraiseql_server/auth/
security_config.rs1use std::env;
7
8use serde_json::Value as JsonValue;
9
10#[derive(Debug, Clone)]
12pub struct SecurityConfigFromSchema {
13 pub audit_logging: AuditLoggingSettings,
15 pub error_sanitization: ErrorSanitizationSettings,
17 pub rate_limiting: RateLimitingSettings,
19 pub state_encryption: StateEncryptionSettings,
21}
22
23#[derive(Debug, Clone)]
24pub struct AuditLoggingSettings {
25 pub enabled: bool,
26 pub log_level: String,
27 pub include_sensitive_data: bool,
28 pub async_logging: bool,
29 pub buffer_size: u32,
30 pub flush_interval_secs: u32,
31}
32
33#[derive(Debug, Clone)]
34pub struct ErrorSanitizationSettings {
35 pub enabled: bool,
36 pub generic_messages: bool,
37 pub internal_logging: bool,
38 pub leak_sensitive_details: bool,
39 pub user_facing_format: String,
40}
41
42#[derive(Debug, Clone)]
43pub struct RateLimitingSettings {
44 pub enabled: bool,
45 pub auth_start_max_requests: u32,
46 pub auth_start_window_secs: u64,
47 pub auth_callback_max_requests: u32,
48 pub auth_callback_window_secs: u64,
49 pub auth_refresh_max_requests: u32,
50 pub auth_refresh_window_secs: u64,
51 pub auth_logout_max_requests: u32,
52 pub auth_logout_window_secs: u64,
53 pub failed_login_max_requests: u32,
54 pub failed_login_window_secs: u64,
55}
56
57#[derive(Debug, Clone)]
58pub struct StateEncryptionSettings {
59 pub enabled: bool,
60 pub algorithm: String,
61 pub key_rotation_enabled: bool,
62 pub nonce_size: u32,
63 pub key_size: u32,
64}
65
66impl Default for SecurityConfigFromSchema {
67 fn default() -> Self {
68 Self {
69 audit_logging: AuditLoggingSettings {
70 enabled: true,
71 log_level: "info".to_string(),
72 include_sensitive_data: false,
73 async_logging: true,
74 buffer_size: 1000,
75 flush_interval_secs: 5,
76 },
77 error_sanitization: ErrorSanitizationSettings {
78 enabled: true,
79 generic_messages: true,
80 internal_logging: true,
81 leak_sensitive_details: false,
82 user_facing_format: "generic".to_string(),
83 },
84 rate_limiting: RateLimitingSettings {
85 enabled: true,
86 auth_start_max_requests: 100,
87 auth_start_window_secs: 60,
88 auth_callback_max_requests: 50,
89 auth_callback_window_secs: 60,
90 auth_refresh_max_requests: 10,
91 auth_refresh_window_secs: 60,
92 auth_logout_max_requests: 20,
93 auth_logout_window_secs: 60,
94 failed_login_max_requests: 5,
95 failed_login_window_secs: 3600,
96 },
97 state_encryption: StateEncryptionSettings {
98 enabled: true,
99 algorithm: "chacha20-poly1305".to_string(),
100 key_rotation_enabled: false,
101 nonce_size: 12,
102 key_size: 32,
103 },
104 }
105 }
106}
107
108impl SecurityConfigFromSchema {
109 pub fn from_json(value: &JsonValue) -> anyhow::Result<Self> {
111 let mut config = Self::default();
112
113 if let Some(audit) = value.get("auditLogging").and_then(|v| v.as_object()) {
114 config.audit_logging.enabled =
115 audit.get("enabled").and_then(|v| v.as_bool()).unwrap_or(true);
116 config.audit_logging.log_level =
117 audit.get("logLevel").and_then(|v| v.as_str()).unwrap_or("info").to_string();
118 config.audit_logging.include_sensitive_data =
119 audit.get("includeSensitiveData").and_then(|v| v.as_bool()).unwrap_or(false);
120 config.audit_logging.async_logging =
121 audit.get("asyncLogging").and_then(|v| v.as_bool()).unwrap_or(true);
122 config.audit_logging.buffer_size =
123 audit.get("bufferSize").and_then(|v| v.as_u64()).unwrap_or(1000) as u32;
124 config.audit_logging.flush_interval_secs =
125 audit.get("flushIntervalSecs").and_then(|v| v.as_u64()).unwrap_or(5) as u32;
126 }
127
128 if let Some(error_san) = value.get("errorSanitization").and_then(|v| v.as_object()) {
129 config.error_sanitization.enabled =
130 error_san.get("enabled").and_then(|v| v.as_bool()).unwrap_or(true);
131 config.error_sanitization.generic_messages =
132 error_san.get("genericMessages").and_then(|v| v.as_bool()).unwrap_or(true);
133 config.error_sanitization.internal_logging =
134 error_san.get("internalLogging").and_then(|v| v.as_bool()).unwrap_or(true);
135 config.error_sanitization.leak_sensitive_details =
136 error_san.get("leakSensitiveDetails").and_then(|v| v.as_bool()).unwrap_or(false);
137 config.error_sanitization.user_facing_format = error_san
138 .get("userFacingFormat")
139 .and_then(|v| v.as_str())
140 .unwrap_or("generic")
141 .to_string();
142 }
143
144 if let Some(rate_limit) = value.get("rateLimiting").and_then(|v| v.as_object()) {
145 config.rate_limiting.enabled =
146 rate_limit.get("enabled").and_then(|v| v.as_bool()).unwrap_or(true);
147
148 if let Some(auth_start) = rate_limit.get("authStart").and_then(|v| v.as_object()) {
149 config.rate_limiting.auth_start_max_requests =
150 auth_start.get("maxRequests").and_then(|v| v.as_u64()).unwrap_or(100) as u32;
151 config.rate_limiting.auth_start_window_secs =
152 auth_start.get("windowSecs").and_then(|v| v.as_u64()).unwrap_or(60);
153 }
154
155 if let Some(auth_callback) = rate_limit.get("authCallback").and_then(|v| v.as_object())
156 {
157 config.rate_limiting.auth_callback_max_requests =
158 auth_callback.get("maxRequests").and_then(|v| v.as_u64()).unwrap_or(50) as u32;
159 config.rate_limiting.auth_callback_window_secs =
160 auth_callback.get("windowSecs").and_then(|v| v.as_u64()).unwrap_or(60);
161 }
162
163 if let Some(auth_refresh) = rate_limit.get("authRefresh").and_then(|v| v.as_object()) {
164 config.rate_limiting.auth_refresh_max_requests =
165 auth_refresh.get("maxRequests").and_then(|v| v.as_u64()).unwrap_or(10) as u32;
166 config.rate_limiting.auth_refresh_window_secs =
167 auth_refresh.get("windowSecs").and_then(|v| v.as_u64()).unwrap_or(60);
168 }
169
170 if let Some(auth_logout) = rate_limit.get("authLogout").and_then(|v| v.as_object()) {
171 config.rate_limiting.auth_logout_max_requests =
172 auth_logout.get("maxRequests").and_then(|v| v.as_u64()).unwrap_or(20) as u32;
173 config.rate_limiting.auth_logout_window_secs =
174 auth_logout.get("windowSecs").and_then(|v| v.as_u64()).unwrap_or(60);
175 }
176
177 if let Some(failed_login) = rate_limit.get("failedLogin").and_then(|v| v.as_object()) {
178 config.rate_limiting.failed_login_max_requests =
179 failed_login.get("maxRequests").and_then(|v| v.as_u64()).unwrap_or(5) as u32;
180 config.rate_limiting.failed_login_window_secs =
181 failed_login.get("windowSecs").and_then(|v| v.as_u64()).unwrap_or(3600);
182 }
183 }
184
185 if let Some(state_enc) = value.get("stateEncryption").and_then(|v| v.as_object()) {
186 config.state_encryption.enabled =
187 state_enc.get("enabled").and_then(|v| v.as_bool()).unwrap_or(true);
188 config.state_encryption.algorithm = state_enc
189 .get("algorithm")
190 .and_then(|v| v.as_str())
191 .unwrap_or("chacha20-poly1305")
192 .to_string();
193 config.state_encryption.key_rotation_enabled =
194 state_enc.get("keyRotationEnabled").and_then(|v| v.as_bool()).unwrap_or(false);
195 config.state_encryption.nonce_size =
196 state_enc.get("nonceSize").and_then(|v| v.as_u64()).unwrap_or(12) as u32;
197 config.state_encryption.key_size =
198 state_enc.get("keySize").and_then(|v| v.as_u64()).unwrap_or(32) as u32;
199 }
200
201 Ok(config)
202 }
203
204 pub fn apply_env_overrides(&mut self) {
206 if let Ok(level) = env::var("AUDIT_LOG_LEVEL") {
208 self.audit_logging.log_level = level;
209 }
210
211 if let Ok(val) = env::var("RATE_LIMIT_AUTH_START") {
213 if let Ok(n) = val.parse() {
214 self.rate_limiting.auth_start_max_requests = n;
215 }
216 }
217 if let Ok(val) = env::var("RATE_LIMIT_AUTH_CALLBACK") {
218 if let Ok(n) = val.parse() {
219 self.rate_limiting.auth_callback_max_requests = n;
220 }
221 }
222 if let Ok(val) = env::var("RATE_LIMIT_AUTH_REFRESH") {
223 if let Ok(n) = val.parse() {
224 self.rate_limiting.auth_refresh_max_requests = n;
225 }
226 }
227 if let Ok(val) = env::var("RATE_LIMIT_AUTH_LOGOUT") {
228 if let Ok(n) = val.parse() {
229 self.rate_limiting.auth_logout_max_requests = n;
230 }
231 }
232 if let Ok(val) = env::var("RATE_LIMIT_FAILED_LOGIN") {
233 if let Ok(n) = val.parse() {
234 self.rate_limiting.failed_login_max_requests = n;
235 }
236 }
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243
244 #[test]
245 fn test_default_config() {
246 let config = SecurityConfigFromSchema::default();
247 assert!(config.audit_logging.enabled);
248 assert!(config.error_sanitization.enabled);
249 assert!(config.rate_limiting.enabled);
250 assert!(config.state_encryption.enabled);
251 }
252
253 #[test]
254 fn test_parse_from_json() {
255 let json = serde_json::json!({
256 "auditLogging": {
257 "enabled": true,
258 "logLevel": "debug",
259 "includeSensitiveData": false
260 },
261 "rateLimiting": {
262 "enabled": true,
263 "authStart": {
264 "maxRequests": 200,
265 "windowSecs": 60
266 }
267 }
268 });
269
270 let config = SecurityConfigFromSchema::from_json(&json).expect("Failed to parse");
271 assert_eq!(config.audit_logging.log_level, "debug");
272 assert_eq!(config.rate_limiting.auth_start_max_requests, 200);
273 }
274
275 #[test]
276 fn test_apply_env_overrides() {
277 let mut config = SecurityConfigFromSchema::default();
280 config.apply_env_overrides();
281 }
283}