use chrono::{DateTime, Duration, Utc};
use serde::{Deserialize, Serialize};
use tracing::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 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 active_count(&self) -> usize {
self.grants.iter().filter(|g| !g.is_expired()).count()
}
pub fn clear(&mut self) {
self.grants.clear();
}
}
#[cfg(test)]
#[path = "../../tests/unit/safety/permissions/permissions_test.rs"]
mod tests;