oxify_authz/
memory.rs

1//! In-memory ReBAC manager
2//!
3//! Ported from OxiRS (<https://github.com/cool-japan/oxirs>)
4//! Original implementation: Copyright (c) OxiRS Contributors
5//! Adapted for OxiFY
6//! License: MIT OR Apache-2.0 (compatible with OxiRS)
7
8use crate::*;
9use std::collections::HashMap;
10use std::sync::Arc;
11use tokio::sync::RwLock;
12
13/// In-memory ReBAC manager
14/// Ported from OxiRS for high-performance hot-path authorization
15pub struct InMemoryRebacManager {
16    /// Relationship tuples indexed by subject
17    tuples_by_subject: Arc<RwLock<HashMap<String, Vec<RelationTuple>>>>,
18
19    /// Relationship tuples indexed by object
20    tuples_by_object: Arc<RwLock<HashMap<String, Vec<RelationTuple>>>>,
21
22    /// Relationship graph for path-based checks
23    graph: Arc<RwLock<RelationshipGraph>>,
24}
25
26/// Relationship graph for traversal-based authorization
27#[derive(Debug, Default)]
28struct RelationshipGraph {
29    /// Edges in the graph: (subject, relation) -> Vec<object>
30    edges: HashMap<(String, String), Vec<String>>,
31}
32
33impl RelationshipGraph {
34    fn add_edge(&mut self, subject: String, relation: String, object: String) {
35        self.edges
36            .entry((subject, relation))
37            .or_default()
38            .push(object);
39    }
40
41    fn remove_edge(&mut self, subject: &str, relation: &str, object: &str) {
42        if let Some(objects) = self
43            .edges
44            .get_mut(&(subject.to_string(), relation.to_string()))
45        {
46            objects.retain(|o| o != object);
47        }
48    }
49
50    /// Check if there's a path from subject to object via the given relation
51    fn has_path(&self, subject: &str, relation: &str, object: &str) -> bool {
52        // Direct check
53        if let Some(objects) = self.edges.get(&(subject.to_string(), relation.to_string())) {
54            if objects.contains(&object.to_string()) {
55                return true;
56            }
57        }
58
59        // TODO: Implement transitive relationship traversal
60        // For now, only direct relationships are checked
61        false
62    }
63}
64
65impl InMemoryRebacManager {
66    /// Create a new in-memory ReBAC manager
67    pub fn new() -> Self {
68        Self {
69            tuples_by_subject: Arc::new(RwLock::new(HashMap::new())),
70            tuples_by_object: Arc::new(RwLock::new(HashMap::new())),
71            graph: Arc::new(RwLock::new(RelationshipGraph::default())),
72        }
73    }
74
75    /// Initialize with predefined tuples (for testing/demo)
76    pub async fn with_tuples(tuples: Vec<RelationTuple>) -> Result<Self> {
77        let manager = Self::new();
78        for tuple in tuples {
79            manager.add_tuple(tuple).await?;
80        }
81        Ok(manager)
82    }
83
84    /// Add a relationship tuple
85    pub async fn add_tuple(&self, tuple: RelationTuple) -> Result<()> {
86        let subject_key = tuple.subject.to_string();
87        let object_key = format!("{}:{}", tuple.namespace, tuple.object_id);
88
89        // Add to subject index (check for duplicates)
90        {
91            let mut tuples_by_subject = self.tuples_by_subject.write().await;
92            let subject_tuples = tuples_by_subject
93                .entry(subject_key.clone())
94                .or_insert_with(Vec::new);
95
96            // Only add if not already present (compare by namespace, object_id, relation, subject)
97            if !subject_tuples.iter().any(|t| {
98                t.namespace == tuple.namespace
99                    && t.object_id == tuple.object_id
100                    && t.relation == tuple.relation
101                    && t.subject == tuple.subject
102            }) {
103                subject_tuples.push(tuple.clone());
104            }
105        }
106
107        // Add to object index (check for duplicates)
108        {
109            let mut tuples_by_object = self.tuples_by_object.write().await;
110            let object_tuples = tuples_by_object
111                .entry(object_key.clone())
112                .or_insert_with(Vec::new);
113
114            // Only add if not already present
115            if !object_tuples.iter().any(|t| {
116                t.namespace == tuple.namespace
117                    && t.object_id == tuple.object_id
118                    && t.relation == tuple.relation
119                    && t.subject == tuple.subject
120            }) {
121                object_tuples.push(tuple.clone());
122            }
123        }
124
125        // Add to graph
126        {
127            let mut graph = self.graph.write().await;
128            graph.add_edge(subject_key, tuple.relation.clone(), object_key);
129        }
130
131        Ok(())
132    }
133
134    /// Remove a relationship tuple
135    pub async fn remove_tuple(&self, tuple: &RelationTuple) -> Result<()> {
136        let subject_key = tuple.subject.to_string();
137        let object_key = format!("{}:{}", tuple.namespace, tuple.object_id);
138
139        // Remove from subject index
140        {
141            let mut tuples_by_subject = self.tuples_by_subject.write().await;
142            if let Some(tuples) = tuples_by_subject.get_mut(&subject_key) {
143                tuples.retain(|t| {
144                    !(t.namespace == tuple.namespace
145                        && t.object_id == tuple.object_id
146                        && t.relation == tuple.relation
147                        && t.subject == tuple.subject)
148                });
149            }
150        }
151
152        // Remove from object index
153        {
154            let mut tuples_by_object = self.tuples_by_object.write().await;
155            if let Some(tuples) = tuples_by_object.get_mut(&object_key) {
156                tuples.retain(|t| {
157                    !(t.namespace == tuple.namespace
158                        && t.object_id == tuple.object_id
159                        && t.relation == tuple.relation
160                        && t.subject == tuple.subject)
161                });
162            }
163        }
164
165        // Remove from graph
166        {
167            let mut graph = self.graph.write().await;
168            graph.remove_edge(&subject_key, &tuple.relation, &object_key);
169        }
170
171        Ok(())
172    }
173
174    /// Check if a subject has a relation to an object
175    pub async fn check(&self, request: &CheckRequest) -> Result<CheckResponse> {
176        let subject_key = request.subject.to_string();
177        let object_key = format!("{}:{}", request.namespace, request.object_id);
178
179        // Check if relationship exists in graph
180        let graph = self.graph.read().await;
181        let has_relation = graph.has_path(&subject_key, &request.relation, &object_key);
182
183        if !has_relation {
184            return Ok(CheckResponse {
185                allowed: false,
186                cached: false,
187            });
188        }
189
190        // Check conditions
191        let tuples_by_subject = self.tuples_by_subject.read().await;
192        if let Some(tuples) = tuples_by_subject.get(&subject_key) {
193            for tuple in tuples {
194                if tuple.namespace == request.namespace
195                    && tuple.object_id == request.object_id
196                    && tuple.relation == request.relation
197                    && !tuple.is_condition_satisfied()
198                {
199                    return Ok(CheckResponse {
200                        allowed: false,
201                        cached: false,
202                    });
203                }
204            }
205        }
206
207        Ok(CheckResponse {
208            allowed: true,
209            cached: false,
210        })
211    }
212
213    /// List all tuples for a subject
214    pub async fn list_subject_tuples(&self, subject: &Subject) -> Result<Vec<RelationTuple>> {
215        let subject_key = subject.to_string();
216        let tuples_by_subject = self.tuples_by_subject.read().await;
217        Ok(tuples_by_subject
218            .get(&subject_key)
219            .cloned()
220            .unwrap_or_default())
221    }
222
223    /// List all tuples for an object
224    pub async fn list_object_tuples(
225        &self,
226        namespace: &str,
227        object_id: &str,
228    ) -> Result<Vec<RelationTuple>> {
229        let object_key = format!("{}:{}", namespace, object_id);
230        let tuples_by_object = self.tuples_by_object.read().await;
231        Ok(tuples_by_object
232            .get(&object_key)
233            .cloned()
234            .unwrap_or_default())
235    }
236
237    /// Batch check multiple requests
238    pub async fn batch_check(&self, requests: &[CheckRequest]) -> Result<Vec<CheckResponse>> {
239        let mut results = Vec::with_capacity(requests.len());
240        for request in requests {
241            results.push(self.check(request).await?);
242        }
243        Ok(results)
244    }
245}
246
247impl Default for InMemoryRebacManager {
248    fn default() -> Self {
249        Self::new()
250    }
251}
252
253#[cfg(test)]
254mod tests {
255    use super::*;
256
257    #[tokio::test]
258    async fn test_basic_relationship() {
259        let manager = InMemoryRebacManager::new();
260
261        // Add relationship: alice can read document:public
262        let tuple = RelationTuple::new(
263            "document",
264            "can_read",
265            "public",
266            Subject::User("alice".to_string()),
267        );
268        manager.add_tuple(tuple).await.unwrap();
269
270        // Check if alice can read document:public
271        let request = CheckRequest {
272            namespace: "document".to_string(),
273            object_id: "public".to_string(),
274            relation: "can_read".to_string(),
275            subject: Subject::User("alice".to_string()),
276            context: None,
277        };
278        let response = manager.check(&request).await.unwrap();
279        assert!(response.allowed);
280
281        // Check if alice can write document:public (should fail)
282        let request = CheckRequest {
283            namespace: "document".to_string(),
284            object_id: "public".to_string(),
285            relation: "can_write".to_string(),
286            subject: Subject::User("alice".to_string()),
287            context: None,
288        };
289        let response = manager.check(&request).await.unwrap();
290        assert!(!response.allowed);
291    }
292
293    #[tokio::test]
294    async fn test_list_tuples() {
295        let manager = InMemoryRebacManager::new();
296
297        // Add multiple relationships for alice
298        manager
299            .add_tuple(RelationTuple::new(
300                "document",
301                "can_read",
302                "public",
303                Subject::User("alice".to_string()),
304            ))
305            .await
306            .unwrap();
307        manager
308            .add_tuple(RelationTuple::new(
309                "document",
310                "can_write",
311                "private",
312                Subject::User("alice".to_string()),
313            ))
314            .await
315            .unwrap();
316
317        // List all tuples for alice
318        let tuples = manager
319            .list_subject_tuples(&Subject::User("alice".to_string()))
320            .await
321            .unwrap();
322        assert_eq!(tuples.len(), 2);
323    }
324
325    #[tokio::test]
326    async fn test_time_based_condition() {
327        let manager = InMemoryRebacManager::new();
328
329        // Add relationship with time window (already expired)
330        let tuple = RelationTuple::with_condition(
331            "document",
332            "temporary",
333            "can_read",
334            Subject::User("alice".to_string()),
335            RelationshipCondition::TimeWindow {
336                not_before: Some(chrono::Utc::now() - chrono::Duration::hours(2)),
337                not_after: Some(chrono::Utc::now() - chrono::Duration::hours(1)),
338            },
339        );
340        manager.add_tuple(tuple).await.unwrap();
341
342        // Check should fail due to expired time window
343        let request = CheckRequest {
344            namespace: "document".to_string(),
345            object_id: "temporary".to_string(),
346            relation: "can_read".to_string(),
347            subject: Subject::User("alice".to_string()),
348            context: None,
349        };
350        let response = manager.check(&request).await.unwrap();
351        assert!(!response.allowed);
352    }
353
354    #[tokio::test]
355    async fn test_remove_tuple() {
356        let manager = InMemoryRebacManager::new();
357
358        // Add relationship
359        let tuple = RelationTuple::new(
360            "document",
361            "can_read",
362            "public",
363            Subject::User("alice".to_string()),
364        );
365        manager.add_tuple(tuple.clone()).await.unwrap();
366
367        // Verify it exists
368        let request = CheckRequest {
369            namespace: "document".to_string(),
370            object_id: "public".to_string(),
371            relation: "can_read".to_string(),
372            subject: Subject::User("alice".to_string()),
373            context: None,
374        };
375        let response = manager.check(&request).await.unwrap();
376        assert!(response.allowed);
377
378        // Remove relationship
379        manager.remove_tuple(&tuple).await.unwrap();
380
381        // Verify it's gone
382        let response = manager.check(&request).await.unwrap();
383        assert!(!response.allowed);
384    }
385
386    #[tokio::test]
387    async fn test_batch_check() {
388        let manager = InMemoryRebacManager::new();
389
390        // Add multiple relationships
391        manager
392            .add_tuple(RelationTuple::new(
393                "document",
394                "can_read",
395                "doc1",
396                Subject::User("alice".to_string()),
397            ))
398            .await
399            .unwrap();
400        manager
401            .add_tuple(RelationTuple::new(
402                "document",
403                "can_read",
404                "doc2",
405                Subject::User("alice".to_string()),
406            ))
407            .await
408            .unwrap();
409
410        // Batch check
411        let requests = vec![
412            CheckRequest {
413                namespace: "document".to_string(),
414                object_id: "doc1".to_string(),
415                relation: "can_read".to_string(),
416                subject: Subject::User("alice".to_string()),
417                context: None,
418            },
419            CheckRequest {
420                namespace: "document".to_string(),
421                object_id: "doc2".to_string(),
422                relation: "can_read".to_string(),
423                subject: Subject::User("alice".to_string()),
424                context: None,
425            },
426            CheckRequest {
427                namespace: "document".to_string(),
428                object_id: "doc3".to_string(),
429                relation: "can_read".to_string(),
430                subject: Subject::User("alice".to_string()),
431                context: None,
432            },
433        ];
434
435        let responses = manager.batch_check(&requests).await.unwrap();
436        assert_eq!(responses.len(), 3);
437        assert!(responses[0].allowed);
438        assert!(responses[1].allowed);
439        assert!(!responses[2].allowed);
440    }
441}