Skip to main content

sz_orm_auth/
authorizer.rs

1//! Role-Based Access Control (RBAC) authorizer.
2//!
3//! This module provides an [`RbacAuthorizer`] that maps roles to a set of
4//! permissions and answers `can(action, resource)` queries. Permissions may
5//! be either `action:resource` pairs or a wildcard `*` that grants full access.
6//!
7//! ## 角色层级(Role Hierarchy)
8//!
9//! v0.2.3 新增:角色可继承父角色的权限。例如 `editor` 继承 `viewer` 的权限,
10//! `admin` 继承 `editor` 的权限。继承通过 [`RbacAuthorizer::with_role_parent`]
11//! 配置,授权时会沿继承链向上查找。
12
13use std::collections::{HashMap, HashSet};
14
15use crate::auth::User;
16use crate::error::AuthError;
17
18pub trait Authorizer: Send + Sync {
19    /// Returns Ok(true) if `user` is allowed to perform `action` on `resource`.
20    fn can(&self, user: &User, action: &str, resource: &str) -> Result<bool, AuthError>;
21}
22
23/// Role-based authorizer backed by a `role -> permissions` map.
24///
25/// 支持角色层级继承(v0.2.3 新增):通过 `with_role_parent` 配置父角色后,
26/// 子角色自动继承父角色的所有权限。继承链支持多级(如 admin -> editor -> viewer),
27/// 并自动检测循环引用。
28pub struct RbacAuthorizer {
29    role_permissions: HashMap<String, HashSet<String>>,
30    /// 角色继承关系:role -> parent_role
31    ///
32    /// 子角色继承父角色的全部权限。查询时会沿继承链递归向上查找。
33    role_parents: HashMap<String, String>,
34}
35
36impl RbacAuthorizer {
37    /// Creates a new authorizer with the `admin` role granted the `*` wildcard.
38    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    /// Grants `permission` to `role`. Returns `self` for chaining.
48    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    /// 配置角色的父角色(继承关系),返回 `self` 用于链式调用。
57    ///
58    /// 子角色将继承父角色的全部权限。支持多级继承(如 editor -> viewer,
59    /// admin -> editor),查询时会沿继承链递归向上查找。
60    ///
61    /// # 循环检测
62    ///
63    /// 如果配置后形成循环(如 A -> B -> A),此方法会忽略该配置并发出警告,
64    /// 不会 panic。
65    pub fn with_role_parent(mut self, role: &str, parent: &str) -> Self {
66        self.set_role_parent(role, parent);
67        self
68    }
69
70    /// 设置角色的父角色(继承关系),原地修改。
71    ///
72    /// 内部使用,`with_role_parent` 的非链式版本。
73    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        // 检测循环引用:如果从 parent 出发能回到 role,则撤销此配置
80        if self.has_cycle(role) {
81            self.role_parents.remove(role);
82        }
83    }
84
85    /// 检测从 `start` 角色出发是否形成循环继承。
86    ///
87    /// 沿继承链最多遍历 64 层(超过则视为循环),避免无限递归。
88    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(&current) {
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    /// Grants `permission` to `role` in place.
105    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    /// Revokes `permission` from `role` if present.
113    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    /// 返回角色的父角色(如果有)。
120    ///
121    /// 用于查询角色继承关系。
122    pub fn role_parent(&self, role: &str) -> Option<&str> {
123        self.role_parents.get(role).map(|s| s.as_str())
124    }
125
126    /// 返回角色的所有祖先角色(沿继承链向上,包括自身)。
127    ///
128    /// 例如继承链 admin -> editor -> viewer,则 `ancestors("admin")`
129    /// 返回 `["admin", "editor", "viewer"]`。
130    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(&current) {
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    /// Returns true if `role` has been granted `permission` (or the wildcard),
149    /// including permissions inherited from parent roles.
150    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    /// Returns all permissions currently attached to `role` (excluding inherited).
165    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    /// Returns all effective permissions for `role`, including inherited from ancestors.
177    ///
178    /// v0.2.3 新增:沿继承链收集所有祖先角色的权限并合并去重。
179    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        // 1. Direct user-level permission (or wildcard).
193        if user.permissions.iter().any(|p| p == permission || p == "*") {
194            return true;
195        }
196        // 2. Role-based permission (or wildcard), including inherited.
197        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        // Fall back to action-level permission (e.g. "read" grants "read:foo").
219        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        // Different resource is not allowed
251        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        // Even if user has no roles, direct permission should grant access.
316        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    // ===== 角色层级(Role Hierarchy)测试 =====
344
345    #[test]
346    fn test_role_inherits_parent_permission() {
347        // editor 继承 viewer 的 read:posts 权限,自身有 write:posts
348        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        // editor 继承 viewer 的 read:posts
355        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        // super_admin 继承 admin 的 * 通配符
362        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        // admin -> editor -> viewer
372        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        // admin_custom 继承 editor 的 write:posts 和 viewer 的 read:posts
380        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        // 自引用应被忽略
415        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        // A -> B -> A 循环应被检测
422        let authz = RbacAuthorizer::new()
423            .with_role_parent("a", "b")
424            .with_role_parent("b", "a");
425        // b -> a 形成循环,应被忽略
426        assert_eq!(authz.role_parent("b"), None);
427        // a -> b 仍然存在
428        assert_eq!(authz.role_parent("a"), Some("b"));
429    }
430
431    #[test]
432    fn test_cycle_detection_three_node() {
433        // A -> B -> C -> A 循环
434        let authz = RbacAuthorizer::new()
435            .with_role_parent("a", "b")
436            .with_role_parent("b", "c")
437            .with_role_parent("c", "a");
438        // c -> a 形成循环,应被忽略
439        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        // editor 继承 viewer 的 read:posts
477        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        // super 继承 admin 的 * 通配符
485        assert!(authz.role_has_permission("super", "anything"));
486    }
487
488    #[test]
489    fn test_no_inheritance_isolated_roles() {
490        // 没有继承关系的角色不应共享权限
491        let authz = RbacAuthorizer::new()
492            .with_role_permission("viewer", "read:posts")
493            .with_role_permission("editor", "write:posts");
494        // editor 没有继承 viewer
495        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        // 菱形继承:D -> B -> A, D -> C -> A
513        // 祖先列表不应有重复
514        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        // 注意:role_parents 是 HashMap,同一 key 只能存一个 parent
521        // 所以 d -> c 会覆盖 d -> b
522        let ancestors = authz.role_ancestors("d");
523        // d 的 parent 只有一个(HashMap 覆盖)
524        assert!(ancestors.contains(&"d".to_string()));
525        assert!(ancestors.contains(&"c".to_string()) || ancestors.contains(&"b".to_string()));
526    }
527}