Skip to main content

laterite_auth/
permission.rs

1//! The permission model.
2//!
3//! Permissions are dotted strings (`"posts.approve"`, `"users.edit"`).
4//! Descriptors elsewhere in the framework declare the permission a nav item,
5//! form field, or action requires; this type answers whether a given identity
6//! holds it. Grants support two wildcard forms:
7//!
8//! - `"*"` grants everything.
9//! - a trailing `".*"` grants a whole namespace, e.g. `"posts.*"` grants
10//!   `"posts.approve"` and `"posts.edit"` but not `"posts"` itself.
11//!
12//! Role grants form the base. A user may carry per-permission **overrides** that
13//! take precedence: an explicit deny wins over everything below a superuser, and
14//! an explicit allow wins over the role. A permission with no override inherits
15//! the role decision. Overrides are exact codes (no wildcards), so they refine a
16//! wildcard role grant one permission at a time.
17
18use std::collections::HashSet;
19
20/// The effective permissions of an identity.
21#[derive(Debug, Clone, Default)]
22pub struct PermissionSet {
23    superuser: bool,
24    grants: HashSet<String>,
25    allow: HashSet<String>,
26    deny: HashSet<String>,
27}
28
29impl PermissionSet {
30    /// Builds a set from granted permission strings, with no per-user overrides.
31    /// A superuser short-circuits every check regardless of the listed grants.
32    pub fn new(superuser: bool, grants: impl IntoIterator<Item = String>) -> Self {
33        Self {
34            superuser,
35            grants: grants.into_iter().collect(),
36            allow: HashSet::new(),
37            deny: HashSet::new(),
38        }
39    }
40
41    /// Builds a set from role `grants` plus per-user overrides: `allow` forces a
42    /// permission on and `deny` forces it off, both taking precedence over the
43    /// role grants (deny over allow).
44    pub fn with_overrides(
45        superuser: bool,
46        grants: impl IntoIterator<Item = String>,
47        allow: impl IntoIterator<Item = String>,
48        deny: impl IntoIterator<Item = String>,
49    ) -> Self {
50        Self {
51            superuser,
52            grants: grants.into_iter().collect(),
53            allow: allow.into_iter().collect(),
54            deny: deny.into_iter().collect(),
55        }
56    }
57
58    pub fn is_superuser(&self) -> bool {
59        self.superuser
60    }
61
62    /// Whether this identity is allowed `needed`. A superuser is always allowed.
63    /// Otherwise a user override decides first (deny wins, then allow); with no
64    /// override the role grants decide.
65    pub fn allows(&self, needed: &str) -> bool {
66        if self.superuser {
67            return true;
68        }
69        if self.deny.contains(needed) {
70            return false;
71        }
72        if self.allow.contains(needed) {
73            return true;
74        }
75        self.role_allows(needed)
76    }
77
78    /// Whether the role grants (ignoring overrides) allow `needed`.
79    fn role_allows(&self, needed: &str) -> bool {
80        if self.grants.contains("*") || self.grants.contains(needed) {
81            return true;
82        }
83        // A trailing-wildcard grant covers any deeper permission under its
84        // namespace prefix.
85        needed.match_indices('.').any(|(dot, _)| {
86            let mut wildcard = String::with_capacity(dot + 2);
87            wildcard.push_str(&needed[..=dot]);
88            wildcard.push('*');
89            self.grants.contains(&wildcard)
90        })
91    }
92
93    /// The raw grants, for inspection and rendering. Excludes the superuser flag.
94    pub fn grants(&self) -> impl Iterator<Item = &str> {
95        self.grants.iter().map(String::as_str)
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    fn set(grants: &[&str]) -> PermissionSet {
104        PermissionSet::new(false, grants.iter().map(|s| s.to_string()))
105    }
106
107    #[test]
108    fn exact_grant_matches() {
109        let p = set(&["posts.approve"]);
110        assert!(p.allows("posts.approve"));
111        assert!(!p.allows("posts.edit"));
112    }
113
114    #[test]
115    fn namespace_wildcard_matches_descendants_only() {
116        let p = set(&["posts.*"]);
117        assert!(p.allows("posts.approve"));
118        assert!(p.allows("posts.edit"));
119        assert!(p.allows("posts.tags.create"));
120        assert!(!p.allows("posts"));
121        assert!(!p.allows("users.edit"));
122    }
123
124    #[test]
125    fn global_wildcard_and_superuser_match_everything() {
126        assert!(set(&["*"]).allows("anything.at.all"));
127        let su = PermissionSet::new(true, std::iter::empty());
128        assert!(su.allows("anything.at.all"));
129        assert!(su.is_superuser());
130    }
131
132    #[test]
133    fn empty_set_grants_nothing() {
134        assert!(!set(&[]).allows("posts.approve"));
135    }
136
137    #[test]
138    fn user_deny_overrides_a_role_grant() {
139        // The role grants the whole namespace, but the user is denied one code.
140        let p = PermissionSet::with_overrides(
141            false,
142            ["posts.*".to_string()],
143            std::iter::empty(),
144            ["posts.approve".to_string()],
145        );
146        assert!(p.allows("posts.edit"));
147        assert!(!p.allows("posts.approve"));
148    }
149
150    #[test]
151    fn user_allow_grants_beyond_the_role() {
152        // The role grants nothing; the user is explicitly allowed one code.
153        let p = PermissionSet::with_overrides(
154            false,
155            std::iter::empty(),
156            ["posts.approve".to_string()],
157            std::iter::empty(),
158        );
159        assert!(p.allows("posts.approve"));
160        assert!(!p.allows("posts.edit"));
161    }
162
163    #[test]
164    fn deny_wins_over_allow_and_superuser_ignores_overrides() {
165        let denied = PermissionSet::with_overrides(
166            false,
167            std::iter::empty(),
168            ["x.y".to_string()],
169            ["x.y".to_string()],
170        );
171        assert!(!denied.allows("x.y"));
172        // A superuser is unaffected by an explicit deny.
173        let su = PermissionSet::with_overrides(
174            true,
175            std::iter::empty(),
176            std::iter::empty(),
177            ["x.y".to_string()],
178        );
179        assert!(su.allows("x.y"));
180    }
181}