1use std::collections::HashMap;
5
6use anyhow::Context;
8use serde::{Deserialize, Serialize};
9
10use crate::config::loader::ConfigurationLoader;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct Settings {
17 pub security: SecurityConfig,
19 pub binary: BinaryConfig,
21 pub server: ServerConfig,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct SecurityConfig {
28 pub filesystem: FilesystemConfig,
30 pub operations: OperationConfig,
32 pub policies: HashMap<String, SecurityPolicy>,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct FilesystemConfig {
39 pub allowed_paths: Vec<String>,
41 pub denied_paths: Vec<String>,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct OperationConfig {
48 pub read_allowed: bool,
50 pub write_requires_policy: bool,
52 pub delete_requires_explicit_allow: bool,
54 pub create_dir_allowed: bool,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct SecurityPolicy {
61 pub patterns: Vec<String>,
63 pub operations: Vec<String>,
65 pub risk_level: RiskLevel,
67 #[serde(default)]
69 pub description: Option<String>,
70}
71
72#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
74#[serde(rename_all = "lowercase")]
75pub enum RiskLevel {
76 Low,
78 Medium,
80 High,
82 Critical,
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct BinaryConfig {
90 pub max_file_size: u64,
92 #[serde(default = "default_false")]
95 pub binary_processing_disabled: bool,
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct ServerConfig {
101 pub name: String,
103 pub version: String,
105}
106
107pub struct SettingsBuilder {
109 security_mode: SecurityMode,
110}
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
114pub enum SecurityMode {
115 Production,
117 Development,
119 Permissive,
121}
122
123impl SettingsBuilder {
124 pub fn new() -> Self {
126 Self {
127 security_mode: SecurityMode::Production,
128 }
129 }
130
131 pub fn secure(mut self) -> Self {
133 self.security_mode = SecurityMode::Production;
134 self
135 }
136
137 pub fn development(mut self) -> Self {
139 self.security_mode = SecurityMode::Development;
140 self
141 }
142
143 pub fn permissive(mut self) -> Self {
145 self.security_mode = SecurityMode::Permissive;
146 self
147 }
148
149 pub fn build(self) -> Settings {
151 let mut policies = HashMap::new();
153
154 policies.insert(
156 "source_code".to_string(),
157 SecurityPolicy {
158 patterns: vec![
159 "**/*.{rs,py,js,ts,jsx,tsx}".to_string(),
160 "**/*.{c,cpp,h,hpp}".to_string(),
161 "**/*.{java,kt,scala}".to_string(),
162 ],
163 operations: vec!["read".to_string(), "write".to_string()],
164 risk_level: RiskLevel::Low,
165 description: Some("Source code files - safe for development".to_string()),
166 },
167 );
168
169 policies.insert(
171 "documentation".to_string(),
172 SecurityPolicy {
173 patterns: vec![
174 "**/*.{md,txt,rst}".to_string(),
175 "**/README*".to_string(),
176 "**/CHANGELOG*".to_string(),
177 ],
178 operations: vec!["read".to_string(), "write".to_string()],
179 risk_level: RiskLevel::Low,
180 description: Some("Documentation files - safe for editing".to_string()),
181 },
182 );
183
184 policies.insert(
186 "config_files".to_string(),
187 SecurityPolicy {
188 patterns: vec![
189 "**/Cargo.toml".to_string(),
190 "**/*.{json,yaml,yml,toml}".to_string(),
191 "**/*.{xml,ini,conf}".to_string(),
192 ],
193 operations: vec!["read".to_string(), "write".to_string()],
194 risk_level: RiskLevel::Medium,
195 description: Some("Configuration files - moderate risk".to_string()),
196 },
197 );
198
199 policies.insert(
201 "build_artifacts".to_string(),
202 SecurityPolicy {
203 patterns: vec![
204 "**/target/**".to_string(),
205 "**/dist/**".to_string(),
206 "**/build/**".to_string(),
207 "**/*.{tmp,bak,log}".to_string(),
208 ],
209 operations: vec!["read".to_string(), "delete".to_string()],
210 risk_level: RiskLevel::Low,
211 description: Some(
212 "Build artifacts and temporary files - safe to clean".to_string(),
213 ),
214 },
215 );
216
217 let (allowed_paths, write_requires_policy, delete_requires_explicit_allow) =
219 match self.security_mode {
220 SecurityMode::Permissive => {
221 policies.insert(
223 "permissive_universal".to_string(),
224 SecurityPolicy {
225 patterns: vec!["**/*".to_string()], operations: vec![
227 "read".to_string(),
228 "write".to_string(),
229 "delete".to_string(),
230 "list".to_string(),
231 "create_dir".to_string(),
232 "move".to_string(),
233 "copy".to_string(),
234 ],
235 risk_level: RiskLevel::Low,
236 description: Some(
237 "Universal permissive policy - allows all operations".to_string(),
238 ),
239 },
240 );
241
242 (
243 vec![
244 "/**/*".to_string(), "**/*".to_string(), ],
247 false, false, )
250 }
251 SecurityMode::Development => {
252 (
254 vec![
255 "~/projects/**/*".to_string(),
256 "~/Documents/**/*".to_string(),
257 "~/Desktop/**/*".to_string(),
258 "./**/*".to_string(), ],
260 false, true, )
263 }
264 SecurityMode::Production => {
265 (
267 vec![
268 "~/projects/**/*".to_string(),
269 "~/Documents/**/*.{md,txt,rst}".to_string(),
270 ],
271 true, true, )
274 }
275 };
276
277 Settings {
278 security: SecurityConfig {
279 filesystem: FilesystemConfig {
280 allowed_paths,
281 denied_paths: vec![
282 "**/.git/**".to_string(),
283 "**/.env*".to_string(),
284 "~/.*/**".to_string(), "**/id_rsa*".to_string(), "**/credentials*".to_string(), "**/secrets*".to_string(), ],
289 },
290 operations: OperationConfig {
291 read_allowed: true,
292 write_requires_policy,
293 delete_requires_explicit_allow,
294 create_dir_allowed: true,
295 },
296 policies,
297 },
298 binary: BinaryConfig {
299 max_file_size: 100 * 1024 * 1024, binary_processing_disabled: true, },
302 server: ServerConfig {
303 name: "airsprotocols-mcpserver-filesystem".to_string(),
304 version: env!("CARGO_PKG_VERSION").to_string(),
305 },
306 }
307 }
308}
309
310impl Default for SettingsBuilder {
311 fn default() -> Self {
312 Self::new()
313 }
314}
315
316impl Default for Settings {
317 fn default() -> Self {
319 SettingsBuilder::new().secure().build()
320 }
321}
322
323impl Settings {
324 pub fn builder() -> SettingsBuilder {
326 SettingsBuilder::new()
327 }
328
329 pub fn load() -> anyhow::Result<Self> {
331 let loader = ConfigurationLoader::new();
333 let (settings, source_info) = loader
334 .load()
335 .context("Failed to load configuration using ConfigurationLoader")?;
336
337 if !cfg!(test) {
339 tracing::info!(
340 "📋 Configuration loaded from {} environment",
341 source_info.environment
342 );
343 if !source_info.files.is_empty() {
344 tracing::info!(" Configuration files: {:?}", source_info.files);
345 }
346 if !source_info.env_vars.is_empty() {
347 tracing::info!(
348 " Environment variables: {} overrides",
349 source_info.env_vars.len()
350 );
351 }
352 if source_info.uses_defaults {
353 tracing::info!(" Using built-in defaults as base configuration");
354 }
355 }
356
357 Self::validate_and_warn(&settings)?;
359
360 Ok(settings)
361 }
362
363 pub fn validate_and_warn(settings: &Settings) -> anyhow::Result<()> {
365 use crate::config::validation::ConfigurationValidator;
366
367 let validation_result = ConfigurationValidator::validate_settings(settings)
368 .context("Failed to validate configuration")?;
369
370 if !validation_result.warnings.is_empty() {
372 tracing::warn!("Configuration warnings:");
373 for warning in &validation_result.warnings {
374 tracing::warn!(" ⚠️ {warning}");
375 }
376 }
377
378 if !validation_result.is_valid {
380 tracing::error!("Configuration errors:");
381 for error in &validation_result.errors {
382 tracing::error!(" ❌ {error}");
383 }
384 return Err(anyhow::anyhow!(
385 "Configuration validation failed with {} error(s)",
386 validation_result.errors.len()
387 ));
388 }
389
390 if !cfg!(test) && validation_result.warnings.is_empty() {
392 tracing::info!("✅ Configuration validation passed");
393 }
394
395 Ok(())
396 }
397
398 pub fn validate(&self) -> anyhow::Result<crate::config::validation::ValidationResult> {
400 use crate::config::validation::ConfigurationValidator;
401 ConfigurationValidator::validate_settings(self)
402 }
403}
404
405fn default_false() -> bool {
407 false
408}
409
410#[cfg(test)]
411#[allow(clippy::unwrap_used)]
412mod tests {
413 use super::*;
414
415 #[test]
416 fn test_default_settings() {
417 let settings = Settings::default();
418
419 assert_eq!(settings.server.name, "airsprotocols-mcpserver-filesystem");
420
421 assert!(settings.security.operations.write_requires_policy);
423 assert!(settings.security.operations.delete_requires_explicit_allow);
424
425 assert_eq!(settings.binary.max_file_size, 100 * 1024 * 1024);
426 assert!(settings.binary.binary_processing_disabled); assert!(settings.security.policies.contains_key("source_code"));
430 assert!(settings.security.policies.contains_key("documentation"));
431 assert!(settings.security.policies.contains_key("config_files"));
432 assert!(settings.security.policies.contains_key("build_artifacts"));
433 }
434
435 #[test]
436 fn test_settings_builder_modes() {
437 let production_settings = Settings::builder().secure().build();
439 assert!(
440 production_settings
441 .security
442 .operations
443 .write_requires_policy
444 );
445 assert!(
446 production_settings
447 .security
448 .operations
449 .delete_requires_explicit_allow
450 );
451
452 let dev_settings = Settings::builder().development().build();
454 assert!(!dev_settings.security.operations.write_requires_policy);
455 assert!(
456 dev_settings
457 .security
458 .operations
459 .delete_requires_explicit_allow
460 );
461
462 let permissive_settings = Settings::builder().permissive().build();
464 assert!(
465 !permissive_settings
466 .security
467 .operations
468 .write_requires_policy
469 );
470 assert!(
471 !permissive_settings
472 .security
473 .operations
474 .delete_requires_explicit_allow
475 );
476 assert!(permissive_settings
477 .security
478 .policies
479 .contains_key("permissive_universal"));
480 }
481
482 #[test]
483 fn test_settings_builder_default() {
484 let default_builder_settings = Settings::builder().build();
486 let explicit_secure_settings = Settings::builder().secure().build();
487
488 assert_eq!(
489 default_builder_settings
490 .security
491 .operations
492 .write_requires_policy,
493 explicit_secure_settings
494 .security
495 .operations
496 .write_requires_policy
497 );
498 assert_eq!(
499 default_builder_settings
500 .security
501 .operations
502 .delete_requires_explicit_allow,
503 explicit_secure_settings
504 .security
505 .operations
506 .delete_requires_explicit_allow
507 );
508 }
509
510 #[test]
511 fn test_settings_load() {
512 let result = Settings::load();
513 assert!(result.is_ok());
514
515 let settings = result.unwrap();
516 assert_eq!(settings.server.name, "airsprotocols-mcpserver-filesystem");
517 }
518
519 #[test]
520 fn test_settings_validation() {
521 let settings = Settings::default();
522 let validation_result = settings.validate();
523
524 assert!(validation_result.is_ok());
525 let result = validation_result.unwrap();
526 assert!(
527 result.is_valid,
528 "Default settings should be valid. Errors: {:?}",
529 result.errors
530 );
531 }
532
533 #[test]
534 fn test_validate_and_warn_success() {
535 let settings = Settings::default();
536 let result = Settings::validate_and_warn(&settings);
537 assert!(
538 result.is_ok(),
539 "validate_and_warn should succeed for default settings"
540 );
541 }
542}