clnrm_core/backend/
volume.rs

1//! Volume mounting support for containers
2//!
3//! Provides secure volume mount configuration with validation and whitelist support.
4
5use crate::config::VolumeConfig;
6use crate::error::{CleanroomError, Result};
7use std::path::{Path, PathBuf};
8
9/// Volume mount configuration
10#[derive(Debug, Clone)]
11pub struct VolumeMount {
12    /// Host path (absolute, validated)
13    host_path: PathBuf,
14    /// Container path (absolute)
15    container_path: PathBuf,
16    /// Read-only flag
17    read_only: bool,
18}
19
20impl VolumeMount {
21    /// Create a new volume mount with validation
22    ///
23    /// # Arguments
24    ///
25    /// * `host_path` - Path on the host system
26    /// * `container_path` - Path inside the container
27    /// * `read_only` - Whether mount is read-only
28    ///
29    /// # Errors
30    ///
31    /// Returns error if:
32    /// - Host path is not absolute
33    /// - Host path does not exist
34    /// - Container path is not absolute
35    /// - Path canonicalization fails
36    ///
37    /// # Example
38    ///
39    /// ```no_run
40    /// use clnrm_core::backend::volume::VolumeMount;
41    ///
42    /// let mount = VolumeMount::new("/tmp/data", "/data", false)?;
43    /// assert!(!mount.is_read_only());
44    /// # Ok::<(), clnrm_core::error::CleanroomError>(())
45    /// ```
46    pub fn new(
47        host_path: impl AsRef<Path>,
48        container_path: impl AsRef<Path>,
49        read_only: bool,
50    ) -> Result<Self> {
51        let host_path = host_path.as_ref();
52        let container_path = container_path.as_ref();
53
54        // Validate host path is absolute
55        if !host_path.is_absolute() {
56            return Err(CleanroomError::validation_error(format!(
57                "Host path must be absolute: {}",
58                host_path.display()
59            )));
60        }
61
62        // Validate host path exists
63        if !host_path.exists() {
64            return Err(CleanroomError::validation_error(format!(
65                "Host path does not exist: {}",
66                host_path.display()
67            )));
68        }
69
70        // Canonicalize host path to resolve symlinks and relative components
71        let host_path = host_path.canonicalize().map_err(|e| {
72            CleanroomError::validation_error(format!(
73                "Failed to canonicalize host path {}: {}",
74                host_path.display(),
75                e
76            ))
77        })?;
78
79        // Validate container path is absolute
80        if !container_path.is_absolute() {
81            return Err(CleanroomError::validation_error(format!(
82                "Container path must be absolute: {}",
83                container_path.display()
84            )));
85        }
86
87        Ok(Self {
88            host_path,
89            container_path: container_path.to_path_buf(),
90            read_only,
91        })
92    }
93
94    /// Create from VolumeConfig with validation
95    pub fn from_config(config: &VolumeConfig) -> Result<Self> {
96        let read_only = config.read_only.unwrap_or(false);
97        Self::new(&config.host_path, &config.container_path, read_only)
98    }
99
100    /// Get host path
101    pub fn host_path(&self) -> &Path {
102        &self.host_path
103    }
104
105    /// Get container path
106    pub fn container_path(&self) -> &Path {
107        &self.container_path
108    }
109
110    /// Check if mount is read-only
111    pub fn is_read_only(&self) -> bool {
112        self.read_only
113    }
114}
115
116/// Volume security validator with whitelist support
117#[derive(Debug, Clone)]
118pub struct VolumeValidator {
119    /// Allowed base directories for mounting
120    whitelist: Vec<PathBuf>,
121}
122
123impl VolumeValidator {
124    /// Create a new volume validator with whitelist
125    ///
126    /// # Example
127    ///
128    /// ```no_run
129    /// use clnrm_core::backend::volume::VolumeValidator;
130    /// use std::path::PathBuf;
131    ///
132    /// let validator = VolumeValidator::new(vec![
133    ///     PathBuf::from("/tmp"),
134    ///     PathBuf::from("/var/data"),
135    /// ]);
136    /// ```
137    pub fn new(whitelist: Vec<PathBuf>) -> Self {
138        Self { whitelist }
139    }
140
141    /// Validate a volume mount against whitelist
142    ///
143    /// # Errors
144    ///
145    /// Returns error if host path is not under any whitelisted directory
146    pub fn validate(&self, mount: &VolumeMount) -> Result<()> {
147        // If whitelist is empty, allow all paths (permissive mode)
148        if self.whitelist.is_empty() {
149            return Ok(());
150        }
151
152        let host_path = mount.host_path();
153
154        // Check if host path is under any whitelisted directory
155        for allowed in &self.whitelist {
156            if host_path.starts_with(allowed) {
157                return Ok(());
158            }
159        }
160
161        Err(CleanroomError::validation_error(format!(
162            "Host path {} is not in whitelist. Allowed directories: {}",
163            host_path.display(),
164            self.whitelist
165                .iter()
166                .map(|p| p.display().to_string())
167                .collect::<Vec<_>>()
168                .join(", ")
169        )))
170    }
171
172    /// Validate multiple mounts
173    pub fn validate_all(&self, mounts: &[VolumeMount]) -> Result<()> {
174        for mount in mounts {
175            self.validate(mount)?;
176        }
177        Ok(())
178    }
179}
180
181impl Default for VolumeValidator {
182    /// Create default validator with common safe directories
183    fn default() -> Self {
184        let mut whitelist = vec![PathBuf::from("/tmp"), PathBuf::from("/var/tmp")];
185
186        // Add system temp directory (varies by OS)
187        whitelist.push(std::env::temp_dir());
188
189        // Add current directory access
190        if let Ok(current_dir) = std::env::current_dir() {
191            whitelist.push(current_dir);
192        }
193
194        Self::new(whitelist)
195    }
196}