aegis-delegate 0.2.0

AEGIS delegation and authority model โ€” delegation trees, session keys, scope constraints
Documentation
// AEGIS Delegate โ€” Delegation Revocation
//
// Reference: AEGIS Specification v1.0.0 ยง9.5
//
// Tracks revoked delegation IDs using a thread-safe set.
// Supports individual revocation and cascade revocation of multiple IDs.

use std::collections::HashSet;
use std::sync::RwLock;

/// Tracks revoked delegation IDs.
///
/// All operations are thread-safe via an internal [`RwLock`].
/// This is an in-memory registry; for persistence, a backing store
/// should be layered on top.
pub struct RevocationRegistry {
    revoked: RwLock<HashSet<String>>,
}

impl RevocationRegistry {
    /// Creates a new empty revocation registry.
    pub fn new() -> Self {
        Self {
            revoked: RwLock::new(HashSet::new()),
        }
    }

    /// Marks a single delegation as revoked.
    pub fn revoke(&self, delegation_id: &str) {
        if let Ok(mut set) = self.revoked.write() {
            set.insert(delegation_id.to_string());
        }
    }

    /// Checks whether a delegation has been revoked.
    pub fn is_revoked(&self, delegation_id: &str) -> bool {
        match self.revoked.read() {
            Ok(set) => set.contains(delegation_id),
            Err(_) => {
                // If the lock is poisoned, fail closed (treat as revoked)
                true
            }
        }
    }

    /// Revokes multiple delegations at once (cascade revocation).
    ///
    /// This is typically used after [`DelegationTree::revoke`] returns a list
    /// of all transitively affected delegation IDs.
    pub fn revoke_cascade(&self, ids: &[String]) {
        if let Ok(mut set) = self.revoked.write() {
            for id in ids {
                set.insert(id.clone());
            }
        }
    }
}

impl Default for RevocationRegistry {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn new_registry_is_empty() {
        let reg = RevocationRegistry::new();
        assert!(!reg.is_revoked("any-id"));
    }

    #[test]
    fn revoke_and_check() {
        let reg = RevocationRegistry::new();
        reg.revoke("del-1");
        assert!(reg.is_revoked("del-1"));
        assert!(!reg.is_revoked("del-2"));
    }

    #[test]
    fn revoke_cascade_marks_all() {
        let reg = RevocationRegistry::new();
        let ids = vec!["d1".to_string(), "d2".to_string(), "d3".to_string()];
        reg.revoke_cascade(&ids);

        assert!(reg.is_revoked("d1"));
        assert!(reg.is_revoked("d2"));
        assert!(reg.is_revoked("d3"));
        assert!(!reg.is_revoked("d4"));
    }

    #[test]
    fn revoke_is_idempotent() {
        let reg = RevocationRegistry::new();
        reg.revoke("del-1");
        reg.revoke("del-1");
        assert!(reg.is_revoked("del-1"));
    }

    #[test]
    fn default_creates_empty_registry() {
        let reg = RevocationRegistry::default();
        assert!(!reg.is_revoked("anything"));
    }

    #[test]
    fn concurrent_access() {
        use std::sync::Arc;
        use std::thread;

        let reg = Arc::new(RevocationRegistry::new());

        let mut handles = vec![];
        for i in 0..10 {
            let reg_clone = Arc::clone(&reg);
            let handle = thread::spawn(move || {
                let id = format!("del-{i}");
                reg_clone.revoke(&id);
            });
            handles.push(handle);
        }

        for h in handles {
            let _ = h.join();
        }

        for i in 0..10 {
            let id = format!("del-{i}");
            assert!(reg.is_revoked(&id));
        }
    }
}