1use super::{GrantTarget, PermissionLevel};
7use serde::{Deserialize, Serialize};
8use std::time::Instant;
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
18pub struct Grant {
19 pub target: GrantTarget,
21 pub level: PermissionLevel,
23 #[serde(skip)]
25 pub expires: Option<Instant>,
26}
27
28impl Grant {
29 pub fn new(target: GrantTarget, level: PermissionLevel) -> Self {
33 Self {
34 target,
35 level,
36 expires: None,
37 }
38 }
39
40 pub fn with_expiration(target: GrantTarget, level: PermissionLevel, expires: Instant) -> Self {
42 Self {
43 target,
44 level,
45 expires: Some(expires),
46 }
47 }
48
49 pub fn read_path(path: impl Into<std::path::PathBuf>, recursive: bool) -> Self {
51 Self::new(GrantTarget::path(path, recursive), PermissionLevel::Read)
52 }
53
54 pub fn write_path(path: impl Into<std::path::PathBuf>, recursive: bool) -> Self {
56 Self::new(GrantTarget::path(path, recursive), PermissionLevel::Write)
57 }
58
59 pub fn execute_path(path: impl Into<std::path::PathBuf>, recursive: bool) -> Self {
61 Self::new(GrantTarget::path(path, recursive), PermissionLevel::Execute)
62 }
63
64 pub fn admin_path(path: impl Into<std::path::PathBuf>, recursive: bool) -> Self {
66 Self::new(GrantTarget::path(path, recursive), PermissionLevel::Admin)
67 }
68
69 pub fn domain(pattern: impl Into<String>, level: PermissionLevel) -> Self {
71 Self::new(GrantTarget::domain(pattern), level)
72 }
73
74 pub fn command(pattern: impl Into<String>, level: PermissionLevel) -> Self {
76 Self::new(GrantTarget::command(pattern), level)
77 }
78
79 pub fn tool(tool_name: impl Into<String>, level: PermissionLevel) -> Self {
81 Self::new(GrantTarget::tool(tool_name), level)
82 }
83
84 pub fn satisfies(&self, request: &PermissionRequest) -> bool {
97 if let Some(expires) = self.expires
99 && Instant::now() >= expires
100 {
101 return false;
102 }
103
104 if !self.target.covers(&request.target) {
106 return false;
107 }
108
109 self.level.satisfies(request.required_level)
111 }
112
113 pub fn is_expired(&self) -> bool {
115 self.expires.is_some_and(|e| Instant::now() >= e)
116 }
117
118 pub fn description(&self) -> String {
120 format!("[{}] {}", self.level, self.target)
121 }
122}
123
124impl std::fmt::Display for Grant {
125 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126 write!(f, "{}", self.description())
127 }
128}
129
130#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
138pub struct PermissionRequest {
139 pub id: String,
141 pub target: GrantTarget,
143 pub required_level: PermissionLevel,
145 pub description: String,
147 pub reason: Option<String>,
149 pub tool_name: Option<String>,
151}
152
153impl PermissionRequest {
154 pub fn new(
156 id: impl Into<String>,
157 target: GrantTarget,
158 required_level: PermissionLevel,
159 description: impl Into<String>,
160 ) -> Self {
161 Self {
162 id: id.into(),
163 target,
164 required_level,
165 description: description.into(),
166 reason: None,
167 tool_name: None,
168 }
169 }
170
171 pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
173 self.reason = Some(reason.into());
174 self
175 }
176
177 pub fn with_tool(mut self, tool_name: impl Into<String>) -> Self {
179 self.tool_name = Some(tool_name.into());
180 self
181 }
182
183 pub fn file_read(id: impl Into<String>, path: impl Into<std::path::PathBuf>) -> Self {
185 let path = path.into();
186 let description = format!("Read file: {}", path.display());
187 Self::new(
188 id,
189 GrantTarget::path(path, false),
190 PermissionLevel::Read,
191 description,
192 )
193 }
194
195 pub fn file_write(id: impl Into<String>, path: impl Into<std::path::PathBuf>) -> Self {
197 let path = path.into();
198 let description = format!("Write file: {}", path.display());
199 Self::new(
200 id,
201 GrantTarget::path(path, false),
202 PermissionLevel::Write,
203 description,
204 )
205 }
206
207 pub fn directory_read(
209 id: impl Into<String>,
210 path: impl Into<std::path::PathBuf>,
211 recursive: bool,
212 ) -> Self {
213 let path = path.into();
214 let description = if recursive {
215 format!("Read directory (recursive): {}", path.display())
216 } else {
217 format!("Read directory: {}", path.display())
218 };
219 Self::new(
220 id,
221 GrantTarget::path(path, recursive),
222 PermissionLevel::Read,
223 description,
224 )
225 }
226
227 pub fn command_execute(id: impl Into<String>, command: impl Into<String>) -> Self {
229 let command = command.into();
230 let description = format!("Execute command: {}", command);
231 Self::new(
232 id,
233 GrantTarget::command(command),
234 PermissionLevel::Execute,
235 description,
236 )
237 }
238
239 pub fn tool_use(
241 id: impl Into<String>,
242 tool_name: impl Into<String>,
243 level: PermissionLevel,
244 ) -> Self {
245 let tool_name = tool_name.into();
246 let description = format!("Use tool: {}", tool_name);
247 Self::new(id, GrantTarget::tool(&tool_name), level, description).with_tool(tool_name)
248 }
249
250 pub fn network_access(
252 id: impl Into<String>,
253 domain: impl Into<String>,
254 level: PermissionLevel,
255 ) -> Self {
256 let domain = domain.into();
257 let description = format!("Access domain: {}", domain);
258 Self::new(id, GrantTarget::domain(domain), level, description)
259 }
260}
261
262impl std::fmt::Display for PermissionRequest {
263 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
264 write!(f, "[{}] {}", self.required_level, self.description)
265 }
266}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271
272 mod grant_tests {
273 use super::*;
274
275 #[test]
276 fn test_grant_satisfies_same_level() {
277 let grant = Grant::read_path("/project/src", true);
278 let request = PermissionRequest::file_read("1", "/project/src/main.rs");
279 assert!(grant.satisfies(&request));
280 }
281
282 #[test]
283 fn test_grant_satisfies_higher_level() {
284 let grant = Grant::write_path("/project/src", true);
285 let request = PermissionRequest::file_read("1", "/project/src/main.rs");
286 assert!(grant.satisfies(&request));
287 }
288
289 #[test]
290 fn test_grant_fails_lower_level() {
291 let grant = Grant::read_path("/project/src", true);
292 let request = PermissionRequest::file_write("1", "/project/src/main.rs");
293 assert!(!grant.satisfies(&request));
294 }
295
296 #[test]
297 fn test_grant_fails_wrong_path() {
298 let grant = Grant::write_path("/project/src", true);
299 let request = PermissionRequest::file_write("1", "/other/file.rs");
300 assert!(!grant.satisfies(&request));
301 }
302
303 #[test]
304 fn test_grant_fails_non_recursive() {
305 let grant = Grant::read_path("/project/src", false);
306 let request = PermissionRequest::file_read("1", "/project/src/utils/mod.rs");
307 assert!(!grant.satisfies(&request));
308 }
309
310 #[test]
311 fn test_admin_grant_satisfies_all_levels() {
312 let grant = Grant::admin_path("/project", true);
313
314 let read_request = PermissionRequest::file_read("1", "/project/src/main.rs");
315 let write_request = PermissionRequest::file_write("2", "/project/src/main.rs");
316
317 assert!(grant.satisfies(&read_request));
318 assert!(grant.satisfies(&write_request));
319 }
320
321 #[test]
322 fn test_domain_grant() {
323 let grant = Grant::domain("*.github.com", PermissionLevel::Read);
324 let request =
325 PermissionRequest::network_access("1", "api.github.com", PermissionLevel::Read);
326 assert!(grant.satisfies(&request));
327 }
328
329 #[test]
330 fn test_command_grant() {
331 let grant = Grant::command("git *", PermissionLevel::Execute);
332 let request = PermissionRequest::command_execute("1", "git status");
333 assert!(grant.satisfies(&request));
334 }
335
336 #[test]
337 fn test_tool_grant() {
338 let grant = Grant::tool("switch_aws_account", PermissionLevel::Execute);
339 let request =
340 PermissionRequest::tool_use("1", "switch_aws_account", PermissionLevel::Execute);
341 assert!(grant.satisfies(&request));
342 }
343
344 #[test]
345 fn test_tool_grant_different_tool_fails() {
346 let grant = Grant::tool("switch_aws_account", PermissionLevel::Execute);
347 let request =
348 PermissionRequest::tool_use("1", "delete_resource", PermissionLevel::Execute);
349 assert!(!grant.satisfies(&request));
350 }
351
352 #[test]
353 fn test_expired_grant() {
354 use std::time::Duration;
355 let expired = Instant::now() - Duration::from_secs(1);
356 let grant = Grant::with_expiration(
357 GrantTarget::path("/project", true),
358 PermissionLevel::Read,
359 expired,
360 );
361
362 let request = PermissionRequest::file_read("1", "/project/file.rs");
363 assert!(!grant.satisfies(&request));
364 }
365
366 #[test]
367 fn test_grant_description() {
368 let grant = Grant::write_path("/project/src", true);
369 let desc = grant.description();
370 assert!(desc.contains("Write"));
371 assert!(desc.contains("/project/src"));
372 }
373 }
374
375 mod request_tests {
376 use super::*;
377
378 #[test]
379 fn test_file_read_request() {
380 let request = PermissionRequest::file_read("test-id", "/path/to/file.rs");
381 assert_eq!(request.id, "test-id");
382 assert_eq!(request.required_level, PermissionLevel::Read);
383 assert!(request.description.contains("Read file"));
384 }
385
386 #[test]
387 fn test_file_write_request() {
388 let request = PermissionRequest::file_write("test-id", "/path/to/file.rs");
389 assert_eq!(request.required_level, PermissionLevel::Write);
390 assert!(request.description.contains("Write file"));
391 }
392
393 #[test]
394 fn test_request_with_reason() {
395 let request = PermissionRequest::file_read("1", "/file.rs")
396 .with_reason("Need to analyze the code");
397 assert_eq!(request.reason, Some("Need to analyze the code".to_string()));
398 }
399
400 #[test]
401 fn test_request_with_tool() {
402 let request = PermissionRequest::file_read("1", "/file.rs").with_tool("read_file");
403 assert_eq!(request.tool_name, Some("read_file".to_string()));
404 }
405
406 #[test]
407 fn test_tool_use_request() {
408 let request = PermissionRequest::tool_use(
409 "test-id",
410 "switch_aws_account",
411 PermissionLevel::Execute,
412 );
413 assert_eq!(request.id, "test-id");
414 assert_eq!(request.required_level, PermissionLevel::Execute);
415 assert!(request.description.contains("Use tool"));
416 assert!(request.description.contains("switch_aws_account"));
417 assert_eq!(request.tool_name, Some("switch_aws_account".to_string()));
418 }
419 }
420
421 mod serialization_tests {
422 use super::*;
423
424 #[test]
425 fn test_grant_serialization() {
426 let grant = Grant::write_path("/project/src", true);
427 let json = serde_json::to_string(&grant).unwrap();
428
429 let deserialized: Grant = serde_json::from_str(&json).unwrap();
430 assert_eq!(deserialized.target, grant.target);
431 assert_eq!(deserialized.level, grant.level);
432 }
433
434 #[test]
435 fn test_request_serialization() {
436 let request =
437 PermissionRequest::file_read("test-id", "/path/to/file.rs").with_reason("testing");
438 let json = serde_json::to_string(&request).unwrap();
439
440 let deserialized: PermissionRequest = serde_json::from_str(&json).unwrap();
441 assert_eq!(deserialized.id, request.id);
442 assert_eq!(deserialized.reason, request.reason);
443 }
444 }
445}