airsprotocols_mcpserver_filesystem/security/
manager.rs

1//! Security manager for access control and validation
2
3// Layer 1: Standard library imports
4use std::sync::Arc;
5use std::time::Instant;
6
7// Layer 2: Third-party crate imports
8use anyhow::Result;
9use globset;
10
11// Layer 3: Internal module imports
12use crate::binary::format::{FileFormat, FormatDetector};
13use crate::config::settings::{RiskLevel, SecurityConfig};
14use crate::filesystem::{
15    validation::{PathValidator, SecurityError},
16    FileOperation,
17};
18use crate::mcp::OperationType;
19use crate::security::approval::ApprovalDecision;
20use crate::security::audit::{AuditLogger, CorrelationId};
21use crate::security::permissions::{
22    PathPermissionRule, PathPermissionValidator, PermissionEvaluation, PermissionLevel,
23};
24use crate::security::policy::PolicyEngine;
25
26/// Main security manager for filesystem operations
27#[derive(Debug)]
28pub struct SecurityManager {
29    path_validator: PathValidator,
30    policy_engine: PolicyEngine,
31    permission_validator: PathPermissionValidator,
32    audit_logger: AuditLogger,
33    format_detector: FormatDetector,
34    config: Arc<SecurityConfig>,
35}
36
37impl SecurityManager {
38    /// Create a new security manager with configuration
39    pub fn new(config: SecurityConfig) -> Result<Self> {
40        let path_validator = PathValidator::new(
41            config.filesystem.allowed_paths.clone(),
42            config.filesystem.denied_paths.clone(),
43        );
44
45        // Create policy engine from configured policies
46        let policy_engine = PolicyEngine::new(config.policies.clone())?;
47
48        // Create permission validator in strict mode for security-first approach
49        // Only policies should define permissions - no auto-generated rules
50        let mut permission_validator = PathPermissionValidator::new(true); // strict mode
51
52        // Convert security policies to permission rules
53        // This ensures all permissions are explicitly defined in configuration
54        for (name, policy) in &config.policies {
55            permission_validator.add_policy(name.clone(), policy.clone());
56
57            // Create permission rules from policy patterns and operations
58            for pattern in &policy.patterns {
59                // Determine permission level based on operations allowed
60                let permission_level = if policy.operations.contains(&"delete".to_string()) {
61                    PermissionLevel::Full
62                } else if policy.operations.contains(&"write".to_string()) {
63                    PermissionLevel::ReadWrite
64                } else if policy.operations.contains(&"read".to_string()) {
65                    PermissionLevel::ReadOnly
66                } else {
67                    PermissionLevel::None
68                };
69
70                let rule = PathPermissionRule::new(
71                    pattern.clone(),
72                    permission_level,
73                    policy.operations.iter().map(|s| s.as_str()).collect(),
74                    100, // Standard priority for policy-based rules
75                    format!("Policy '{name}' rule for pattern: {pattern}"),
76                )?;
77
78                permission_validator.add_rule(rule);
79            }
80        }
81
82        Ok(Self {
83            path_validator,
84            policy_engine,
85            permission_validator,
86            audit_logger: AuditLogger::new(),
87            format_detector: FormatDetector::new(),
88            config: Arc::new(config),
89        })
90    }
91
92    /// Validate operation-specific permissions
93    ///
94    /// This method provides granular validation for specific operation types,
95    /// integrating with the path permission system and policy engine.
96    pub async fn validate_operation_permission(
97        &self,
98        operation: &FileOperation,
99    ) -> Result<ApprovalDecision> {
100        let correlation_id = CorrelationId::new();
101        let start_time = Instant::now();
102
103        // Log operation request
104        self.audit_logger
105            .log_operation_requested(correlation_id, operation);
106
107        // 0. Validate binary file restrictions (security hardening)
108        if let Err(err) = self
109            .validate_binary_file_restriction(&operation.path, correlation_id)
110            .await
111        {
112            let execution_time_ms = start_time.elapsed().as_millis() as u64;
113            self.audit_logger.log_operation_failed(
114                correlation_id,
115                operation,
116                &err.to_string(),
117                execution_time_ms,
118            );
119            return Err(err);
120        }
121
122        // 1. Validate basic path security first
123        match self.path_validator.validate_path(&operation.path) {
124            Ok(_validated_path) => {}
125            Err(security_error) => {
126                let execution_time_ms = start_time.elapsed().as_millis() as u64;
127                let error_msg = match security_error {
128                    SecurityError::AccessDenied => "Access denied",
129                    SecurityError::InvalidInput => "Invalid input detected",
130                    SecurityError::PolicyViolation => "Security policy violation",
131                };
132                self.audit_logger.log_operation_failed(
133                    correlation_id,
134                    operation,
135                    error_msg,
136                    execution_time_ms,
137                );
138                return Err(anyhow::anyhow!("{}", error_msg));
139            }
140        }
141
142        // 2. Validate operation-specific permissions
143        let operations = std::iter::once(operation.operation_type).collect();
144        let permission_result = self.permission_validator.evaluate_permissions(
145            &operation.path,
146            &operations,
147            Some(&format!(
148                "operation_validation_{:?}",
149                operation.operation_type
150            )),
151        );
152
153        if !permission_result.allowed {
154            let execution_time_ms = start_time.elapsed().as_millis() as u64;
155            let reason = format!(
156                "Operation {:?} denied by permission system: {}",
157                operation.operation_type, permission_result.decision_reason
158            );
159            self.audit_logger.log_operation_failed(
160                correlation_id,
161                operation,
162                &reason,
163                execution_time_ms,
164            );
165            return Err(anyhow::anyhow!("{}", reason));
166        }
167
168        // 3. Apply operation-specific configuration rules
169        let operation_allowed = match operation.operation_type {
170            OperationType::Read => {
171                // Read operations: always allowed if path permissions pass
172                self.config.operations.read_allowed
173            }
174            OperationType::Write => {
175                // Write operations: check if policy is required
176                if self.config.operations.write_requires_policy {
177                    // Policy validation required for writes
178                    self.validate_operation_against_policies(operation, correlation_id)
179                        .await?
180                } else {
181                    // Write allowed by configuration
182                    true
183                }
184            }
185            OperationType::Delete => {
186                // Delete operations: check explicit allow requirement
187                if self.config.operations.delete_requires_explicit_allow {
188                    // Must have explicit delete permission in a policy
189                    self.validate_delete_permission(operation, correlation_id)
190                        .await?
191                } else {
192                    // Delete allowed by configuration
193                    true
194                }
195            }
196            OperationType::CreateDir => {
197                // Directory creation: check configuration
198                self.config.operations.create_dir_allowed
199            }
200            OperationType::List | OperationType::Move | OperationType::Copy => {
201                // Other operations: allowed if permission system passes
202                true
203            }
204        };
205
206        if !operation_allowed {
207            let execution_time_ms = start_time.elapsed().as_millis() as u64;
208            let reason = format!(
209                "Operation {:?} denied by operation configuration",
210                operation.operation_type
211            );
212            self.audit_logger.log_operation_failed(
213                correlation_id,
214                operation,
215                &reason,
216                execution_time_ms,
217            );
218            return Err(anyhow::anyhow!("{}", reason));
219        }
220
221        // 4. Final policy engine validation
222        let policy_start = Instant::now();
223        let policy_decision = self.policy_engine.evaluate_operation(operation);
224        let policy_time_ms = policy_start.elapsed().as_millis() as u64;
225
226        // Log policy evaluation
227        self.audit_logger.log_policy_evaluated(
228            correlation_id,
229            &policy_decision,
230            None,
231            policy_time_ms,
232        );
233
234        let execution_time_ms = start_time.elapsed().as_millis() as u64;
235
236        if policy_decision.is_allowed() {
237            // Log successful operation
238            self.audit_logger.log_operation_completed(
239                correlation_id,
240                operation,
241                execution_time_ms,
242                None,
243            );
244            Ok(ApprovalDecision::Approved)
245        } else {
246            // Log failed operation
247            let reason = format!(
248                "Operation {:?} denied by policy engine: {}",
249                operation.operation_type,
250                policy_decision.reason()
251            );
252            self.audit_logger.log_operation_failed(
253                correlation_id,
254                operation,
255                &reason,
256                execution_time_ms,
257            );
258            Err(anyhow::anyhow!("{}", reason))
259        }
260    }
261
262    /// Validate binary file restrictions for security hardening
263    /// This method rejects all binary file operations to prevent security risks
264    async fn validate_binary_file_restriction(
265        &self,
266        path: &std::path::Path,
267        correlation_id: CorrelationId,
268    ) -> Result<()> {
269        // First, try to detect format from file extension (fast check)
270        let format_from_extension = self.format_detector.detect_from_extension(path);
271
272        // Check if it's a known binary format based on extension
273        if self.is_binary_format(&format_from_extension) {
274            let error_msg = format!(
275                "Binary file access denied for security reasons: {} (detected format: {:?})",
276                path.display(),
277                format_from_extension
278            );
279
280            // Log security violation for binary file access attempt
281            self.audit_logger.log_security_violation(
282                correlation_id,
283                "binary_file_denied",
284                path,
285                &error_msg,
286                RiskLevel::High,
287            );
288
289            return Err(anyhow::anyhow!("{}", error_msg));
290        }
291
292        // For file operations on existing files, read a small sample to verify format
293        // Only check if file exists and we can read it safely
294        if path.exists() && path.is_file() {
295            match std::fs::read(path) {
296                Ok(bytes) if !bytes.is_empty() => {
297                    // Only check first 512 bytes for format detection (efficient)
298                    let sample_size = std::cmp::min(bytes.len(), 512);
299                    let format_from_content = self
300                        .format_detector
301                        .detect_from_bytes(&bytes[..sample_size]);
302
303                    if self.is_binary_format(&format_from_content) {
304                        let error_msg = format!(
305                            "Binary file access denied for security reasons: {} (content analysis: {:?})",
306                            path.display(),
307                            format_from_content
308                        );
309
310                        // Log security violation for binary content detection
311                        self.audit_logger.log_security_violation(
312                            correlation_id,
313                            "binary_content_denied",
314                            path,
315                            &error_msg,
316                            RiskLevel::High,
317                        );
318
319                        return Err(anyhow::anyhow!("{}", error_msg));
320                    }
321                }
322                Err(_) => {
323                    // If we can't read the file, let the regular file system handle the error
324                    // This avoids false positives for permission issues
325                }
326                Ok(_) => {
327                    // Empty file - allow operation (common for new text files)
328                }
329            }
330        }
331
332        // File is safe (text or unknown but not detectably binary)
333        Ok(())
334    }
335
336    /// Helper method to determine if a file format is considered binary
337    fn is_binary_format(&self, format: &FileFormat) -> bool {
338        match format {
339            // All image formats are binary
340            FileFormat::Jpeg
341            | FileFormat::Png
342            | FileFormat::Gif
343            | FileFormat::WebP
344            | FileFormat::Tiff
345            | FileFormat::Bmp => true,
346
347            // Document formats are binary
348            FileFormat::Pdf => true,
349
350            // Text formats are allowed
351            FileFormat::Text => false,
352
353            // Unknown formats are allowed (benefit of the doubt for text files)
354            // This prevents false positives while maintaining security
355            FileFormat::Unknown => false,
356        }
357    }
358
359    /// Validate operation against security policies (for write operations)
360    async fn validate_operation_against_policies(
361        &self,
362        operation: &FileOperation,
363        correlation_id: CorrelationId,
364    ) -> Result<bool> {
365        // Check if any policy explicitly allows this operation
366        for policy in self.config.policies.values() {
367            // Check if the file path matches any pattern in this policy
368            for pattern in &policy.patterns {
369                if let Ok(glob) = globset::Glob::new(pattern) {
370                    if glob.compile_matcher().is_match(&operation.path) {
371                        // Check if the operation is allowed by this policy
372                        let operation_name = operation.operation_type.as_str();
373
374                        if policy.operations.contains(&operation_name.to_string()) {
375                            // Log successful policy match as information (not a violation)
376                            // Use the existing audit infrastructure for successful operations
377                            return Ok(true);
378                        }
379                    }
380                }
381            }
382        }
383
384        // No policy allows this operation - log as security violation
385        self.audit_logger.log_security_violation(
386            correlation_id,
387            "policy_denied",
388            &operation.path,
389            &format!(
390                "Operation {:?} denied - no matching policy found",
391                operation.operation_type
392            ),
393            RiskLevel::High,
394        );
395
396        Ok(false)
397    }
398
399    /// Validate explicit delete permission requirement
400    async fn validate_delete_permission(
401        &self,
402        operation: &FileOperation,
403        correlation_id: CorrelationId,
404    ) -> Result<bool> {
405        // For delete operations, we need explicit "delete" permission in a policy
406        for policy in self.config.policies.values() {
407            // Check if the file path matches any pattern in this policy
408            for pattern in &policy.patterns {
409                if let Ok(glob) = globset::Glob::new(pattern) {
410                    if glob.compile_matcher().is_match(&operation.path) {
411                        // Check if delete is explicitly allowed
412                        if policy.operations.contains(&"delete".to_string()) {
413                            // Log successful delete permission (not a violation)
414                            return Ok(true);
415                        }
416                    }
417                }
418            }
419        }
420
421        // No explicit delete permission found - log as security violation
422        self.audit_logger.log_security_violation(
423            correlation_id,
424            "delete_denied",
425            &operation.path,
426            "Delete operation denied - no explicit delete permission found",
427            RiskLevel::High,
428        );
429
430        Ok(false)
431    }
432
433    /// Validate read access to a path
434    pub async fn validate_read_access(&self, operation: &FileOperation) -> Result<()> {
435        let correlation_id = CorrelationId::new();
436        let start_time = Instant::now();
437
438        // Log operation request
439        self.audit_logger
440            .log_operation_requested(correlation_id, operation);
441
442        // Validate path security
443        match self.path_validator.validate_path(&operation.path) {
444            Ok(_validated_path) => {}
445            Err(security_error) => {
446                let execution_time_ms = start_time.elapsed().as_millis() as u64;
447                let error_msg = match security_error {
448                    SecurityError::AccessDenied => "Access denied",
449                    SecurityError::InvalidInput => "Invalid input detected",
450                    SecurityError::PolicyViolation => "Security policy violation",
451                };
452                self.audit_logger.log_operation_failed(
453                    correlation_id,
454                    operation,
455                    error_msg,
456                    execution_time_ms,
457                );
458                return Err(anyhow::anyhow!("{}", error_msg));
459            }
460        }
461
462        // Validate path permissions
463        let operations = std::iter::once(operation.operation_type).collect();
464        let permission_result = self.permission_validator.evaluate_permissions(
465            &operation.path,
466            &operations,
467            Some("read_access_validation"),
468        );
469
470        if !permission_result.allowed {
471            let execution_time_ms = start_time.elapsed().as_millis() as u64;
472            self.audit_logger.log_operation_failed(
473                correlation_id,
474                operation,
475                &format!("Permission denied: {}", permission_result.decision_reason),
476                execution_time_ms,
477            );
478            return Err(anyhow::anyhow!(
479                "Permission denied: {}",
480                permission_result.decision_reason
481            ));
482        }
483
484        // Use policy engine to validate read access
485        let policy_start = Instant::now();
486        let policy_decision = self.policy_engine.evaluate_operation(operation);
487        let policy_time_ms = policy_start.elapsed().as_millis() as u64;
488
489        // Log policy evaluation
490        self.audit_logger.log_policy_evaluated(
491            correlation_id,
492            &policy_decision,
493            None, // Policy name will be included in the decision
494            policy_time_ms,
495        );
496
497        let execution_time_ms = start_time.elapsed().as_millis() as u64;
498
499        if policy_decision.is_allowed() {
500            // Log successful operation
501            self.audit_logger.log_operation_completed(
502                correlation_id,
503                operation,
504                execution_time_ms,
505                None, // No size information available
506            );
507            Ok(())
508        } else {
509            // Log failed operation
510            self.audit_logger.log_operation_failed(
511                correlation_id,
512                operation,
513                &format!("Policy denied read access: {}", policy_decision.reason()),
514                execution_time_ms,
515            );
516            Err(anyhow::anyhow!(
517                "Policy denied read access: {}",
518                policy_decision.reason()
519            ))
520        }
521    }
522
523    /// Validate write access to a path (requires policy evaluation)
524    pub async fn validate_write_access(
525        &self,
526        operation: &FileOperation,
527    ) -> Result<ApprovalDecision> {
528        let correlation_id = CorrelationId::new();
529        let start_time = Instant::now();
530
531        // Log operation request
532        self.audit_logger
533            .log_operation_requested(correlation_id, operation);
534
535        // Validate path security first
536        match self.path_validator.validate_path(&operation.path) {
537            Ok(_validated_path) => {}
538            Err(security_error) => {
539                let execution_time_ms = start_time.elapsed().as_millis() as u64;
540                let error_msg = match security_error {
541                    SecurityError::AccessDenied => "Access denied",
542                    SecurityError::InvalidInput => "Invalid input detected",
543                    SecurityError::PolicyViolation => "Security policy violation",
544                };
545                self.audit_logger.log_operation_failed(
546                    correlation_id,
547                    operation,
548                    error_msg,
549                    execution_time_ms,
550                );
551                return Err(anyhow::anyhow!("{}", error_msg));
552            }
553        }
554
555        // Validate path permissions
556        let operations = std::iter::once(operation.operation_type).collect();
557        let permission_result = self.permission_validator.evaluate_permissions(
558            &operation.path,
559            &operations,
560            Some("write_access_validation"),
561        );
562
563        if !permission_result.allowed {
564            let execution_time_ms = start_time.elapsed().as_millis() as u64;
565            self.audit_logger.log_operation_failed(
566                correlation_id,
567                operation,
568                &format!("Permission denied: {}", permission_result.decision_reason),
569                execution_time_ms,
570            );
571            return Err(anyhow::anyhow!(
572                "Permission denied: {}",
573                permission_result.decision_reason
574            ));
575        }
576
577        // Use policy engine for real security evaluation instead of auto-approval
578        let policy_start = Instant::now();
579        let policy_decision = self.policy_engine.evaluate_operation(operation);
580        let policy_time_ms = policy_start.elapsed().as_millis() as u64;
581
582        // Log policy evaluation
583        self.audit_logger.log_policy_evaluated(
584            correlation_id,
585            &policy_decision,
586            None, // Policy name will be included in the decision
587            policy_time_ms,
588        );
589
590        let execution_time_ms = start_time.elapsed().as_millis() as u64;
591
592        if self.config.operations.write_requires_policy
593            && self.requires_approval(operation.operation_type)
594        {
595            if policy_decision.is_allowed() {
596                // Policy allows operation - auto-approve and log completion
597                let decision = ApprovalDecision::Approved;
598
599                self.audit_logger.log_operation_completed(
600                    correlation_id,
601                    operation,
602                    execution_time_ms,
603                    None, // No size information available
604                );
605
606                Ok(decision)
607            } else {
608                // Policy denies operation - log and convert to ApprovalDecision::Denied
609                self.audit_logger.log_operation_failed(
610                    correlation_id,
611                    operation,
612                    &format!("Policy denied write access: {}", policy_decision.reason()),
613                    execution_time_ms,
614                );
615                Ok(ApprovalDecision::Denied)
616            }
617        } else {
618            // For operations that don't require policy evaluation, still check with policy engine
619            if policy_decision.is_allowed() {
620                self.audit_logger.log_operation_completed(
621                    correlation_id,
622                    operation,
623                    execution_time_ms,
624                    None, // No size information available
625                );
626                Ok(ApprovalDecision::Approved)
627            } else {
628                self.audit_logger.log_operation_failed(
629                    correlation_id,
630                    operation,
631                    &format!("Policy denied write access: {}", policy_decision.reason()),
632                    execution_time_ms,
633                );
634                Ok(ApprovalDecision::Denied)
635            }
636        }
637    }
638
639    /// Add a permission rule to the path permission validator
640    pub fn add_permission_rule(&mut self, rule: PathPermissionRule) {
641        self.permission_validator.add_rule(rule);
642    }
643
644    /// Get permission evaluation for a specific path and operations
645    pub fn evaluate_path_permissions(
646        &self,
647        path: &std::path::Path,
648        operations: &std::collections::HashSet<OperationType>,
649        context: Option<&str>,
650    ) -> PermissionEvaluation {
651        self.permission_validator
652            .evaluate_permissions(path, operations, context)
653    }
654
655    /// Get permission coverage statistics
656    pub fn get_permission_coverage(&self) -> std::collections::HashMap<String, usize> {
657        self.permission_validator.get_coverage_stats()
658    }
659
660    /// Check if an operation type requires human approval
661    fn requires_approval(&self, operation_type: OperationType) -> bool {
662        match operation_type {
663            OperationType::Read | OperationType::List => false,
664            OperationType::Write
665            | OperationType::CreateDir
666            | OperationType::Delete
667            | OperationType::Move
668            | OperationType::Copy => true,
669        }
670    }
671}
672
673#[cfg(test)]
674#[allow(clippy::unwrap_used, clippy::expect_used)]
675mod tests {
676    use super::*;
677    use crate::config::SecurityConfig;
678    use std::path::PathBuf;
679
680    fn create_test_config() -> SecurityConfig {
681        use crate::config::settings::{
682            FilesystemConfig, OperationConfig, RiskLevel, SecurityPolicy,
683        };
684        use std::collections::HashMap;
685
686        let mut policies = HashMap::new();
687
688        // Simple universal policy that allows all operations on all patterns
689        policies.insert(
690            "universal_test_policy".to_string(),
691            SecurityPolicy {
692                patterns: vec!["**/*".to_string()], // Match everything
693                operations: vec![
694                    "read".to_string(),
695                    "write".to_string(),
696                    "list".to_string(),
697                    "create_dir".to_string(),
698                    "move".to_string(),
699                    "copy".to_string(),
700                    "delete".to_string(),
701                ],
702                risk_level: RiskLevel::Low,
703                description: Some("Universal test policy allowing all operations".to_string()),
704            },
705        );
706
707        SecurityConfig {
708            filesystem: FilesystemConfig {
709                allowed_paths: vec!["**/*".to_string()], // Allow everything for testing
710                denied_paths: vec!["**/.git/**".to_string()],
711            },
712            operations: OperationConfig {
713                read_allowed: true,
714                write_requires_policy: false, // Don't require policies for writes in test
715                delete_requires_explicit_allow: false, // Don't require explicit delete permissions in test
716                create_dir_allowed: true,
717            },
718            policies,
719        }
720    }
721
722    #[test]
723    fn test_security_manager_creation() {
724        let config = create_test_config();
725        let manager = SecurityManager::new(config).unwrap();
726
727        // Test should pass with our permissive test config
728        assert!(manager.config.operations.read_allowed);
729        assert!(!manager.config.operations.write_requires_policy); // Permissive in test config
730        assert!(manager.config.operations.create_dir_allowed);
731    }
732
733    #[tokio::test]
734    async fn test_validate_read_access_success() {
735        let config = create_test_config();
736        let manager = SecurityManager::new(config).unwrap();
737
738        let operation = FileOperation::new(OperationType::Read, PathBuf::from("src/main.rs"));
739
740        let result = manager.validate_read_access(&operation).await;
741        assert!(result.is_ok());
742    }
743
744    #[tokio::test]
745    async fn test_validate_read_access_denied_path() {
746        let config = create_test_config();
747        let manager = SecurityManager::new(config).unwrap();
748
749        let operation = FileOperation::new(OperationType::Read, PathBuf::from(".git/config"));
750
751        let result = manager.validate_read_access(&operation).await;
752        assert!(result.is_err());
753    }
754
755    #[tokio::test]
756    async fn test_validate_write_access_with_approval() {
757        let config = create_test_config();
758        let manager = SecurityManager::new(config).unwrap();
759
760        let operation = FileOperation::new(OperationType::Write, PathBuf::from("src/new_file.rs"));
761
762        let result = manager.validate_write_access(&operation).await;
763        assert!(result.is_ok());
764        // Returns approval decision (currently auto-approved after policy validation)
765        assert_eq!(result.unwrap(), ApprovalDecision::Approved);
766    }
767
768    #[test]
769    fn test_requires_approval() {
770        let config = create_test_config();
771        let manager = SecurityManager::new(config).unwrap();
772
773        assert!(!manager.requires_approval(OperationType::Read));
774        assert!(!manager.requires_approval(OperationType::List));
775        assert!(manager.requires_approval(OperationType::Write));
776        assert!(manager.requires_approval(OperationType::Delete));
777        assert!(manager.requires_approval(OperationType::CreateDir));
778    }
779
780    // ===== NEW OPERATION-TYPE RESTRICTIONS TESTS =====
781
782    #[tokio::test]
783    async fn test_validate_operation_permission_read_success() {
784        let config = create_test_config();
785        let manager = SecurityManager::new(config).unwrap();
786
787        let operation = FileOperation::new(OperationType::Read, PathBuf::from("src/main.rs"));
788
789        let result = manager.validate_operation_permission(&operation).await;
790        assert!(result.is_ok());
791        assert_eq!(result.unwrap(), ApprovalDecision::Approved);
792    }
793
794    #[tokio::test]
795    async fn test_validate_operation_permission_write_success() {
796        let config = create_test_config();
797        let manager = SecurityManager::new(config).unwrap();
798
799        let operation = FileOperation::new(OperationType::Write, PathBuf::from("src/new_file.rs"));
800
801        let result = manager.validate_operation_permission(&operation).await;
802        assert!(result.is_ok());
803        assert_eq!(result.unwrap(), ApprovalDecision::Approved);
804    }
805
806    #[tokio::test]
807    async fn test_validate_operation_permission_delete_success() {
808        use crate::config::settings::{RiskLevel, SecurityPolicy};
809
810        let mut config = create_test_config();
811
812        // Add a policy that allows delete operations
813        let delete_policy = SecurityPolicy {
814            patterns: vec!["src/**".to_string()],
815            operations: vec![
816                "read".to_string(),
817                "write".to_string(),
818                "delete".to_string(),
819            ],
820            risk_level: RiskLevel::Low,
821            description: None,
822        };
823        config
824            .policies
825            .insert("delete_policy".to_string(), delete_policy);
826
827        let manager = SecurityManager::new(config).unwrap();
828
829        // Test with a file in src/ directory that has explicit delete permission
830        let operation =
831            FileOperation::new(OperationType::Delete, PathBuf::from("src/temp_file.txt"));
832
833        let result = manager.validate_operation_permission(&operation).await;
834        assert!(result.is_ok());
835        assert_eq!(result.unwrap(), ApprovalDecision::Approved);
836    }
837
838    #[tokio::test]
839    async fn test_validate_operation_permission_delete_denied() {
840        use crate::config::settings::{
841            FilesystemConfig, OperationConfig, RiskLevel, SecurityPolicy,
842        };
843        use std::collections::HashMap;
844
845        // Create a restrictive config that denies delete operations
846        let mut policies = HashMap::new();
847        policies.insert(
848            "no_delete_policy".to_string(),
849            SecurityPolicy {
850                patterns: vec!["**/*".to_string()],
851                operations: vec!["read".to_string(), "write".to_string()], // No delete operation
852                risk_level: RiskLevel::Low,
853                description: None,
854            },
855        );
856
857        let restrictive_config = SecurityConfig {
858            filesystem: FilesystemConfig {
859                allowed_paths: vec!["**/*".to_string()],
860                denied_paths: vec![],
861            },
862            operations: OperationConfig {
863                read_allowed: true,
864                write_requires_policy: false,
865                delete_requires_explicit_allow: true, // Require explicit delete permission
866                create_dir_allowed: true,
867            },
868            policies,
869        };
870
871        let manager = SecurityManager::new(restrictive_config).unwrap();
872
873        // Test with source code that doesn't have explicit delete permission
874        let operation = FileOperation::new(OperationType::Delete, PathBuf::from("src/main.rs"));
875
876        let result = manager.validate_operation_permission(&operation).await;
877        // Should fail because policy doesn't include "delete" operation
878        assert!(result.is_err());
879        let error_msg = result.unwrap_err().to_string();
880        assert!(error_msg.contains("Delete") || error_msg.contains("delete"));
881    }
882
883    #[tokio::test]
884    async fn test_validate_operation_permission_create_dir_success() {
885        let config = create_test_config();
886        let manager = SecurityManager::new(config).unwrap();
887
888        let operation =
889            FileOperation::new(OperationType::CreateDir, PathBuf::from("src/new_module"));
890
891        let result = manager.validate_operation_permission(&operation).await;
892        assert!(result.is_ok());
893        assert_eq!(result.unwrap(), ApprovalDecision::Approved);
894    }
895
896    #[tokio::test]
897    async fn test_validate_operation_permission_list_success() {
898        let config = create_test_config();
899        let manager = SecurityManager::new(config).unwrap();
900
901        // List the src directory itself
902        let operation = FileOperation::new(OperationType::List, PathBuf::from("src"));
903
904        let result = manager.validate_operation_permission(&operation).await;
905        assert!(result.is_ok());
906        assert_eq!(result.unwrap(), ApprovalDecision::Approved);
907    }
908
909    #[tokio::test]
910    async fn test_validate_operation_permission_move_success() {
911        let config = create_test_config();
912        let manager = SecurityManager::new(config).unwrap();
913
914        let operation = FileOperation::new(OperationType::Move, PathBuf::from("src/old_file.rs"));
915
916        let result = manager.validate_operation_permission(&operation).await;
917        assert!(result.is_ok());
918        assert_eq!(result.unwrap(), ApprovalDecision::Approved);
919    }
920
921    #[tokio::test]
922    async fn test_validate_operation_permission_copy_success() {
923        let config = create_test_config();
924        let manager = SecurityManager::new(config).unwrap();
925
926        let operation = FileOperation::new(OperationType::Copy, PathBuf::from("src/main.rs"));
927
928        let result = manager.validate_operation_permission(&operation).await;
929        assert!(result.is_ok());
930        assert_eq!(result.unwrap(), ApprovalDecision::Approved);
931    }
932
933    #[tokio::test]
934    async fn test_validate_operation_permission_denied_path() {
935        let config = create_test_config();
936        let manager = SecurityManager::new(config).unwrap();
937
938        // Test with denied path (.git directory)
939        let operation = FileOperation::new(OperationType::Read, PathBuf::from(".git/config"));
940
941        let result = manager.validate_operation_permission(&operation).await;
942        assert!(result.is_err());
943        let error_msg = result.unwrap_err().to_string();
944        // Be more flexible about the error message - it could be path validation or permission denied
945        assert!(
946            error_msg.contains("Path validation failed")
947                || error_msg.contains("denied")
948                || error_msg.contains("not allowed")
949                || error_msg.contains("Security policy violation")
950        );
951    }
952
953    #[tokio::test]
954    async fn test_validate_operation_against_policies_write_allowed() {
955        let config = create_test_config();
956        let manager = SecurityManager::new(config).unwrap();
957
958        let operation = FileOperation::new(OperationType::Write, PathBuf::from("src/main.rs"));
959        let correlation_id = CorrelationId::new();
960
961        let result = manager
962            .validate_operation_against_policies(&operation, correlation_id)
963            .await;
964        assert!(result.is_ok());
965        assert!(result.unwrap()); // Should return true for allowed operation
966    }
967
968    #[tokio::test]
969    async fn test_validate_operation_against_policies_write_denied() {
970        use crate::config::settings::{
971            FilesystemConfig, OperationConfig, RiskLevel, SecurityPolicy,
972        };
973        use std::collections::HashMap;
974
975        // Create a restrictive config for this specific test
976        let mut policies = HashMap::new();
977        policies.insert(
978            "read_only_policy".to_string(),
979            SecurityPolicy {
980                patterns: vec!["**/*".to_string()],
981                operations: vec!["read".to_string()], // Only allow read, not write
982                risk_level: RiskLevel::Low,
983                description: None,
984            },
985        );
986
987        let restrictive_config = SecurityConfig {
988            filesystem: FilesystemConfig {
989                allowed_paths: vec!["**/*".to_string()],
990                denied_paths: vec![],
991            },
992            operations: OperationConfig {
993                read_allowed: true,
994                write_requires_policy: true, // Require policy for writes
995                delete_requires_explicit_allow: false,
996                create_dir_allowed: true,
997            },
998            policies,
999        };
1000
1001        let manager = SecurityManager::new(restrictive_config).unwrap();
1002
1003        // Test with a write operation that should be denied by the read-only policy
1004        let operation = FileOperation::new(OperationType::Write, PathBuf::from("test/file.txt"));
1005        let correlation_id = CorrelationId::new();
1006
1007        let result = manager
1008            .validate_operation_against_policies(&operation, correlation_id)
1009            .await;
1010        assert!(result.is_ok());
1011        assert!(!result.unwrap()); // Should return false for denied operation
1012    }
1013
1014    #[tokio::test]
1015    async fn test_validate_delete_permission_allowed() {
1016        use crate::config::settings::{RiskLevel, SecurityPolicy};
1017
1018        let mut config = create_test_config();
1019
1020        // Add a policy that explicitly allows delete operations
1021        config.policies.insert(
1022            "delete_policy".to_string(),
1023            SecurityPolicy {
1024                patterns: vec!["src/**".to_string()],
1025                operations: vec!["delete".to_string()],
1026                risk_level: RiskLevel::Low,
1027                description: None,
1028            },
1029        );
1030
1031        let manager = SecurityManager::new(config).unwrap();
1032
1033        // Test with a file in src/ that has explicit delete permission
1034        let operation =
1035            FileOperation::new(OperationType::Delete, PathBuf::from("src/temp_file.txt"));
1036        let correlation_id = CorrelationId::new();
1037
1038        let result = manager
1039            .validate_delete_permission(&operation, correlation_id)
1040            .await;
1041        assert!(result.is_ok());
1042        assert!(result.unwrap()); // Should return true for allowed delete
1043    }
1044
1045    #[tokio::test]
1046    async fn test_validate_delete_permission_denied() {
1047        use crate::config::settings::{
1048            FilesystemConfig, OperationConfig, RiskLevel, SecurityPolicy,
1049        };
1050        use std::collections::HashMap;
1051
1052        // Create a restrictive config for this test
1053        let mut policies = HashMap::new();
1054        policies.insert(
1055            "restrictive_policy".to_string(),
1056            SecurityPolicy {
1057                patterns: vec!["**/*".to_string()],
1058                operations: vec!["read".to_string(), "write".to_string()], // No delete operation
1059                risk_level: RiskLevel::Low,
1060                description: None,
1061            },
1062        );
1063
1064        let restrictive_config = SecurityConfig {
1065            filesystem: FilesystemConfig {
1066                allowed_paths: vec!["**/*".to_string()],
1067                denied_paths: vec![],
1068            },
1069            operations: OperationConfig {
1070                read_allowed: true,
1071                write_requires_policy: false,
1072                delete_requires_explicit_allow: true, // Require explicit delete permission
1073                create_dir_allowed: true,
1074            },
1075            policies,
1076        };
1077
1078        let manager = SecurityManager::new(restrictive_config).unwrap();
1079
1080        // Test with source code that doesn't have explicit delete permission
1081        let operation = FileOperation::new(OperationType::Delete, PathBuf::from("src/main.rs"));
1082        let correlation_id = CorrelationId::new();
1083
1084        let result = manager
1085            .validate_delete_permission(&operation, correlation_id)
1086            .await;
1087        assert!(result.is_ok());
1088        assert!(!result.unwrap()); // Should return false for denied delete
1089    }
1090
1091    #[tokio::test]
1092    async fn test_operation_type_specific_configuration() {
1093        use crate::config::settings::Settings;
1094
1095        // Test with production settings (default)
1096        let production_settings = Settings::default();
1097        let production_manager = SecurityManager::new(production_settings.security).unwrap();
1098
1099        // Test that configuration settings are properly applied
1100        assert!(production_manager.config.operations.read_allowed);
1101        assert!(production_manager.config.operations.write_requires_policy);
1102        assert!(
1103            production_manager
1104                .config
1105                .operations
1106                .delete_requires_explicit_allow
1107        );
1108        assert!(production_manager.config.operations.create_dir_allowed);
1109
1110        // Test with permissive settings for testing
1111        let permissive_settings = Settings::builder().permissive().build();
1112        let permissive_manager = SecurityManager::new(permissive_settings.security).unwrap();
1113
1114        // Test that permissive configuration is properly applied
1115        assert!(permissive_manager.config.operations.read_allowed);
1116        assert!(!permissive_manager.config.operations.write_requires_policy);
1117        assert!(
1118            !permissive_manager
1119                .config
1120                .operations
1121                .delete_requires_explicit_allow
1122        );
1123        assert!(permissive_manager.config.operations.create_dir_allowed);
1124
1125        // Also test with explicit restrictive configuration
1126        let restrictive_config = create_test_config();
1127        let restrictive_manager = SecurityManager::new(restrictive_config).unwrap();
1128
1129        // Our test config is actually permissive now
1130        assert!(restrictive_manager.config.operations.read_allowed);
1131        assert!(!restrictive_manager.config.operations.write_requires_policy); // Permissive
1132        assert!(
1133            !restrictive_manager
1134                .config
1135                .operations
1136                .delete_requires_explicit_allow
1137        ); // Permissive
1138        assert!(restrictive_manager.config.operations.create_dir_allowed);
1139    }
1140
1141    /// Test binary file rejection for security hardening (Task 011)
1142    #[tokio::test]
1143    async fn test_binary_file_rejection_security_hardening() {
1144        let config = create_test_config();
1145        let manager = SecurityManager::new(config).expect("Should create security manager");
1146
1147        // Test various binary file types should be rejected
1148        let binary_files = vec![
1149            ("/tmp/test.jpg", "JPEG image"),
1150            ("/tmp/test.png", "PNG image"),
1151            ("/tmp/test.gif", "GIF image"),
1152            ("/tmp/test.pdf", "PDF document"),
1153            ("/tmp/test.bmp", "BMP image"),
1154            ("/tmp/test.tiff", "TIFF image"),
1155            ("/tmp/test.webp", "WebP image"),
1156        ];
1157
1158        for (path, description) in binary_files {
1159            let operation = FileOperation::new(OperationType::Read, PathBuf::from(path));
1160
1161            let result = manager.validate_operation_permission(&operation).await;
1162            assert!(
1163                result.is_err(),
1164                "Binary file {description} should be rejected for security"
1165            );
1166
1167            let error_msg = result.unwrap_err().to_string();
1168            assert!(
1169                error_msg.contains("Binary file access denied")
1170                    || error_msg.contains("binary")
1171                    || error_msg.contains("security"),
1172                "Error message should indicate binary file security restriction: {error_msg}"
1173            );
1174        }
1175    }
1176
1177    #[tokio::test]
1178    async fn test_text_files_allowed_after_binary_hardening() {
1179        let config = create_test_config();
1180        let manager = SecurityManager::new(config).expect("Should create security manager");
1181
1182        // Test that text files are still allowed
1183        let text_files = vec![
1184            "/tmp/test.txt",
1185            "/tmp/test.md",
1186            "/tmp/test.rs",
1187            "/tmp/test.py",
1188            "/tmp/test.js",
1189            "/tmp/test.json",
1190            "/tmp/test.toml",
1191            "/tmp/test.yml",
1192        ];
1193
1194        for path in text_files {
1195            let operation = FileOperation::new(OperationType::Read, PathBuf::from(path));
1196
1197            let result = manager.validate_operation_permission(&operation).await;
1198            // Should not fail due to binary restriction (may fail for other policy reasons)
1199            if let Err(err) = &result {
1200                let error_msg = err.to_string();
1201                assert!(
1202                    !error_msg.contains("Binary file access denied")
1203                        && !error_msg.contains("binary")
1204                        && !error_msg.contains("Binary"),
1205                    "Text file {path} should not trigger binary file restriction: {error_msg}"
1206                );
1207            }
1208        }
1209    }
1210
1211    #[tokio::test]
1212    async fn test_binary_file_content_detection() {
1213        use std::fs;
1214        use tempfile::TempDir;
1215
1216        let config = create_test_config();
1217        let manager = SecurityManager::new(config).expect("Should create security manager");
1218
1219        // Create temporary directory for test files
1220        let temp_dir = TempDir::new().expect("Should create temp dir");
1221
1222        // Create a file with binary content (fake JPEG header)
1223        let binary_file = temp_dir.path().join("fake.txt"); // .txt extension but binary content
1224        let jpeg_header = vec![0xFF, 0xD8, 0xFF, 0xE0]; // JPEG magic bytes
1225        fs::write(&binary_file, &jpeg_header).expect("Should write binary file");
1226
1227        let operation = FileOperation::new(OperationType::Read, binary_file);
1228
1229        let result = manager.validate_operation_permission(&operation).await;
1230        assert!(
1231            result.is_err(),
1232            "File with binary content should be rejected regardless of extension"
1233        );
1234
1235        let error_msg = result.unwrap_err().to_string();
1236        assert!(
1237            error_msg.contains("Binary file access denied")
1238                || error_msg.contains("content analysis"),
1239            "Error should indicate content-based binary detection: {error_msg}"
1240        );
1241    }
1242}