1use gatehouse::{
32 AndPolicy, EvaluationSession, NotPolicy, PermissionChecker, Policy, PolicyBuilder, PolicyDomain,
33};
34use std::sync::Arc;
35use uuid::Uuid;
36
37#[derive(Debug, Clone)]
40struct User {
41 id: Uuid,
42 is_admin: bool,
43 suspended: bool,
45}
46
47#[derive(Debug, Clone)]
48struct Document {
49 owner_id: Uuid,
50 legal_hold: bool,
53}
54
55#[derive(Debug, Clone)]
56struct Access;
57
58struct DocumentDomain;
62
63impl PolicyDomain for DocumentDomain {
64 type Subject = User;
65 type Action = Access;
66 type Resource = Document;
67 type Context = ();
68}
69
70type DocPolicy = Box<dyn Policy<DocumentDomain>>;
71
72fn admin_override() -> DocPolicy {
75 PolicyBuilder::<DocumentDomain>::new("AdminOverride")
76 .subjects(|user| user.is_admin)
77 .build()
78}
79
80fn document_owner() -> DocPolicy {
81 PolicyBuilder::<DocumentDomain>::new("DocumentOwner")
82 .when(|user, _action, document, _ctx| user.id == document.owner_id)
83 .build()
84}
85
86fn account_suspended() -> DocPolicy {
92 PolicyBuilder::<DocumentDomain>::new("AccountSuspended")
93 .subjects(|user| user.suspended)
94 .forbid()
95 .build()
96}
97
98fn legal_hold() -> DocPolicy {
101 PolicyBuilder::<DocumentDomain>::new("LegalHold")
102 .resources(|document| document.legal_hold)
103 .forbid()
104 .build()
105}
106
107fn document_checker() -> PermissionChecker<DocumentDomain> {
108 let mut checker = PermissionChecker::named("DocumentChecker");
109 checker.add_policy(admin_override());
113 checker.add_policy(document_owner());
114 checker.add_policy(account_suspended());
115 checker.add_policy(legal_hold());
116 checker
117}
118
119#[tokio::main]
122async fn main() {
123 let owner_id = Uuid::new_v4();
124 let admin = User {
125 id: Uuid::new_v4(),
126 is_admin: true,
127 suspended: false,
128 };
129 let suspended_owner = User {
130 id: owner_id,
131 is_admin: false,
132 suspended: true,
133 };
134 let owner = User {
135 id: owner_id,
136 is_admin: false,
137 suspended: false,
138 };
139 let stranger = User {
140 id: Uuid::new_v4(),
141 is_admin: false,
142 suspended: false,
143 };
144
145 let normal_doc = Document {
146 owner_id,
147 legal_hold: false,
148 };
149 let held_doc = Document {
150 owner_id,
151 legal_hold: true,
152 };
153
154 let checker = document_checker();
155 let session = EvaluationSession::empty();
156 let action = Access;
157 let context = ();
158
159 let cases = [
161 (&admin, &normal_doc, "admin, normal doc"),
162 (&owner, &normal_doc, "owner, own normal doc"),
163 (&admin, &held_doc, "admin, LEGAL-HOLD doc"),
164 (&suspended_owner, &normal_doc, "SUSPENDED owner, own doc"),
165 (&stranger, &normal_doc, "stranger, someone else's doc"),
166 ];
167
168 println!("{:<32} {:>10} forbidden by", "case", "decision");
169 println!("{}", "-".repeat(60));
170 for (subject, document, label) in cases {
171 let decision = checker
172 .bind(&session, subject, &action, &context)
173 .check(document)
174 .await;
175 println!(
176 "{label:<32} {:>10} {}",
177 verdict(decision.is_granted()),
178 decision.forbidden_by().unwrap_or("-"),
179 );
180 }
181
182 checker
184 .bind(&session, &admin, &action, &context)
185 .check(&normal_doc)
186 .await
187 .assert_granted_by("AdminOverride");
188 checker
189 .bind(&session, &owner, &action, &context)
190 .check(&normal_doc)
191 .await
192 .assert_granted_by("DocumentOwner");
193
194 checker
196 .bind(&session, &admin, &action, &context)
197 .check(&held_doc)
198 .await
199 .assert_forbidden_by("LegalHold");
200 checker
201 .bind(&session, &suspended_owner, &action, &context)
202 .check(&normal_doc)
203 .await
204 .assert_forbidden_by("AccountSuspended");
205
206 let stranger_decision = checker
209 .bind(&session, &stranger, &action, &context)
210 .check(&normal_doc)
211 .await;
212 stranger_decision.assert_denied();
213 assert_eq!(stranger_decision.forbidden_by(), None);
214
215 println!("\nWhy 'admin, LEGAL-HOLD doc' is blocked:");
219 let decision = checker
220 .bind(&session, &admin, &action, &context)
221 .check(&held_doc)
222 .await;
223 println!("{}", decision.display_trace());
224
225 scoped_exclusion_demo().await;
226}
227
228async fn scoped_exclusion_demo() {
238 #[derive(Debug, Clone)]
239 struct Member {
240 is_owner: bool,
241 is_collaborator: bool,
242 muted: bool,
243 }
244 #[derive(Debug, Clone)]
245 struct Thread;
246
247 struct ThreadDomain;
248
249 impl PolicyDomain for ThreadDomain {
250 type Subject = Member;
251 type Action = Access;
252 type Resource = Thread;
253 type Context = ();
254 }
255
256 let owner_policy = PolicyBuilder::<ThreadDomain>::new("ThreadOwner")
257 .subjects(|member| member.is_owner)
258 .build();
259 let collaborator_policy: Arc<dyn Policy<ThreadDomain>> = Arc::from(
260 PolicyBuilder::<ThreadDomain>::new("Collaborator")
261 .subjects(|member| member.is_collaborator)
262 .build(),
263 );
264 let muted = PolicyBuilder::<ThreadDomain>::new("Muted")
270 .subjects(|member| member.muted)
271 .build();
272
273 let collaborator_unless_muted = AndPolicy::try_new(vec![
274 collaborator_policy,
275 Arc::new(NotPolicy::new(muted)) as Arc<dyn Policy<ThreadDomain>>,
276 ])
277 .expect("gate has the grant arm and the guard");
278
279 let mut checker = PermissionChecker::<ThreadDomain>::named("ThreadChecker");
280 checker.add_policy(owner_policy);
281 checker.add_policy(collaborator_unless_muted);
282
283 let muted_collaborator = Member {
284 is_owner: false,
285 is_collaborator: true,
286 muted: true,
287 };
288 let muted_owner = Member {
289 is_owner: true,
290 is_collaborator: false,
291 muted: true,
292 };
293
294 let session = EvaluationSession::empty();
295 let action = Access;
296 let context = ();
297
298 checker
300 .bind(&session, &muted_collaborator, &action, &context)
301 .check(&Thread)
302 .await
303 .assert_denied();
304 checker
307 .bind(&session, &muted_owner, &action, &context)
308 .check(&Thread)
309 .await
310 .assert_granted_by("ThreadOwner");
311
312 println!("\nScoped exclusion: muted collaborator blocked, muted owner unaffected.");
313}
314
315fn verdict(granted: bool) -> &'static str {
316 if granted {
317 "GRANTED"
318 } else {
319 "DENIED"
320 }
321}