Skip to main content

backbone_authorization/
cache.rs

1//! Permission cache backend trait and in-memory implementation
2//!
3//! Provides a pluggable cache backend for permission caching.
4//! The default is `InMemoryPermissionCache`; enable the `redis` feature
5//! for distributed caching via `RedisPermissionCache`.
6
7use crate::types::Permission;
8use async_trait::async_trait;
9use std::collections::{HashMap, HashSet};
10use std::sync::Arc;
11use tokio::sync::RwLock;
12
13/// Errors from cache operations
14#[derive(Debug, thiserror::Error)]
15pub enum CacheError {
16    /// Cache operation failed
17    #[error("Cache operation failed: {0}")]
18    OperationFailed(String),
19
20    /// Serialization error (e.g., when using Redis)
21    #[error("Serialization error: {0}")]
22    SerializationError(String),
23
24    /// Connection error (e.g., Redis connection lost)
25    #[error("Connection error: {0}")]
26    ConnectionError(String),
27}
28
29/// Trait for permission cache backends
30///
31/// Implementations must be Send + Sync for use across async tasks.
32#[async_trait]
33pub trait PermissionCacheBackend: Send + Sync {
34    /// Get cached permissions for a user
35    ///
36    /// Returns `None` if the user's permissions are not cached or have expired.
37    async fn get(&self, user_id: &str) -> Result<Option<HashSet<Permission>>, CacheError>;
38
39    /// Cache permissions for a user with a TTL
40    async fn set(
41        &self,
42        user_id: &str,
43        permissions: &HashSet<Permission>,
44        ttl_seconds: u64,
45    ) -> Result<(), CacheError>;
46
47    /// Delete cached permissions for a specific user
48    async fn delete(&self, user_id: &str) -> Result<(), CacheError>;
49
50    /// Clear all cached permissions
51    async fn clear(&self) -> Result<(), CacheError>;
52}
53
54/// Cached entry with expiration
55struct CachedEntry {
56    permissions: HashSet<Permission>,
57    expires_at: chrono::DateTime<chrono::Utc>,
58}
59
60/// In-memory permission cache using HashMap with TTL
61///
62/// This is the default cache backend. Permissions are stored in-memory
63/// with per-entry expiration timestamps.
64pub struct InMemoryPermissionCache {
65    cache: Arc<RwLock<HashMap<String, CachedEntry>>>,
66}
67
68impl InMemoryPermissionCache {
69    /// Create a new in-memory permission cache
70    pub fn new() -> Self {
71        Self {
72            cache: Arc::new(RwLock::new(HashMap::new())),
73        }
74    }
75}
76
77impl Default for InMemoryPermissionCache {
78    fn default() -> Self {
79        Self::new()
80    }
81}
82
83#[async_trait]
84impl PermissionCacheBackend for InMemoryPermissionCache {
85    async fn get(&self, user_id: &str) -> Result<Option<HashSet<Permission>>, CacheError> {
86        let cache = self.cache.read().await;
87        if let Some(entry) = cache.get(user_id) {
88            if entry.expires_at > chrono::Utc::now() {
89                return Ok(Some(entry.permissions.clone()));
90            }
91        }
92        Ok(None)
93    }
94
95    async fn set(
96        &self,
97        user_id: &str,
98        permissions: &HashSet<Permission>,
99        ttl_seconds: u64,
100    ) -> Result<(), CacheError> {
101        let expires_at = chrono::Utc::now() + chrono::Duration::seconds(ttl_seconds as i64);
102        let entry = CachedEntry {
103            permissions: permissions.clone(),
104            expires_at,
105        };
106        let mut cache = self.cache.write().await;
107        cache.insert(user_id.to_string(), entry);
108        Ok(())
109    }
110
111    async fn delete(&self, user_id: &str) -> Result<(), CacheError> {
112        let mut cache = self.cache.write().await;
113        cache.remove(user_id);
114        Ok(())
115    }
116
117    async fn clear(&self) -> Result<(), CacheError> {
118        let mut cache = self.cache.write().await;
119        cache.clear();
120        Ok(())
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127    use crate::types::{Permission, UserAction, RoleAction};
128
129    fn test_permissions() -> HashSet<Permission> {
130        let mut perms = HashSet::new();
131        perms.insert(Permission::User(UserAction::Read));
132        perms.insert(Permission::Role(RoleAction::List));
133        perms
134    }
135
136    #[tokio::test]
137    async fn test_inmemory_get_set() {
138        let cache = InMemoryPermissionCache::new();
139        let perms = test_permissions();
140
141        cache.set("user1", &perms, 300).await.unwrap();
142
143        let result = cache.get("user1").await.unwrap();
144        assert!(result.is_some());
145        assert_eq!(result.unwrap(), perms);
146
147        // Non-existent user returns None
148        let result = cache.get("user2").await.unwrap();
149        assert!(result.is_none());
150    }
151
152    #[tokio::test]
153    async fn test_inmemory_ttl_expiry() {
154        let cache = InMemoryPermissionCache::new();
155        let perms = test_permissions();
156
157        // Set with 1 second TTL
158        cache.set("user1", &perms, 1).await.unwrap();
159
160        // Should be present immediately
161        assert!(cache.get("user1").await.unwrap().is_some());
162
163        // Wait for expiry
164        tokio::time::sleep(std::time::Duration::from_secs(2)).await;
165
166        // Should be expired
167        assert!(cache.get("user1").await.unwrap().is_none());
168    }
169
170    #[tokio::test]
171    async fn test_inmemory_delete() {
172        let cache = InMemoryPermissionCache::new();
173        let perms = test_permissions();
174
175        cache.set("user1", &perms, 300).await.unwrap();
176        assert!(cache.get("user1").await.unwrap().is_some());
177
178        cache.delete("user1").await.unwrap();
179        assert!(cache.get("user1").await.unwrap().is_none());
180
181        // Deleting non-existent key is not an error
182        cache.delete("nonexistent").await.unwrap();
183    }
184
185    #[tokio::test]
186    async fn test_inmemory_clear() {
187        let cache = InMemoryPermissionCache::new();
188        let perms = test_permissions();
189
190        cache.set("user1", &perms, 300).await.unwrap();
191        cache.set("user2", &perms, 300).await.unwrap();
192        cache.set("user3", &perms, 300).await.unwrap();
193
194        cache.clear().await.unwrap();
195
196        assert!(cache.get("user1").await.unwrap().is_none());
197        assert!(cache.get("user2").await.unwrap().is_none());
198        assert!(cache.get("user3").await.unwrap().is_none());
199    }
200}