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 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); // Default
355                assert_eq!(dir_permissions, 0o700); // Default
356                assert!(!require_secure_filesystem); // Default false
357                assert!(!enable_filesystem_monitoring); // Default false
358            }
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        // The debug output for 0o600 is "384" in decimal, not "600"
446        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}