use crate::permissions::{Grant, PermissionRequest};
#[derive(Debug, Clone)]
pub enum PolicyDecision {
Allow,
AllowWithGrant(Grant),
Deny {
reason: Option<String>,
},
AskUser,
}
impl PolicyDecision {
pub fn allow() -> Self {
Self::Allow
}
pub fn allow_with_grant(grant: Grant) -> Self {
Self::AllowWithGrant(grant)
}
pub fn deny(reason: impl Into<String>) -> Self {
Self::Deny {
reason: Some(reason.into()),
}
}
pub fn deny_silent() -> Self {
Self::Deny { reason: None }
}
pub fn ask_user() -> Self {
Self::AskUser
}
}
pub trait PermissionPolicy: Send + Sync + 'static {
fn decide(&self, request: &PermissionRequest) -> PolicyDecision;
fn supports_interaction(&self) -> bool {
true
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct AutoApprovePolicy;
impl AutoApprovePolicy {
pub fn new() -> Self {
Self
}
}
impl PermissionPolicy for AutoApprovePolicy {
fn decide(&self, _request: &PermissionRequest) -> PolicyDecision {
PolicyDecision::Allow
}
fn supports_interaction(&self) -> bool {
false }
}
#[derive(Debug, Clone, Default)]
pub struct DenyAllPolicy {
reason: Option<String>,
}
impl DenyAllPolicy {
pub fn new() -> Self {
Self {
reason: Some("Permission denied by policy".to_string()),
}
}
pub fn with_reason(reason: impl Into<String>) -> Self {
Self {
reason: Some(reason.into()),
}
}
}
impl PermissionPolicy for DenyAllPolicy {
fn decide(&self, _request: &PermissionRequest) -> PolicyDecision {
PolicyDecision::Deny {
reason: self.reason.clone(),
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct InteractivePolicy;
impl InteractivePolicy {
pub fn new() -> Self {
Self
}
}
impl PermissionPolicy for InteractivePolicy {
fn decide(&self, _request: &PermissionRequest) -> PolicyDecision {
PolicyDecision::AskUser
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::permissions::{GrantTarget, PermissionLevel};
use std::path::PathBuf;
fn make_request(target: GrantTarget, level: PermissionLevel) -> PermissionRequest {
PermissionRequest {
id: "test-id".to_string(),
target,
required_level: level,
description: "Test operation".to_string(),
reason: None,
tool_name: Some("test_tool".to_string()),
}
}
#[test]
fn test_auto_approve_policy() {
let policy = AutoApprovePolicy::new();
let request = make_request(
GrantTarget::Path {
path: PathBuf::from("/etc/passwd"),
recursive: false,
},
PermissionLevel::Read,
);
match policy.decide(&request) {
PolicyDecision::Allow => {}
other => panic!("Expected Allow, got {:?}", other),
}
}
#[test]
fn test_deny_all_policy() {
let policy = DenyAllPolicy::new();
let request = make_request(
GrantTarget::Path {
path: PathBuf::from("/tmp/test"),
recursive: false,
},
PermissionLevel::Write,
);
match policy.decide(&request) {
PolicyDecision::Deny { reason } => {
assert!(reason.is_some());
}
other => panic!("Expected Deny, got {:?}", other),
}
}
#[test]
fn test_deny_all_policy_custom_reason() {
let policy = DenyAllPolicy::with_reason("Sandbox mode");
let request = make_request(
GrantTarget::Command {
pattern: "rm".to_string(),
},
PermissionLevel::Execute,
);
match policy.decide(&request) {
PolicyDecision::Deny { reason } => {
assert_eq!(reason, Some("Sandbox mode".to_string()));
}
other => panic!("Expected Deny, got {:?}", other),
}
}
#[test]
fn test_interactive_policy() {
let policy = InteractivePolicy::new();
let request = make_request(
GrantTarget::Domain {
pattern: "api.example.com".to_string(),
},
PermissionLevel::Read,
);
match policy.decide(&request) {
PolicyDecision::AskUser => {}
other => panic!("Expected AskUser, got {:?}", other),
}
}
#[test]
fn test_policy_decision_constructors() {
let allow = PolicyDecision::allow();
assert!(matches!(allow, PolicyDecision::Allow));
let deny = PolicyDecision::deny("test reason");
assert!(matches!(deny, PolicyDecision::Deny { reason: Some(_) }));
let deny_silent = PolicyDecision::deny_silent();
assert!(matches!(deny_silent, PolicyDecision::Deny { reason: None }));
let ask = PolicyDecision::ask_user();
assert!(matches!(ask, PolicyDecision::AskUser));
}
}