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