1use super::types::{Capability, NodeId};
6use chrono::{DateTime, Duration, Utc};
7use std::collections::HashMap;
8
9#[derive(Debug, Clone)]
11pub struct ConsentEntry {
12 pub capability: Capability,
13 pub granted_at: DateTime<Utc>,
14 pub expires_at: Option<DateTime<Utc>>,
15 pub one_time: bool,
16 pub used: bool,
17}
18
19impl ConsentEntry {
20 pub fn is_valid(&self) -> bool {
22 if self.one_time && self.used {
23 return false;
24 }
25 if let Some(expires) = self.expires_at {
26 Utc::now() < expires
27 } else {
28 true
29 }
30 }
31}
32
33#[derive(Debug, Clone, Default)]
35pub struct ConsentStore {
36 entries: HashMap<NodeId, Vec<ConsentEntry>>,
37}
38
39impl ConsentStore {
40 pub fn new() -> Self {
41 Self::default()
42 }
43
44 pub fn grant(&mut self, node_id: &NodeId, capability: Capability) {
46 let entry = ConsentEntry {
47 capability,
48 granted_at: Utc::now(),
49 expires_at: None,
50 one_time: false,
51 used: false,
52 };
53 self.entries.entry(node_id.clone()).or_default().push(entry);
54 }
55
56 pub fn grant_with_expiry(
58 &mut self,
59 node_id: &NodeId,
60 capability: Capability,
61 duration: Duration,
62 ) -> ConsentEntry {
63 let now = Utc::now();
64 let entry = ConsentEntry {
65 capability,
66 granted_at: now,
67 expires_at: Some(now + duration),
68 one_time: false,
69 used: false,
70 };
71 self.entries
72 .entry(node_id.clone())
73 .or_default()
74 .push(entry.clone());
75 entry
76 }
77
78 pub fn grant_one_time(&mut self, node_id: &NodeId, capability: Capability) -> ConsentEntry {
80 let entry = ConsentEntry {
81 capability,
82 granted_at: Utc::now(),
83 expires_at: None,
84 one_time: true,
85 used: false,
86 };
87 self.entries
88 .entry(node_id.clone())
89 .or_default()
90 .push(entry.clone());
91 entry
92 }
93
94 pub fn revoke(&mut self, node_id: &NodeId, capability: &Capability) {
96 if let Some(entries) = self.entries.get_mut(node_id) {
97 entries.retain(|e| &e.capability != capability);
98 if entries.is_empty() {
99 self.entries.remove(node_id);
100 }
101 }
102 }
103
104 pub fn is_granted(&self, node_id: &NodeId, capability: &Capability) -> bool {
107 self.entries.get(node_id).is_some_and(|entries| {
108 entries
109 .iter()
110 .any(|e| &e.capability == capability && e.is_valid())
111 })
112 }
113
114 pub fn consume_one_time(&mut self, node_id: &NodeId, capability: &Capability) -> bool {
117 if let Some(entries) = self.entries.get_mut(node_id) {
118 for entry in entries.iter_mut() {
119 if &entry.capability == capability
120 && entry.one_time
121 && !entry.used
122 && entry.is_valid()
123 {
124 entry.used = true;
125 return true;
126 }
127 }
128 }
129 false
130 }
131
132 pub fn granted_capabilities(&self, node_id: &NodeId) -> Vec<&Capability> {
134 self.entries
135 .get(node_id)
136 .map(|entries| {
137 entries
138 .iter()
139 .filter(|e| e.is_valid())
140 .map(|e| &e.capability)
141 .collect()
142 })
143 .unwrap_or_default()
144 }
145
146 pub fn list_grants(&self, node_id: &NodeId) -> Vec<&ConsentEntry> {
148 self.entries
149 .get(node_id)
150 .map(|entries| entries.iter().collect())
151 .unwrap_or_default()
152 }
153
154 pub fn revoke_all(&mut self, node_id: &NodeId) {
156 self.entries.remove(node_id);
157 }
158
159 pub fn cleanup_expired(&mut self) -> usize {
161 let mut removed = 0;
162 for entries in self.entries.values_mut() {
163 let before = entries.len();
164 entries.retain(|e| e.is_valid());
165 removed += before - entries.len();
166 }
167 self.entries.retain(|_, v| !v.is_empty());
169 removed
170 }
171
172 pub fn node_count(&self) -> usize {
174 self.entries.len()
175 }
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181
182 #[test]
183 fn test_consent_grant_and_check() {
184 let mut store = ConsentStore::new();
185 let node = NodeId::new("node-1");
186
187 assert!(!store.is_granted(&node, &Capability::Shell));
188
189 store.grant(&node, Capability::Shell);
190 assert!(store.is_granted(&node, &Capability::Shell));
191 assert!(!store.is_granted(&node, &Capability::FileSystem));
192 }
193
194 #[test]
195 fn test_consent_revoke() {
196 let mut store = ConsentStore::new();
197 let node = NodeId::new("node-1");
198
199 store.grant(&node, Capability::Shell);
200 store.grant(&node, Capability::FileSystem);
201 assert!(store.is_granted(&node, &Capability::Shell));
202
203 store.revoke(&node, &Capability::Shell);
204 assert!(!store.is_granted(&node, &Capability::Shell));
205 assert!(store.is_granted(&node, &Capability::FileSystem));
206 }
207
208 #[test]
209 fn test_consent_revoke_all() {
210 let mut store = ConsentStore::new();
211 let node = NodeId::new("node-1");
212
213 store.grant(&node, Capability::Shell);
214 store.grant(&node, Capability::Screenshot);
215 assert_eq!(store.node_count(), 1);
216
217 store.revoke_all(&node);
218 assert!(!store.is_granted(&node, &Capability::Shell));
219 assert_eq!(store.node_count(), 0);
220 }
221
222 #[test]
223 fn test_consent_granted_capabilities() {
224 let mut store = ConsentStore::new();
225 let node = NodeId::new("node-1");
226
227 store.grant(&node, Capability::Shell);
228 store.grant(&node, Capability::Clipboard);
229 let caps = store.granted_capabilities(&node);
230 assert_eq!(caps.len(), 2);
231 }
232
233 #[test]
234 fn test_consent_multiple_nodes() {
235 let mut store = ConsentStore::new();
236 let n1 = NodeId::new("node-1");
237 let n2 = NodeId::new("node-2");
238
239 store.grant(&n1, Capability::Shell);
240 store.grant(&n2, Capability::FileSystem);
241
242 assert!(store.is_granted(&n1, &Capability::Shell));
243 assert!(!store.is_granted(&n1, &Capability::FileSystem));
244 assert!(store.is_granted(&n2, &Capability::FileSystem));
245 assert_eq!(store.node_count(), 2);
246 }
247
248 #[test]
251 fn test_consent_grant_with_expiry() {
252 let mut store = ConsentStore::new();
253 let node = NodeId::new("node-1");
254
255 let entry = store.grant_with_expiry(&node, Capability::Shell, Duration::hours(1));
256 assert!(entry.expires_at.is_some());
257 assert!(!entry.one_time);
258 assert!(store.is_granted(&node, &Capability::Shell));
259 }
260
261 #[test]
262 fn test_consent_expired_denied() {
263 let mut store = ConsentStore::new();
264 let node = NodeId::new("node-1");
265
266 store.grant_with_expiry(&node, Capability::Shell, Duration::seconds(-1));
268 assert!(!store.is_granted(&node, &Capability::Shell));
269 }
270
271 #[test]
272 fn test_consent_one_time_grant() {
273 let mut store = ConsentStore::new();
274 let node = NodeId::new("node-1");
275
276 let entry = store.grant_one_time(&node, Capability::Shell);
277 assert!(entry.one_time);
278 assert!(!entry.used);
279 assert!(store.is_granted(&node, &Capability::Shell));
280 }
281
282 #[test]
283 fn test_consent_one_time_consumed() {
284 let mut store = ConsentStore::new();
285 let node = NodeId::new("node-1");
286
287 store.grant_one_time(&node, Capability::Shell);
288 assert!(store.consume_one_time(&node, &Capability::Shell));
289 }
290
291 #[test]
292 fn test_consent_one_time_reuse_denied() {
293 let mut store = ConsentStore::new();
294 let node = NodeId::new("node-1");
295
296 store.grant_one_time(&node, Capability::Shell);
297 assert!(store.consume_one_time(&node, &Capability::Shell));
298 assert!(!store.is_granted(&node, &Capability::Shell));
300 assert!(!store.consume_one_time(&node, &Capability::Shell));
302 }
303
304 #[test]
305 fn test_consent_cleanup_expired() {
306 let mut store = ConsentStore::new();
307 let node = NodeId::new("node-1");
308
309 store.grant_with_expiry(&node, Capability::Shell, Duration::seconds(-1));
311 store.grant(&node, Capability::FileSystem);
313
314 let removed = store.cleanup_expired();
315 assert_eq!(removed, 1);
316 assert!(!store.is_granted(&node, &Capability::Shell));
317 assert!(store.is_granted(&node, &Capability::FileSystem));
318 }
319
320 #[test]
321 fn test_consent_list_grants() {
322 let mut store = ConsentStore::new();
323 let node = NodeId::new("node-1");
324
325 store.grant(&node, Capability::Shell);
326 store.grant_one_time(&node, Capability::FileSystem);
327 store.grant_with_expiry(&node, Capability::Screenshot, Duration::hours(1));
328
329 let grants = store.list_grants(&node);
330 assert_eq!(grants.len(), 3);
331 }
332
333 #[test]
334 fn test_consent_revoke_still_works() {
335 let mut store = ConsentStore::new();
336 let node = NodeId::new("node-1");
337
338 store.grant(&node, Capability::Shell);
339 store.grant_one_time(&node, Capability::Shell);
340 assert!(store.is_granted(&node, &Capability::Shell));
341
342 store.revoke(&node, &Capability::Shell);
344 assert!(!store.is_granted(&node, &Capability::Shell));
345 assert!(store.list_grants(&node).is_empty());
346 }
347
348 #[test]
349 fn test_consent_multiple_nodes_isolation() {
350 let mut store = ConsentStore::new();
351 let n1 = NodeId::new("node-1");
352 let n2 = NodeId::new("node-2");
353
354 store.grant_one_time(&n1, Capability::Shell);
355 store.grant(&n2, Capability::Shell);
356
357 store.consume_one_time(&n1, &Capability::Shell);
359 assert!(!store.is_granted(&n1, &Capability::Shell));
360 assert!(store.is_granted(&n2, &Capability::Shell));
361 }
362
363 #[test]
364 fn test_consent_mixed_entries() {
365 let mut store = ConsentStore::new();
366 let node = NodeId::new("node-1");
367
368 store.grant(&node, Capability::Shell);
370 store.grant_one_time(&node, Capability::FileSystem);
371 store.grant_with_expiry(&node, Capability::Screenshot, Duration::seconds(-1));
372
373 let valid = store.granted_capabilities(&node);
375 assert_eq!(valid.len(), 2);
376
377 let all = store.list_grants(&node);
379 assert_eq!(all.len(), 3);
380
381 let removed = store.cleanup_expired();
383 assert_eq!(removed, 1);
384 assert_eq!(store.list_grants(&node).len(), 2);
385 }
386}