use std::sync::Arc;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use super::config::{PermissionConfig, PermissionType, RiskLevel};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PermissionContext {
pub permission_type: PermissionType,
pub resource: String,
pub operation_description: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub details: Option<serde_json::Value>,
}
impl PermissionContext {
pub fn new(
permission_type: PermissionType,
resource: impl Into<String>,
operation_description: impl Into<String>,
) -> Self {
Self {
permission_type,
resource: resource.into(),
operation_description: operation_description.into(),
details: None,
}
}
pub fn with_details(mut self, details: serde_json::Value) -> Self {
self.details = Some(details);
self
}
pub fn risk_level(&self) -> RiskLevel {
self.permission_type.risk_level()
}
pub fn format_request_message(&self) -> String {
let risk_label = self.risk_level().label();
format!(
"{} - {}\n\nResource: {}\nOperation: {}",
risk_label,
self.permission_type.description(),
self.resource,
self.operation_description
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PermissionResult {
Granted,
Denied,
RequiresConfirmation(PermissionContext),
}
#[derive(Debug, Clone, thiserror::Error)]
pub enum PermissionError {
#[error("Permission denied: {0}")]
Denied(String),
#[error("Permission check failed: {0}")]
CheckFailed(String),
#[error("Confirmation required for {permission_type:?} on {resource}")]
ConfirmationRequired {
permission_type: PermissionType,
resource: String,
},
}
impl PermissionError {
pub fn confirmation_required(context: PermissionContext) -> Self {
Self::ConfirmationRequired {
permission_type: context.permission_type,
resource: context.resource,
}
}
}
#[async_trait]
pub trait PermissionChecker: Send + Sync {
async fn needs_confirmation(&self, perm_type: PermissionType, resource: &str) -> bool;
async fn is_granted(&self, perm_type: PermissionType, resource: &str) -> bool {
!self.needs_confirmation(perm_type, resource).await
}
async fn request_confirmation(&self, ctx: PermissionContext) -> Result<bool, PermissionError>;
fn grant_session_permission(&self, perm_type: PermissionType, resource: String);
async fn check_or_request(&self, ctx: PermissionContext) -> Result<bool, PermissionError> {
if self.is_granted(ctx.permission_type, &ctx.resource).await {
return Ok(true);
}
self.request_confirmation(ctx).await
}
}
#[derive(Debug)]
pub struct ConfigPermissionChecker {
config: Arc<PermissionConfig>,
}
impl ConfigPermissionChecker {
pub fn new(config: Arc<PermissionConfig>) -> Self {
Self { config }
}
pub fn config(&self) -> &PermissionConfig {
&self.config
}
}
#[async_trait]
impl PermissionChecker for ConfigPermissionChecker {
async fn needs_confirmation(&self, perm_type: PermissionType, resource: &str) -> bool {
self.config.needs_confirmation(perm_type, resource)
}
async fn request_confirmation(&self, _ctx: PermissionContext) -> Result<bool, PermissionError> {
Err(PermissionError::confirmation_required(_ctx))
}
fn grant_session_permission(&self, perm_type: PermissionType, resource: String) {
self.config.grant_session_permission(perm_type, resource);
}
}
#[derive(Debug)]
pub struct LoggingPermissionChecker<T: PermissionChecker> {
inner: T,
}
impl<T: PermissionChecker> LoggingPermissionChecker<T> {
pub fn new(inner: T) -> Self {
Self { inner }
}
}
#[async_trait]
impl<T: PermissionChecker> PermissionChecker for LoggingPermissionChecker<T> {
async fn needs_confirmation(&self, perm_type: PermissionType, resource: &str) -> bool {
let needs = self.inner.needs_confirmation(perm_type, resource).await;
tracing::debug!(
"Permission check: {:?} for '{}' - needs_confirmation: {}",
perm_type,
resource,
needs
);
needs
}
async fn request_confirmation(&self, ctx: PermissionContext) -> Result<bool, PermissionError> {
tracing::info!(
"Requesting user confirmation: {:?} for '{}'",
ctx.permission_type,
ctx.resource
);
let result = self.inner.request_confirmation(ctx).await;
tracing::debug!("User confirmation result: {:?}", result);
result
}
fn grant_session_permission(&self, perm_type: PermissionType, resource: String) {
tracing::info!(
"Granting session permission: {:?} for '{}'",
perm_type,
resource
);
self.inner.grant_session_permission(perm_type, resource);
}
}
#[derive(Debug, Clone)]
pub struct AllowAllPermissionChecker;
#[async_trait]
impl PermissionChecker for AllowAllPermissionChecker {
async fn needs_confirmation(&self, _perm_type: PermissionType, _resource: &str) -> bool {
false
}
async fn request_confirmation(&self, _ctx: PermissionContext) -> Result<bool, PermissionError> {
Ok(true)
}
fn grant_session_permission(&self, _perm_type: PermissionType, _resource: String) {
}
}
#[derive(Debug, Clone)]
pub struct DenyDangerousPermissionChecker;
#[async_trait]
impl PermissionChecker for DenyDangerousPermissionChecker {
async fn needs_confirmation(&self, perm_type: PermissionType, _resource: &str) -> bool {
matches!(perm_type.risk_level(), RiskLevel::High | RiskLevel::Medium)
}
async fn request_confirmation(&self, ctx: PermissionContext) -> Result<bool, PermissionError> {
Err(PermissionError::Denied(format!(
"{} operation denied: {}",
ctx.permission_type.description(),
ctx.resource
)))
}
fn grant_session_permission(&self, _perm_type: PermissionType, _resource: String) {
}
}
#[async_trait]
pub trait PermissionCheckerExt: PermissionChecker {
async fn check_write_file(&self, path: &str) -> Result<(), PermissionError> {
let ctx = PermissionContext::new(
PermissionType::WriteFile,
path,
format!("Write file: {}", path),
);
if self.check_or_request(ctx).await? {
Ok(())
} else {
Err(PermissionError::Denied(format!(
"Write permission denied for: {}",
path
)))
}
}
async fn check_execute_command(&self, command: &str) -> Result<(), PermissionError> {
let ctx = PermissionContext::new(
PermissionType::ExecuteCommand,
command,
format!("Execute command: {}", command),
);
if self.check_or_request(ctx).await? {
Ok(())
} else {
Err(PermissionError::Denied(format!(
"Command execution denied for: {}",
command
)))
}
}
async fn check_http_request(&self, url: &str) -> Result<(), PermissionError> {
let ctx = PermissionContext::new(
PermissionType::HttpRequest,
url,
format!("HTTP request to: {}", url),
);
if self.check_or_request(ctx).await? {
Ok(())
} else {
Err(PermissionError::Denied(format!(
"HTTP request denied for: {}",
url
)))
}
}
async fn check_delete(&self, path: &str) -> Result<(), PermissionError> {
let ctx = PermissionContext::new(
PermissionType::DeleteOperation,
path,
format!("Delete: {}", path),
);
if self.check_or_request(ctx).await? {
Ok(())
} else {
Err(PermissionError::Denied(format!(
"Delete permission denied for: {}",
path
)))
}
}
async fn check_git_write(&self, operation: &str) -> Result<(), PermissionError> {
let ctx = PermissionContext::new(
PermissionType::GitWrite,
operation,
format!("Git operation: {}", operation),
);
if self.check_or_request(ctx).await? {
Ok(())
} else {
Err(PermissionError::Denied(format!(
"Git write denied for: {}",
operation
)))
}
}
async fn check_terminal_session(&self, command: &str) -> Result<(), PermissionError> {
let ctx = PermissionContext::new(
PermissionType::TerminalSession,
command,
format!("Terminal session: {}", command),
);
if self.check_or_request(ctx).await? {
Ok(())
} else {
Err(PermissionError::Denied(format!(
"Terminal session denied for: {}",
command
)))
}
}
}
#[async_trait]
impl<T: PermissionChecker + ?Sized> PermissionCheckerExt for T {}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_allow_all_checker() {
let checker = AllowAllPermissionChecker;
assert!(
!checker
.needs_confirmation(PermissionType::WriteFile, "/tmp/test")
.await
);
assert!(
!checker
.needs_confirmation(PermissionType::ExecuteCommand, "rm -rf /")
.await
);
let ctx = PermissionContext::new(PermissionType::WriteFile, "/tmp/test", "test");
assert!(checker.request_confirmation(ctx).await.unwrap());
}
#[tokio::test]
async fn test_deny_dangerous_checker() {
let checker = DenyDangerousPermissionChecker;
assert!(
checker
.needs_confirmation(PermissionType::WriteFile, "/tmp/test")
.await
);
assert!(
checker
.needs_confirmation(PermissionType::ExecuteCommand, "ls")
.await
);
}
#[tokio::test]
async fn test_config_checker() {
let config = Arc::new(PermissionConfig::new());
let checker = ConfigPermissionChecker::new(config);
assert!(
checker
.needs_confirmation(PermissionType::WriteFile, "/tmp/test")
.await
);
checker.grant_session_permission(PermissionType::WriteFile, "/tmp/*".to_string());
assert!(
!checker
.needs_confirmation(PermissionType::WriteFile, "/tmp/test")
.await
);
}
#[test]
fn test_permission_context() {
let ctx = PermissionContext::new(
PermissionType::WriteFile,
"/tmp/test.txt",
"Write configuration file",
);
assert_eq!(ctx.permission_type, PermissionType::WriteFile);
assert_eq!(ctx.resource, "/tmp/test.txt");
assert!(ctx.operation_description.contains("Write configuration"));
assert_eq!(ctx.risk_level(), RiskLevel::Medium);
let message = ctx.format_request_message();
assert!(message.contains("Medium Risk"));
assert!(message.contains("/tmp/test.txt"));
}
}