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}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201    use std::fs;
202
203    #[test]
204    fn test_volume_mount_creation() -> Result<()> {
205        // Create a temporary directory for testing
206        let temp_dir = std::env::temp_dir();
207        let host_path = temp_dir.join("test_volume");
208        fs::create_dir_all(&host_path)?;
209
210        let mount = VolumeMount::new(&host_path, "/data", false)?;
211        assert_eq!(mount.container_path(), Path::new("/data"));
212        assert!(!mount.is_read_only());
213
214        // Cleanup
215        fs::remove_dir(&host_path)?;
216        Ok(())
217    }
218
219    #[test]
220    fn test_volume_mount_read_only() -> Result<()> {
221        let temp_dir = std::env::temp_dir();
222        let host_path = temp_dir.join("test_volume_ro");
223        fs::create_dir_all(&host_path)?;
224
225        let mount = VolumeMount::new(&host_path, "/data", true)?;
226        assert!(mount.is_read_only());
227
228        fs::remove_dir(&host_path)?;
229        Ok(())
230    }
231
232    #[test]
233    fn test_volume_mount_nonexistent_path() {
234        let result = VolumeMount::new("/nonexistent/path/xyz123", "/data", false);
235        assert!(result.is_err());
236    }
237
238    #[test]
239    fn test_volume_mount_relative_host_path() {
240        let result = VolumeMount::new("relative/path", "/data", false);
241        assert!(result.is_err());
242    }
243
244    #[test]
245    fn test_volume_mount_relative_container_path() -> Result<()> {
246        let temp_dir = std::env::temp_dir();
247        let result = VolumeMount::new(&temp_dir, "relative/path", false);
248        assert!(result.is_err());
249        Ok(())
250    }
251
252    #[test]
253    fn test_validator_whitelist() -> Result<()> {
254        let temp_dir = std::env::temp_dir();
255        let host_path = temp_dir.join("test_validator");
256        fs::create_dir_all(&host_path)?;
257
258        let validator = VolumeValidator::new(vec![temp_dir.clone()]);
259        let mount = VolumeMount::new(&host_path, "/data", false)?;
260
261        assert!(validator.validate(&mount).is_ok());
262
263        fs::remove_dir(&host_path)?;
264        Ok(())
265    }
266
267    #[test]
268    fn test_validator_rejects_non_whitelisted() -> Result<()> {
269        let temp_dir = std::env::temp_dir();
270        let host_path = temp_dir.join("test_validator_reject");
271        fs::create_dir_all(&host_path)?;
272
273        let validator = VolumeValidator::new(vec![PathBuf::from("/allowed")]);
274        let mount = VolumeMount::new(&host_path, "/data", false)?;
275
276        assert!(validator.validate(&mount).is_err());
277
278        fs::remove_dir(&host_path)?;
279        Ok(())
280    }
281
282    #[test]
283    fn test_validator_empty_whitelist_allows_all() -> Result<()> {
284        let temp_dir = std::env::temp_dir();
285        let host_path = temp_dir.join("test_validator_empty");
286        fs::create_dir_all(&host_path)?;
287
288        let validator = VolumeValidator::new(vec![]);
289        let mount = VolumeMount::new(&host_path, "/data", false)?;
290
291        assert!(validator.validate(&mount).is_ok());
292
293        fs::remove_dir(&host_path)?;
294        Ok(())
295    }
296
297    #[test]
298    fn test_volume_from_config() -> Result<()> {
299        let temp_dir = std::env::temp_dir();
300        let host_path = temp_dir.join("test_config");
301        fs::create_dir_all(&host_path)?;
302
303        let config = VolumeConfig {
304            host_path: host_path.to_string_lossy().to_string(),
305            container_path: "/data".to_string(),
306            read_only: Some(true),
307        };
308
309        let mount = VolumeMount::from_config(&config)?;
310        assert!(mount.is_read_only());
311        assert_eq!(mount.container_path(), Path::new("/data"));
312
313        fs::remove_dir(&host_path)?;
314        Ok(())
315    }
316}