kirino 0.4.0

A customizable, zero-trust authentication framework.
Documentation
use std::{
    collections::HashMap,
    marker::PhantomData,
    sync::RwLock,
    time::{Duration, Instant},
};

use super::traits::{Permission, Subject};

pub trait PermissionCache<S, P>: Send + Sync
where
    S: Subject,
    P: Permission,
{
    fn get(&self, subject: &S, permission: &P) -> Option<bool>;
    fn set(&self, subject: &S, permission: &P, granted: bool);
    fn invalidate_subject(&self, subject: &S);
    fn invalidate_all(&self);
}

struct CacheEntry {
    granted: bool,
    expires_at: Instant,
}

pub struct TtlPermissionCache<S, P>
where
    S: Subject,
    P: Permission,
{
    cache: RwLock<HashMap<(String, String), CacheEntry>>,
    ttl: Duration,
    _phantom: PhantomData<(S, P)>,
}

impl<S, P> TtlPermissionCache<S, P>
where
    S: Subject,
    P: Permission,
{
    #[must_use]
    pub fn new(ttl: Duration) -> Self {
        Self {
            cache: RwLock::new(HashMap::new()),
            ttl,
            _phantom: PhantomData,
        }
    }
}

impl<S, P> PermissionCache<S, P> for TtlPermissionCache<S, P>
where
    S: Subject,
    P: Permission,
{
    fn get(&self, subject: &S, permission: &P) -> Option<bool> {
        let key = (
            subject.subject_id().to_string(),
            permission.name().to_string(),
        );
        let Ok(cache) = self.cache.read() else {
            return None;
        };
        cache.get(&key).and_then(|entry| {
            if Instant::now() < entry.expires_at {
                Some(entry.granted)
            } else {
                None
            }
        })
    }

    fn set(&self, subject: &S, permission: &P, granted: bool) {
        let key = (
            subject.subject_id().to_string(),
            permission.name().to_string(),
        );
        if let Ok(mut cache) = self.cache.write() {
            cache.insert(
                key,
                CacheEntry {
                    granted,
                    expires_at: Instant::now() + self.ttl,
                },
            );
        }
    }

    fn invalidate_subject(&self, subject: &S) {
        let sid = subject.subject_id().to_string();
        if let Ok(mut cache) = self.cache.write() {
            cache.retain(|(s, _), _| s != &sid);
        }
    }

    fn invalidate_all(&self) {
        if let Ok(mut cache) = self.cache.write() {
            cache.clear();
        }
    }
}

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

    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
    struct TestPerm(&'static str);

    impl Permission for TestPerm {
        fn name(&self) -> &str {
            self.0
        }
    }

    #[derive(Debug, Clone, PartialEq, Eq, Hash)]
    struct TestSubject(String);

    impl Subject for TestSubject {
        fn subject_id(&self) -> &str {
            &self.0
        }
    }

    #[test]
    fn test_cache_set_and_get() {
        let cache = TtlPermissionCache::<TestSubject, TestPerm>::new(Duration::from_secs(60));
        let subj = TestSubject("user1".to_string());
        let perm = TestPerm("read");

        assert!(cache.get(&subj, &perm).is_none());
        cache.set(&subj, &perm, true);
        assert_eq!(cache.get(&subj, &perm), Some(true));
    }

    #[test]
    fn test_cache_ttl_expiry() {
        let cache = TtlPermissionCache::<TestSubject, TestPerm>::new(Duration::from_millis(1));
        let subj = TestSubject("user1".to_string());
        let perm = TestPerm("read");

        cache.set(&subj, &perm, true);
        std::thread::sleep(Duration::from_millis(5));
        assert!(cache.get(&subj, &perm).is_none());
    }

    #[test]
    fn test_cache_invalidate_subject() {
        let cache = TtlPermissionCache::<TestSubject, TestPerm>::new(Duration::from_secs(60));
        let u1 = TestSubject("user1".to_string());
        let u2 = TestSubject("user2".to_string());
        let perm = TestPerm("read");

        cache.set(&u1, &perm, true);
        cache.set(&u2, &perm, false);
        cache.invalidate_subject(&u1);
        assert!(cache.get(&u1, &perm).is_none());
        assert_eq!(cache.get(&u2, &perm), Some(false));
    }

    #[test]
    fn test_cache_invalidate_all() {
        let cache = TtlPermissionCache::<TestSubject, TestPerm>::new(Duration::from_secs(60));
        let subj = TestSubject("user1".to_string());
        let perm = TestPerm("read");

        cache.set(&subj, &perm, true);
        cache.invalidate_all();
        assert!(cache.get(&subj, &perm).is_none());
    }
}