pulseengine_mcp_auth/
config.rs

1//! Authentication configuration
2
3use serde::{Deserialize, Serialize};
4use std::path::PathBuf;
5
6/// Authentication configuration
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct AuthConfig {
9    /// Storage backend configuration
10    pub storage: StorageConfig,
11    /// Enable authentication (if false, all requests are allowed)
12    pub enabled: bool,
13    /// Cache size for API keys
14    pub cache_size: usize,
15    /// Session timeout in seconds
16    pub session_timeout_secs: u64,
17    /// Maximum failed attempts before rate limiting
18    pub max_failed_attempts: u32,
19    /// Rate limiting window in seconds
20    pub rate_limit_window_secs: u64,
21}
22
23/// Storage configuration for authentication data
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub enum StorageConfig {
26    /// File-based storage with security options
27    File {
28        /// Path to storage directory
29        path: PathBuf,
30        /// File permissions (Unix mode, e.g., 0o600)
31        #[serde(default = "default_file_permissions")]
32        file_permissions: u32,
33        /// Directory permissions (Unix mode, e.g., 0o700)
34        #[serde(default = "default_dir_permissions")]
35        dir_permissions: u32,
36        /// Require secure file system (reject if on network/shared drive)
37        #[serde(default)]
38        require_secure_filesystem: bool,
39        /// Enable file system monitoring for unauthorized changes
40        #[serde(default)]
41        enable_filesystem_monitoring: bool,
42    },
43    /// Environment variable storage
44    Environment {
45        /// Prefix for environment variables
46        prefix: String,
47    },
48    /// Memory-only storage (for testing)
49    Memory,
50}
51
52fn default_file_permissions() -> u32 {
53    0o600 // Owner read/write only
54}
55
56fn default_dir_permissions() -> u32 {
57    0o700 // Owner read/write/execute only
58}
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, // 1 hour
77            max_failed_attempts: 5,
78            rate_limit_window_secs: 900, // 15 minutes
79        }
80    }
81}
82
83impl AuthConfig {
84    /// Create a disabled authentication configuration
85    pub fn disabled() -> Self {
86        Self {
87            enabled: false,
88            ..Default::default()
89        }
90    }
91
92    /// Create a memory-only configuration (for testing)
93    pub fn memory() -> Self {
94        Self {
95            storage: StorageConfig::Memory,
96            ..Default::default()
97        }
98    }
99
100    /// Create an application-specific configuration
101    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, // 1 hour
113            max_failed_attempts: 5,
114            rate_limit_window_secs: 900, // 15 minutes
115        }
116    }
117
118    /// Create an application-specific configuration with custom base path
119    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, // 1 hour
131            max_failed_attempts: 5,
132            rate_limit_window_secs: 900, // 15 minutes
133        }
134    }
135
136    /// Get the default storage path for an application
137    fn get_app_storage_path(app_name: &str) -> PathBuf {
138        // Check for environment variable override first
139        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    /// Build the storage path for an application name
149    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        // Check default storage config
185        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); // Other values should still be defaults
211        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); // Default
351                assert_eq!(dir_permissions, 0o700); // Default
352                assert!(!require_secure_filesystem); // Default false
353                assert!(!enable_filesystem_monitoring); // Default false
354            }
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        // The debug output for 0o600 is "384" in decimal, not "600"
442        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}