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 storage = StorageConfig::File {
231 path: PathBuf::from("/tmp/test"),
232 file_permissions: 0o644,
233 dir_permissions: 0o755,
234 require_secure_filesystem: false,
235 enable_filesystem_monitoring: true,
236 };
237
238 match storage {
239 StorageConfig::File {
240 path,
241 file_permissions,
242 dir_permissions,
243 require_secure_filesystem,
244 enable_filesystem_monitoring,
245 } => {
246 assert_eq!(path, PathBuf::from("/tmp/test"));
247 assert_eq!(file_permissions, 0o644);
248 assert_eq!(dir_permissions, 0o755);
249 assert!(!require_secure_filesystem);
250 assert!(enable_filesystem_monitoring);
251 }
252 _ => panic!("Expected File storage config"),
253 }
254 }
255
256 #[test]
257 fn test_storage_config_environment() {
258 let storage = StorageConfig::Environment {
259 prefix: "MCP_AUTH".to_string(),
260 };
261
262 match storage {
263 StorageConfig::Environment { prefix } => {
264 assert_eq!(prefix, "MCP_AUTH");
265 }
266 _ => panic!("Expected Environment storage config"),
267 }
268 }
269
270 #[test]
271 fn test_storage_config_memory() {
272 let storage = StorageConfig::Memory;
273 assert!(matches!(storage, StorageConfig::Memory));
274 }
275
276 #[test]
277 fn test_auth_config_serialization() {
278 let config = AuthConfig {
279 storage: StorageConfig::File {
280 path: PathBuf::from("/test/path"),
281 file_permissions: 0o600,
282 dir_permissions: 0o700,
283 require_secure_filesystem: true,
284 enable_filesystem_monitoring: false,
285 },
286 enabled: true,
287 cache_size: 500,
288 session_timeout_secs: 7200,
289 max_failed_attempts: 3,
290 rate_limit_window_secs: 1800,
291 };
292
293 let json = serde_json::to_string(&config).unwrap();
294 let deserialized: AuthConfig = serde_json::from_str(&json).unwrap();
295
296 assert_eq!(deserialized.enabled, config.enabled);
297 assert_eq!(deserialized.cache_size, config.cache_size);
298 assert_eq!(
299 deserialized.session_timeout_secs,
300 config.session_timeout_secs
301 );
302 assert_eq!(deserialized.max_failed_attempts, config.max_failed_attempts);
303 assert_eq!(
304 deserialized.rate_limit_window_secs,
305 config.rate_limit_window_secs
306 );
307
308 match (config.storage, deserialized.storage) {
309 (
310 StorageConfig::File {
311 path: p1,
312 file_permissions: fp1,
313 dir_permissions: dp1,
314 ..
315 },
316 StorageConfig::File {
317 path: p2,
318 file_permissions: fp2,
319 dir_permissions: dp2,
320 ..
321 },
322 ) => {
323 assert_eq!(p1, p2);
324 assert_eq!(fp1, fp2);
325 assert_eq!(dp1, dp2);
326 }
327 _ => panic!("Storage configs don't match"),
328 }
329 }
330
331 #[test]
332 fn test_storage_config_file_with_defaults() {
333 let json = r#"{
334 "File": {
335 "path": "/test/path"
336 }
337 }"#;
338
339 let storage: StorageConfig = serde_json::from_str(json).unwrap();
340
341 match storage {
342 StorageConfig::File {
343 path,
344 file_permissions,
345 dir_permissions,
346 require_secure_filesystem,
347 enable_filesystem_monitoring,
348 } => {
349 assert_eq!(path, PathBuf::from("/test/path"));
350 assert_eq!(file_permissions, 0o600); assert_eq!(dir_permissions, 0o700); assert!(!require_secure_filesystem); assert!(!enable_filesystem_monitoring); }
355 _ => panic!("Expected File storage config"),
356 }
357 }
358
359 #[test]
360 fn test_storage_config_environment_serialization() {
361 let storage = StorageConfig::Environment {
362 prefix: "TEST_PREFIX".to_string(),
363 };
364
365 let json = serde_json::to_string(&storage).unwrap();
366 let deserialized: StorageConfig = serde_json::from_str(&json).unwrap();
367
368 match deserialized {
369 StorageConfig::Environment { prefix } => {
370 assert_eq!(prefix, "TEST_PREFIX");
371 }
372 _ => panic!("Expected Environment storage config"),
373 }
374 }
375
376 #[test]
377 fn test_storage_config_memory_serialization() {
378 let storage = StorageConfig::Memory;
379
380 let json = serde_json::to_string(&storage).unwrap();
381 let deserialized: StorageConfig = serde_json::from_str(&json).unwrap();
382
383 assert!(matches!(deserialized, StorageConfig::Memory));
384 }
385
386 #[test]
387 fn test_auth_config_custom_values() {
388 let config = AuthConfig {
389 storage: StorageConfig::Environment {
390 prefix: "CUSTOM".to_string(),
391 },
392 enabled: false,
393 cache_size: 2000,
394 session_timeout_secs: 1800,
395 max_failed_attempts: 10,
396 rate_limit_window_secs: 300,
397 };
398
399 assert!(!config.enabled);
400 assert_eq!(config.cache_size, 2000);
401 assert_eq!(config.session_timeout_secs, 1800);
402 assert_eq!(config.max_failed_attempts, 10);
403 assert_eq!(config.rate_limit_window_secs, 300);
404
405 match config.storage {
406 StorageConfig::Environment { prefix } => {
407 assert_eq!(prefix, "CUSTOM");
408 }
409 _ => panic!("Expected Environment storage"),
410 }
411 }
412
413 #[test]
414 fn test_auth_config_clone() {
415 let original = AuthConfig::default();
416 let cloned = original.clone();
417
418 assert_eq!(cloned.enabled, original.enabled);
419 assert_eq!(cloned.cache_size, original.cache_size);
420 assert_eq!(cloned.session_timeout_secs, original.session_timeout_secs);
421 assert_eq!(cloned.max_failed_attempts, original.max_failed_attempts);
422 assert_eq!(
423 cloned.rate_limit_window_secs,
424 original.rate_limit_window_secs
425 );
426 }
427
428 #[test]
429 fn test_storage_config_debug() {
430 let file_storage = StorageConfig::File {
431 path: PathBuf::from("/test"),
432 file_permissions: 0o600,
433 dir_permissions: 0o700,
434 require_secure_filesystem: true,
435 enable_filesystem_monitoring: false,
436 };
437
438 let debug_str = format!("{:?}", file_storage);
439 assert!(debug_str.contains("File"));
440 assert!(debug_str.contains("/test"));
441 assert!(debug_str.contains("384"));
443
444 let env_storage = StorageConfig::Environment {
445 prefix: "TEST".to_string(),
446 };
447
448 let debug_str = format!("{:?}", env_storage);
449 assert!(debug_str.contains("Environment"));
450 assert!(debug_str.contains("TEST"));
451
452 let memory_storage = StorageConfig::Memory;
453 let debug_str = format!("{:?}", memory_storage);
454 assert!(debug_str.contains("Memory"));
455 }
456}