Skip to main content

aegis_delegate/
revocation.rs

1// AEGIS Delegate — Delegation Revocation
2//
3// Reference: AEGIS Specification v1.0.0 §9.5
4//
5// Tracks revoked delegation IDs using a thread-safe set.
6// Supports individual revocation and cascade revocation of multiple IDs.
7
8use std::collections::HashSet;
9use std::sync::RwLock;
10
11/// Tracks revoked delegation IDs.
12///
13/// All operations are thread-safe via an internal [`RwLock`].
14/// This is an in-memory registry; for persistence, a backing store
15/// should be layered on top.
16pub struct RevocationRegistry {
17    revoked: RwLock<HashSet<String>>,
18}
19
20impl RevocationRegistry {
21    /// Creates a new empty revocation registry.
22    pub fn new() -> Self {
23        Self {
24            revoked: RwLock::new(HashSet::new()),
25        }
26    }
27
28    /// Marks a single delegation as revoked.
29    pub fn revoke(&self, delegation_id: &str) {
30        if let Ok(mut set) = self.revoked.write() {
31            set.insert(delegation_id.to_string());
32        }
33    }
34
35    /// Checks whether a delegation has been revoked.
36    pub fn is_revoked(&self, delegation_id: &str) -> bool {
37        match self.revoked.read() {
38            Ok(set) => set.contains(delegation_id),
39            Err(_) => {
40                // If the lock is poisoned, fail closed (treat as revoked)
41                true
42            }
43        }
44    }
45
46    /// Revokes multiple delegations at once (cascade revocation).
47    ///
48    /// This is typically used after [`DelegationTree::revoke`] returns a list
49    /// of all transitively affected delegation IDs.
50    pub fn revoke_cascade(&self, ids: &[String]) {
51        if let Ok(mut set) = self.revoked.write() {
52            for id in ids {
53                set.insert(id.clone());
54            }
55        }
56    }
57}
58
59impl Default for RevocationRegistry {
60    fn default() -> Self {
61        Self::new()
62    }
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68
69    #[test]
70    fn new_registry_is_empty() {
71        let reg = RevocationRegistry::new();
72        assert!(!reg.is_revoked("any-id"));
73    }
74
75    #[test]
76    fn revoke_and_check() {
77        let reg = RevocationRegistry::new();
78        reg.revoke("del-1");
79        assert!(reg.is_revoked("del-1"));
80        assert!(!reg.is_revoked("del-2"));
81    }
82
83    #[test]
84    fn revoke_cascade_marks_all() {
85        let reg = RevocationRegistry::new();
86        let ids = vec!["d1".to_string(), "d2".to_string(), "d3".to_string()];
87        reg.revoke_cascade(&ids);
88
89        assert!(reg.is_revoked("d1"));
90        assert!(reg.is_revoked("d2"));
91        assert!(reg.is_revoked("d3"));
92        assert!(!reg.is_revoked("d4"));
93    }
94
95    #[test]
96    fn revoke_is_idempotent() {
97        let reg = RevocationRegistry::new();
98        reg.revoke("del-1");
99        reg.revoke("del-1");
100        assert!(reg.is_revoked("del-1"));
101    }
102
103    #[test]
104    fn default_creates_empty_registry() {
105        let reg = RevocationRegistry::default();
106        assert!(!reg.is_revoked("anything"));
107    }
108
109    #[test]
110    fn concurrent_access() {
111        use std::sync::Arc;
112        use std::thread;
113
114        let reg = Arc::new(RevocationRegistry::new());
115
116        let mut handles = vec![];
117        for i in 0..10 {
118            let reg_clone = Arc::clone(&reg);
119            let handle = thread::spawn(move || {
120                let id = format!("del-{i}");
121                reg_clone.revoke(&id);
122            });
123            handles.push(handle);
124        }
125
126        for h in handles {
127            let _ = h.join();
128        }
129
130        for i in 0..10 {
131            let id = format!("del-{i}");
132            assert!(reg.is_revoked(&id));
133        }
134    }
135}