use std::fmt;
use std::str::FromStr;
use regex::Regex;
use serde::{Deserialize, Serialize};
use tracing::{Level, instrument};
use crate::error::{Result, SettingsError};
use crate::types::Permissions;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum PermissionRule {
Allow,
Ask,
Deny,
#[default]
Unset,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Permission {
tool: String,
pattern: Option<PermissionPattern>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum PermissionPattern {
Exact(String),
Prefix(String),
Glob(String),
}
impl Permission {
#[instrument(level = Level::TRACE, skip(name))]
pub fn for_tool(name: impl Into<String>) -> Self {
Self {
tool: name.into(),
pattern: None,
}
}
#[instrument(level = Level::TRACE, skip(tool, pattern))]
pub fn exact(tool: impl Into<String>, pattern: impl Into<String>) -> Self {
Self {
tool: tool.into(),
pattern: Some(PermissionPattern::Exact(pattern.into())),
}
}
#[instrument(level = Level::TRACE, skip(tool, prefix))]
pub fn prefix(tool: impl Into<String>, prefix: impl Into<String>) -> Self {
Self {
tool: tool.into(),
pattern: Some(PermissionPattern::Prefix(prefix.into())),
}
}
#[instrument(level = Level::TRACE, skip(tool, glob))]
pub fn glob(tool: impl Into<String>, glob: impl Into<String>) -> Self {
Self {
tool: tool.into(),
pattern: Some(PermissionPattern::Glob(glob.into())),
}
}
#[instrument(level = Level::TRACE)]
pub fn parse(s: &str) -> Result<Self> {
let s = s.trim();
if let Some(paren_start) = s.find('(') {
if !s.ends_with(')') {
return Err(SettingsError::InvalidPermission(format!(
"malformed permission pattern: {}",
s
)));
}
let tool = s[..paren_start].to_string();
let pattern_str = &s[paren_start + 1..s.len() - 1];
if tool.is_empty() {
return Err(SettingsError::InvalidPermission(
"empty tool name".to_string(),
));
}
let pattern = if let Some(prefix) = pattern_str.strip_suffix(":*") {
PermissionPattern::Prefix(prefix.to_string())
} else if pattern_str.contains('*') || pattern_str.contains("**") {
PermissionPattern::Glob(pattern_str.to_string())
} else {
PermissionPattern::Exact(pattern_str.to_string())
};
Ok(Self {
tool,
pattern: Some(pattern),
})
} else {
if s.is_empty() {
return Err(SettingsError::InvalidPermission(
"empty permission".to_string(),
));
}
Ok(Self::for_tool(s))
}
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn tool(&self) -> &str {
&self.tool
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn pattern(&self) -> Option<&PermissionPattern> {
self.pattern.as_ref()
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn matches(&self, tool: &str, arg: Option<&str>) -> bool {
if self.tool != tool {
return false;
}
match (&self.pattern, arg) {
(None, _) => true,
(Some(_), None) => false,
(Some(pattern), Some(arg)) => pattern.matches(arg),
}
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn to_pattern_string(&self) -> String {
match &self.pattern {
None => self.tool.clone(),
Some(PermissionPattern::Exact(p)) => format!("{}({})", self.tool, p),
Some(PermissionPattern::Prefix(p)) => format!("{}({}:*)", self.tool, p),
Some(PermissionPattern::Glob(p)) => format!("{}({})", self.tool, p),
}
}
}
impl PermissionPattern {
#[instrument(level = Level::TRACE, skip(self))]
pub fn matches(&self, arg: &str) -> bool {
match self {
PermissionPattern::Exact(pattern) => arg == pattern,
PermissionPattern::Prefix(prefix) => {
arg == prefix || arg.starts_with(&format!("{} ", prefix))
}
PermissionPattern::Glob(glob) => glob_matches(glob, arg),
}
}
}
impl FromStr for Permission {
type Err = SettingsError;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
Permission::parse(s)
}
}
impl From<&str> for Permission {
fn from(value: &str) -> Self {
Self::parse(value).unwrap_or_else(|_| Self {
tool: value.to_string(),
pattern: None,
})
}
}
impl fmt::Display for Permission {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_pattern_string())
}
}
impl Serialize for Permission {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_pattern_string())
}
}
impl<'de> Deserialize<'de> for Permission {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Permission::parse(&s).map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PermissionSet {
#[serde(default, skip_serializing_if = "Option::is_none")]
default_mode: Option<String>,
#[serde(default)]
allow: Vec<Permission>,
#[serde(default)]
ask: Vec<Permission>,
#[serde(default)]
deny: Vec<Permission>,
}
impl PermissionSet {
#[instrument(level = Level::TRACE)]
pub fn new() -> Self {
Self::default()
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn is_empty(&self) -> bool {
self.default_mode.is_none()
&& self.allow.is_empty()
&& self.deny.is_empty()
&& self.ask.is_empty()
}
#[instrument(level = Level::TRACE, skip(self, mode))]
pub fn with_default_mode(mut self, mode: impl Into<String>) -> Self {
self.default_mode = Some(mode.into());
self
}
pub fn default_mode(&self) -> Option<&str> {
self.default_mode.as_deref()
}
#[instrument(level = Level::TRACE, skip(self, mode))]
pub fn set_default_mode(&mut self, mode: impl Into<String>) -> &mut Self {
self.default_mode = Some(mode.into());
self
}
#[instrument(level = Level::TRACE)]
pub fn from_permissions(perms: &Permissions) -> Result<Self> {
let mut set = Self::new();
for pattern in &perms.allow {
set.allow.push(Permission::parse(pattern)?);
}
for pattern in &perms.ask {
set.ask.push(Permission::parse(pattern)?);
}
for pattern in &perms.deny {
set.deny.push(Permission::parse(pattern)?);
}
Ok(set)
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn to_permissions(&self) -> Permissions {
Permissions {
allow: self.allow.iter().map(|p| p.to_pattern_string()).collect(),
ask: self.ask.iter().map(|p| p.to_pattern_string()).collect(),
deny: self.deny.iter().map(|p| p.to_pattern_string()).collect(),
}
}
#[instrument(level = Level::TRACE, skip(self, perm))]
pub fn allow(mut self, perm: impl Into<Permission>) -> Self {
let perm = perm.into();
if !self.allow.contains(&perm) {
self.allow.push(perm);
}
self
}
#[instrument(level = Level::TRACE, skip(self, perm))]
pub fn ask(mut self, perm: impl Into<Permission>) -> Self {
let perm = perm.into();
if !self.ask.contains(&perm) {
self.ask.push(perm);
}
self
}
#[instrument(level = Level::TRACE, skip(self, perm))]
pub fn deny(mut self, perm: impl Into<Permission>) -> Self {
let perm = perm.into();
if !self.deny.contains(&perm) {
self.deny.push(perm);
}
self
}
#[instrument(level = Level::TRACE, skip(self, perm))]
pub fn insert_allow(&mut self, perm: impl Into<Permission>) -> &mut Self {
let perm = perm.into();
if !self.allow.contains(&perm) {
self.allow.push(perm);
}
self
}
#[instrument(level = Level::TRACE, skip(self, perm))]
pub fn insert_ask(&mut self, perm: impl Into<Permission>) -> &mut Self {
let perm = perm.into();
if !self.ask.contains(&perm) {
self.ask.push(perm);
}
self
}
#[instrument(level = Level::TRACE, skip(self, perm))]
pub fn insert_deny(&mut self, perm: impl Into<Permission>) -> &mut Self {
let perm = perm.into();
if !self.deny.contains(&perm) {
self.deny.push(perm);
}
self
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn remove_allow(&mut self, perm: &Permission) -> &mut Self {
self.allow.retain(|p| p != perm);
self
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn remove_ask(&mut self, perm: &Permission) -> &mut Self {
self.ask.retain(|p| p != perm);
self
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn remove_deny(&mut self, perm: &Permission) -> &mut Self {
self.deny.retain(|p| p != perm);
self
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn remove(&mut self, perm: &Permission) -> &mut Self {
self.remove_allow(perm).remove_ask(perm).remove_deny(perm)
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn check(&self, tool: &str, arg: Option<&str>) -> PermissionRule {
for perm in &self.deny {
if perm.matches(tool, arg) {
return PermissionRule::Deny;
}
}
for perm in &self.ask {
if perm.matches(tool, arg) {
return PermissionRule::Ask;
}
}
for perm in &self.allow {
if perm.matches(tool, arg) {
return PermissionRule::Allow;
}
}
PermissionRule::Unset
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn is_allowed(&self, tool: &str, arg: Option<&str>) -> bool {
self.check(tool, arg) == PermissionRule::Allow
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn is_denied(&self, tool: &str, arg: Option<&str>) -> bool {
self.check(tool, arg) == PermissionRule::Deny
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn requires_ask(&self, tool: &str, arg: Option<&str>) -> bool {
self.check(tool, arg) == PermissionRule::Ask
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn allowed(&self) -> &[Permission] {
&self.allow
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn asking(&self) -> &[Permission] {
&self.ask
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn denied(&self) -> &[Permission] {
&self.deny
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn for_tool(&self, tool: &str) -> ToolPermissions {
ToolPermissions {
tool: tool.to_string(),
allow: self
.allow
.iter()
.filter(|p| p.tool() == tool)
.cloned()
.collect(),
ask: self
.ask
.iter()
.filter(|p| p.tool() == tool)
.cloned()
.collect(),
deny: self
.deny
.iter()
.filter(|p| p.tool() == tool)
.cloned()
.collect(),
}
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn clear(&mut self) {
self.default_mode = None;
self.allow.clear();
self.ask.clear();
self.deny.clear();
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn merge(&self, other: &Self) -> Self {
let mut this = self.clone();
if other.default_mode.is_some() {
this.default_mode = other.default_mode.clone();
}
for perm in &other.allow {
if !this.allow.contains(perm) {
this.allow.push(perm.clone());
}
}
for perm in &other.ask {
if !this.ask.contains(perm) {
this.ask.push(perm.clone());
}
}
for perm in &other.deny {
if !this.deny.contains(perm) {
this.deny.push(perm.clone());
}
}
this
}
}
#[derive(Debug, Clone)]
pub struct ToolPermissions {
pub tool: String,
pub allow: Vec<Permission>,
pub ask: Vec<Permission>,
pub deny: Vec<Permission>,
}
impl ToolPermissions {
#[instrument(level = Level::TRACE, skip(self))]
pub fn check(&self, arg: Option<&str>) -> PermissionRule {
for perm in &self.deny {
if perm.matches(&self.tool, arg) {
return PermissionRule::Deny;
}
}
for perm in &self.ask {
if perm.matches(&self.tool, arg) {
return PermissionRule::Ask;
}
}
for perm in &self.allow {
if perm.matches(&self.tool, arg) {
return PermissionRule::Allow;
}
}
PermissionRule::Unset
}
}
fn glob_matches(pattern: &str, path: &str) -> bool {
let regex_pattern = pattern
.replace('.', "\\.")
.replace("**", "<<<DOUBLESTAR>>>")
.replace('*', "[^/]*")
.replace("<<<DOUBLESTAR>>>", ".*")
.replace('?', ".");
let regex_pattern = format!("^{}$", regex_pattern);
Regex::new(®ex_pattern)
.map(|re| re.is_match(path))
.unwrap_or(false)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_tool_only() {
let perm = Permission::parse("Edit").unwrap();
assert_eq!(perm.tool(), "Edit");
assert!(perm.pattern().is_none());
assert!(perm.matches("Edit", None));
assert!(perm.matches("Edit", Some("anything")));
assert!(!perm.matches("Read", None));
}
#[test]
fn test_parse_exact_pattern() {
let perm = Permission::parse("Read(.env)").unwrap();
assert_eq!(perm.tool(), "Read");
assert!(matches!(perm.pattern(), Some(PermissionPattern::Exact(_))));
assert!(perm.matches("Read", Some(".env")));
assert!(!perm.matches("Read", Some(".env.local")));
assert!(!perm.matches("Read", None));
}
#[test]
fn test_parse_prefix_wildcard() {
let perm = Permission::parse("Bash(git:*)").unwrap();
assert_eq!(perm.tool(), "Bash");
assert!(matches!(perm.pattern(), Some(PermissionPattern::Prefix(_))));
assert!(perm.matches("Bash", Some("git")));
assert!(perm.matches("Bash", Some("git status")));
assert!(perm.matches("Bash", Some("git commit -m 'test'")));
assert!(!perm.matches("Bash", Some("gitk")));
assert!(!perm.matches("Bash", Some("npm install")));
}
#[test]
fn test_parse_glob_pattern() {
let perm = Permission::parse("Read(**/*.rs)").unwrap();
assert_eq!(perm.tool(), "Read");
assert!(matches!(perm.pattern(), Some(PermissionPattern::Glob(_))));
assert!(perm.matches("Read", Some("src/main.rs")));
assert!(perm.matches("Read", Some("lib/utils/helper.rs")));
assert!(!perm.matches("Read", Some("src/main.py")));
}
#[test]
fn test_permission_display() {
assert_eq!(Permission::for_tool("Edit").to_string(), "Edit");
assert_eq!(Permission::exact("Read", ".env").to_string(), "Read(.env)");
assert_eq!(Permission::prefix("Bash", "git").to_string(), "Bash(git:*)");
assert_eq!(
Permission::glob("Read", "**/*.rs").to_string(),
"Read(**/*.rs)"
);
}
#[test]
fn test_permission_serde() {
let perm = Permission::prefix("Bash", "git");
let json = serde_json::to_string(&perm).unwrap();
assert_eq!(json, "\"Bash(git:*)\"");
let parsed: Permission = serde_json::from_str(&json).unwrap();
assert_eq!(perm, parsed);
}
#[test]
fn test_permission_set_check() {
let set = PermissionSet::new()
.allow(Permission::prefix("Bash", "git"))
.deny(Permission::exact("Read", ".env"))
.ask(Permission::for_tool("Write"));
assert_eq!(set.check("Bash", Some("git status")), PermissionRule::Allow);
assert_eq!(set.check("Read", Some(".env")), PermissionRule::Deny);
assert_eq!(set.check("Write", Some("file.txt")), PermissionRule::Ask);
assert_eq!(set.check("Edit", None), PermissionRule::Unset);
}
#[test]
fn test_permission_set_precedence() {
let set = PermissionSet::new()
.allow(Permission::for_tool("Bash"))
.deny(Permission::prefix("Bash", "rm"));
assert_eq!(set.check("Bash", Some("rm -rf /")), PermissionRule::Deny);
assert_eq!(set.check("Bash", Some("ls")), PermissionRule::Allow);
}
#[test]
fn test_permission_set_from_permissions() {
let perms = Permissions {
allow: vec!["Bash(git:*)".to_string(), "Edit".to_string()],
ask: vec!["Write".to_string()],
deny: vec!["Read(.env)".to_string()],
};
let set = PermissionSet::from_permissions(&perms).unwrap();
assert_eq!(set.allowed().len(), 2);
assert_eq!(set.asking().len(), 1);
assert_eq!(set.denied().len(), 1);
let back = set.to_permissions();
assert_eq!(perms.allow, back.allow);
assert_eq!(perms.ask, back.ask);
assert_eq!(perms.deny, back.deny);
}
#[test]
fn test_permission_set_for_tool() {
let set = PermissionSet::new()
.allow(Permission::prefix("Bash", "git"))
.allow(Permission::prefix("Bash", "npm"))
.deny(Permission::exact("Read", ".env"));
let bash_perms = set.for_tool("Bash");
assert_eq!(bash_perms.allow.len(), 2);
assert_eq!(bash_perms.deny.len(), 0);
let read_perms = set.for_tool("Read");
assert_eq!(read_perms.deny.len(), 1);
}
#[test]
fn test_permission_set_merge() {
let mut base = PermissionSet::new().allow(Permission::prefix("Bash", "git"));
let overlay = PermissionSet::new()
.allow(Permission::prefix("Bash", "npm"))
.deny(Permission::exact("Read", ".env"));
base = base.merge(&overlay);
assert_eq!(base.allowed().len(), 2);
assert_eq!(base.denied().len(), 1);
}
#[test]
fn test_default_mode_builder() {
let set = PermissionSet::new().with_default_mode("bypassPermissions");
assert_eq!(set.default_mode(), Some("bypassPermissions"));
assert!(!set.is_empty());
}
#[test]
fn test_default_mode_serialization() {
let set = PermissionSet::new()
.with_default_mode("bypassPermissions")
.allow(Permission::prefix("Bash", "git"));
let json = serde_json::to_string(&set).unwrap();
assert!(json.contains("\"defaultMode\":\"bypassPermissions\""));
let parsed: PermissionSet = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.default_mode(), Some("bypassPermissions"));
assert_eq!(parsed.allowed().len(), 1);
}
#[test]
fn test_default_mode_deserialization() {
let json = r#"{"defaultMode": "bypassPermissions", "allow": ["Edit"]}"#;
let set: PermissionSet = serde_json::from_str(json).unwrap();
assert_eq!(set.default_mode(), Some("bypassPermissions"));
assert_eq!(set.allowed().len(), 1);
}
#[test]
fn test_default_mode_merge_precedence() {
let base = PermissionSet::new().with_default_mode("default");
let overlay = PermissionSet::new().with_default_mode("bypassPermissions");
let merged = base.merge(&overlay);
assert_eq!(merged.default_mode(), Some("bypassPermissions"));
}
#[test]
fn test_default_mode_merge_preserves_lower() {
let base = PermissionSet::new().with_default_mode("bypassPermissions");
let overlay = PermissionSet::new(); let merged = base.merge(&overlay);
assert_eq!(merged.default_mode(), Some("bypassPermissions"));
}
#[test]
fn test_glob_matches() {
assert!(glob_matches("*.rs", "main.rs"));
assert!(!glob_matches("*.rs", "src/main.rs"));
assert!(glob_matches("**/*.rs", "src/main.rs"));
assert!(glob_matches("**/*.rs", "a/b/c/d.rs"));
assert!(glob_matches("src/*.rs", "src/lib.rs"));
assert!(!glob_matches("src/*.rs", "src/sub/lib.rs"));
assert!(glob_matches("src/**/*.rs", "src/sub/lib.rs"));
}
}