Skip to main content

in_ram_rebac/
in_ram_rebac.rs

1//! In-RAM FactSource-backed ReBAC example.
2//!
3//! This is the small, production-shaped version of the v0.3 fact model: the
4//! application owns one shared relationship source, each request creates a fresh
5//! `EvaluationSession`, and list endpoints batch relationship checks through
6//! the same `PermissionChecker` used for single-resource checks.
7
8use async_trait::async_trait;
9use dashmap::DashSet;
10use gatehouse::{
11    EvaluationSession, FactLoadResult, FactRegistry, FactSource, PermissionChecker, PolicyDomain,
12    RebacPolicy, RelationshipQuery,
13};
14use std::fmt;
15use std::sync::Arc;
16use uuid::Uuid;
17
18type RelationshipKey = RelationshipQuery<Uuid, Uuid, Relation>;
19
20#[derive(Debug, Clone)]
21struct User {
22    id: Uuid,
23}
24
25#[derive(Debug, Clone)]
26struct Document {
27    id: Uuid,
28    title: &'static str,
29}
30
31#[derive(Debug, Clone)]
32struct View;
33
34struct DocumentDomain;
35
36impl PolicyDomain for DocumentDomain {
37    type Subject = User;
38    type Action = View;
39    type Resource = Document;
40    type Context = ();
41}
42
43#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
44enum Relation {
45    Viewer,
46    Editor,
47}
48
49impl fmt::Display for Relation {
50    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51        match self {
52            Self::Viewer => f.write_str("viewer"),
53            Self::Editor => f.write_str("editor"),
54        }
55    }
56}
57
58#[derive(Default)]
59struct InRamRelationships {
60    grants: DashSet<RelationshipKey>,
61}
62
63impl InRamRelationships {
64    fn grant(&self, subject_id: Uuid, resource_id: Uuid, relation: Relation) {
65        self.grants.insert(RelationshipKey {
66            subject_id,
67            resource_id,
68            relation,
69        });
70    }
71}
72
73#[async_trait]
74impl FactSource<RelationshipKey> for InRamRelationships {
75    async fn load_many(&self, keys: &[RelationshipKey]) -> Vec<FactLoadResult<bool>> {
76        keys.iter()
77            .map(|key| FactLoadResult::Found(self.grants.contains(key)))
78            .collect()
79    }
80}
81
82fn request_session(registry: &FactRegistry) -> EvaluationSession {
83    registry.session()
84}
85
86fn build_checker() -> PermissionChecker<DocumentDomain> {
87    let mut checker = PermissionChecker::new();
88    checker.add_policy(RebacPolicy::<DocumentDomain, Uuid, Uuid, Relation>::new(
89        |user: &User| user.id,
90        |document: &Document| document.id,
91        Relation::Viewer,
92    ));
93    checker
94}
95
96#[tokio::main]
97async fn main() {
98    let user = User { id: Uuid::new_v4() };
99    let documents = vec![
100        Document {
101            id: Uuid::new_v4(),
102            title: "roadmap",
103        },
104        Document {
105            id: Uuid::new_v4(),
106            title: "incident report",
107        },
108        Document {
109            id: Uuid::new_v4(),
110            title: "finance plan",
111        },
112    ];
113
114    let store = Arc::new(InRamRelationships::default());
115    store.grant(user.id, documents[0].id, Relation::Viewer);
116    store.grant(user.id, documents[1].id, Relation::Viewer);
117    // The store can hold more relation types than any one policy consumes. This
118    // editor grant is never matched below (the checker only asks about Viewer),
119    // and is here to show the source and the policy stack are decoupled.
120    store.grant(user.id, documents[1].id, Relation::Editor);
121    let relationships: Arc<dyn FactSource<RelationshipKey>> = store;
122    let registry = FactRegistry::builder()
123        .with_arc::<RelationshipKey>(Arc::clone(&relationships))
124        .build();
125
126    let checker = build_checker();
127    let context = ();
128
129    let first_request = request_session(&registry);
130    let visible = checker
131        .bind(&first_request, &user, &View, &context)
132        .filter(documents.clone())
133        .await;
134    println!(
135        "batch list — visible documents: {:?}",
136        visible
137            .iter()
138            .map(|document| document.title)
139            .collect::<Vec<_>>()
140    );
141
142    // A fresh session for a single-resource check. The user has no viewer
143    // relationship on the finance plan, so this denies.
144    let second_request = request_session(&registry);
145    let can_view_finance = checker
146        .bind(&second_request, &user, &View, &context)
147        .check(&documents[2])
148        .await;
149    println!(
150        "single check — can view '{}'? {}",
151        documents[2].title,
152        if can_view_finance.is_granted() {
153            "yes"
154        } else {
155            "no"
156        }
157    );
158    assert!(!can_view_finance.is_granted());
159
160    let shared = registry.clone();
161    let concurrent_requests = (0..4)
162        .map(|_| {
163            let checker = checker.clone();
164            let user = user.clone();
165            let documents = documents.clone();
166            let registry = shared.clone();
167            tokio::spawn(async move {
168                let session = request_session(&registry);
169                let context = ();
170                checker
171                    .bind(&session, &user, &View, &context)
172                    .filter(documents)
173                    .await
174                    .len()
175            })
176        })
177        .collect::<Vec<_>>();
178
179    println!("\n4 concurrent requests sharing one FactSource (each builds its own session):");
180    for (index, request) in concurrent_requests.into_iter().enumerate() {
181        let visible_count = request.await.unwrap();
182        println!("  request {index}: {visible_count} visible document(s)");
183        assert_eq!(visible_count, 2);
184    }
185}