1use crate::errors::{AuthError, Result};
20use serde::{Deserialize, Serialize};
21use std::collections::{HashMap, HashSet, VecDeque};
22use std::sync::Arc;
23use tokio::sync::RwLock;
24
25#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
29pub struct RelationTuple {
30 pub object: String,
32 pub relation: String,
34 pub subject: String,
37}
38
39impl RelationTuple {
40 pub fn new(
42 object: impl Into<String>,
43 relation: impl Into<String>,
44 subject: impl Into<String>,
45 ) -> Self {
46 Self {
47 object: object.into(),
48 relation: relation.into(),
49 subject: subject.into(),
50 }
51 }
52
53 pub fn parse_subject(&self) -> Option<(&str, &str, Option<&str>)> {
58 let (ns_id, userset) = if let Some(hash_pos) = self.subject.find('#') {
59 (&self.subject[..hash_pos], Some(&self.subject[hash_pos + 1..]))
60 } else {
61 (self.subject.as_str(), None)
62 };
63 let colon_pos = ns_id.find(':')?;
64 Some((&ns_id[..colon_pos], &ns_id[colon_pos + 1..], userset))
65 }
66
67 pub fn parse_object(&self) -> Option<(&str, &str)> {
71 let pos = self.object.find(':')?;
72 Some((&self.object[..pos], &self.object[pos + 1..]))
73 }
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct NamespaceConfig {
81 pub name: String,
83 pub relations: HashMap<String, RelationDef>,
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct RelationDef {
90 #[serde(default = "default_true")]
92 pub direct: bool,
93 #[serde(default)]
96 pub union: Vec<String>,
97 #[serde(default)]
101 pub tuple_to_userset: Vec<TupleToUserset>,
102}
103
104fn default_true() -> bool {
105 true
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct TupleToUserset {
111 pub tupleset_relation: String,
113 pub computed_userset_relation: String,
115}
116
117pub struct ZanzibarStore {
121 namespaces: Arc<RwLock<HashMap<String, NamespaceConfig>>>,
123 tuples: Arc<RwLock<HashMap<String, Vec<RelationTuple>>>>,
125 max_depth: usize,
127}
128
129impl ZanzibarStore {
130 pub fn new(max_depth: usize) -> Self {
132 Self {
133 namespaces: Arc::new(RwLock::new(HashMap::new())),
134 tuples: Arc::new(RwLock::new(HashMap::new())),
135 max_depth,
136 }
137 }
138
139 pub async fn add_namespace(&self, config: NamespaceConfig) {
141 self.namespaces
142 .write()
143 .await
144 .insert(config.name.clone(), config);
145 }
146
147 pub async fn write_tuple(&self, tuple: RelationTuple) -> Result<()> {
149 if let Some((ns, _)) = tuple.parse_object() {
151 let namespaces = self.namespaces.read().await;
152 if let Some(ns_config) = namespaces.get(ns) {
153 if !ns_config.relations.contains_key(&tuple.relation) {
154 return Err(AuthError::validation(&format!(
155 "Relation '{}' not defined in namespace '{}'",
156 tuple.relation, ns
157 )));
158 }
159 }
160 }
161
162 self.tuples
163 .write()
164 .await
165 .entry(tuple.object.clone())
166 .or_default()
167 .push(tuple);
168 Ok(())
169 }
170
171 pub async fn delete_tuple(&self, tuple: &RelationTuple) -> bool {
173 let mut tuples = self.tuples.write().await;
174 if let Some(list) = tuples.get_mut(&tuple.object) {
175 let before = list.len();
176 list.retain(|t| t != tuple);
177 list.len() < before
178 } else {
179 false
180 }
181 }
182
183 pub async fn read_tuples(
185 &self,
186 object: &str,
187 relation: Option<&str>,
188 ) -> Vec<RelationTuple> {
189 let tuples = self.tuples.read().await;
190 match tuples.get(object) {
191 Some(list) => {
192 if let Some(rel) = relation {
193 list.iter().filter(|t| t.relation == rel).cloned().collect()
194 } else {
195 list.clone()
196 }
197 }
198 None => Vec::new(),
199 }
200 }
201
202 pub async fn check(
207 &self,
208 object: &str,
209 relation: &str,
210 subject: &str,
211 ) -> Result<bool> {
212 let mut visited = HashSet::new();
213 self.check_internal(object, relation, subject, 0, &mut visited)
214 .await
215 }
216
217 #[allow(clippy::only_used_in_recursion)]
218 fn check_internal<'a>(
219 &'a self,
220 object: &'a str,
221 relation: &'a str,
222 subject: &'a str,
223 depth: usize,
224 visited: &'a mut HashSet<String>,
225 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<bool>> + Send + 'a>> {
226 Box::pin(async move {
227 if depth > self.max_depth {
228 return Err(AuthError::internal(
229 "Zanzibar check exceeded maximum traversal depth",
230 ));
231 }
232
233 let visit_key = format!("{object}#{relation}@{subject}");
234 if !visited.insert(visit_key) {
235 return Ok(false); }
237
238 let direct_tuples = self.read_tuples(object, Some(relation)).await;
240 for t in &direct_tuples {
241 if t.subject == subject {
242 return Ok(true);
243 }
244
245 if let Some((_, sub_id, Some(sub_rel))) = t.parse_subject() {
247 let (sub_ns, _) = t.subject.split_once('#').unwrap_or((&t.subject, ""));
248 if self
250 .check_internal(sub_ns, sub_rel, subject, depth + 1, visited)
251 .await?
252 {
253 let _ = sub_id; return Ok(true);
255 }
256 }
257 }
258
259 if let Some((ns, _)) = object.split_once(':') {
261 let namespaces = self.namespaces.read().await;
262 if let Some(ns_config) = namespaces.get(ns) {
263 if let Some(rel_def) = ns_config.relations.get(relation) {
264 for union_rel in &rel_def.union {
266 if self
267 .check_internal(object, union_rel, subject, depth + 1, visited)
268 .await?
269 {
270 return Ok(true);
271 }
272 }
273
274 for ttu in &rel_def.tuple_to_userset {
276 let parent_tuples = self
277 .read_tuples(object, Some(&ttu.tupleset_relation))
278 .await;
279 for pt in &parent_tuples {
280 if self
281 .check_internal(
282 &pt.subject,
283 &ttu.computed_userset_relation,
284 subject,
285 depth + 1,
286 visited,
287 )
288 .await?
289 {
290 return Ok(true);
291 }
292 }
293 }
294 }
295 }
296 }
297
298 Ok(false)
299 })
300 }
301
302 pub async fn expand(
304 &self,
305 object: &str,
306 relation: &str,
307 ) -> Result<Vec<String>> {
308 let mut result = Vec::new();
309 let mut visited = HashSet::new();
310 self.expand_internal(object, relation, 0, &mut result, &mut visited)
311 .await?;
312 Ok(result)
313 }
314
315 fn expand_internal<'a>(
316 &'a self,
317 object: &'a str,
318 relation: &'a str,
319 depth: usize,
320 result: &'a mut Vec<String>,
321 visited: &'a mut HashSet<String>,
322 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send + 'a>> {
323 Box::pin(async move {
324 if depth > self.max_depth {
325 return Ok(());
326 }
327
328 let visit_key = format!("{object}#{relation}");
329 if !visited.insert(visit_key) {
330 return Ok(());
331 }
332
333 let tuples = self.read_tuples(object, Some(relation)).await;
335 for t in &tuples {
336 if t.subject.contains('#') {
337 let (ref_obj, ref_rel) = t.subject.split_once('#').unwrap();
339 self.expand_internal(ref_obj, ref_rel, depth + 1, result, visited)
340 .await?;
341 } else if !result.contains(&t.subject) {
342 result.push(t.subject.clone());
343 }
344 }
345
346 if let Some((ns, _)) = object.split_once(':') {
348 let namespaces = self.namespaces.read().await;
349 if let Some(ns_config) = namespaces.get(ns) {
350 if let Some(rel_def) = ns_config.relations.get(relation) {
351 for union_rel in &rel_def.union {
352 self.expand_internal(object, union_rel, depth + 1, result, visited)
353 .await?;
354 }
355 for ttu in &rel_def.tuple_to_userset {
356 let parent_tuples = self
357 .read_tuples(object, Some(&ttu.tupleset_relation))
358 .await;
359 for pt in &parent_tuples {
360 self.expand_internal(
361 &pt.subject,
362 &ttu.computed_userset_relation,
363 depth + 1,
364 result,
365 visited,
366 )
367 .await?;
368 }
369 }
370 }
371 }
372 }
373
374 Ok(())
375 })
376 }
377
378 pub async fn list_objects(
381 &self,
382 object_type: &str,
383 relation: &str,
384 subject: &str,
385 ) -> Result<Vec<String>> {
386 let prefix = format!("{object_type}:");
387 let tuples = self.tuples.read().await;
388
389 let mut found = Vec::new();
390 let mut queue: VecDeque<String> = VecDeque::new();
391 let mut seen = HashSet::new();
392
393 for key in tuples.keys() {
395 if key.starts_with(&prefix) {
396 queue.push_back(key.clone());
397 }
398 }
399
400 while let Some(obj) = queue.pop_front() {
401 if !seen.insert(obj.clone()) {
402 continue;
403 }
404 drop(tuples);
406 if self.check(&obj, relation, subject).await? {
407 found.push(obj);
408 }
409 break; }
412
413 let candidates: Vec<String> = {
415 let tuples = self.tuples.read().await;
416 tuples
417 .keys()
418 .filter(|k| k.starts_with(&prefix))
419 .cloned()
420 .collect()
421 };
422
423 let mut result = Vec::new();
424 for obj in candidates {
425 if self.check(&obj, relation, subject).await? {
426 result.push(obj);
427 }
428 }
429 Ok(result)
430 }
431
432 pub async fn tuple_count(&self) -> usize {
434 self.tuples
435 .read()
436 .await
437 .values()
438 .map(|v| v.len())
439 .sum()
440 }
441}
442
443impl Default for ZanzibarStore {
444 fn default() -> Self {
445 Self::new(15)
446 }
447}
448
449#[cfg(test)]
450mod tests {
451 use super::*;
452
453 #[test]
456 fn test_tuple_parse_object() {
457 let t = RelationTuple::new("document:readme", "viewer", "user:alice");
458 let (ns, id) = t.parse_object().unwrap();
459 assert_eq!(ns, "document");
460 assert_eq!(id, "readme");
461 }
462
463 #[test]
464 fn test_tuple_parse_subject_direct() {
465 let t = RelationTuple::new("document:readme", "viewer", "user:alice");
466 let (ns, id, userset) = t.parse_subject().unwrap();
467 assert_eq!(ns, "user");
468 assert_eq!(id, "alice");
469 assert_eq!(userset, None);
470 }
471
472 #[test]
473 fn test_tuple_parse_subject_userset() {
474 let t = RelationTuple::new("document:readme", "viewer", "group:eng#member");
475 let (ns, id, userset) = t.parse_subject().unwrap();
476 assert_eq!(ns, "group");
477 assert_eq!(id, "eng");
478 assert_eq!(userset, Some("member"));
479 }
480
481 #[test]
482 fn test_tuple_serialization() {
483 let t = RelationTuple::new("doc:1", "viewer", "user:alice");
484 let json = serde_json::to_string(&t).unwrap();
485 let parsed: RelationTuple = serde_json::from_str(&json).unwrap();
486 assert_eq!(parsed, t);
487 }
488
489 #[tokio::test]
492 async fn test_direct_relation_check() {
493 let store = ZanzibarStore::default();
494 store
495 .write_tuple(RelationTuple::new("document:readme", "viewer", "user:alice"))
496 .await
497 .unwrap();
498
499 assert!(store.check("document:readme", "viewer", "user:alice").await.unwrap());
500 assert!(!store.check("document:readme", "viewer", "user:bob").await.unwrap());
501 assert!(!store.check("document:readme", "editor", "user:alice").await.unwrap());
502 }
503
504 #[tokio::test]
507 async fn test_union_relation() {
508 let store = ZanzibarStore::default();
509
510 store
512 .add_namespace(NamespaceConfig {
513 name: "document".to_string(),
514 relations: HashMap::from([
515 (
516 "editor".to_string(),
517 RelationDef {
518 direct: true,
519 union: vec![],
520 tuple_to_userset: vec![],
521 },
522 ),
523 (
524 "viewer".to_string(),
525 RelationDef {
526 direct: true,
527 union: vec!["editor".to_string()],
528 tuple_to_userset: vec![],
529 },
530 ),
531 ]),
532 })
533 .await;
534
535 store
536 .write_tuple(RelationTuple::new("document:readme", "editor", "user:alice"))
537 .await
538 .unwrap();
539
540 assert!(store.check("document:readme", "viewer", "user:alice").await.unwrap());
542 assert!(store.check("document:readme", "editor", "user:alice").await.unwrap());
544 assert!(!store.check("document:readme", "viewer", "user:bob").await.unwrap());
546 }
547
548 #[tokio::test]
551 async fn test_tuple_to_userset() {
552 let store = ZanzibarStore::default();
553
554 store
556 .add_namespace(NamespaceConfig {
557 name: "document".to_string(),
558 relations: HashMap::from([
559 (
560 "parent".to_string(),
561 RelationDef {
562 direct: true,
563 union: vec![],
564 tuple_to_userset: vec![],
565 },
566 ),
567 (
568 "viewer".to_string(),
569 RelationDef {
570 direct: true,
571 union: vec![],
572 tuple_to_userset: vec![TupleToUserset {
573 tupleset_relation: "parent".to_string(),
574 computed_userset_relation: "viewer".to_string(),
575 }],
576 },
577 ),
578 ]),
579 })
580 .await;
581
582 store
583 .add_namespace(NamespaceConfig {
584 name: "folder".to_string(),
585 relations: HashMap::from([(
586 "viewer".to_string(),
587 RelationDef {
588 direct: true,
589 union: vec![],
590 tuple_to_userset: vec![],
591 },
592 )]),
593 })
594 .await;
595
596 store
598 .write_tuple(RelationTuple::new("folder:docs", "viewer", "user:alice"))
599 .await
600 .unwrap();
601
602 store
604 .write_tuple(RelationTuple::new("document:readme", "parent", "folder:docs"))
605 .await
606 .unwrap();
607
608 assert!(store.check("document:readme", "viewer", "user:alice").await.unwrap());
610 assert!(!store.check("document:readme", "viewer", "user:bob").await.unwrap());
612 }
613
614 #[tokio::test]
617 async fn test_group_membership_userset() {
618 let store = ZanzibarStore::default();
619
620 store
621 .add_namespace(NamespaceConfig {
622 name: "group".to_string(),
623 relations: HashMap::from([(
624 "member".to_string(),
625 RelationDef {
626 direct: true,
627 union: vec![],
628 tuple_to_userset: vec![],
629 },
630 )]),
631 })
632 .await;
633
634 store
635 .add_namespace(NamespaceConfig {
636 name: "document".to_string(),
637 relations: HashMap::from([(
638 "viewer".to_string(),
639 RelationDef {
640 direct: true,
641 union: vec![],
642 tuple_to_userset: vec![],
643 },
644 )]),
645 })
646 .await;
647
648 store
650 .write_tuple(RelationTuple::new("group:eng", "member", "user:alice"))
651 .await
652 .unwrap();
653
654 store
656 .write_tuple(RelationTuple::new(
657 "document:readme",
658 "viewer",
659 "group:eng#member",
660 ))
661 .await
662 .unwrap();
663
664 assert!(store.check("document:readme", "viewer", "user:alice").await.unwrap());
666 assert!(!store.check("document:readme", "viewer", "user:bob").await.unwrap());
668 }
669
670 #[tokio::test]
673 async fn test_expand() {
674 let store = ZanzibarStore::default();
675
676 store
677 .write_tuple(RelationTuple::new("document:readme", "viewer", "user:alice"))
678 .await
679 .unwrap();
680 store
681 .write_tuple(RelationTuple::new("document:readme", "viewer", "user:bob"))
682 .await
683 .unwrap();
684 store
685 .write_tuple(RelationTuple::new("document:readme", "editor", "user:carol"))
686 .await
687 .unwrap();
688
689 let viewers = store.expand("document:readme", "viewer").await.unwrap();
690 assert_eq!(viewers.len(), 2);
691 assert!(viewers.contains(&"user:alice".to_string()));
692 assert!(viewers.contains(&"user:bob".to_string()));
693 }
694
695 #[tokio::test]
698 async fn test_delete_tuple() {
699 let store = ZanzibarStore::default();
700 let tuple = RelationTuple::new("document:readme", "viewer", "user:alice");
701
702 store.write_tuple(tuple.clone()).await.unwrap();
703 assert!(store.check("document:readme", "viewer", "user:alice").await.unwrap());
704
705 assert!(store.delete_tuple(&tuple).await);
706 assert!(!store.check("document:readme", "viewer", "user:alice").await.unwrap());
707 }
708
709 #[tokio::test]
710 async fn test_delete_nonexistent_tuple() {
711 let store = ZanzibarStore::default();
712 let tuple = RelationTuple::new("document:readme", "viewer", "user:alice");
713 assert!(!store.delete_tuple(&tuple).await);
714 }
715
716 #[tokio::test]
719 async fn test_tuple_count() {
720 let store = ZanzibarStore::default();
721 assert_eq!(store.tuple_count().await, 0);
722
723 store
724 .write_tuple(RelationTuple::new("doc:1", "viewer", "user:a"))
725 .await
726 .unwrap();
727 store
728 .write_tuple(RelationTuple::new("doc:2", "viewer", "user:b"))
729 .await
730 .unwrap();
731 assert_eq!(store.tuple_count().await, 2);
732 }
733
734 #[tokio::test]
737 async fn test_invalid_relation_rejected() {
738 let store = ZanzibarStore::default();
739 store
740 .add_namespace(NamespaceConfig {
741 name: "document".to_string(),
742 relations: HashMap::from([(
743 "viewer".to_string(),
744 RelationDef {
745 direct: true,
746 union: vec![],
747 tuple_to_userset: vec![],
748 },
749 )]),
750 })
751 .await;
752
753 let result = store
754 .write_tuple(RelationTuple::new("document:readme", "admin", "user:alice"))
755 .await;
756 assert!(result.is_err());
757 }
758
759 #[tokio::test]
762 async fn test_cycle_protection() {
763 let store = ZanzibarStore::new(5);
764
765 store
767 .write_tuple(RelationTuple::new("group:a", "member", "group:b#member"))
768 .await
769 .unwrap();
770 store
771 .write_tuple(RelationTuple::new("group:b", "member", "group:a#member"))
772 .await
773 .unwrap();
774
775 let result = store.check("group:a", "member", "user:alice").await;
777 match result {
779 Ok(v) => assert!(!v),
780 Err(_) => {} }
782 }
783
784 #[tokio::test]
787 async fn test_read_tuples_with_filter() {
788 let store = ZanzibarStore::default();
789 store
790 .write_tuple(RelationTuple::new("doc:1", "viewer", "user:a"))
791 .await
792 .unwrap();
793 store
794 .write_tuple(RelationTuple::new("doc:1", "editor", "user:b"))
795 .await
796 .unwrap();
797
798 let viewers = store.read_tuples("doc:1", Some("viewer")).await;
799 assert_eq!(viewers.len(), 1);
800 assert_eq!(viewers[0].subject, "user:a");
801
802 let all = store.read_tuples("doc:1", None).await;
803 assert_eq!(all.len(), 2);
804 }
805
806 #[tokio::test]
807 async fn test_read_tuples_empty() {
808 let store = ZanzibarStore::default();
809 let result = store.read_tuples("doc:nonexistent", None).await;
810 assert!(result.is_empty());
811 }
812}