mixtape_core/permission/
grant.rs1use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
11pub enum Scope {
12 #[default]
14 Session,
15
16 Persistent,
18}
19
20impl std::fmt::Display for Scope {
21 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22 match self {
23 Scope::Session => write!(f, "Session"),
24 Scope::Persistent => write!(f, "Persistent"),
25 }
26 }
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct Grant {
50 pub tool: String,
52
53 pub params_hash: Option<String>,
58
59 #[serde(default)]
61 pub scope: Scope,
62
63 pub created_at: DateTime<Utc>,
65}
66
67impl Grant {
68 pub fn tool(name: impl Into<String>) -> Self {
70 Self {
71 tool: name.into(),
72 params_hash: None,
73 scope: Scope::default(),
74 created_at: Utc::now(),
75 }
76 }
77
78 pub fn exact(name: impl Into<String>, params_hash: impl Into<String>) -> Self {
83 Self {
84 tool: name.into(),
85 params_hash: Some(params_hash.into()),
86 scope: Scope::default(),
87 created_at: Utc::now(),
88 }
89 }
90
91 pub fn with_scope(mut self, scope: Scope) -> Self {
93 self.scope = scope;
94 self
95 }
96
97 pub fn is_tool_wide(&self) -> bool {
99 self.params_hash.is_none()
100 }
101
102 pub fn matches(&self, params_hash: &str) -> bool {
104 match &self.params_hash {
105 None => true, Some(h) => h == params_hash,
107 }
108 }
109}
110
111impl PartialEq for Grant {
112 fn eq(&self, other: &Self) -> bool {
113 self.tool == other.tool
114 && self.params_hash == other.params_hash
115 && self.scope == other.scope
116 }
117}
118
119impl Eq for Grant {}
120
121pub fn hash_params(params: &serde_json::Value) -> String {
126 use sha2::{Digest, Sha256};
127
128 let canonical = canonicalize_json(params);
129 let json = serde_json::to_string(&canonical).unwrap_or_default();
130 let hash = Sha256::digest(json.as_bytes());
131 format!("{:x}", hash)
132}
133
134fn canonicalize_json(value: &serde_json::Value) -> serde_json::Value {
136 use serde_json::Value;
137 use std::collections::BTreeMap;
138
139 match value {
140 Value::Object(map) => {
141 let sorted: BTreeMap<_, _> = map
142 .iter()
143 .map(|(k, v)| (k.clone(), canonicalize_json(v)))
144 .collect();
145 Value::Object(sorted.into_iter().collect())
146 }
147 Value::Array(arr) => Value::Array(arr.iter().map(canonicalize_json).collect()),
148 other => other.clone(),
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 #[test]
157 fn test_grant_tool() {
158 let grant = Grant::tool("echo");
159 assert_eq!(grant.tool, "echo");
160 assert!(grant.params_hash.is_none());
161 assert!(grant.is_tool_wide());
162 assert_eq!(grant.scope, Scope::Session);
163 }
164
165 #[test]
166 fn test_grant_exact() {
167 let grant = Grant::exact("database", "abc123");
168 assert_eq!(grant.tool, "database");
169 assert_eq!(grant.params_hash, Some("abc123".to_string()));
170 assert!(!grant.is_tool_wide());
171 }
172
173 #[test]
174 fn test_grant_with_scope() {
175 let grant = Grant::tool("test").with_scope(Scope::Persistent);
176 assert_eq!(grant.scope, Scope::Persistent);
177 }
178
179 #[test]
180 fn test_grant_matches() {
181 let tool_grant = Grant::tool("test");
182 assert!(tool_grant.matches("any_hash"));
183 assert!(tool_grant.matches("other_hash"));
184
185 let exact_grant = Grant::exact("test", "specific_hash");
186 assert!(exact_grant.matches("specific_hash"));
187 assert!(!exact_grant.matches("other_hash"));
188 }
189
190 #[test]
191 fn test_hash_params() {
192 let params = serde_json::json!({"key": "value"});
193 let hash = hash_params(¶ms);
194 assert!(!hash.is_empty());
195 assert_eq!(hash.len(), 64); let params2 = serde_json::json!({"key": "value"});
199 assert_eq!(hash_params(¶ms2), hash);
200
201 let params3 = serde_json::json!({"key": "other"});
203 assert_ne!(hash_params(¶ms3), hash);
204 }
205
206 #[test]
207 fn test_hash_params_canonical_order() {
208 let params1 = serde_json::json!({"a": 1, "b": 2, "c": 3});
210 let params2 = serde_json::json!({"c": 3, "b": 2, "a": 1});
211 assert_eq!(hash_params(¶ms1), hash_params(¶ms2));
212
213 let nested1 = serde_json::json!({"outer": {"z": 1, "a": 2}});
215 let nested2 = serde_json::json!({"outer": {"a": 2, "z": 1}});
216 assert_eq!(hash_params(&nested1), hash_params(&nested2));
217 }
218
219 #[test]
220 fn test_scope_display() {
221 assert_eq!(Scope::Session.to_string(), "Session");
222 assert_eq!(Scope::Persistent.to_string(), "Persistent");
223 }
224
225 #[test]
226 fn test_grant_equality() {
227 let g1 = Grant::tool("test");
228 let g2 = Grant::tool("test");
229 assert_eq!(g1, g2); let g3 = Grant::exact("test", "hash");
232 assert_ne!(g1, g3); }
234
235 #[test]
236 fn test_grant_serialization() {
237 let grant = Grant::exact("tool", "hash123").with_scope(Scope::Persistent);
238 let json = serde_json::to_string(&grant).unwrap();
239 let parsed: Grant = serde_json::from_str(&json).unwrap();
240
241 assert_eq!(grant.tool, parsed.tool);
242 assert_eq!(grant.params_hash, parsed.params_hash);
243 assert_eq!(grant.scope, parsed.scope);
244 }
245}