use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SecurityLevel {
Permissive,
#[default]
Standard,
Strict,
Paranoid,
}
impl SecurityLevel {
pub fn default_max_actions_per_hour(&self) -> u32 {
match self {
Self::Permissive => 1000,
Self::Standard => 100,
Self::Strict => 50,
Self::Paranoid => 20,
}
}
pub fn default_workspace_only(&self) -> bool {
match self {
Self::Permissive => false,
Self::Standard | Self::Strict | Self::Paranoid => true,
}
}
pub fn is_read_only(&self) -> bool {
matches!(self, Self::Paranoid)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct SecurityConfig {
pub level: SecurityLevel,
pub workspace_only: bool,
pub max_actions_per_hour: u32,
pub forbidden_paths: Vec<String>,
pub allowed_roots: Vec<PathBuf>,
pub forbidden_extensions: Vec<String>,
pub read_only: Option<bool>,
pub max_file_size: u64,
pub allow_symlinks: bool,
}
impl Default for SecurityConfig {
fn default() -> Self {
Self {
level: SecurityLevel::default(),
workspace_only: true,
max_actions_per_hour: 100,
forbidden_paths: vec![
"/etc".to_string(),
"/root".to_string(),
"/sys".to_string(),
"/proc".to_string(),
"~/.ssh".to_string(),
"~/.gnupg".to_string(),
"~/.aws".to_string(),
],
allowed_roots: Vec::new(),
forbidden_extensions: vec![
".exe".to_string(),
".dll".to_string(),
".bat".to_string(),
".cmd".to_string(),
".sh".to_string(),
],
read_only: None,
max_file_size: 10 * 1024 * 1024, allow_symlinks: false,
}
}
}
impl SecurityConfig {
pub fn from_level(level: SecurityLevel) -> Self {
Self {
level,
workspace_only: level.default_workspace_only(),
max_actions_per_hour: level.default_max_actions_per_hour(),
read_only: Some(level.is_read_only()),
..Self::default()
}
}
pub fn is_read_only(&self) -> bool {
self.read_only.unwrap_or_else(|| self.level.is_read_only())
}
pub fn validate(&self) -> Result<(), String> {
if self.max_actions_per_hour == 0 {
return Err("max_actions_per_hour must be greater than 0".to_string());
}
for path in &self.forbidden_paths {
if path.contains('\0') {
return Err(format!("Forbidden path contains null byte: {}", path));
}
}
Ok(())
}
pub fn merge(&mut self, other: SecurityConfig) {
if other.level != SecurityLevel::default() {
self.level = other.level;
}
if !other.forbidden_paths.is_empty() {
self.forbidden_paths = other.forbidden_paths;
}
if !other.allowed_roots.is_empty() {
self.allowed_roots = other.allowed_roots;
}
if other.max_file_size != 0 {
self.max_file_size = other.max_file_size;
}
if other.read_only.is_some() {
self.read_only = other.read_only;
}
if other.max_actions_per_hour != SecurityLevel::default().default_max_actions_per_hour() {
self.max_actions_per_hour = other.max_actions_per_hour;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_security_level_defaults() {
assert_eq!(
SecurityLevel::Permissive.default_max_actions_per_hour(),
1000
);
assert_eq!(SecurityLevel::Standard.default_max_actions_per_hour(), 100);
assert_eq!(SecurityLevel::Strict.default_max_actions_per_hour(), 50);
assert_eq!(SecurityLevel::Paranoid.default_max_actions_per_hour(), 20);
assert!(!SecurityLevel::Permissive.default_workspace_only());
assert!(SecurityLevel::Standard.default_workspace_only());
assert!(SecurityLevel::Paranoid.is_read_only());
}
#[test]
fn test_config_from_level() {
let config = SecurityConfig::from_level(SecurityLevel::Strict);
assert_eq!(config.max_actions_per_hour, 50);
assert!(config.workspace_only);
assert!(!config.is_read_only());
let config = SecurityConfig::from_level(SecurityLevel::Paranoid);
assert!(config.is_read_only());
}
#[test]
fn test_config_validation() {
let mut config = SecurityConfig::default();
assert!(config.validate().is_ok());
config.max_actions_per_hour = 0;
assert!(config.validate().is_err());
}
#[test]
fn test_config_merge() {
let mut base = SecurityConfig::default();
let other = SecurityConfig {
level: SecurityLevel::Strict,
max_actions_per_hour: 50,
..Default::default()
};
base.merge(other);
assert_eq!(base.level, SecurityLevel::Strict);
assert_eq!(base.max_actions_per_hour, 50);
}
}