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
101#[cfg(test)]
102mod tests {
103    use super::*;
104    use std::path::PathBuf;
105
106    #[test]
107    fn test_default_file_permissions() {
108        assert_eq!(default_file_permissions(), 0o600);
109    }
110
111    #[test]
112    fn test_default_dir_permissions() {
113        assert_eq!(default_dir_permissions(), 0o700);
114    }
115
116    #[test]
117    fn test_auth_config_default() {
118        let config = AuthConfig::default();
119
120        assert!(config.enabled);
121        assert_eq!(config.cache_size, 1000);
122        assert_eq!(config.session_timeout_secs, 3600);
123        assert_eq!(config.max_failed_attempts, 5);
124        assert_eq!(config.rate_limit_window_secs, 900);
125
126        // Check default storage config
127        match config.storage {
128            StorageConfig::File {
129                path,
130                file_permissions,
131                dir_permissions,
132                require_secure_filesystem,
133                enable_filesystem_monitoring,
134            } => {
135                assert!(path.to_string_lossy().contains(".pulseengine"));
136                assert!(path.to_string_lossy().contains("mcp-auth"));
137                assert!(path.to_string_lossy().contains("keys.enc"));
138                assert_eq!(file_permissions, 0o600);
139                assert_eq!(dir_permissions, 0o700);
140                assert!(require_secure_filesystem);
141                assert!(!enable_filesystem_monitoring);
142            }
143            _ => panic!("Expected File storage config"),
144        }
145    }
146
147    #[test]
148    fn test_auth_config_disabled() {
149        let config = AuthConfig::disabled();
150
151        assert!(!config.enabled);
152        assert_eq!(config.cache_size, 1000); // Other values should still be defaults
153        assert_eq!(config.session_timeout_secs, 3600);
154        assert_eq!(config.max_failed_attempts, 5);
155        assert_eq!(config.rate_limit_window_secs, 900);
156    }
157
158    #[test]
159    fn test_auth_config_memory() {
160        let config = AuthConfig::memory();
161
162        assert!(config.enabled);
163        assert!(matches!(config.storage, StorageConfig::Memory));
164        assert_eq!(config.cache_size, 1000);
165        assert_eq!(config.session_timeout_secs, 3600);
166        assert_eq!(config.max_failed_attempts, 5);
167        assert_eq!(config.rate_limit_window_secs, 900);
168    }
169
170    #[test]
171    fn test_storage_config_file() {
172        let storage = StorageConfig::File {
173            path: PathBuf::from("/tmp/test"),
174            file_permissions: 0o644,
175            dir_permissions: 0o755,
176            require_secure_filesystem: false,
177            enable_filesystem_monitoring: true,
178        };
179
180        match storage {
181            StorageConfig::File {
182                path,
183                file_permissions,
184                dir_permissions,
185                require_secure_filesystem,
186                enable_filesystem_monitoring,
187            } => {
188                assert_eq!(path, PathBuf::from("/tmp/test"));
189                assert_eq!(file_permissions, 0o644);
190                assert_eq!(dir_permissions, 0o755);
191                assert!(!require_secure_filesystem);
192                assert!(enable_filesystem_monitoring);
193            }
194            _ => panic!("Expected File storage config"),
195        }
196    }
197
198    #[test]
199    fn test_storage_config_environment() {
200        let storage = StorageConfig::Environment {
201            prefix: "MCP_AUTH".to_string(),
202        };
203
204        match storage {
205            StorageConfig::Environment { prefix } => {
206                assert_eq!(prefix, "MCP_AUTH");
207            }
208            _ => panic!("Expected Environment storage config"),
209        }
210    }
211
212    #[test]
213    fn test_storage_config_memory() {
214        let storage = StorageConfig::Memory;
215        assert!(matches!(storage, StorageConfig::Memory));
216    }
217
218    #[test]
219    fn test_auth_config_serialization() {
220        let config = AuthConfig {
221            storage: StorageConfig::File {
222                path: PathBuf::from("/test/path"),
223                file_permissions: 0o600,
224                dir_permissions: 0o700,
225                require_secure_filesystem: true,
226                enable_filesystem_monitoring: false,
227            },
228            enabled: true,
229            cache_size: 500,
230            session_timeout_secs: 7200,
231            max_failed_attempts: 3,
232            rate_limit_window_secs: 1800,
233        };
234
235        let json = serde_json::to_string(&config).unwrap();
236        let deserialized: AuthConfig = serde_json::from_str(&json).unwrap();
237
238        assert_eq!(deserialized.enabled, config.enabled);
239        assert_eq!(deserialized.cache_size, config.cache_size);
240        assert_eq!(
241            deserialized.session_timeout_secs,
242            config.session_timeout_secs
243        );
244        assert_eq!(deserialized.max_failed_attempts, config.max_failed_attempts);
245        assert_eq!(
246            deserialized.rate_limit_window_secs,
247            config.rate_limit_window_secs
248        );
249
250        match (config.storage, deserialized.storage) {
251            (
252                StorageConfig::File {
253                    path: p1,
254                    file_permissions: fp1,
255                    dir_permissions: dp1,
256                    ..
257                },
258                StorageConfig::File {
259                    path: p2,
260                    file_permissions: fp2,
261                    dir_permissions: dp2,
262                    ..
263                },
264            ) => {
265                assert_eq!(p1, p2);
266                assert_eq!(fp1, fp2);
267                assert_eq!(dp1, dp2);
268            }
269            _ => panic!("Storage configs don't match"),
270        }
271    }
272
273    #[test]
274    fn test_storage_config_file_with_defaults() {
275        let json = r#"{
276            "File": {
277                "path": "/test/path"
278            }
279        }"#;
280
281        let storage: StorageConfig = serde_json::from_str(json).unwrap();
282
283        match storage {
284            StorageConfig::File {
285                path,
286                file_permissions,
287                dir_permissions,
288                require_secure_filesystem,
289                enable_filesystem_monitoring,
290            } => {
291                assert_eq!(path, PathBuf::from("/test/path"));
292                assert_eq!(file_permissions, 0o600); // Default
293                assert_eq!(dir_permissions, 0o700); // Default
294                assert!(!require_secure_filesystem); // Default false
295                assert!(!enable_filesystem_monitoring); // Default false
296            }
297            _ => panic!("Expected File storage config"),
298        }
299    }
300
301    #[test]
302    fn test_storage_config_environment_serialization() {
303        let storage = StorageConfig::Environment {
304            prefix: "TEST_PREFIX".to_string(),
305        };
306
307        let json = serde_json::to_string(&storage).unwrap();
308        let deserialized: StorageConfig = serde_json::from_str(&json).unwrap();
309
310        match deserialized {
311            StorageConfig::Environment { prefix } => {
312                assert_eq!(prefix, "TEST_PREFIX");
313            }
314            _ => panic!("Expected Environment storage config"),
315        }
316    }
317
318    #[test]
319    fn test_storage_config_memory_serialization() {
320        let storage = StorageConfig::Memory;
321
322        let json = serde_json::to_string(&storage).unwrap();
323        let deserialized: StorageConfig = serde_json::from_str(&json).unwrap();
324
325        assert!(matches!(deserialized, StorageConfig::Memory));
326    }
327
328    #[test]
329    fn test_auth_config_custom_values() {
330        let config = AuthConfig {
331            storage: StorageConfig::Environment {
332                prefix: "CUSTOM".to_string(),
333            },
334            enabled: false,
335            cache_size: 2000,
336            session_timeout_secs: 1800,
337            max_failed_attempts: 10,
338            rate_limit_window_secs: 300,
339        };
340
341        assert!(!config.enabled);
342        assert_eq!(config.cache_size, 2000);
343        assert_eq!(config.session_timeout_secs, 1800);
344        assert_eq!(config.max_failed_attempts, 10);
345        assert_eq!(config.rate_limit_window_secs, 300);
346
347        match config.storage {
348            StorageConfig::Environment { prefix } => {
349                assert_eq!(prefix, "CUSTOM");
350            }
351            _ => panic!("Expected Environment storage"),
352        }
353    }
354
355    #[test]
356    fn test_auth_config_clone() {
357        let original = AuthConfig::default();
358        let cloned = original.clone();
359
360        assert_eq!(cloned.enabled, original.enabled);
361        assert_eq!(cloned.cache_size, original.cache_size);
362        assert_eq!(cloned.session_timeout_secs, original.session_timeout_secs);
363        assert_eq!(cloned.max_failed_attempts, original.max_failed_attempts);
364        assert_eq!(
365            cloned.rate_limit_window_secs,
366            original.rate_limit_window_secs
367        );
368    }
369
370    #[test]
371    fn test_storage_config_debug() {
372        let file_storage = StorageConfig::File {
373            path: PathBuf::from("/test"),
374            file_permissions: 0o600,
375            dir_permissions: 0o700,
376            require_secure_filesystem: true,
377            enable_filesystem_monitoring: false,
378        };
379
380        let debug_str = format!("{:?}", file_storage);
381        assert!(debug_str.contains("File"));
382        assert!(debug_str.contains("/test"));
383        // The debug output for 0o600 is "384" in decimal, not "600"
384        assert!(debug_str.contains("384"));
385
386        let env_storage = StorageConfig::Environment {
387            prefix: "TEST".to_string(),
388        };
389
390        let debug_str = format!("{:?}", env_storage);
391        assert!(debug_str.contains("Environment"));
392        assert!(debug_str.contains("TEST"));
393
394        let memory_storage = StorageConfig::Memory;
395        let debug_str = format!("{:?}", memory_storage);
396        assert!(debug_str.contains("Memory"));
397    }
398}