use crate::permission::policy::{PermissionPolicy, SecurityLevel};
use crate::permission::types::{
AuditEntry, CachedPermission, PermissionAction, PermissionDecision, PermissionRequest,
PermissionResponse,
};
use parking_lot::RwLock;
use sh_layer1::generate_short_id;
use std::collections::HashMap;
use std::sync::Arc;
pub type PermissionPromptCallback =
Box<dyn Fn(&PermissionRequest) -> PermissionResponse + Send + Sync>;
pub type PermissionResult<T> = Result<T, PermissionError>;
#[derive(Debug, thiserror::Error)]
pub enum PermissionError {
#[error("Permission denied: {0}")]
Denied(String),
#[error("Action blocked by security policy: {0}")]
BlockedByPolicy(String),
#[error("No permission prompt callback configured")]
NoCallback,
#[error("Permission cache error: {0}")]
CacheError(String),
#[error("Invalid permission request: {0}")]
InvalidRequest(String),
}
pub struct PermissionManager {
policy: RwLock<PermissionPolicy>,
cache: RwLock<HashMap<String, CachedPermission>>,
audit_log: RwLock<Vec<AuditEntry>>,
prompt_callback: RwLock<Option<Arc<PermissionPromptCallback>>>,
}
impl Default for PermissionManager {
fn default() -> Self {
Self::new(PermissionPolicy::default())
}
}
impl PermissionManager {
pub fn new(policy: PermissionPolicy) -> Self {
Self {
policy: RwLock::new(policy),
cache: RwLock::new(HashMap::new()),
audit_log: RwLock::new(Vec::new()),
prompt_callback: RwLock::new(None),
}
}
pub fn set_prompt_callback(&self, callback: PermissionPromptCallback) {
*self.prompt_callback.write() = Some(Arc::new(callback));
}
pub fn clear_prompt_callback(&self) {
*self.prompt_callback.write() = None;
}
pub fn set_policy(&self, policy: PermissionPolicy) {
*self.policy.write() = policy;
}
pub fn security_level(&self) -> SecurityLevel {
self.policy.read().level
}
pub fn check_permission(
&self,
request: PermissionRequest,
) -> PermissionResult<PermissionResponse> {
let category = request.action.category();
if self.policy.read().is_category_blocked(category) {
return Err(PermissionError::BlockedByPolicy(format!(
"Category '{}' is blocked by security policy",
category
)));
}
if let Some(block_reason) = self.check_blocked(&request.action) {
return Err(PermissionError::BlockedByPolicy(block_reason));
}
if self.policy.read().should_auto_approve(category) {
let response = PermissionResponse::allow(request.id.clone());
self.log_audit(&request, &response, false);
return Ok(response);
}
if self.policy.read().enable_cache {
if let Some(cached) = self.check_cache(&request) {
let response = PermissionResponse {
request_id: request.id.clone(),
decision: cached.decision,
reason: Some("From cache".to_string()),
timestamp: chrono::Utc::now(),
};
self.log_audit(&request, &response, true);
return Ok(response);
}
}
if let Some(callback) = self.prompt_callback.read().as_ref() {
let response = callback(&request);
let description = request.action.description();
if response.decision.should_remember() && self.policy.read().enable_cache {
self.cache_permission(&request.action, response.decision);
}
self.log_audit(&request, &response, false);
if !response.decision.is_allowed() {
return Err(PermissionError::Denied(format!(
"User denied: {}",
description
)));
}
Ok(response)
} else {
if self.policy.read().level == SecurityLevel::Trusted {
let response = PermissionResponse::allow(request.id.clone());
self.log_audit(&request, &response, false);
Ok(response)
} else {
Err(PermissionError::NoCallback)
}
}
}
fn check_blocked(&self, action: &PermissionAction) -> Option<String> {
let policy = self.policy.read();
match action {
PermissionAction::CommandExecute { command, .. } => {
for blocked in &policy.blocked_commands {
if command.starts_with(blocked) || command.contains(blocked) {
return Some(format!(
"Command '{}' is blocked by security policy",
command
));
}
}
None
}
PermissionAction::FileRead { path }
| PermissionAction::FileWrite { path, .. }
| PermissionAction::FileDelete { path } => {
if policy.is_path_blocked(path) {
return Some(format!("Path '{}' is blocked by security policy", path));
}
None
}
PermissionAction::NetworkRequest { url, .. } => {
if policy.blocked_urls.iter().any(|u| url.contains(u)) {
return Some(format!("URL '{}' is blocked by security policy", url));
}
None
}
_ => None,
}
}
fn check_cache(&self, request: &PermissionRequest) -> Option<CachedPermission> {
let cache = self.cache.read();
let key = self.cache_key(&request.action);
if let Some(cached) = cache.get(&key) {
if cached.is_valid() {
let mut cached = cached.clone();
cached.use_once();
self.cache.write().insert(key, cached.clone());
return Some(cached);
}
}
None
}
fn cache_permission(&self, action: &PermissionAction, decision: PermissionDecision) {
let key = self.cache_key(action);
let expire_seconds = self.policy.read().cache_expire_seconds;
let cached = CachedPermission {
action_pattern: key.clone(),
decision,
cached_at: chrono::Utc::now(),
expires_at: if expire_seconds > 0 {
Some(chrono::Utc::now() + chrono::Duration::seconds(expire_seconds as i64))
} else {
None
},
use_count: 0,
};
self.cache.write().insert(key, cached);
}
fn cache_key(&self, action: &PermissionAction) -> String {
match action {
PermissionAction::CommandExecute { command, args } => {
format!("cmd:{}:{}", command, args.join(" "))
}
PermissionAction::FileRead { path } => format!("read:{}", path),
PermissionAction::FileWrite { path, .. } => format!("write:{}", path),
PermissionAction::FileDelete { path } => format!("delete:{}", path),
PermissionAction::NetworkRequest { url, method } => format!("net:{}:{}", method, url),
PermissionAction::EnvAccess { names } => format!("env:{}", names.join(",")),
PermissionAction::PackageInstall { packages } => format!("pkg:{}", packages.join(",")),
PermissionAction::SystemAccess { resource } => format!("sys:{}", resource),
PermissionAction::Custom { description } => format!("custom:{}", description),
}
}
fn log_audit(
&self,
request: &PermissionRequest,
response: &PermissionResponse,
from_cache: bool,
) {
if !self.policy.read().audit_enabled {
return;
}
let entry = AuditEntry {
id: generate_short_id(),
request: request.clone(),
response: response.clone(),
from_cache,
timestamp: chrono::Utc::now(),
};
let max = self.policy.read().max_audit_entries;
let mut log = self.audit_log.write();
log.push(entry);
if log.len() > max {
let excess = log.len() - max;
log.drain(0..excess);
}
}
pub fn get_audit_log(&self) -> Vec<AuditEntry> {
self.audit_log.read().clone()
}
pub fn clear_audit_log(&self) {
self.audit_log.write().clear();
}
pub fn cache_stats(&self) -> (usize, usize) {
let cache = self.cache.read();
let total = cache.len();
let valid = cache.values().filter(|c| c.is_valid()).count();
(total, valid)
}
pub fn clear_cache(&self) {
self.cache.write().clear();
}
pub fn request_command(&self, command: &str, args: Vec<String>) -> PermissionRequest {
PermissionRequest::new(PermissionAction::CommandExecute {
command: command.to_string(),
args,
})
}
pub fn request_file_read(&self, path: &str) -> PermissionRequest {
PermissionRequest::new(PermissionAction::FileRead {
path: path.to_string(),
})
}
pub fn request_file_write(
&self,
path: &str,
content_preview: Option<&str>,
) -> PermissionRequest {
PermissionRequest::new(PermissionAction::FileWrite {
path: path.to_string(),
content_preview: content_preview.map(|s| s.to_string()),
})
}
pub fn request_file_delete(&self, path: &str) -> PermissionRequest {
PermissionRequest::new(PermissionAction::FileDelete {
path: path.to_string(),
})
}
pub fn request_network(&self, url: &str, method: &str) -> PermissionRequest {
PermissionRequest::new(PermissionAction::NetworkRequest {
url: url.to_string(),
method: method.to_string(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_permission_manager_creation() {
let manager = PermissionManager::default();
assert_eq!(manager.security_level(), SecurityLevel::Standard);
}
#[test]
fn test_trusted_policy_auto_approves() {
let manager = PermissionManager::new(PermissionPolicy::trusted());
let request = PermissionRequest::new(PermissionAction::FileRead {
path: "/test/file.txt".to_string(),
});
let result = manager.check_permission(request);
assert!(result.is_ok());
}
#[test]
fn test_blocked_path_denied() {
let manager = PermissionManager::default();
let request = PermissionRequest::new(PermissionAction::FileRead {
path: ".env".to_string(),
});
let result = manager.check_permission(request);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
PermissionError::BlockedByPolicy(_)
));
}
#[test]
fn test_cache_stats() {
let manager = PermissionManager::default();
let (total, valid) = manager.cache_stats();
assert_eq!(total, 0);
assert_eq!(valid, 0);
}
}