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
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 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); 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); assert_eq!(dir_permissions, 0o700); assert!(!require_secure_filesystem); assert!(!enable_filesystem_monitoring); }
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 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}