use anyhow::Result;
use std::path::{Path, PathBuf};
use tracing::debug;
pub struct PathValidator {
smith_root: PathBuf,
}
impl PathValidator {
pub fn new() -> Self {
Self {
smith_root: PathBuf::from("/tmp/smith"),
}
}
pub fn with_smith_root(smith_root: PathBuf) -> Self {
Self { smith_root }
}
pub fn validate_write_path(
&self,
path: &Path,
scope_paths: &[String],
actor: &str,
) -> Result<PathBuf> {
let abs_path = self.resolve_absolute_path(path, actor)?;
let canonical_path = self.canonicalize_path(&abs_path)?;
self.validate_scope_permission(&canonical_path, scope_paths)?;
self.validate_actor_isolation(&canonical_path, actor)?;
debug!(
"Validated write path: {} for actor: {}",
canonical_path.display(),
actor
);
Ok(canonical_path)
}
fn resolve_absolute_path(&self, path: &Path, actor: &str) -> Result<PathBuf> {
let abs_path = if path.is_relative() {
let actor_base = self.smith_root.join(actor);
actor_base.join(path)
} else {
path.to_path_buf()
};
Ok(abs_path)
}
fn canonicalize_path(&self, path: &Path) -> Result<PathBuf> {
let canonical_path = path.canonicalize().unwrap_or_else(|_| {
if let Some(parent) = path.parent() {
if let Ok(canonical_parent) = parent.canonicalize() {
canonical_parent.join(path.file_name().unwrap_or_default())
} else {
path.to_path_buf()
}
} else {
path.to_path_buf()
}
});
Ok(canonical_path)
}
fn validate_scope_permission(
&self,
canonical_path: &Path,
scope_paths: &[String],
) -> Result<()> {
let mut allowed = false;
for allowed_prefix in scope_paths {
let allowed_path = PathBuf::from(allowed_prefix);
if canonical_path.starts_with(&allowed_path) {
allowed = true;
break;
}
}
if !allowed {
return Err(anyhow::anyhow!(
"Path {} is not within any allowed scope prefix",
canonical_path.display()
));
}
Ok(())
}
fn validate_actor_isolation(&self, canonical_path: &Path, actor: &str) -> Result<()> {
let expected_actor_prefix = self.smith_root.join(actor);
if !canonical_path.starts_with(&expected_actor_prefix) {
return Err(anyhow::anyhow!(
"Path {} violates actor isolation - must be within {}",
canonical_path.display(),
expected_actor_prefix.display()
));
}
Ok(())
}
}
impl Default for PathValidator {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn test_path_validator_creation() {
let validator = PathValidator::new();
assert_eq!(validator.smith_root, PathBuf::from("/tmp/smith"));
let custom_root = PathBuf::from("/custom/root");
let custom_validator = PathValidator::with_smith_root(custom_root.clone());
assert_eq!(custom_validator.smith_root, custom_root);
}
#[test]
fn test_resolve_absolute_path() {
let validator = PathValidator::new();
let relative_path = Path::new("test.txt");
let resolved = validator
.resolve_absolute_path(relative_path, "test-actor")
.unwrap();
assert_eq!(resolved, PathBuf::from("/tmp/smith/test-actor/test.txt"));
let absolute_path = Path::new("/tmp/smith/actor/file.txt");
let resolved = validator
.resolve_absolute_path(absolute_path, "actor")
.unwrap();
assert_eq!(resolved, absolute_path);
}
#[test]
fn test_scope_validation() {
let validator = PathValidator::new();
let scope_paths = vec!["/tmp/smith".to_string(), "/opt/allowed".to_string()];
let allowed_path = PathBuf::from("/tmp/smith/actor/file.txt");
assert!(validator
.validate_scope_permission(&allowed_path, &scope_paths)
.is_ok());
let forbidden_path = PathBuf::from("/etc/passwd");
assert!(validator
.validate_scope_permission(&forbidden_path, &scope_paths)
.is_err());
}
#[test]
fn test_actor_isolation() {
let validator = PathValidator::new();
let actor_path = PathBuf::from("/tmp/smith/test-actor/file.txt");
assert!(validator
.validate_actor_isolation(&actor_path, "test-actor")
.is_ok());
let other_actor_path = PathBuf::from("/tmp/smith/other-actor/file.txt");
assert!(validator
.validate_actor_isolation(&other_actor_path, "test-actor")
.is_err());
let outside_path = PathBuf::from("/tmp/other/file.txt");
assert!(validator
.validate_actor_isolation(&outside_path, "test-actor")
.is_err());
}
#[test]
fn test_full_validation_workflow() {
let _temp_dir = tempdir().unwrap();
let validator = PathValidator::new();
let scope_paths = vec!["/tmp/smith".to_string()];
let relative_path = Path::new("test.txt");
let result = validator.validate_write_path(relative_path, &scope_paths, "test-actor");
assert!(result.is_ok());
let bad_absolute_path = Path::new("/tmp/smith/other-actor/test.txt");
let result = validator.validate_write_path(bad_absolute_path, &scope_paths, "test-actor");
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("violates actor isolation"));
}
}