use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio::sync::RwLock;
use crate::core::{PluginError, PluginResult, SecurityContext};
#[derive(Debug, Clone)]
pub struct SandboxConfig {
pub temp_directory: PathBuf,
pub max_memory: Option<u64>,
pub max_execution_time: Option<u64>,
pub allowed_env_vars: Vec<String>,
pub filesystem_root: Option<PathBuf>,
pub network_isolation: bool,
}
impl Default for SandboxConfig {
fn default() -> Self {
Self {
temp_directory: std::env::temp_dir().join("pluggable-sandbox"),
max_memory: Some(128 * 1024 * 1024), max_execution_time: Some(300), allowed_env_vars: vec!["PATH".to_string(), "HOME".to_string(), "USER".to_string()],
filesystem_root: None,
network_isolation: false,
}
}
}
#[derive(Debug)]
pub struct Sandbox {
plugin_name: String,
config: SandboxConfig,
security_context: SecurityContext,
isolated_env: HashMap<String, String>,
temp_dir: Option<PathBuf>,
active: bool,
}
impl Sandbox {
pub fn new(
plugin_name: String,
config: SandboxConfig,
security_context: SecurityContext,
) -> Self {
Self {
plugin_name,
config,
security_context,
isolated_env: HashMap::new(),
temp_dir: None,
active: false,
}
}
pub async fn initialize(&mut self) -> PluginResult<()> {
if self.active {
return Err(PluginError::SandboxError(
"Sandbox is already active".to_string(),
));
}
let plugin_temp_dir = self.config.temp_directory.join(&self.plugin_name);
tokio::fs::create_dir_all(&plugin_temp_dir)
.await
.map_err(|e| {
PluginError::SandboxError(format!("Failed to create temp directory: {e}"))
})?;
self.temp_dir = Some(plugin_temp_dir);
self.setup_environment()?;
self.active = true;
Ok(())
}
fn setup_environment(&mut self) -> PluginResult<()> {
self.isolated_env.clear();
for var_name in &self.config.allowed_env_vars {
if let Ok(value) = std::env::var(var_name) {
self.isolated_env.insert(var_name.clone(), value);
}
}
if let Some(temp_dir) = &self.temp_dir {
self.isolated_env.insert(
"PLUGIN_TEMP_DIR".to_string(),
temp_dir.to_string_lossy().to_string(),
);
}
self.isolated_env
.insert("PLUGIN_NAME".to_string(), self.plugin_name.clone());
self.isolated_env
.insert("PLUGIN_SANDBOX".to_string(), "true".to_string());
Ok(())
}
pub fn environment(&self) -> &HashMap<String, String> {
&self.isolated_env
}
pub fn temp_directory(&self) -> Option<&PathBuf> {
self.temp_dir.as_ref()
}
pub fn is_active(&self) -> bool {
self.active
}
pub fn plugin_name(&self) -> &str {
&self.plugin_name
}
pub fn security_context(&self) -> &SecurityContext {
&self.security_context
}
pub fn validate_file_access(&self, path: &Path) -> PluginResult<()> {
if !self.active {
return Err(PluginError::SandboxError(
"Sandbox is not active".to_string(),
));
}
if let Some(root) = &self.config.filesystem_root {
if !path.starts_with(root) {
return Err(PluginError::PermissionDenied {
plugin: self.plugin_name.clone(),
action: format!("File access outside sandbox root: {}", path.display()),
});
}
}
if let Some(temp_dir) = &self.temp_dir {
if path.starts_with(temp_dir) {
return Ok(());
}
}
use crate::core::security::{AccessType, Permission};
let permissions_to_check = vec![
Permission::FileSystem {
path: path.to_path_buf(),
access: AccessType::ReadWrite,
},
Permission::FileSystem {
path: path.to_path_buf(),
access: AccessType::Read,
},
Permission::FileSystem {
path: path.to_path_buf(),
access: AccessType::Write,
},
Permission::FileSystem {
path: path.to_path_buf(),
access: AccessType::Execute,
},
];
for permission in permissions_to_check {
if self.security_context.has_permission(&permission) {
return Ok(());
}
}
Err(PluginError::PermissionDenied {
plugin: self.plugin_name.clone(),
action: format!("File access denied: {}", path.display()),
})
}
pub fn validate_network_access(&self, host: &str, port: u16) -> PluginResult<()> {
if !self.active {
return Err(PluginError::SandboxError(
"Sandbox is not active".to_string(),
));
}
if self.config.network_isolation {
return Err(PluginError::PermissionDenied {
plugin: self.plugin_name.clone(),
action: "Network access disabled by sandbox isolation".to_string(),
});
}
use crate::core::security::Permission;
let permission = Permission::Network {
hosts: vec![host.to_string()],
ports: vec![port],
};
if self.security_context.has_permission(&permission) {
Ok(())
} else {
Err(PluginError::PermissionDenied {
plugin: self.plugin_name.clone(),
action: format!("Network access denied: {host}:{port}"),
})
}
}
pub fn validate_process_execution(&self, command: &str) -> PluginResult<()> {
if !self.active {
return Err(PluginError::SandboxError(
"Sandbox is not active".to_string(),
));
}
use crate::core::security::Permission;
let permission = Permission::Process {
commands: vec![command.to_string()],
};
if self.security_context.has_permission(&permission) {
Ok(())
} else {
Err(PluginError::PermissionDenied {
plugin: self.plugin_name.clone(),
action: format!("Process execution denied: {command}"),
})
}
}
pub async fn cleanup(&mut self) -> PluginResult<()> {
if !self.active {
return Ok(());
}
if let Some(temp_dir) = &self.temp_dir {
if temp_dir.exists() {
tokio::fs::remove_dir_all(temp_dir).await.map_err(|e| {
PluginError::CleanupFailed(format!("Failed to cleanup temp directory: {e}"))
})?;
}
}
self.temp_dir = None;
self.isolated_env.clear();
self.active = false;
Ok(())
}
}
impl Drop for Sandbox {
fn drop(&mut self) {
if self.active {
if let Some(temp_dir) = &self.temp_dir {
if temp_dir.exists() {
let _ = std::fs::remove_dir_all(temp_dir);
}
}
}
}
}
#[derive(Debug, Default)]
pub struct SandboxManager {
active_sandboxes: Arc<RwLock<HashMap<String, Sandbox>>>,
default_config: SandboxConfig,
}
impl SandboxManager {
pub fn new() -> Self {
Self {
active_sandboxes: Arc::new(RwLock::new(HashMap::new())),
default_config: SandboxConfig::default(),
}
}
pub fn with_config(config: SandboxConfig) -> Self {
Self {
active_sandboxes: Arc::new(RwLock::new(HashMap::new())),
default_config: config,
}
}
pub async fn create_sandbox(
&self,
plugin_name: String,
security_context: SecurityContext,
) -> PluginResult<()> {
self.create_sandbox_with_config(plugin_name, self.default_config.clone(), security_context)
.await
}
pub async fn create_sandbox_with_config(
&self,
plugin_name: String,
config: SandboxConfig,
security_context: SecurityContext,
) -> PluginResult<()> {
let mut sandboxes = self.active_sandboxes.write().await;
if sandboxes.contains_key(&plugin_name) {
return Err(PluginError::SandboxError(format!(
"Sandbox for plugin '{plugin_name}' already exists"
)));
}
let mut sandbox = Sandbox::new(plugin_name.clone(), config, security_context);
sandbox.initialize().await?;
sandboxes.insert(plugin_name, sandbox);
Ok(())
}
pub async fn has_sandbox(&self, plugin_name: &str) -> bool {
let sandboxes = self.active_sandboxes.read().await;
sandboxes.contains_key(plugin_name)
}
pub async fn validate_file_access(&self, plugin_name: &str, path: &Path) -> PluginResult<()> {
let sandboxes = self.active_sandboxes.read().await;
if let Some(sandbox) = sandboxes.get(plugin_name) {
sandbox.validate_file_access(path)
} else {
Err(PluginError::SandboxError(format!(
"No sandbox found for plugin '{plugin_name}'"
)))
}
}
pub async fn validate_network_access(
&self,
plugin_name: &str,
host: &str,
port: u16,
) -> PluginResult<()> {
let sandboxes = self.active_sandboxes.read().await;
if let Some(sandbox) = sandboxes.get(plugin_name) {
sandbox.validate_network_access(host, port)
} else {
Err(PluginError::SandboxError(format!(
"No sandbox found for plugin '{plugin_name}'"
)))
}
}
pub async fn validate_process_execution(
&self,
plugin_name: &str,
command: &str,
) -> PluginResult<()> {
let sandboxes = self.active_sandboxes.read().await;
if let Some(sandbox) = sandboxes.get(plugin_name) {
sandbox.validate_process_execution(command)
} else {
Err(PluginError::SandboxError(format!(
"No sandbox found for plugin '{plugin_name}'"
)))
}
}
pub async fn remove_sandbox(&self, plugin_name: &str) -> PluginResult<()> {
let mut sandboxes = self.active_sandboxes.write().await;
if let Some(mut sandbox) = sandboxes.remove(plugin_name) {
sandbox.cleanup().await?;
}
Ok(())
}
pub async fn active_sandboxes(&self) -> Vec<String> {
let sandboxes = self.active_sandboxes.read().await;
sandboxes.keys().cloned().collect()
}
pub async fn cleanup_all(&self) -> PluginResult<()> {
let mut sandboxes = self.active_sandboxes.write().await;
for (_, mut sandbox) in sandboxes.drain() {
if let Err(e) = sandbox.cleanup().await {
eprintln!(
"Warning: Failed to cleanup sandbox for '{}': {}",
sandbox.plugin_name(),
e
);
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::security::{Permission, SecurityContext};
use std::collections::HashSet;
use tempfile::TempDir;
#[tokio::test]
async fn test_sandbox_creation() {
let temp_dir = TempDir::new().unwrap();
let config = SandboxConfig {
temp_directory: temp_dir.path().to_path_buf(),
..Default::default()
};
let permissions = HashSet::new();
let security_context = SecurityContext::new("test-plugin".to_string(), permissions);
let mut sandbox = Sandbox::new("test-plugin".to_string(), config, security_context);
assert!(!sandbox.is_active());
sandbox.initialize().await.unwrap();
assert!(sandbox.is_active());
assert!(sandbox.temp_directory().is_some());
}
#[tokio::test]
async fn test_sandbox_environment() {
let temp_dir = TempDir::new().unwrap();
let config = SandboxConfig {
temp_directory: temp_dir.path().to_path_buf(),
allowed_env_vars: vec!["PATH".to_string()],
..Default::default()
};
let permissions = HashSet::new();
let security_context = SecurityContext::new("test-plugin".to_string(), permissions);
let mut sandbox = Sandbox::new("test-plugin".to_string(), config, security_context);
sandbox.initialize().await.unwrap();
let env = sandbox.environment();
assert!(env.contains_key("PLUGIN_NAME"));
assert!(env.contains_key("PLUGIN_SANDBOX"));
assert!(env.contains_key("PLUGIN_TEMP_DIR"));
assert_eq!(env.get("PLUGIN_SANDBOX").unwrap(), "true");
}
#[tokio::test]
async fn test_sandbox_file_access_validation() {
let temp_dir = TempDir::new().unwrap();
let config = SandboxConfig {
temp_directory: temp_dir.path().to_path_buf(),
..Default::default()
};
let mut permissions = HashSet::new();
permissions.insert(Permission::fs_read("/tmp"));
let security_context = SecurityContext::new("test-plugin".to_string(), permissions);
let mut sandbox = Sandbox::new("test-plugin".to_string(), config, security_context);
sandbox.initialize().await.unwrap();
assert!(sandbox.validate_file_access(Path::new("/tmp/test")).is_ok());
assert!(sandbox
.validate_file_access(Path::new("/etc/passwd"))
.is_err());
if let Some(temp_dir) = sandbox.temp_directory() {
let test_file = temp_dir.join("test.txt");
assert!(sandbox.validate_file_access(&test_file).is_ok());
}
}
#[tokio::test]
async fn test_sandbox_network_access_validation() {
let temp_dir = TempDir::new().unwrap();
let config = SandboxConfig {
temp_directory: temp_dir.path().to_path_buf(),
network_isolation: true,
..Default::default()
};
let permissions = HashSet::new();
let security_context = SecurityContext::new("test-plugin".to_string(), permissions);
let mut sandbox = Sandbox::new("test-plugin".to_string(), config, security_context);
sandbox.initialize().await.unwrap();
assert!(sandbox.validate_network_access("localhost", 8080).is_err());
}
#[tokio::test]
async fn test_sandbox_manager() {
let manager = SandboxManager::new();
let permissions = HashSet::new();
let security_context = SecurityContext::new("test-plugin".to_string(), permissions);
manager
.create_sandbox("test-plugin".to_string(), security_context)
.await
.unwrap();
assert!(manager.has_sandbox("test-plugin").await);
assert_eq!(manager.active_sandboxes().await.len(), 1);
manager.remove_sandbox("test-plugin").await.unwrap();
assert!(!manager.has_sandbox("test-plugin").await);
assert_eq!(manager.active_sandboxes().await.len(), 0);
}
#[tokio::test]
async fn test_sandbox_cleanup() {
let temp_dir = TempDir::new().unwrap();
let plugin_temp = temp_dir.path().join("test-plugin");
let config = SandboxConfig {
temp_directory: temp_dir.path().to_path_buf(),
..Default::default()
};
let permissions = HashSet::new();
let security_context = SecurityContext::new("test-plugin".to_string(), permissions);
let mut sandbox = Sandbox::new("test-plugin".to_string(), config, security_context);
sandbox.initialize().await.unwrap();
assert!(plugin_temp.exists());
sandbox.cleanup().await.unwrap();
assert!(!sandbox.is_active());
}
}