role_system/
cache.rs

1//! Fine-grained cache management for the role system.
2
3use crate::{core::UserPermissions, metrics::RoleSystemMetrics};
4use chrono::{DateTime, Utc};
5use dashmap::DashMap;
6use std::collections::HashSet;
7use std::sync::Arc;
8
9/// Cache tag for organizing cache entries.
10#[derive(Debug, Clone, PartialEq, Eq, Hash)]
11pub enum CacheTag {
12    /// Cache entries for a specific subject.
13    Subject(String),
14    /// Cache entries involving a specific role.
15    Role(String),
16    /// Cache entries for a specific resource type.
17    ResourceType(String),
18    /// Cache entries for a specific action.
19    Action(String),
20    /// Cache entries with context dependencies.
21    ContextDependent,
22}
23
24/// Cache entry with metadata for invalidation.
25#[derive(Debug, Clone)]
26pub struct CacheEntry {
27    /// The cached user permissions.
28    pub permissions: UserPermissions,
29    /// Tags associated with this cache entry.
30    pub tags: HashSet<CacheTag>,
31    /// When the entry was created.
32    pub created_at: DateTime<Utc>,
33}
34
35impl CacheEntry {
36    /// Create a new cache entry.
37    pub fn new(permissions: UserPermissions, tags: HashSet<CacheTag>) -> Self {
38        Self {
39            permissions,
40            tags,
41            created_at: Utc::now(),
42        }
43    }
44
45    /// Check if the entry has a specific tag.
46    pub fn has_tag(&self, tag: &CacheTag) -> bool {
47        self.tags.contains(tag)
48    }
49
50    /// Check if the entry is expired based on TTL.
51    pub fn is_expired(&self, ttl_seconds: u64) -> bool {
52        self.permissions.is_expired(ttl_seconds)
53    }
54}
55
56/// Advanced cache manager with fine-grained invalidation.
57#[derive(Debug)]
58pub struct CacheManager {
59    /// Main cache storage: (subject_id, permission_key) -> CacheEntry
60    cache: DashMap<(String, String), CacheEntry>,
61    /// Tag index: CacheTag -> Set of cache keys
62    tag_index: DashMap<CacheTag, HashSet<(String, String)>>,
63    /// Metrics for cache operations
64    metrics: Arc<RoleSystemMetrics>,
65}
66
67impl CacheManager {
68    /// Create a new cache manager.
69    pub fn new(metrics: Arc<RoleSystemMetrics>) -> Self {
70        Self {
71            cache: DashMap::new(),
72            tag_index: DashMap::new(),
73            metrics,
74        }
75    }
76
77    /// Insert a cache entry with tags.
78    pub fn insert(
79        &self,
80        key: (String, String),
81        permissions: UserPermissions,
82        tags: HashSet<CacheTag>,
83    ) {
84        let entry = CacheEntry::new(permissions, tags.clone());
85
86        // Insert into main cache
87        self.cache.insert(key.clone(), entry);
88
89        // Update tag index
90        for tag in tags {
91            self.tag_index.entry(tag).or_default().insert(key.clone());
92        }
93    }
94
95    /// Get a cache entry if it exists and is valid.
96    pub fn get(&self, key: &(String, String), ttl_seconds: u64) -> Option<UserPermissions> {
97        if let Some(entry) = self.cache.get(key) {
98            if !entry.is_expired(ttl_seconds) {
99                self.metrics.record_cache_hit();
100                return Some(entry.permissions.clone());
101            } else {
102                // Remove expired entry
103                drop(entry);
104                self.remove_expired_entry(key);
105            }
106        }
107
108        self.metrics.record_cache_miss();
109        None
110    }
111
112    /// Invalidate cache entries by tag.
113    pub fn invalidate_by_tag(&self, tag: &CacheTag) {
114        if let Some(keys) = self.tag_index.get(tag) {
115            let keys_to_remove: Vec<_> = keys.iter().cloned().collect();
116            drop(keys); // Release the lock
117
118            for key in keys_to_remove {
119                self.remove_entry(&key);
120            }
121        }
122    }
123
124    /// Invalidate cache entries for a specific subject.
125    pub fn invalidate_subject(&self, subject_id: &str) {
126        self.invalidate_by_tag(&CacheTag::Subject(subject_id.to_string()));
127    }
128
129    /// Invalidate cache entries involving a specific role.
130    pub fn invalidate_role(&self, role_name: &str) {
131        self.invalidate_by_tag(&CacheTag::Role(role_name.to_string()));
132    }
133
134    /// Invalidate cache entries for a specific resource type.
135    pub fn invalidate_resource_type(&self, resource_type: &str) {
136        self.invalidate_by_tag(&CacheTag::ResourceType(resource_type.to_string()));
137    }
138
139    /// Invalidate all context-dependent cache entries.
140    pub fn invalidate_context_dependent(&self) {
141        self.invalidate_by_tag(&CacheTag::ContextDependent);
142    }
143
144    /// Remove expired entries.
145    pub fn cleanup_expired(&self, ttl_seconds: u64) {
146        let expired_keys: Vec<_> = self
147            .cache
148            .iter()
149            .filter(|entry| entry.value().is_expired(ttl_seconds))
150            .map(|entry| entry.key().clone())
151            .collect();
152
153        for key in expired_keys {
154            self.remove_expired_entry(&key);
155        }
156    }
157
158    /// Get cache statistics.
159    pub fn stats(&self) -> CacheStats {
160        let total_entries = self.cache.len();
161        let tag_count = self.tag_index.len();
162
163        let mut tags_per_entry = 0;
164        for entry in self.cache.iter() {
165            tags_per_entry += entry.value().tags.len();
166        }
167
168        let avg_tags_per_entry = if total_entries > 0 {
169            tags_per_entry as f64 / total_entries as f64
170        } else {
171            0.0
172        };
173
174        CacheStats {
175            total_entries,
176            tag_count,
177            avg_tags_per_entry,
178        }
179    }
180
181    /// Clear all cache entries.
182    pub fn clear(&self) {
183        self.cache.clear();
184        self.tag_index.clear();
185    }
186
187    /// Generate cache tags for a permission check.
188    pub fn generate_tags(
189        subject_id: &str,
190        action: &str,
191        resource_type: &str,
192        roles: &[String],
193        has_context: bool,
194    ) -> HashSet<CacheTag> {
195        let mut tags = HashSet::new();
196
197        // Subject tag
198        tags.insert(CacheTag::Subject(subject_id.to_string()));
199
200        // Action tag
201        tags.insert(CacheTag::Action(action.to_string()));
202
203        // Resource type tag
204        tags.insert(CacheTag::ResourceType(resource_type.to_string()));
205
206        // Role tags
207        for role in roles {
208            tags.insert(CacheTag::Role(role.clone()));
209        }
210
211        // Context dependency tag
212        if has_context {
213            tags.insert(CacheTag::ContextDependent);
214        }
215
216        tags
217    }
218
219    // Private helper methods
220
221    fn remove_entry(&self, key: &(String, String)) {
222        if let Some((_, entry)) = self.cache.remove(key) {
223            // Remove from tag index
224            for tag in &entry.tags {
225                if let Some(mut keys) = self.tag_index.get_mut(tag) {
226                    keys.remove(key);
227                    if keys.is_empty() {
228                        drop(keys);
229                        self.tag_index.remove(tag);
230                    }
231                }
232            }
233        }
234    }
235
236    fn remove_expired_entry(&self, key: &(String, String)) {
237        self.remove_entry(key);
238    }
239}
240
241/// Cache statistics.
242#[derive(Debug, Clone)]
243pub struct CacheStats {
244    /// Total number of cache entries.
245    pub total_entries: usize,
246    /// Number of unique tags.
247    pub tag_count: usize,
248    /// Average number of tags per entry.
249    pub avg_tags_per_entry: f64,
250}
251
252/// Trait for components that can provide cache invalidation.
253pub trait CacheInvalidation {
254    /// Invalidate cache entries for a subject.
255    fn invalidate_subject_cache(&self, subject_id: &str);
256
257    /// Invalidate cache entries for a role.
258    fn invalidate_role_cache(&self, role_name: &str);
259
260    /// Invalidate cache entries for a resource type.
261    fn invalidate_resource_type_cache(&self, resource_type: &str);
262
263    /// Cleanup expired cache entries.
264    fn cleanup_expired_cache(&self, ttl_seconds: u64);
265}
266
267#[cfg(test)]
268mod tests {
269    use super::*;
270    use crate::metrics::RoleSystemMetrics;
271    use std::collections::HashMap;
272    use std::sync::Arc;
273
274    #[test]
275    fn test_cache_manager_basic_operations() {
276        let metrics = Arc::new(RoleSystemMetrics::new());
277        let cache = CacheManager::new(metrics.clone());
278
279        let key = ("user1".to_string(), "read:documents".to_string());
280        let mut permissions_map = HashMap::new();
281        permissions_map.insert("read".to_string(), crate::core::AccessResult::Granted);
282        let permissions = UserPermissions::new(permissions_map);
283
284        let mut tags = HashSet::new();
285        tags.insert(CacheTag::Subject("user1".to_string()));
286        tags.insert(CacheTag::Action("read".to_string()));
287        tags.insert(CacheTag::ResourceType("documents".to_string()));
288
289        // Insert entry
290        cache.insert(key.clone(), permissions.clone(), tags);
291
292        // Retrieve entry
293        let retrieved = cache.get(&key, 300).unwrap();
294        assert_eq!(
295            retrieved.computed_permissions.len(),
296            permissions.computed_permissions.len()
297        );
298
299        // Check stats
300        let stats = cache.stats();
301        assert_eq!(stats.total_entries, 1);
302        assert_eq!(stats.tag_count, 3);
303    }
304
305    #[test]
306    fn test_cache_invalidation_by_tag() {
307        let metrics = Arc::new(RoleSystemMetrics::new());
308        let cache = CacheManager::new(metrics);
309
310        let key1 = ("user1".to_string(), "read:documents".to_string());
311        let key2 = ("user2".to_string(), "read:documents".to_string());
312
313        let permissions = UserPermissions::new(HashMap::new());
314
315        let mut tags1 = HashSet::new();
316        tags1.insert(CacheTag::Subject("user1".to_string()));
317        tags1.insert(CacheTag::ResourceType("documents".to_string()));
318
319        let mut tags2 = HashSet::new();
320        tags2.insert(CacheTag::Subject("user2".to_string()));
321        tags2.insert(CacheTag::ResourceType("documents".to_string()));
322
323        cache.insert(key1.clone(), permissions.clone(), tags1);
324        cache.insert(key2.clone(), permissions.clone(), tags2);
325
326        assert_eq!(cache.stats().total_entries, 2);
327
328        // Invalidate by resource type - should remove both entries
329        cache.invalidate_resource_type("documents");
330
331        assert_eq!(cache.stats().total_entries, 0);
332    }
333
334    #[test]
335    fn test_cache_tag_generation() {
336        let tags = CacheManager::generate_tags(
337            "user1",
338            "read",
339            "documents",
340            &["reader".to_string(), "user".to_string()],
341            true,
342        );
343
344        assert!(tags.contains(&CacheTag::Subject("user1".to_string())));
345        assert!(tags.contains(&CacheTag::Action("read".to_string())));
346        assert!(tags.contains(&CacheTag::ResourceType("documents".to_string())));
347        assert!(tags.contains(&CacheTag::Role("reader".to_string())));
348        assert!(tags.contains(&CacheTag::Role("user".to_string())));
349        assert!(tags.contains(&CacheTag::ContextDependent));
350    }
351
352    #[test]
353    fn test_expired_cache_cleanup() {
354        let metrics = Arc::new(RoleSystemMetrics::new());
355        let cache = CacheManager::new(metrics);
356
357        let key = ("user1".to_string(), "read:documents".to_string());
358
359        // Create an expired permissions object
360        let mut permissions_map = HashMap::new();
361        permissions_map.insert("read".to_string(), crate::core::AccessResult::Granted);
362        let mut permissions = UserPermissions::new(permissions_map);
363        permissions.last_updated = Utc::now() - chrono::Duration::seconds(400); // Older than typical TTL
364
365        let tags = HashSet::new();
366        cache.insert(key.clone(), permissions, tags);
367
368        assert_eq!(cache.stats().total_entries, 1);
369
370        // Cleanup with TTL of 300 seconds - should remove the expired entry
371        cache.cleanup_expired(300);
372
373        assert_eq!(cache.stats().total_entries, 0);
374    }
375}