airsprotocols_mcpserver_filesystem/config/
settings.rs

1//! Configuration settings management for AIRS MCP-FS
2
3// Layer 1: Standard library imports
4use std::collections::HashMap;
5
6// Layer 2: Third-party crate imports
7use anyhow::Context;
8use serde::{Deserialize, Serialize};
9
10// Layer 3: Internal module imports
11// (None needed yet)
12use crate::config::loader::ConfigurationLoader;
13
14/// Main configuration structure for AIRS MCP-FS
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct Settings {
17    /// Security configuration
18    pub security: SecurityConfig,
19    /// Binary processing settings
20    pub binary: BinaryConfig,
21    /// MCP server configuration
22    pub server: ServerConfig,
23}
24
25/// Security-related configuration with comprehensive policy framework
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct SecurityConfig {
28    /// Filesystem access configuration
29    pub filesystem: FilesystemConfig,
30    /// Operation-level security rules
31    pub operations: OperationConfig,
32    /// Named security policies for different file types and patterns
33    pub policies: HashMap<String, SecurityPolicy>,
34}
35
36/// Filesystem access control configuration
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct FilesystemConfig {
39    /// Allowed file paths patterns (glob syntax)
40    pub allowed_paths: Vec<String>,
41    /// Denied file paths patterns (glob syntax, takes precedence)
42    pub denied_paths: Vec<String>,
43}
44
45/// Operation-level security configuration
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct OperationConfig {
48    /// Allow read operations in allowed_paths
49    pub read_allowed: bool,
50    /// Write operations require explicit policy match
51    pub write_requires_policy: bool,
52    /// Delete operations require explicit "delete" permission in policy
53    pub delete_requires_explicit_allow: bool,
54    /// Directory creation allowed in allowed_paths
55    pub create_dir_allowed: bool,
56}
57
58/// Named security policy for specific file patterns and operations
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct SecurityPolicy {
61    /// File patterns this policy applies to (glob syntax)
62    pub patterns: Vec<String>,
63    /// Allowed operations for files matching patterns
64    pub operations: Vec<String>,
65    /// Risk level for audit logging and monitoring
66    pub risk_level: RiskLevel,
67    /// Optional description of this policy
68    #[serde(default)]
69    pub description: Option<String>,
70}
71
72/// Risk level for operations and audit logging
73#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
74#[serde(rename_all = "lowercase")]
75pub enum RiskLevel {
76    /// Low risk operations (normal source code, documentation)
77    Low,
78    /// Medium risk operations (configuration files, build scripts)
79    Medium,
80    /// High risk operations (system files, credentials)
81    High,
82    /// Critical risk operations (security-sensitive files)
83    Critical,
84}
85
86/// Binary processing configuration (Security Hardened)
87/// Note: Binary file processing is disabled for security reasons
88#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct BinaryConfig {
90    /// Maximum file size in bytes for text files
91    pub max_file_size: u64,
92    /// Binary file processing is permanently disabled for security
93    /// This field is kept for configuration compatibility but ignored
94    #[serde(default = "default_false")]
95    pub binary_processing_disabled: bool,
96}
97
98/// MCP server configuration
99#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct ServerConfig {
101    /// Server name for MCP capabilities
102    pub name: String,
103    /// Server version
104    pub version: String,
105}
106
107/// Builder for creating Settings with different security configurations
108pub struct SettingsBuilder {
109    security_mode: SecurityMode,
110}
111
112/// Security mode for configuring Settings behavior
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
114pub enum SecurityMode {
115    /// Production mode: Secure by default, requires explicit policies for sensitive operations
116    Production,
117    /// Development mode: Balanced security, allows most development tasks
118    Development,
119    /// Permissive mode: Minimal restrictions, suitable for testing and development
120    Permissive,
121}
122
123impl SettingsBuilder {
124    /// Create a new builder with production security mode (secure by default)
125    pub fn new() -> Self {
126        Self {
127            security_mode: SecurityMode::Production,
128        }
129    }
130
131    /// Set security mode to production (secure by default)
132    pub fn secure(mut self) -> Self {
133        self.security_mode = SecurityMode::Production;
134        self
135    }
136
137    /// Set security mode to development (balanced security)
138    pub fn development(mut self) -> Self {
139        self.security_mode = SecurityMode::Development;
140        self
141    }
142
143    /// Set security mode to permissive (minimal restrictions)
144    pub fn permissive(mut self) -> Self {
145        self.security_mode = SecurityMode::Permissive;
146        self
147    }
148
149    /// Build the Settings with the configured security mode
150    pub fn build(self) -> Settings {
151        // Create default security policies that apply to all modes
152        let mut policies = HashMap::new();
153
154        // Source code policy - low risk, read and write allowed
155        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        // Documentation policy - low risk, read and write allowed
170        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        // Configuration policy - medium risk, read and write with caution
185        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        // Build artifacts policy - low risk, delete allowed for cleanup
200        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        // Configure security settings based on mode
218        let (allowed_paths, write_requires_policy, delete_requires_explicit_allow) =
219            match self.security_mode {
220                SecurityMode::Permissive => {
221                    // Permissive mode: Allow all operations, suitable for testing
222                    policies.insert(
223                        "permissive_universal".to_string(),
224                        SecurityPolicy {
225                            patterns: vec!["**/*".to_string()], // Match all paths
226                            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(), // Allow all absolute paths
245                            "**/*".to_string(),  // Allow all relative paths
246                        ],
247                        false, // Don't require policies for writes
248                        false, // Don't require explicit delete permissions
249                    )
250                }
251                SecurityMode::Development => {
252                    // Development mode: Balanced security, reasonable for development work
253                    (
254                        vec![
255                            "~/projects/**/*".to_string(),
256                            "~/Documents/**/*".to_string(),
257                            "~/Desktop/**/*".to_string(),
258                            "./**/*".to_string(), // Current directory and subdirectories
259                        ],
260                        false, // Allow writes without strict policy requirements
261                        true,  // Still require explicit delete permissions for safety
262                    )
263                }
264                SecurityMode::Production => {
265                    // Production mode: Secure by default
266                    (
267                        vec![
268                            "~/projects/**/*".to_string(),
269                            "~/Documents/**/*.{md,txt,rst}".to_string(),
270                        ],
271                        true, // Require policies for writes
272                        true, // Require explicit delete permissions
273                    )
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(),         // Hidden directories
285                        "**/id_rsa*".to_string(),      // SSH keys
286                        "**/credentials*".to_string(), // Credential files
287                        "**/secrets*".to_string(),     // Secret files
288                    ],
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, // 100MB for text files
300                binary_processing_disabled: true, // Security hardening - always disabled
301            },
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    /// Default Settings use production security mode (secure by default)
318    fn default() -> Self {
319        SettingsBuilder::new().secure().build()
320    }
321}
322
323impl Settings {
324    /// Create a new SettingsBuilder for configuring security modes
325    pub fn builder() -> SettingsBuilder {
326        SettingsBuilder::new()
327    }
328
329    /// Load settings from configuration file or use defaults with validation
330    pub fn load() -> anyhow::Result<Self> {
331        // Use the new configuration loader for real configuration loading
332        let loader = ConfigurationLoader::new();
333        let (settings, source_info) = loader
334            .load()
335            .context("Failed to load configuration using ConfigurationLoader")?;
336
337        // Log configuration source information in non-test mode
338        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        // Validate the loaded configuration before returning
358        Self::validate_and_warn(&settings)?;
359
360        Ok(settings)
361    }
362
363    /// Validate configuration and display warnings/errors
364    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        // Log warnings if any
371        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 there are errors, fail the configuration load
379        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        // In non-test mode, also log a success message
391        if !cfg!(test) && validation_result.warnings.is_empty() {
392            tracing::info!("✅ Configuration validation passed");
393        }
394
395        Ok(())
396    }
397
398    /// Validate configuration and return detailed results for programmatic use
399    pub fn validate(&self) -> anyhow::Result<crate::config::validation::ValidationResult> {
400        use crate::config::validation::ConfigurationValidator;
401        ConfigurationValidator::validate_settings(self)
402    }
403}
404
405/// Helper function for serde default values
406fn 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        // Default should be secure (production mode)
422        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); // Security hardening
427
428        // Test that security policies are properly configured
429        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        // Test production mode (secure)
438        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        // Test development mode (balanced)
453        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        // Test permissive mode (minimal restrictions)
463        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        // Test that builder defaults to secure mode
485        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}