#![allow(dead_code, unused_imports, unused_variables)]
use chrono::{DateTime, Duration, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use tracing::{debug, info};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PermissionGrant {
pub tool_pattern: String,
#[serde(default)]
pub resource_pattern: Option<String>,
#[serde(default)]
pub expires_at: Option<DateTime<Utc>>,
#[serde(default)]
pub reason: Option<String>,
}
impl PermissionGrant {
pub fn permanent(tool_pattern: &str) -> Self {
Self {
tool_pattern: tool_pattern.to_string(),
resource_pattern: None,
expires_at: None,
reason: None,
}
}
pub fn temporary(tool_pattern: &str, duration: Duration) -> Self {
Self {
tool_pattern: tool_pattern.to_string(),
resource_pattern: None,
expires_at: Some(Utc::now() + duration),
reason: None,
}
}
pub fn session(tool_pattern: &str) -> Self {
Self::temporary(tool_pattern, Duration::hours(24))
}
pub fn with_resource(mut self, pattern: &str) -> Self {
self.resource_pattern = Some(pattern.to_string());
self
}
pub fn with_reason(mut self, reason: &str) -> Self {
self.reason = Some(reason.to_string());
self
}
pub fn is_expired(&self) -> bool {
self.expires_at.map(|exp| Utc::now() > exp).unwrap_or(false)
}
pub fn matches_tool(&self, tool_name: &str) -> bool {
if self.is_expired() {
return false;
}
pattern_matches(&self.tool_pattern, tool_name)
}
pub fn matches(&self, tool_name: &str, resource_path: Option<&str>) -> bool {
if !self.matches_tool(tool_name) {
return false;
}
if let Some(ref res_pattern) = self.resource_pattern {
if let Some(path) = resource_path {
return pattern_matches(res_pattern, path);
}
return false; }
true }
}
fn pattern_matches(pattern: &str, value: &str) -> bool {
if pattern == "*" {
return true;
}
if let Some(prefix) = pattern.strip_suffix('*') {
return value.starts_with(prefix);
}
if let Some(suffix) = pattern.strip_prefix('*') {
return value.ends_with(suffix);
}
pattern == value
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PermissionStore {
grants: Vec<PermissionGrant>,
}
impl PermissionStore {
pub fn new() -> Self {
Self { grants: Vec::new() }
}
pub fn from_config(grants: &[PermissionGrant]) -> Self {
Self {
grants: grants.to_vec(),
}
}
pub fn add(&mut self, grant: PermissionGrant) {
info!(
"Permission granted: {} (expires: {:?})",
grant.tool_pattern, grant.expires_at
);
self.grants.push(grant);
}
pub fn is_authorized(&self, tool_name: &str, resource_path: Option<&str>) -> bool {
self.grants
.iter()
.any(|g| g.matches(tool_name, resource_path))
}
pub fn cleanup_expired(&mut self) -> usize {
let before = self.grants.len();
self.grants.retain(|g| !g.is_expired());
let removed = before - self.grants.len();
if removed > 0 {
debug!("Cleaned up {} expired permission grant(s)", removed);
}
removed
}
pub fn active_count(&self) -> usize {
self.grants.iter().filter(|g| !g.is_expired()).count()
}
pub fn clear(&mut self) {
self.grants.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_permanent_grant() {
let grant = PermissionGrant::permanent("file_write");
assert!(!grant.is_expired());
assert!(grant.matches_tool("file_write"));
assert!(!grant.matches_tool("file_delete"));
}
#[test]
fn test_wildcard_grant() {
let grant = PermissionGrant::permanent("file_*");
assert!(grant.matches_tool("file_write"));
assert!(grant.matches_tool("file_edit"));
assert!(grant.matches_tool("file_delete"));
assert!(!grant.matches_tool("shell_exec"));
}
#[test]
fn test_temporary_grant_not_expired() {
let grant = PermissionGrant::temporary("shell_exec", Duration::hours(1));
assert!(!grant.is_expired());
assert!(grant.matches_tool("shell_exec"));
}
#[test]
fn test_expired_grant() {
let mut grant = PermissionGrant::permanent("test");
grant.expires_at = Some(Utc::now() - Duration::hours(1));
assert!(grant.is_expired());
assert!(!grant.matches_tool("test"));
}
#[test]
fn test_resource_pattern() {
let grant = PermissionGrant::permanent("file_write").with_resource("./src/*");
assert!(grant.matches("file_write", Some("./src/main.rs")));
assert!(!grant.matches("file_write", Some("./tests/test.rs")));
assert!(!grant.matches("file_write", None));
}
#[test]
fn test_permission_store() {
let mut store = PermissionStore::new();
assert!(!store.is_authorized("file_write", None));
store.add(PermissionGrant::permanent("file_write"));
assert!(store.is_authorized("file_write", None));
assert!(!store.is_authorized("file_delete", None));
assert_eq!(store.active_count(), 1);
}
#[test]
fn test_pattern_matches() {
assert!(pattern_matches("*", "anything"));
assert!(pattern_matches("file_*", "file_write"));
assert!(pattern_matches("*_exec", "shell_exec"));
assert!(pattern_matches("exact", "exact"));
assert!(!pattern_matches("exact", "other"));
}
}