laterite_auth/
permission.rs1use std::collections::HashSet;
19
20#[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 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 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 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 fn role_allows(&self, needed: &str) -> bool {
80 if self.grants.contains("*") || self.grants.contains(needed) {
81 return true;
82 }
83 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 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 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 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 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}