Skip to main content

rbac_policy/
rbac_policy.rs

1//! # Role-Based Access Control Policy Example
2//!
3//! The built-in `RbacPolicy` takes two resolver closures: which roles the
4//! (action, resource) pair requires, and which roles the subject holds.
5//! Access is granted when at least one required role is held.
6//!
7//! The role identifier type is generic — this example uses a domain enum so
8//! the output reads as names and the compiler checks the role set. `Uuid`
9//! role ids (for integration with an external identity system) work the same
10//! way; only the resolver closures change.
11//!
12//! To run this example:
13//! ```
14//! cargo run --example rbac_policy
15//! ```
16
17use gatehouse::*;
18use std::collections::HashSet;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21enum Role {
22    Admin,
23    Editor,
24    Viewer,
25}
26
27#[derive(Debug, Clone)]
28struct User {
29    name: &'static str,
30    roles: HashSet<Role>,
31}
32
33#[derive(Debug, Clone)]
34struct Document {
35    name: &'static str,
36    /// Roles that may read this document. Any one of them suffices.
37    required_roles: HashSet<Role>,
38}
39
40#[derive(Debug, Clone)]
41struct ReadAction;
42
43struct DocumentDomain;
44
45impl PolicyDomain for DocumentDomain {
46    type Subject = User;
47    type Action = ReadAction;
48    type Resource = Document;
49    type Context = ();
50}
51
52fn user<const N: usize>(name: &'static str, roles: [Role; N]) -> User {
53    User {
54        name,
55        roles: roles.into_iter().collect(),
56    }
57}
58
59fn document<const N: usize>(name: &'static str, required_roles: [Role; N]) -> Document {
60    Document {
61        name,
62        required_roles: required_roles.into_iter().collect(),
63    }
64}
65
66#[tokio::main]
67async fn main() {
68    // The first resolver reads the requirement off the resource/action; the
69    // second extracts the subject's roles. The role type (`Role`) is inferred
70    // from the closures' return types.
71    let rbac_policy = RbacPolicy::<DocumentDomain, _, _>::new(
72        |_action: &ReadAction, doc: &Document| doc.required_roles.iter().copied().collect(),
73        |user: &User| user.roles.iter().copied().collect(),
74    );
75
76    let mut checker = PermissionChecker::<DocumentDomain>::new();
77    checker.add_policy(rbac_policy);
78
79    let admin = user("admin", [Role::Admin]);
80    let editor = user("editor", [Role::Editor]);
81    let multi_role = user("editor+viewer", [Role::Editor, Role::Viewer]);
82    let no_roles = user("no-roles", []);
83
84    let admin_doc = document("admin handbook", [Role::Admin]);
85    let editor_doc = document("style guide", [Role::Editor]);
86    let shared_doc = document("team wiki", [Role::Editor, Role::Viewer]);
87
88    // (user, document, expected outcome)
89    let cases = [
90        (&admin, &admin_doc, true),
91        (&admin, &editor_doc, false), // admin role is not editor — no hierarchy here
92        (&editor, &admin_doc, false),
93        (&editor, &editor_doc, true),
94        (&multi_role, &editor_doc, true),
95        (&multi_role, &shared_doc, true),
96        (&no_roles, &admin_doc, false),
97        (&no_roles, &shared_doc, false),
98    ];
99
100    let session = EvaluationSession::empty();
101    let action = ReadAction;
102    let context = ();
103
104    println!("{:<16} {:<16} verdict", "user", "document");
105    println!("{}", "-".repeat(42));
106    for (user, document, expected_granted) in cases {
107        let decision = checker
108            .bind(&session, user, &action, &context)
109            .check(document)
110            .await;
111        println!(
112            "{:<16} {:<16} {}",
113            user.name,
114            document.name,
115            if decision.is_granted() {
116                "GRANTED"
117            } else {
118                "DENIED"
119            }
120        );
121        assert_eq!(decision.is_granted(), expected_granted);
122    }
123
124    // Note the admin/style-guide denial above: `RbacPolicy` is a flat
125    // role-match with no built-in hierarchy. If admins should read
126    // everything, either include `Role::Admin` in each document's required
127    // set or add a separate admin-override policy to the checker.
128
129    println!("\nWhy the editor is denied the admin handbook:");
130    let decision = checker
131        .bind(&session, &editor, &action, &context)
132        .check(&admin_doc)
133        .await;
134    println!("{}", decision.display_trace());
135}