use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use crate::core::sandbox::{Sandbox, SandboxConfig};
use crate::core::{PluginError, PluginResult};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum AccessType {
Read,
Write,
Execute,
ReadWrite,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum SystemCapability {
GetHostname,
GetSystemInfo,
GetCurrentUser,
ListProcesses,
GetEnvironment,
SetEnvironment,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Permission {
FileSystem { path: PathBuf, access: AccessType },
Network { hosts: Vec<String>, ports: Vec<u16> },
Process { commands: Vec<String> },
Environment { variables: Vec<String> },
System { capabilities: Vec<SystemCapability> },
TempDir,
}
impl Permission {
pub fn fs_read(path: impl Into<PathBuf>) -> Self {
Self::FileSystem {
path: path.into(),
access: AccessType::Read,
}
}
pub fn fs_write(path: impl Into<PathBuf>) -> Self {
Self::FileSystem {
path: path.into(),
access: AccessType::Write,
}
}
pub fn fs_read_write(path: impl Into<PathBuf>) -> Self {
Self::FileSystem {
path: path.into(),
access: AccessType::ReadWrite,
}
}
pub fn process(commands: Vec<String>) -> Self {
Self::Process { commands }
}
pub fn network(hosts: Vec<String>, ports: Vec<u16>) -> Self {
Self::Network { hosts, ports }
}
pub fn environment(variables: Vec<String>) -> Self {
Self::Environment { variables }
}
pub fn system(capabilities: Vec<SystemCapability>) -> Self {
Self::System { capabilities }
}
}
#[derive(Debug, Clone)]
pub struct SecurityContext {
plugin_name: String,
granted_permissions: HashSet<Permission>,
temp_dir: Option<PathBuf>,
}
impl SecurityContext {
pub fn new(plugin_name: String, permissions: HashSet<Permission>) -> Self {
Self {
plugin_name,
granted_permissions: permissions,
temp_dir: None,
}
}
pub fn has_permission(&self, permission: &Permission) -> bool {
if matches!(permission, Permission::TempDir) {
return true;
}
if self.granted_permissions.contains(permission) {
return true;
}
self.check_broader_permissions(permission)
}
fn check_broader_permissions(&self, requested: &Permission) -> bool {
match requested {
Permission::FileSystem { path, access } => {
for granted in &self.granted_permissions {
if let Permission::FileSystem {
path: granted_path,
access: granted_access,
} = granted
{
if self.path_is_covered(path, granted_path)
&& self.access_is_covered(access, granted_access)
{
return true;
}
}
}
false
}
Permission::Network { hosts, ports } => {
for granted in &self.granted_permissions {
if let Permission::Network {
hosts: granted_hosts,
ports: granted_ports,
} = granted
{
if self.hosts_are_covered(hosts, granted_hosts)
&& self.ports_are_covered(ports, granted_ports)
{
return true;
}
}
}
false
}
Permission::Process { commands } => {
for granted in &self.granted_permissions {
if let Permission::Process {
commands: granted_commands,
} = granted
{
if self.commands_are_covered(commands, granted_commands) {
return true;
}
}
}
false
}
Permission::Environment { variables } => {
for granted in &self.granted_permissions {
if let Permission::Environment {
variables: granted_vars,
} = granted
{
if self.variables_are_covered(variables, granted_vars) {
return true;
}
}
}
false
}
Permission::System { capabilities } => {
for granted in &self.granted_permissions {
if let Permission::System {
capabilities: granted_caps,
} = granted
{
if self.capabilities_are_covered(capabilities, granted_caps) {
return true;
}
}
}
false
}
Permission::TempDir => true, }
}
fn path_is_covered(&self, requested: &Path, granted: &Path) -> bool {
if requested == granted {
return true;
}
requested.starts_with(granted)
}
fn access_is_covered(&self, requested: &AccessType, granted: &AccessType) -> bool {
match (requested, granted) {
(a, b) if a == b => true,
(AccessType::Read, AccessType::ReadWrite) => true,
(AccessType::Write, AccessType::ReadWrite) => true,
_ => false,
}
}
fn hosts_are_covered(&self, requested: &[String], granted: &[String]) -> bool {
requested.iter().all(|req_host| {
granted.iter().any(|granted_host| {
req_host == granted_host || granted_host == "*" || granted_host == "localhost"
})
})
}
fn ports_are_covered(&self, requested: &[u16], granted: &[u16]) -> bool {
requested.iter().all(|req_port| granted.contains(req_port))
}
fn commands_are_covered(&self, requested: &[String], granted: &[String]) -> bool {
requested.iter().all(|req_cmd| {
granted
.iter()
.any(|granted_cmd| req_cmd == granted_cmd || granted_cmd == "*")
})
}
fn variables_are_covered(&self, requested: &[String], granted: &[String]) -> bool {
requested.iter().all(|req_var| {
granted
.iter()
.any(|granted_var| req_var == granted_var || granted_var == "*")
})
}
fn capabilities_are_covered(
&self,
requested: &[SystemCapability],
granted: &[SystemCapability],
) -> bool {
requested.iter().all(|req_cap| granted.contains(req_cap))
}
pub fn set_temp_dir(&mut self, temp_dir: PathBuf) {
self.temp_dir = Some(temp_dir);
}
pub fn temp_dir(&self) -> Option<&PathBuf> {
self.temp_dir.as_ref()
}
pub fn plugin_name(&self) -> &str {
&self.plugin_name
}
pub fn granted_permissions(&self) -> &HashSet<Permission> {
&self.granted_permissions
}
}
#[derive(Debug, Default)]
pub struct SecurityManager {
plugin_permissions: HashMap<String, HashSet<Permission>>,
global_restrictions: HashSet<Permission>,
}
impl SecurityManager {
pub fn new() -> Self {
Self::default()
}
pub fn grant_permissions(&mut self, plugin_name: &str, permissions: Vec<Permission>) {
let permission_set: HashSet<Permission> = permissions.into_iter().collect();
self.plugin_permissions
.insert(plugin_name.to_string(), permission_set);
}
pub fn check_permission(&self, plugin_name: &str, permission: &Permission) -> bool {
if self.global_restrictions.contains(permission) {
return false;
}
if let Some(permissions) = self.plugin_permissions.get(plugin_name) {
let context = SecurityContext::new(plugin_name.to_string(), permissions.clone());
context.has_permission(permission)
} else {
false
}
}
pub fn create_context(&self, plugin_name: &str) -> PluginResult<SecurityContext> {
let permissions = self
.plugin_permissions
.get(plugin_name)
.cloned()
.unwrap_or_default();
Ok(SecurityContext::new(plugin_name.to_string(), permissions))
}
pub fn add_global_restriction(&mut self, permission: Permission) {
self.global_restrictions.insert(permission);
}
pub fn remove_global_restriction(&mut self, permission: &Permission) {
self.global_restrictions.remove(permission);
}
pub fn validate_plugin_permissions(
&self,
plugin_name: &str,
requested_permissions: &[Permission],
) -> PluginResult<()> {
for permission in requested_permissions {
if self.global_restrictions.contains(permission) {
return Err(PluginError::PermissionDenied {
plugin: plugin_name.to_string(),
action: format!("Permission denied by global restriction: {permission:?}"),
});
}
}
Ok(())
}
pub fn get_plugin_permissions(&self, plugin_name: &str) -> Vec<Permission> {
self.plugin_permissions
.get(plugin_name)
.cloned()
.unwrap_or_default()
.into_iter()
.collect()
}
pub fn revoke_plugin_permissions(&mut self, plugin_name: &str) {
self.plugin_permissions.remove(plugin_name);
}
pub fn get_plugins_with_permissions(&self) -> Vec<String> {
self.plugin_permissions.keys().cloned().collect()
}
pub fn create_sandbox(&self, plugin_name: &str) -> PluginResult<Sandbox> {
let security_context = self.create_context(plugin_name)?;
let config = SandboxConfig::default();
Ok(Sandbox::new(
plugin_name.to_string(),
config,
security_context,
))
}
pub fn create_sandbox_with_config(
&self,
plugin_name: &str,
config: SandboxConfig,
) -> PluginResult<Sandbox> {
let security_context = self.create_context(plugin_name)?;
Ok(Sandbox::new(
plugin_name.to_string(),
config,
security_context,
))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_permission_creation() {
let fs_read = Permission::fs_read("/tmp");
assert!(matches!(fs_read, Permission::FileSystem { .. }));
let process = Permission::process(vec!["git".to_string()]);
assert!(matches!(process, Permission::Process { .. }));
let network = Permission::network(vec!["localhost".to_string()], vec![8080]);
assert!(matches!(network, Permission::Network { .. }));
}
#[test]
fn test_security_context_basic_permissions() {
let mut permissions = HashSet::new();
permissions.insert(Permission::fs_read("/tmp"));
permissions.insert(Permission::TempDir);
let context = SecurityContext::new("test-plugin".to_string(), permissions);
assert!(context.has_permission(&Permission::fs_read("/tmp")));
assert!(context.has_permission(&Permission::TempDir));
assert!(!context.has_permission(&Permission::fs_write("/tmp")));
}
#[test]
fn test_security_context_broader_permissions() {
let mut permissions = HashSet::new();
permissions.insert(Permission::fs_read_write("/tmp"));
let context = SecurityContext::new("test-plugin".to_string(), permissions);
assert!(context.has_permission(&Permission::fs_read("/tmp")));
assert!(context.has_permission(&Permission::fs_write("/tmp")));
assert!(context.has_permission(&Permission::fs_read_write("/tmp")));
}
#[test]
fn test_security_context_path_hierarchy() {
let mut permissions = HashSet::new();
permissions.insert(Permission::fs_read("/tmp"));
let context = SecurityContext::new("test-plugin".to_string(), permissions);
assert!(context.has_permission(&Permission::fs_read("/tmp/subdir")));
assert!(context.has_permission(&Permission::fs_read("/tmp/file.txt")));
assert!(!context.has_permission(&Permission::fs_read("/")));
assert!(!context.has_permission(&Permission::fs_read("/home")));
}
#[test]
fn test_security_manager_grant_and_check() {
let mut manager = SecurityManager::new();
let permissions = vec![
Permission::fs_read("/tmp"),
Permission::process(vec!["git".to_string()]),
];
manager.grant_permissions("test-plugin", permissions);
assert!(manager.check_permission("test-plugin", &Permission::fs_read("/tmp")));
assert!(
manager.check_permission("test-plugin", &Permission::process(vec!["git".to_string()]))
);
assert!(!manager.check_permission("test-plugin", &Permission::fs_write("/tmp")));
}
#[test]
fn test_security_manager_global_restrictions() {
let mut manager = SecurityManager::new();
manager.grant_permissions("test-plugin", vec![Permission::fs_read("/etc")]);
manager.add_global_restriction(Permission::fs_read("/etc"));
assert!(!manager.check_permission("test-plugin", &Permission::fs_read("/etc")));
}
#[test]
fn test_security_manager_validation() {
let mut manager = SecurityManager::new();
manager.add_global_restriction(Permission::fs_read("/etc"));
let result = manager.validate_plugin_permissions(
"test-plugin",
&[Permission::fs_read("/etc"), Permission::fs_read("/tmp")],
);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
PluginError::PermissionDenied { .. }
));
}
#[test]
fn test_security_context_network_wildcards() {
let mut permissions = HashSet::new();
permissions.insert(Permission::network(vec!["*".to_string()], vec![8080, 3000]));
let context = SecurityContext::new("test-plugin".to_string(), permissions);
assert!(context.has_permission(&Permission::network(
vec!["localhost".to_string()],
vec![8080]
)));
assert!(context.has_permission(&Permission::network(
vec!["example.com".to_string()],
vec![3000]
)));
assert!(!context.has_permission(&Permission::network(
vec!["localhost".to_string()],
vec![9000]
)));
}
#[test]
fn test_security_context_process_wildcards() {
let mut permissions = HashSet::new();
permissions.insert(Permission::process(vec!["*".to_string()]));
let context = SecurityContext::new("test-plugin".to_string(), permissions);
assert!(context.has_permission(&Permission::process(vec!["git".to_string()])));
assert!(context.has_permission(&Permission::process(vec!["npm".to_string()])));
assert!(context.has_permission(&Permission::process(vec!["any-command".to_string()])));
}
}