1use crate::*;
23use std::collections::{HashMap, HashSet};
24use std::sync::Arc;
25use tokio::sync::RwLock;
26
27type SubjectIndex = Arc<RwLock<HashMap<String, HashSet<(String, String, String)>>>>;
29
30type ObjectIndex = Arc<RwLock<HashMap<(String, String, String), HashSet<String>>>>;
32
33type DirectTuples = Arc<RwLock<HashSet<(String, String, String, String)>>>;
35
36#[derive(Debug, Clone)]
38pub struct LeopardConfig {
39 pub max_depth: usize,
41 pub eager_compute: bool,
43 pub indexed_namespaces: Vec<String>,
45}
46
47impl Default for LeopardConfig {
48 fn default() -> Self {
49 Self {
50 max_depth: 10,
51 eager_compute: false,
52 indexed_namespaces: Vec::new(),
53 }
54 }
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, Hash)]
59pub struct ReachabilityEntry {
60 pub subject: String,
62 pub namespace: String,
64 pub object_id: String,
66 pub relation: String,
68 pub depth: usize,
70 pub is_computed: bool,
72}
73
74#[derive(Debug, Clone, Default)]
76pub struct LeopardStats {
77 pub direct_entries: u64,
79 pub computed_entries: u64,
81 pub hits: u64,
83 pub misses: u64,
85}
86
87impl LeopardStats {
88 pub fn hit_rate(&self) -> f64 {
90 let total = self.hits + self.misses;
91 if total == 0 {
92 0.0
93 } else {
94 self.hits as f64 / total as f64
95 }
96 }
97
98 pub fn total_entries(&self) -> u64 {
100 self.direct_entries + self.computed_entries
101 }
102}
103
104pub struct LeopardIndex {
106 by_subject: SubjectIndex,
108
109 by_object: ObjectIndex,
111
112 direct_tuples: DirectTuples,
114
115 namespace_configs: Arc<HashMap<String, NamespaceConfig>>,
117
118 #[allow(dead_code)]
120 config: LeopardConfig,
121
122 stats: Arc<RwLock<LeopardStats>>,
124}
125
126impl LeopardIndex {
127 pub fn new(namespace_configs: Arc<HashMap<String, NamespaceConfig>>) -> Self {
129 Self::with_config(namespace_configs, LeopardConfig::default())
130 }
131
132 pub fn with_config(
134 namespace_configs: Arc<HashMap<String, NamespaceConfig>>,
135 config: LeopardConfig,
136 ) -> Self {
137 Self {
138 by_subject: Arc::new(RwLock::new(HashMap::new())),
139 by_object: Arc::new(RwLock::new(HashMap::new())),
140 direct_tuples: Arc::new(RwLock::new(HashSet::new())),
141 namespace_configs,
142 config,
143 stats: Arc::new(RwLock::new(LeopardStats::default())),
144 }
145 }
146
147 pub async fn index_tuple(&self, tuple: &RelationTuple) -> Result<usize> {
149 let subject_key = tuple.subject.to_string();
150
151 {
153 let mut direct = self.direct_tuples.write().await;
154 direct.insert((
155 subject_key.clone(),
156 tuple.namespace.clone(),
157 tuple.object_id.clone(),
158 tuple.relation.clone(),
159 ));
160 }
161
162 self.add_entry(
164 &subject_key,
165 &tuple.namespace,
166 &tuple.object_id,
167 &tuple.relation,
168 false,
169 )
170 .await;
171
172 let mut computed_count = 0;
174 if let Some(ns_config) = self.namespace_configs.get(&tuple.namespace) {
175 for rel_config in &ns_config.relations {
177 if rel_config.inherits_from.contains(&tuple.relation) {
178 self.add_entry(
181 &subject_key,
182 &tuple.namespace,
183 &tuple.object_id,
184 &rel_config.name,
185 true,
186 )
187 .await;
188 computed_count += 1;
189 }
190 }
191 }
192
193 Ok(computed_count + 1) }
195
196 pub async fn remove_tuple(&self, tuple: &RelationTuple) -> Result<usize> {
198 let subject_key = tuple.subject.to_string();
199
200 {
202 let mut direct = self.direct_tuples.write().await;
203 direct.remove(&(
204 subject_key.clone(),
205 tuple.namespace.clone(),
206 tuple.object_id.clone(),
207 tuple.relation.clone(),
208 ));
209 }
210
211 self.remove_entry(
213 &subject_key,
214 &tuple.namespace,
215 &tuple.object_id,
216 &tuple.relation,
217 )
218 .await;
219
220 let mut removed_count = 1;
222 if let Some(ns_config) = self.namespace_configs.get(&tuple.namespace) {
223 for rel_config in &ns_config.relations {
224 if rel_config.inherits_from.contains(&tuple.relation) {
225 let still_has = self
227 .check_has_through_other_path(
228 &subject_key,
229 &tuple.namespace,
230 &tuple.object_id,
231 &rel_config.name,
232 &tuple.relation,
233 )
234 .await;
235
236 if !still_has {
237 self.remove_entry(
238 &subject_key,
239 &tuple.namespace,
240 &tuple.object_id,
241 &rel_config.name,
242 )
243 .await;
244 removed_count += 1;
245 }
246 }
247 }
248 }
249
250 Ok(removed_count)
251 }
252
253 pub async fn check(&self, request: &CheckRequest) -> Option<bool> {
255 let subject_key = request.subject.to_string();
256 let key = (
257 request.namespace.clone(),
258 request.object_id.clone(),
259 request.relation.clone(),
260 );
261
262 let by_subject = self.by_subject.read().await;
263 if let Some(entries) = by_subject.get(&subject_key) {
264 let result = entries.contains(&key);
265
266 let mut stats = self.stats.write().await;
268 if result {
269 stats.hits += 1;
270 } else {
271 stats.misses += 1;
272 }
273
274 Some(result)
275 } else {
276 let mut stats = self.stats.write().await;
277 stats.misses += 1;
278 Some(false)
279 }
280 }
281
282 pub async fn expand(&self, namespace: &str, object_id: &str, relation: &str) -> Vec<String> {
284 let key = (
285 namespace.to_string(),
286 object_id.to_string(),
287 relation.to_string(),
288 );
289
290 let by_object = self.by_object.read().await;
291 by_object
292 .get(&key)
293 .map(|subjects| subjects.iter().cloned().collect())
294 .unwrap_or_default()
295 }
296
297 pub async fn list_subject_access(&self, subject: &Subject) -> Vec<(String, String, String)> {
299 let subject_key = subject.to_string();
300
301 let by_subject = self.by_subject.read().await;
302 by_subject
303 .get(&subject_key)
304 .map(|entries| entries.iter().cloned().collect())
305 .unwrap_or_default()
306 }
307
308 pub async fn stats(&self) -> LeopardStats {
310 self.stats.read().await.clone()
311 }
312
313 pub async fn clear(&self) {
315 self.by_subject.write().await.clear();
316 self.by_object.write().await.clear();
317 self.direct_tuples.write().await.clear();
318 *self.stats.write().await = LeopardStats::default();
319 }
320
321 pub async fn bulk_load(&self, tuples: &[RelationTuple]) -> Result<usize> {
323 let mut total = 0;
324 for tuple in tuples {
325 total += self.index_tuple(tuple).await?;
326 }
327 Ok(total)
328 }
329
330 async fn add_entry(
333 &self,
334 subject: &str,
335 namespace: &str,
336 object_id: &str,
337 relation: &str,
338 is_computed: bool,
339 ) {
340 let key = (
341 namespace.to_string(),
342 object_id.to_string(),
343 relation.to_string(),
344 );
345
346 {
348 let mut by_subject = self.by_subject.write().await;
349 by_subject
350 .entry(subject.to_string())
351 .or_default()
352 .insert(key.clone());
353 }
354
355 {
357 let mut by_object = self.by_object.write().await;
358 by_object
359 .entry(key)
360 .or_default()
361 .insert(subject.to_string());
362 }
363
364 {
366 let mut stats = self.stats.write().await;
367 if is_computed {
368 stats.computed_entries += 1;
369 } else {
370 stats.direct_entries += 1;
371 }
372 }
373 }
374
375 async fn remove_entry(&self, subject: &str, namespace: &str, object_id: &str, relation: &str) {
376 let key = (
377 namespace.to_string(),
378 object_id.to_string(),
379 relation.to_string(),
380 );
381
382 {
384 let mut by_subject = self.by_subject.write().await;
385 if let Some(entries) = by_subject.get_mut(subject) {
386 entries.remove(&key);
387 if entries.is_empty() {
388 by_subject.remove(subject);
389 }
390 }
391 }
392
393 {
395 let mut by_object = self.by_object.write().await;
396 if let Some(subjects) = by_object.get_mut(&key) {
397 subjects.remove(subject);
398 if subjects.is_empty() {
399 by_object.remove(&key);
400 }
401 }
402 }
403 }
404
405 async fn check_has_through_other_path(
406 &self,
407 subject: &str,
408 namespace: &str,
409 object_id: &str,
410 relation: &str,
411 excluded_source: &str,
412 ) -> bool {
413 if let Some(ns_config) = self.namespace_configs.get(namespace) {
415 if let Some(rel_config) = ns_config.relations.iter().find(|r| r.name == relation) {
416 let direct = self.direct_tuples.read().await;
417
418 for source_rel in &rel_config.inherits_from {
420 if source_rel != excluded_source
421 && direct.contains(&(
422 subject.to_string(),
423 namespace.to_string(),
424 object_id.to_string(),
425 source_rel.clone(),
426 ))
427 {
428 return true;
429 }
430 }
431 }
432 }
433 false
434 }
435}
436
437#[cfg(test)]
438mod tests {
439 use super::*;
440
441 fn test_namespace_configs() -> Arc<HashMap<String, NamespaceConfig>> {
442 let mut configs = HashMap::new();
443 configs.insert(
444 "document".to_string(),
445 NamespaceConfig::document_namespace(),
446 );
447 configs.insert("folder".to_string(), NamespaceConfig::folder_namespace());
448 Arc::new(configs)
449 }
450
451 #[tokio::test]
452 async fn test_leopard_direct_lookup() {
453 let configs = test_namespace_configs();
454 let index = LeopardIndex::new(configs);
455
456 let tuple = RelationTuple::new(
458 "document",
459 "owner",
460 "doc123",
461 Subject::User("alice".to_string()),
462 );
463 index.index_tuple(&tuple).await.unwrap();
464
465 let request = CheckRequest {
467 namespace: "document".to_string(),
468 object_id: "doc123".to_string(),
469 relation: "owner".to_string(),
470 subject: Subject::User("alice".to_string()),
471 context: None,
472 };
473 assert_eq!(index.check(&request).await, Some(true));
474
475 let request2 = CheckRequest {
477 namespace: "document".to_string(),
478 object_id: "doc123".to_string(),
479 relation: "owner".to_string(),
480 subject: Subject::User("bob".to_string()),
481 context: None,
482 };
483 assert_eq!(index.check(&request2).await, Some(false));
484 }
485
486 #[tokio::test]
487 async fn test_leopard_inherited_relations() {
488 let configs = test_namespace_configs();
489 let index = LeopardIndex::new(configs);
490
491 let tuple = RelationTuple::new(
493 "document",
494 "owner",
495 "doc123",
496 Subject::User("alice".to_string()),
497 );
498 let count = index.index_tuple(&tuple).await.unwrap();
499
500 assert!(count >= 2);
502
503 let request = CheckRequest {
505 namespace: "document".to_string(),
506 object_id: "doc123".to_string(),
507 relation: "viewer".to_string(),
508 subject: Subject::User("alice".to_string()),
509 context: None,
510 };
511 assert_eq!(index.check(&request).await, Some(true));
512
513 let request2 = CheckRequest {
515 namespace: "document".to_string(),
516 object_id: "doc123".to_string(),
517 relation: "editor".to_string(),
518 subject: Subject::User("alice".to_string()),
519 context: None,
520 };
521 assert_eq!(index.check(&request2).await, Some(true));
522 }
523
524 #[tokio::test]
525 async fn test_leopard_remove_tuple() {
526 let configs = test_namespace_configs();
527 let index = LeopardIndex::new(configs);
528
529 let tuple = RelationTuple::new(
531 "document",
532 "owner",
533 "doc123",
534 Subject::User("alice".to_string()),
535 );
536 index.index_tuple(&tuple).await.unwrap();
537 index.remove_tuple(&tuple).await.unwrap();
538
539 let request = CheckRequest {
541 namespace: "document".to_string(),
542 object_id: "doc123".to_string(),
543 relation: "owner".to_string(),
544 subject: Subject::User("alice".to_string()),
545 context: None,
546 };
547 assert_eq!(index.check(&request).await, Some(false));
548
549 let request2 = CheckRequest {
551 namespace: "document".to_string(),
552 object_id: "doc123".to_string(),
553 relation: "viewer".to_string(),
554 subject: Subject::User("alice".to_string()),
555 context: None,
556 };
557 assert_eq!(index.check(&request2).await, Some(false));
558 }
559
560 #[tokio::test]
561 async fn test_leopard_expand() {
562 let configs = test_namespace_configs();
563 let index = LeopardIndex::new(configs);
564
565 for user in ["alice", "bob", "charlie"] {
567 let tuple = RelationTuple::new(
568 "document",
569 "viewer",
570 "doc123",
571 Subject::User(user.to_string()),
572 );
573 index.index_tuple(&tuple).await.unwrap();
574 }
575
576 let viewers = index.expand("document", "doc123", "viewer").await;
578 assert_eq!(viewers.len(), 3);
579 assert!(viewers.contains(&"user:alice".to_string()));
580 assert!(viewers.contains(&"user:bob".to_string()));
581 assert!(viewers.contains(&"user:charlie".to_string()));
582 }
583
584 #[tokio::test]
585 async fn test_leopard_stats() {
586 let configs = test_namespace_configs();
587 let index = LeopardIndex::new(configs);
588
589 let tuple = RelationTuple::new(
591 "document",
592 "owner",
593 "doc123",
594 Subject::User("alice".to_string()),
595 );
596 index.index_tuple(&tuple).await.unwrap();
597
598 let stats = index.stats().await;
599 assert!(stats.direct_entries > 0);
600 assert!(stats.computed_entries > 0); let request = CheckRequest {
604 namespace: "document".to_string(),
605 object_id: "doc123".to_string(),
606 relation: "owner".to_string(),
607 subject: Subject::User("alice".to_string()),
608 context: None,
609 };
610 index.check(&request).await;
611 index.check(&request).await;
612
613 let stats = index.stats().await;
614 assert_eq!(stats.hits, 2);
615 }
616
617 #[tokio::test]
618 async fn test_leopard_bulk_load() {
619 let configs = test_namespace_configs();
620 let index = LeopardIndex::new(configs);
621
622 let tuples: Vec<RelationTuple> = (0..100)
623 .map(|i| {
624 RelationTuple::new(
625 "document",
626 "viewer",
627 format!("doc{}", i),
628 Subject::User(format!("user{}", i % 10)),
629 )
630 })
631 .collect();
632
633 let count = index.bulk_load(&tuples).await.unwrap();
634 assert_eq!(count, 100); let request = CheckRequest {
638 namespace: "document".to_string(),
639 object_id: "doc50".to_string(),
640 relation: "viewer".to_string(),
641 subject: Subject::User("user0".to_string()),
642 context: None,
643 };
644 assert_eq!(index.check(&request).await, Some(true));
645 }
646}