use super::{GrantTarget, PermissionLevel};
use serde::{Deserialize, Serialize};
use std::time::Instant;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Grant {
pub target: GrantTarget,
pub level: PermissionLevel,
#[serde(skip)]
pub expires: Option<Instant>,
}
impl Grant {
pub fn new(target: GrantTarget, level: PermissionLevel) -> Self {
Self {
target,
level,
expires: None,
}
}
pub fn with_expiration(target: GrantTarget, level: PermissionLevel, expires: Instant) -> Self {
Self {
target,
level,
expires: Some(expires),
}
}
pub fn read_path(path: impl Into<std::path::PathBuf>, recursive: bool) -> Self {
Self::new(GrantTarget::path(path, recursive), PermissionLevel::Read)
}
pub fn write_path(path: impl Into<std::path::PathBuf>, recursive: bool) -> Self {
Self::new(GrantTarget::path(path, recursive), PermissionLevel::Write)
}
pub fn execute_path(path: impl Into<std::path::PathBuf>, recursive: bool) -> Self {
Self::new(GrantTarget::path(path, recursive), PermissionLevel::Execute)
}
pub fn admin_path(path: impl Into<std::path::PathBuf>, recursive: bool) -> Self {
Self::new(GrantTarget::path(path, recursive), PermissionLevel::Admin)
}
pub fn domain(pattern: impl Into<String>, level: PermissionLevel) -> Self {
Self::new(GrantTarget::domain(pattern), level)
}
pub fn command(pattern: impl Into<String>, level: PermissionLevel) -> Self {
Self::new(GrantTarget::command(pattern), level)
}
pub fn tool(tool_name: impl Into<String>, level: PermissionLevel) -> Self {
Self::new(GrantTarget::tool(tool_name), level)
}
pub fn satisfies(&self, request: &PermissionRequest) -> bool {
if let Some(expires) = self.expires
&& Instant::now() >= expires
{
return false;
}
if !self.target.covers(&request.target) {
return false;
}
self.level.satisfies(request.required_level)
}
pub fn is_expired(&self) -> bool {
self.expires.is_some_and(|e| Instant::now() >= e)
}
pub fn description(&self) -> String {
format!("[{}] {}", self.level, self.target)
}
}
impl std::fmt::Display for Grant {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.description())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PermissionRequest {
pub id: String,
pub target: GrantTarget,
pub required_level: PermissionLevel,
pub description: String,
pub reason: Option<String>,
pub tool_name: Option<String>,
}
impl PermissionRequest {
pub fn new(
id: impl Into<String>,
target: GrantTarget,
required_level: PermissionLevel,
description: impl Into<String>,
) -> Self {
Self {
id: id.into(),
target,
required_level,
description: description.into(),
reason: None,
tool_name: None,
}
}
pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
self.reason = Some(reason.into());
self
}
pub fn with_tool(mut self, tool_name: impl Into<String>) -> Self {
self.tool_name = Some(tool_name.into());
self
}
pub fn file_read(id: impl Into<String>, path: impl Into<std::path::PathBuf>) -> Self {
let path = path.into();
let description = format!("Read file: {}", path.display());
Self::new(
id,
GrantTarget::path(path, false),
PermissionLevel::Read,
description,
)
}
pub fn file_write(id: impl Into<String>, path: impl Into<std::path::PathBuf>) -> Self {
let path = path.into();
let description = format!("Write file: {}", path.display());
Self::new(
id,
GrantTarget::path(path, false),
PermissionLevel::Write,
description,
)
}
pub fn directory_read(
id: impl Into<String>,
path: impl Into<std::path::PathBuf>,
recursive: bool,
) -> Self {
let path = path.into();
let description = if recursive {
format!("Read directory (recursive): {}", path.display())
} else {
format!("Read directory: {}", path.display())
};
Self::new(
id,
GrantTarget::path(path, recursive),
PermissionLevel::Read,
description,
)
}
pub fn command_execute(id: impl Into<String>, command: impl Into<String>) -> Self {
let command = command.into();
let description = format!("Execute command: {}", command);
Self::new(
id,
GrantTarget::command(command),
PermissionLevel::Execute,
description,
)
}
pub fn tool_use(
id: impl Into<String>,
tool_name: impl Into<String>,
level: PermissionLevel,
) -> Self {
let tool_name = tool_name.into();
let description = format!("Use tool: {}", tool_name);
Self::new(id, GrantTarget::tool(&tool_name), level, description).with_tool(tool_name)
}
pub fn network_access(
id: impl Into<String>,
domain: impl Into<String>,
level: PermissionLevel,
) -> Self {
let domain = domain.into();
let description = format!("Access domain: {}", domain);
Self::new(id, GrantTarget::domain(domain), level, description)
}
}
impl std::fmt::Display for PermissionRequest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "[{}] {}", self.required_level, self.description)
}
}
#[cfg(test)]
mod tests {
use super::*;
mod grant_tests {
use super::*;
#[test]
fn test_grant_satisfies_same_level() {
let grant = Grant::read_path("/project/src", true);
let request = PermissionRequest::file_read("1", "/project/src/main.rs");
assert!(grant.satisfies(&request));
}
#[test]
fn test_grant_satisfies_higher_level() {
let grant = Grant::write_path("/project/src", true);
let request = PermissionRequest::file_read("1", "/project/src/main.rs");
assert!(grant.satisfies(&request));
}
#[test]
fn test_grant_fails_lower_level() {
let grant = Grant::read_path("/project/src", true);
let request = PermissionRequest::file_write("1", "/project/src/main.rs");
assert!(!grant.satisfies(&request));
}
#[test]
fn test_grant_fails_wrong_path() {
let grant = Grant::write_path("/project/src", true);
let request = PermissionRequest::file_write("1", "/other/file.rs");
assert!(!grant.satisfies(&request));
}
#[test]
fn test_grant_fails_non_recursive() {
let grant = Grant::read_path("/project/src", false);
let request = PermissionRequest::file_read("1", "/project/src/utils/mod.rs");
assert!(!grant.satisfies(&request));
}
#[test]
fn test_admin_grant_satisfies_all_levels() {
let grant = Grant::admin_path("/project", true);
let read_request = PermissionRequest::file_read("1", "/project/src/main.rs");
let write_request = PermissionRequest::file_write("2", "/project/src/main.rs");
assert!(grant.satisfies(&read_request));
assert!(grant.satisfies(&write_request));
}
#[test]
fn test_domain_grant() {
let grant = Grant::domain("*.github.com", PermissionLevel::Read);
let request =
PermissionRequest::network_access("1", "api.github.com", PermissionLevel::Read);
assert!(grant.satisfies(&request));
}
#[test]
fn test_command_grant() {
let grant = Grant::command("git *", PermissionLevel::Execute);
let request = PermissionRequest::command_execute("1", "git status");
assert!(grant.satisfies(&request));
}
#[test]
fn test_tool_grant() {
let grant = Grant::tool("switch_aws_account", PermissionLevel::Execute);
let request =
PermissionRequest::tool_use("1", "switch_aws_account", PermissionLevel::Execute);
assert!(grant.satisfies(&request));
}
#[test]
fn test_tool_grant_different_tool_fails() {
let grant = Grant::tool("switch_aws_account", PermissionLevel::Execute);
let request =
PermissionRequest::tool_use("1", "delete_resource", PermissionLevel::Execute);
assert!(!grant.satisfies(&request));
}
#[test]
fn test_expired_grant() {
use std::time::Duration;
let expired = Instant::now() - Duration::from_secs(1);
let grant = Grant::with_expiration(
GrantTarget::path("/project", true),
PermissionLevel::Read,
expired,
);
let request = PermissionRequest::file_read("1", "/project/file.rs");
assert!(!grant.satisfies(&request));
}
#[test]
fn test_grant_description() {
let grant = Grant::write_path("/project/src", true);
let desc = grant.description();
assert!(desc.contains("Write"));
assert!(desc.contains("/project/src"));
}
}
mod request_tests {
use super::*;
#[test]
fn test_file_read_request() {
let request = PermissionRequest::file_read("test-id", "/path/to/file.rs");
assert_eq!(request.id, "test-id");
assert_eq!(request.required_level, PermissionLevel::Read);
assert!(request.description.contains("Read file"));
}
#[test]
fn test_file_write_request() {
let request = PermissionRequest::file_write("test-id", "/path/to/file.rs");
assert_eq!(request.required_level, PermissionLevel::Write);
assert!(request.description.contains("Write file"));
}
#[test]
fn test_request_with_reason() {
let request = PermissionRequest::file_read("1", "/file.rs")
.with_reason("Need to analyze the code");
assert_eq!(request.reason, Some("Need to analyze the code".to_string()));
}
#[test]
fn test_request_with_tool() {
let request = PermissionRequest::file_read("1", "/file.rs").with_tool("read_file");
assert_eq!(request.tool_name, Some("read_file".to_string()));
}
#[test]
fn test_tool_use_request() {
let request = PermissionRequest::tool_use(
"test-id",
"switch_aws_account",
PermissionLevel::Execute,
);
assert_eq!(request.id, "test-id");
assert_eq!(request.required_level, PermissionLevel::Execute);
assert!(request.description.contains("Use tool"));
assert!(request.description.contains("switch_aws_account"));
assert_eq!(request.tool_name, Some("switch_aws_account".to_string()));
}
}
mod serialization_tests {
use super::*;
#[test]
fn test_grant_serialization() {
let grant = Grant::write_path("/project/src", true);
let json = serde_json::to_string(&grant).unwrap();
let deserialized: Grant = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.target, grant.target);
assert_eq!(deserialized.level, grant.level);
}
#[test]
fn test_request_serialization() {
let request =
PermissionRequest::file_read("test-id", "/path/to/file.rs").with_reason("testing");
let json = serde_json::to_string(&request).unwrap();
let deserialized: PermissionRequest = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.id, request.id);
assert_eq!(deserialized.reason, request.reason);
}
}
}