Skip to main content

combinator_policy/
combinator_policy.rs

1//! # Combinator Policy Short-Circuit Evaluation Example
2//!
3//! This example demonstrates how the permission system's combinators use
4//! short-circuit evaluation for efficiency.
5//!
6//! To run this example:
7//! ```
8//! cargo run --example combinator_policy
9//! ```
10
11use async_trait::async_trait;
12use gatehouse::*;
13use std::sync::atomic::{AtomicUsize, Ordering};
14use std::sync::Arc;
15use uuid::Uuid;
16
17// Define simple types for the example
18#[derive(Debug, Clone)]
19struct User {
20    id: Uuid,
21}
22impl User {
23    fn new() -> Self {
24        Self { id: Uuid::new_v4() }
25    }
26}
27
28#[derive(Debug, Clone)]
29struct Document {
30    id: Uuid,
31}
32impl Document {
33    fn new() -> Self {
34        Self { id: Uuid::new_v4() }
35    }
36}
37
38#[derive(Debug, Clone)]
39struct ViewAction;
40
41struct DocumentDomain;
42
43impl PolicyDomain for DocumentDomain {
44    type Subject = User;
45    type Action = ViewAction;
46    type Resource = Document;
47    type Context = ();
48}
49
50// A policy that records when it's evaluated
51struct CountingPolicy {
52    allow: bool,
53    name: String,
54    counter: Arc<AtomicUsize>,
55}
56
57#[async_trait]
58impl Policy<DocumentDomain> for CountingPolicy {
59    async fn evaluate(&self, ctx: &EvalCtx<'_, DocumentDomain>) -> PolicyEvalResult {
60        // Increment evaluation counter
61        self.counter.fetch_add(1, Ordering::SeqCst);
62        println!("Evaluating policy: {}", self.name);
63
64        if self.allow {
65            ctx.grant(format!("{} grants access", self.name))
66        } else {
67            ctx.not_applicable(format!("{} denies access", self.name))
68        }
69    }
70
71    fn policy_type(&self) -> std::borrow::Cow<'static, str> {
72        std::borrow::Cow::Owned(self.name.clone())
73    }
74}
75
76#[tokio::main]
77async fn main() {
78    let user = User::new();
79    let document = Document::new();
80    let action = ViewAction;
81    let context = ();
82    // These policies have no fact sources, so each evaluation binds
83    // `EvaluationSession::empty()`. The checker contributes its own `OR` root
84    // to the trace; the combinator's short-circuit behaviour (what this example
85    // measures) happens inside it regardless.
86
87    println!("=== AND Policy Short-Circuit Example ===");
88    {
89        let counter = Arc::new(AtomicUsize::new(0));
90
91        // Create an AND policy with a non-grant policy first
92        let and_policy = CountingPolicy {
93            allow: false,
94            name: "DenyFirst".to_string(),
95            counter: counter.clone(),
96        }
97        .and(CountingPolicy {
98            allow: true,
99            name: "AllowSecond".to_string(),
100            counter: counter.clone(),
101        });
102
103        let mut checker = PermissionChecker::<DocumentDomain>::new();
104        checker.add_policy(and_policy);
105
106        println!("Evaluating AND(DenyFirst, AllowSecond):");
107        let session = EvaluationSession::empty();
108        let result = checker
109            .bind(&session, &user, &action, &context)
110            .check(&document)
111            .await;
112        println!(
113            "Result: {}",
114            if result.is_granted() {
115                "Access granted"
116            } else {
117                "Access denied"
118            }
119        );
120        println!("Policies evaluated: {}", counter.load(Ordering::SeqCst));
121        println!("Trace:\n{}", result.trace().format());
122
123        // The second policy should not be evaluated due to short-circuiting
124        assert_eq!(counter.load(Ordering::SeqCst), 1);
125    }
126
127    println!("\n=== OR Policy Short-Circuit Example ===");
128    {
129        let counter = Arc::new(AtomicUsize::new(0));
130
131        // Create an OR policy with an allow policy first
132        let or_policy = CountingPolicy {
133            allow: true,
134            name: "AllowFirst".to_string(),
135            counter: counter.clone(),
136        }
137        .or(CountingPolicy {
138            allow: false,
139            name: "DenySecond".to_string(),
140            counter: counter.clone(),
141        });
142
143        let mut checker = PermissionChecker::<DocumentDomain>::new();
144        checker.add_policy(or_policy);
145
146        println!("Evaluating OR(AllowFirst, DenySecond):");
147        let session = EvaluationSession::empty();
148        let result = checker
149            .bind(&session, &user, &action, &context)
150            .check(&document)
151            .await;
152        println!(
153            "Result: {}",
154            if result.is_granted() {
155                "Access granted"
156            } else {
157                "Access denied"
158            }
159        );
160        println!("Policies evaluated: {}", counter.load(Ordering::SeqCst));
161        println!("Trace:\n{}", result.trace().format());
162
163        // The second policy should not be evaluated due to short-circuiting
164        assert_eq!(counter.load(Ordering::SeqCst), 1);
165    }
166
167    println!("\n=== Complex Nested Policy Example ===");
168    {
169        let counter = Arc::new(AtomicUsize::new(0));
170
171        // Create a complex nested policy: OR(AND(Deny, Allow), Allow)
172        let inner_and = CountingPolicy {
173            allow: false,
174            name: "DenyInner".to_string(),
175            counter: counter.clone(),
176        }
177        .and(CountingPolicy {
178            allow: true,
179            name: "AllowInner".to_string(),
180            counter: counter.clone(),
181        });
182
183        let complex_policy = inner_and.or(CountingPolicy {
184            allow: true,
185            name: "AllowOuter".to_string(),
186            counter: counter.clone(),
187        });
188
189        let mut checker = PermissionChecker::<DocumentDomain>::new();
190        checker.add_policy(complex_policy);
191
192        println!("Evaluating OR(AND(DenyInner, AllowInner), AllowOuter):");
193        let session = EvaluationSession::empty();
194        let result = checker
195            .bind(&session, &user, &action, &context)
196            .check(&document)
197            .await;
198        println!(
199            "Result: {} for document with ID {} for user with ID {}",
200            if result.is_granted() {
201                "Access granted"
202            } else {
203                "Access denied"
204            },
205            document.id,
206            user.id
207        );
208        println!("Policies evaluated: {}", counter.load(Ordering::SeqCst));
209        println!("Trace:\n{}", result.trace().format());
210
211        // The inner AND should evaluate only DenyInner (shorts-circuit),
212        // then the OR continues to AllowOuter which grants access
213        assert_eq!(counter.load(Ordering::SeqCst), 2);
214    }
215}