1use serde::{Deserialize, Serialize};
4use std::path::PathBuf;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct AuthConfig {
9 pub storage: StorageConfig,
11 pub enabled: bool,
13 pub cache_size: usize,
15 pub session_timeout_secs: u64,
17 pub max_failed_attempts: u32,
19 pub rate_limit_window_secs: u64,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
25pub enum StorageConfig {
26 File {
28 path: PathBuf,
30 #[serde(default = "default_file_permissions")]
32 file_permissions: u32,
33 #[serde(default = "default_dir_permissions")]
35 dir_permissions: u32,
36 #[serde(default)]
38 require_secure_filesystem: bool,
39 #[serde(default)]
41 enable_filesystem_monitoring: bool,
42 },
43 Environment {
45 prefix: String,
47 },
48 Memory,
50}
51
52fn default_file_permissions() -> u32 {
53 0o600 }
55
56fn default_dir_permissions() -> u32 {
57 0o700 }
59
60impl Default for AuthConfig {
61 fn default() -> Self {
62 Self {
63 storage: StorageConfig::File {
64 path: dirs::home_dir()
65 .unwrap_or_else(|| PathBuf::from("."))
66 .join(".pulseengine")
67 .join("mcp-auth")
68 .join("keys.enc"),
69 file_permissions: 0o600,
70 dir_permissions: 0o700,
71 require_secure_filesystem: true,
72 enable_filesystem_monitoring: false,
73 },
74 enabled: true,
75 cache_size: 1000,
76 session_timeout_secs: 3600, max_failed_attempts: 5,
78 rate_limit_window_secs: 900, }
80 }
81}
82
83impl AuthConfig {
84 pub fn disabled() -> Self {
86 Self {
87 enabled: false,
88 ..Default::default()
89 }
90 }
91
92 pub fn memory() -> Self {
94 Self {
95 storage: StorageConfig::Memory,
96 ..Default::default()
97 }
98 }
99
100 pub fn for_application(app_name: &str) -> Self {
102 Self {
103 storage: StorageConfig::File {
104 path: Self::get_app_storage_path(app_name),
105 file_permissions: 0o600,
106 dir_permissions: 0o700,
107 require_secure_filesystem: true,
108 enable_filesystem_monitoring: false,
109 },
110 enabled: true,
111 cache_size: 1000,
112 session_timeout_secs: 3600, max_failed_attempts: 5,
114 rate_limit_window_secs: 900, }
116 }
117
118 pub fn with_custom_path(app_name: &str, base_path: PathBuf) -> Self {
120 Self {
121 storage: StorageConfig::File {
122 path: base_path.join(app_name).join("mcp-auth").join("keys.enc"),
123 file_permissions: 0o600,
124 dir_permissions: 0o700,
125 require_secure_filesystem: true,
126 enable_filesystem_monitoring: false,
127 },
128 enabled: true,
129 cache_size: 1000,
130 session_timeout_secs: 3600, max_failed_attempts: 5,
132 rate_limit_window_secs: 900, }
134 }
135
136 fn get_app_storage_path(app_name: &str) -> PathBuf {
138 if let Ok(app_name_override) = std::env::var("PULSEENGINE_MCP_APP_NAME") {
140 if !app_name_override.trim().is_empty() {
141 return Self::build_storage_path(&app_name_override);
142 }
143 }
144
145 Self::build_storage_path(app_name)
146 }
147
148 fn build_storage_path(app_name: &str) -> PathBuf {
150 dirs::home_dir()
151 .unwrap_or_else(|| PathBuf::from("."))
152 .join(".pulseengine")
153 .join(app_name)
154 .join("mcp-auth")
155 .join("keys.enc")
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162 use std::path::PathBuf;
163
164 #[test]
165 fn test_default_file_permissions() {
166 assert_eq!(default_file_permissions(), 0o600);
167 }
168
169 #[test]
170 fn test_default_dir_permissions() {
171 assert_eq!(default_dir_permissions(), 0o700);
172 }
173
174 #[test]
175 fn test_auth_config_default() {
176 let config = AuthConfig::default();
177
178 assert!(config.enabled);
179 assert_eq!(config.cache_size, 1000);
180 assert_eq!(config.session_timeout_secs, 3600);
181 assert_eq!(config.max_failed_attempts, 5);
182 assert_eq!(config.rate_limit_window_secs, 900);
183
184 match config.storage {
186 StorageConfig::File {
187 path,
188 file_permissions,
189 dir_permissions,
190 require_secure_filesystem,
191 enable_filesystem_monitoring,
192 } => {
193 assert!(path.to_string_lossy().contains(".pulseengine"));
194 assert!(path.to_string_lossy().contains("mcp-auth"));
195 assert!(path.to_string_lossy().contains("keys.enc"));
196 assert_eq!(file_permissions, 0o600);
197 assert_eq!(dir_permissions, 0o700);
198 assert!(require_secure_filesystem);
199 assert!(!enable_filesystem_monitoring);
200 }
201 _ => panic!("Expected File storage config"),
202 }
203 }
204
205 #[test]
206 fn test_auth_config_disabled() {
207 let config = AuthConfig::disabled();
208
209 assert!(!config.enabled);
210 assert_eq!(config.cache_size, 1000); assert_eq!(config.session_timeout_secs, 3600);
212 assert_eq!(config.max_failed_attempts, 5);
213 assert_eq!(config.rate_limit_window_secs, 900);
214 }
215
216 #[test]
217 fn test_auth_config_memory() {
218 let config = AuthConfig::memory();
219
220 assert!(config.enabled);
221 assert!(matches!(config.storage, StorageConfig::Memory));
222 assert_eq!(config.cache_size, 1000);
223 assert_eq!(config.session_timeout_secs, 3600);
224 assert_eq!(config.max_failed_attempts, 5);
225 assert_eq!(config.rate_limit_window_secs, 900);
226 }
227
228 #[test]
229 fn test_storage_config_file() {
230 let expected_path = std::env::temp_dir()
231 .join("mcp-auth-config-test")
232 .join("test_storage");
233
234 let storage = StorageConfig::File {
235 path: expected_path.clone(),
236 file_permissions: 0o644,
237 dir_permissions: 0o755,
238 require_secure_filesystem: false,
239 enable_filesystem_monitoring: true,
240 };
241
242 match storage {
243 StorageConfig::File {
244 path,
245 file_permissions,
246 dir_permissions,
247 require_secure_filesystem,
248 enable_filesystem_monitoring,
249 } => {
250 assert_eq!(path, expected_path);
251 assert_eq!(file_permissions, 0o644);
252 assert_eq!(dir_permissions, 0o755);
253 assert!(!require_secure_filesystem);
254 assert!(enable_filesystem_monitoring);
255 }
256 _ => panic!("Expected File storage config"),
257 }
258 }
259
260 #[test]
261 fn test_storage_config_environment() {
262 let storage = StorageConfig::Environment {
263 prefix: "MCP_AUTH".to_string(),
264 };
265
266 match storage {
267 StorageConfig::Environment { prefix } => {
268 assert_eq!(prefix, "MCP_AUTH");
269 }
270 _ => panic!("Expected Environment storage config"),
271 }
272 }
273
274 #[test]
275 fn test_storage_config_memory() {
276 let storage = StorageConfig::Memory;
277 assert!(matches!(storage, StorageConfig::Memory));
278 }
279
280 #[test]
281 fn test_auth_config_serialization() {
282 let config = AuthConfig {
283 storage: StorageConfig::File {
284 path: PathBuf::from("/test/path"),
285 file_permissions: 0o600,
286 dir_permissions: 0o700,
287 require_secure_filesystem: true,
288 enable_filesystem_monitoring: false,
289 },
290 enabled: true,
291 cache_size: 500,
292 session_timeout_secs: 7200,
293 max_failed_attempts: 3,
294 rate_limit_window_secs: 1800,
295 };
296
297 let json = serde_json::to_string(&config).unwrap();
298 let deserialized: AuthConfig = serde_json::from_str(&json).unwrap();
299
300 assert_eq!(deserialized.enabled, config.enabled);
301 assert_eq!(deserialized.cache_size, config.cache_size);
302 assert_eq!(
303 deserialized.session_timeout_secs,
304 config.session_timeout_secs
305 );
306 assert_eq!(deserialized.max_failed_attempts, config.max_failed_attempts);
307 assert_eq!(
308 deserialized.rate_limit_window_secs,
309 config.rate_limit_window_secs
310 );
311
312 match (config.storage, deserialized.storage) {
313 (
314 StorageConfig::File {
315 path: p1,
316 file_permissions: fp1,
317 dir_permissions: dp1,
318 ..
319 },
320 StorageConfig::File {
321 path: p2,
322 file_permissions: fp2,
323 dir_permissions: dp2,
324 ..
325 },
326 ) => {
327 assert_eq!(p1, p2);
328 assert_eq!(fp1, fp2);
329 assert_eq!(dp1, dp2);
330 }
331 _ => panic!("Storage configs don't match"),
332 }
333 }
334
335 #[test]
336 fn test_storage_config_file_with_defaults() {
337 let json = r#"{
338 "File": {
339 "path": "/test/path"
340 }
341 }"#;
342
343 let storage: StorageConfig = serde_json::from_str(json).unwrap();
344
345 match storage {
346 StorageConfig::File {
347 path,
348 file_permissions,
349 dir_permissions,
350 require_secure_filesystem,
351 enable_filesystem_monitoring,
352 } => {
353 assert_eq!(path, PathBuf::from("/test/path"));
354 assert_eq!(file_permissions, 0o600); assert_eq!(dir_permissions, 0o700); assert!(!require_secure_filesystem); assert!(!enable_filesystem_monitoring); }
359 _ => panic!("Expected File storage config"),
360 }
361 }
362
363 #[test]
364 fn test_storage_config_environment_serialization() {
365 let storage = StorageConfig::Environment {
366 prefix: "TEST_PREFIX".to_string(),
367 };
368
369 let json = serde_json::to_string(&storage).unwrap();
370 let deserialized: StorageConfig = serde_json::from_str(&json).unwrap();
371
372 match deserialized {
373 StorageConfig::Environment { prefix } => {
374 assert_eq!(prefix, "TEST_PREFIX");
375 }
376 _ => panic!("Expected Environment storage config"),
377 }
378 }
379
380 #[test]
381 fn test_storage_config_memory_serialization() {
382 let storage = StorageConfig::Memory;
383
384 let json = serde_json::to_string(&storage).unwrap();
385 let deserialized: StorageConfig = serde_json::from_str(&json).unwrap();
386
387 assert!(matches!(deserialized, StorageConfig::Memory));
388 }
389
390 #[test]
391 fn test_auth_config_custom_values() {
392 let config = AuthConfig {
393 storage: StorageConfig::Environment {
394 prefix: "CUSTOM".to_string(),
395 },
396 enabled: false,
397 cache_size: 2000,
398 session_timeout_secs: 1800,
399 max_failed_attempts: 10,
400 rate_limit_window_secs: 300,
401 };
402
403 assert!(!config.enabled);
404 assert_eq!(config.cache_size, 2000);
405 assert_eq!(config.session_timeout_secs, 1800);
406 assert_eq!(config.max_failed_attempts, 10);
407 assert_eq!(config.rate_limit_window_secs, 300);
408
409 match config.storage {
410 StorageConfig::Environment { prefix } => {
411 assert_eq!(prefix, "CUSTOM");
412 }
413 _ => panic!("Expected Environment storage"),
414 }
415 }
416
417 #[test]
418 fn test_auth_config_clone() {
419 let original = AuthConfig::default();
420 let cloned = original.clone();
421
422 assert_eq!(cloned.enabled, original.enabled);
423 assert_eq!(cloned.cache_size, original.cache_size);
424 assert_eq!(cloned.session_timeout_secs, original.session_timeout_secs);
425 assert_eq!(cloned.max_failed_attempts, original.max_failed_attempts);
426 assert_eq!(
427 cloned.rate_limit_window_secs,
428 original.rate_limit_window_secs
429 );
430 }
431
432 #[test]
433 fn test_storage_config_debug() {
434 let file_storage = StorageConfig::File {
435 path: PathBuf::from("/test"),
436 file_permissions: 0o600,
437 dir_permissions: 0o700,
438 require_secure_filesystem: true,
439 enable_filesystem_monitoring: false,
440 };
441
442 let debug_str = format!("{:?}", file_storage);
443 assert!(debug_str.contains("File"));
444 assert!(debug_str.contains("/test"));
445 assert!(debug_str.contains("384"));
447
448 let env_storage = StorageConfig::Environment {
449 prefix: "TEST".to_string(),
450 };
451
452 let debug_str = format!("{:?}", env_storage);
453 assert!(debug_str.contains("Environment"));
454 assert!(debug_str.contains("TEST"));
455
456 let memory_storage = StorageConfig::Memory;
457 let debug_str = format!("{:?}", memory_storage);
458 assert!(debug_str.contains("Memory"));
459 }
460}