1use std::collections::{HashMap, HashSet};
14
15use crate::auth::User;
16use crate::error::AuthError;
17
18pub trait Authorizer: Send + Sync {
19 fn can(&self, user: &User, action: &str, resource: &str) -> Result<bool, AuthError>;
21}
22
23pub struct RbacAuthorizer {
29 role_permissions: HashMap<String, HashSet<String>>,
30 role_parents: HashMap<String, String>,
34}
35
36impl RbacAuthorizer {
37 pub fn new() -> Self {
39 let mut role_permissions = HashMap::new();
40 role_permissions.insert("admin".to_string(), HashSet::from(["*".to_string()]));
41 Self {
42 role_permissions,
43 role_parents: HashMap::new(),
44 }
45 }
46
47 pub fn with_role_permission(mut self, role: &str, permission: &str) -> Self {
49 self.role_permissions
50 .entry(role.to_string())
51 .or_default()
52 .insert(permission.to_string());
53 self
54 }
55
56 pub fn with_role_parent(mut self, role: &str, parent: &str) -> Self {
66 self.set_role_parent(role, parent);
67 self
68 }
69
70 fn set_role_parent(&mut self, role: &str, parent: &str) {
74 if role == parent {
75 return;
76 }
77 self.role_parents
78 .insert(role.to_string(), parent.to_string());
79 if self.has_cycle(role) {
81 self.role_parents.remove(role);
82 }
83 }
84
85 fn has_cycle(&self, start: &str) -> bool {
89 let mut current = start.to_string();
90 for _ in 0..64 {
91 match self.role_parents.get(¤t) {
92 Some(parent) => {
93 if parent == start {
94 return true;
95 }
96 current = parent.clone();
97 }
98 None => return false,
99 }
100 }
101 true
102 }
103
104 pub fn grant(&mut self, role: &str, permission: &str) {
106 self.role_permissions
107 .entry(role.to_string())
108 .or_default()
109 .insert(permission.to_string());
110 }
111
112 pub fn revoke(&mut self, role: &str, permission: &str) {
114 if let Some(perms) = self.role_permissions.get_mut(role) {
115 perms.remove(permission);
116 }
117 }
118
119 pub fn role_parent(&self, role: &str) -> Option<&str> {
123 self.role_parents.get(role).map(|s| s.as_str())
124 }
125
126 pub fn role_ancestors(&self, role: &str) -> Vec<String> {
131 let mut result = vec![role.to_string()];
132 let mut current = role.to_string();
133 for _ in 0..64 {
134 match self.role_parents.get(¤t) {
135 Some(parent) => {
136 if result.contains(parent) {
137 break;
138 }
139 result.push(parent.clone());
140 current = parent.clone();
141 }
142 None => break,
143 }
144 }
145 result
146 }
147
148 pub fn role_has_permission(&self, role: &str, permission: &str) -> bool {
151 for ancestor in self.role_ancestors(role) {
152 if self
153 .role_permissions
154 .get(&ancestor)
155 .map(|perms| perms.contains(permission) || perms.contains("*"))
156 .unwrap_or(false)
157 {
158 return true;
159 }
160 }
161 false
162 }
163
164 pub fn permissions_for_role(&self, role: &str) -> Vec<String> {
166 self.role_permissions
167 .get(role)
168 .map(|perms| {
169 let mut v: Vec<String> = perms.iter().cloned().collect();
170 v.sort();
171 v
172 })
173 .unwrap_or_default()
174 }
175
176 pub fn effective_permissions_for_role(&self, role: &str) -> Vec<String> {
180 let mut all: HashSet<String> = HashSet::new();
181 for ancestor in self.role_ancestors(role) {
182 if let Some(perms) = self.role_permissions.get(&ancestor) {
183 all.extend(perms.iter().cloned());
184 }
185 }
186 let mut v: Vec<String> = all.into_iter().collect();
187 v.sort();
188 v
189 }
190
191 fn check_permission(&self, user: &User, permission: &str) -> bool {
192 if user.permissions.iter().any(|p| p == permission || p == "*") {
194 return true;
195 }
196 for role in &user.roles {
198 if self.role_has_permission(role, permission) {
199 return true;
200 }
201 }
202 false
203 }
204}
205
206impl Default for RbacAuthorizer {
207 fn default() -> Self {
208 Self::new()
209 }
210}
211
212impl Authorizer for RbacAuthorizer {
213 fn can(&self, user: &User, action: &str, resource: &str) -> Result<bool, AuthError> {
214 let specific = format!("{}:{}", action, resource);
215 if self.check_permission(user, &specific) {
216 return Ok(true);
217 }
218 if self.check_permission(user, action) {
220 return Ok(true);
221 }
222 Ok(false)
223 }
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229
230 fn make_user(id: i64, name: &str) -> User {
231 User::new(id, name)
232 }
233
234 #[test]
235 fn test_admin_role_has_all_permissions() {
236 let authz = RbacAuthorizer::new();
237 let user = make_user(1, "root").with_roles(vec!["admin".to_string()]);
238
239 assert!(authz.can(&user, "read", "posts").unwrap());
240 assert!(authz.can(&user, "delete", "users").unwrap());
241 assert!(authz.can(&user, "anything", "anything").unwrap());
242 }
243
244 #[test]
245 fn test_direct_user_permission_grants_action_resource() {
246 let authz = RbacAuthorizer::new();
247 let user = make_user(1, "alice").with_permissions(vec!["read:posts".to_string()]);
248
249 assert!(authz.can(&user, "read", "posts").unwrap());
250 assert!(!authz.can(&user, "read", "users").unwrap());
252 assert!(!authz.can(&user, "delete", "posts").unwrap());
253 }
254
255 #[test]
256 fn test_action_only_permission_grants_all_resources() {
257 let authz = RbacAuthorizer::new();
258 let user = make_user(1, "bob").with_permissions(vec!["read".to_string()]);
259
260 assert!(authz.can(&user, "read", "posts").unwrap());
261 assert!(authz.can(&user, "read", "users").unwrap());
262 assert!(!authz.can(&user, "write", "posts").unwrap());
263 }
264
265 #[test]
266 fn test_role_grants_permission() {
267 let authz = RbacAuthorizer::new()
268 .with_role_permission("editor", "write:posts")
269 .with_role_permission("viewer", "read:posts");
270
271 let editor = make_user(1, "ed").with_roles(vec!["editor".to_string()]);
272 let viewer = make_user(2, "vi").with_roles(vec!["viewer".to_string()]);
273
274 assert!(authz.can(&editor, "write", "posts").unwrap());
275 assert!(!authz.can(&editor, "delete", "posts").unwrap());
276
277 assert!(authz.can(&viewer, "read", "posts").unwrap());
278 assert!(!authz.can(&viewer, "write", "posts").unwrap());
279 }
280
281 #[test]
282 fn test_grant_and_revoke() {
283 let mut authz = RbacAuthorizer::new();
284 authz.grant("editor", "write:posts");
285 let user = make_user(1, "ed").with_roles(vec!["editor".to_string()]);
286 assert!(authz.can(&user, "write", "posts").unwrap());
287
288 authz.revoke("editor", "write:posts");
289 assert!(!authz.can(&user, "write", "posts").unwrap());
290 }
291
292 #[test]
293 fn test_user_with_no_permissions_is_denied() {
294 let authz = RbacAuthorizer::new();
295 let user = make_user(1, "anon");
296 assert!(!authz.can(&user, "read", "anything").unwrap());
297 }
298
299 #[test]
300 fn test_permissions_for_role() {
301 let authz = RbacAuthorizer::new()
302 .with_role_permission("editor", "write:posts")
303 .with_role_permission("editor", "read:posts");
304
305 let perms = authz.permissions_for_role("editor");
306 assert_eq!(
307 perms,
308 vec!["read:posts".to_string(), "write:posts".to_string()]
309 );
310 assert!(authz.permissions_for_role("nonexistent").is_empty());
311 }
312
313 #[test]
314 fn test_user_permission_overrides_role() {
315 let authz = RbacAuthorizer::new();
317 let user = make_user(1, "lone")
318 .with_permissions(vec!["read:posts".to_string(), "write:posts".to_string()]);
319 assert!(authz.can(&user, "read", "posts").unwrap());
320 assert!(authz.can(&user, "write", "posts").unwrap());
321 }
322
323 #[test]
324 fn test_multiple_roles_combination() {
325 let authz = RbacAuthorizer::new()
326 .with_role_permission("reader", "read:posts")
327 .with_role_permission("writer", "write:posts");
328 let user =
329 make_user(1, "combo").with_roles(vec!["reader".to_string(), "writer".to_string()]);
330 assert!(authz.can(&user, "read", "posts").unwrap());
331 assert!(authz.can(&user, "write", "posts").unwrap());
332 assert!(!authz.can(&user, "delete", "posts").unwrap());
333 }
334
335 #[test]
336 fn test_default_admin_wildcard() {
337 let authz = RbacAuthorizer::new();
338 assert!(authz.role_has_permission("admin", "anything"));
339 assert!(authz.role_has_permission("admin", "*"));
340 assert!(!authz.role_has_permission("editor", "anything"));
341 }
342
343 #[test]
346 fn test_role_inherits_parent_permission() {
347 let authz = RbacAuthorizer::new()
349 .with_role_permission("viewer", "read:posts")
350 .with_role_permission("editor", "write:posts")
351 .with_role_parent("editor", "viewer");
352
353 let editor = make_user(1, "ed").with_roles(vec!["editor".to_string()]);
354 assert!(authz.can(&editor, "read", "posts").unwrap());
356 assert!(authz.can(&editor, "write", "posts").unwrap());
357 }
358
359 #[test]
360 fn test_role_inherits_wildcard_from_parent() {
361 let authz = RbacAuthorizer::new().with_role_parent("super_admin", "admin");
363
364 let super_admin = make_user(1, "super").with_roles(vec!["super_admin".to_string()]);
365 assert!(authz.can(&super_admin, "anything", "anything").unwrap());
366 assert!(authz.can(&super_admin, "delete", "all").unwrap());
367 }
368
369 #[test]
370 fn test_multi_level_inheritance() {
371 let authz = RbacAuthorizer::new()
373 .with_role_permission("viewer", "read:posts")
374 .with_role_permission("editor", "write:posts")
375 .with_role_parent("editor", "viewer")
376 .with_role_parent("admin_custom", "editor");
377
378 let admin_custom = make_user(1, "ac").with_roles(vec!["admin_custom".to_string()]);
379 assert!(authz.can(&admin_custom, "read", "posts").unwrap());
381 assert!(authz.can(&admin_custom, "write", "posts").unwrap());
382 assert!(!authz.can(&admin_custom, "delete", "posts").unwrap());
383 }
384
385 #[test]
386 fn test_role_ancestors() {
387 let authz = RbacAuthorizer::new()
388 .with_role_parent("editor", "viewer")
389 .with_role_parent("admin_custom", "editor");
390
391 let ancestors = authz.role_ancestors("admin_custom");
392 assert_eq!(
393 ancestors,
394 vec![
395 "admin_custom".to_string(),
396 "editor".to_string(),
397 "viewer".to_string()
398 ]
399 );
400
401 let ancestors2 = authz.role_ancestors("viewer");
402 assert_eq!(ancestors2, vec!["viewer".to_string()]);
403 }
404
405 #[test]
406 fn test_role_parent() {
407 let authz = RbacAuthorizer::new().with_role_parent("editor", "viewer");
408 assert_eq!(authz.role_parent("editor"), Some("viewer"));
409 assert_eq!(authz.role_parent("viewer"), None);
410 }
411
412 #[test]
413 fn test_cycle_detection_self_reference() {
414 let authz = RbacAuthorizer::new().with_role_parent("a", "a");
416 assert_eq!(authz.role_parent("a"), None);
417 }
418
419 #[test]
420 fn test_cycle_detection_two_node() {
421 let authz = RbacAuthorizer::new()
423 .with_role_parent("a", "b")
424 .with_role_parent("b", "a");
425 assert_eq!(authz.role_parent("b"), None);
427 assert_eq!(authz.role_parent("a"), Some("b"));
429 }
430
431 #[test]
432 fn test_cycle_detection_three_node() {
433 let authz = RbacAuthorizer::new()
435 .with_role_parent("a", "b")
436 .with_role_parent("b", "c")
437 .with_role_parent("c", "a");
438 assert_eq!(authz.role_parent("c"), None);
440 }
441
442 #[test]
443 fn test_effective_permissions_includes_inherited() {
444 let authz = RbacAuthorizer::new()
445 .with_role_permission("viewer", "read:posts")
446 .with_role_permission("viewer", "read:comments")
447 .with_role_permission("editor", "write:posts")
448 .with_role_parent("editor", "viewer");
449
450 let effective = authz.effective_permissions_for_role("editor");
451 assert!(effective.contains(&"read:posts".to_string()));
452 assert!(effective.contains(&"read:comments".to_string()));
453 assert!(effective.contains(&"write:posts".to_string()));
454 }
455
456 #[test]
457 fn test_effective_permissions_no_inheritance() {
458 let authz = RbacAuthorizer::new().with_role_permission("viewer", "read:posts");
459 let effective = authz.effective_permissions_for_role("viewer");
460 assert_eq!(effective, vec!["read:posts".to_string()]);
461 }
462
463 #[test]
464 fn test_effective_permissions_nonexistent_role() {
465 let authz = RbacAuthorizer::new();
466 let effective = authz.effective_permissions_for_role("nonexistent");
467 assert!(effective.is_empty());
468 }
469
470 #[test]
471 fn test_role_has_permission_with_inheritance() {
472 let authz = RbacAuthorizer::new()
473 .with_role_permission("viewer", "read:posts")
474 .with_role_parent("editor", "viewer");
475
476 assert!(authz.role_has_permission("editor", "read:posts"));
478 assert!(!authz.role_has_permission("editor", "delete:posts"));
479 }
480
481 #[test]
482 fn test_role_has_permission_wildcard_inherited() {
483 let authz = RbacAuthorizer::new().with_role_parent("super", "admin");
484 assert!(authz.role_has_permission("super", "anything"));
486 }
487
488 #[test]
489 fn test_no_inheritance_isolated_roles() {
490 let authz = RbacAuthorizer::new()
492 .with_role_permission("viewer", "read:posts")
493 .with_role_permission("editor", "write:posts");
494 assert!(!authz.role_has_permission("editor", "read:posts"));
496 assert!(authz.role_has_permission("editor", "write:posts"));
497 }
498
499 #[test]
500 fn test_user_with_inherited_role_can() {
501 let authz = RbacAuthorizer::new()
502 .with_role_permission("viewer", "read:posts")
503 .with_role_parent("editor", "viewer");
504
505 let user = make_user(1, "u").with_roles(vec!["editor".to_string()]);
506 assert!(authz.can(&user, "read", "posts").unwrap());
507 assert!(!authz.can(&user, "delete", "posts").unwrap());
508 }
509
510 #[test]
511 fn test_diamond_inheritance_no_duplicate() {
512 let authz = RbacAuthorizer::new()
515 .with_role_parent("b", "a")
516 .with_role_parent("c", "a")
517 .with_role_parent("d", "b")
518 .with_role_parent("d", "c");
519
520 let ancestors = authz.role_ancestors("d");
523 assert!(ancestors.contains(&"d".to_string()));
525 assert!(ancestors.contains(&"c".to_string()) || ancestors.contains(&"b".to_string()));
526 }
527}