Skip to main content

karbon_framework/security/
role_hierarchy.rs

1use std::collections::{HashMap, HashSet};
2
3/// Symfony-style role hierarchy.
4///
5/// Define which roles inherit from which others:
6/// ```
7/// use karbon_framework::security::RoleHierarchy;
8///
9/// let hierarchy = RoleHierarchy::new(&[
10///     ("ROLE_SUPER_ADMIN", &["ROLE_ADMIN"]),
11///     ("ROLE_ADMIN", &["ROLE_REDACTEUR"]),
12///     ("ROLE_REDACTEUR", &["ROLE_USER"]),
13/// ]);
14///
15/// // A user with ROLE_SUPER_ADMIN effectively has all roles
16/// let resolved = hierarchy.resolve(&["ROLE_SUPER_ADMIN".to_string()]);
17/// assert!(resolved.contains("ROLE_ADMIN"));
18/// assert!(resolved.contains("ROLE_REDACTEUR"));
19/// assert!(resolved.contains("ROLE_USER"));
20/// ```
21#[derive(Debug, Clone)]
22pub struct RoleHierarchy {
23    map: HashMap<String, Vec<String>>,
24}
25
26impl RoleHierarchy {
27    /// Build from a list of (role, children) pairs.
28    /// Each entry means: `role` inherits all `children` roles (and their children, transitively).
29    pub fn new(entries: &[(&str, &[&str])]) -> Self {
30        let map: HashMap<String, Vec<String>> = entries
31            .iter()
32            .map(|(role, children)| {
33                (
34                    role.to_string(),
35                    children.iter().map(|c| c.to_string()).collect(),
36                )
37            })
38            .collect();
39
40        Self { map }
41    }
42
43    /// Resolve a set of user roles into the full set of effective roles
44    /// (including all inherited roles, transitively).
45    pub fn resolve(&self, user_roles: &[String]) -> HashSet<String> {
46        let mut resolved = HashSet::new();
47        for role in user_roles {
48            self.resolve_recursive(role, &mut resolved);
49        }
50        resolved
51    }
52
53    /// Check if a user with the given roles effectively has `required_role`
54    /// (directly or via hierarchy inheritance).
55    pub fn has_role(&self, user_roles: &[String], required_role: &str) -> bool {
56        // Fast path: direct match
57        if user_roles.iter().any(|r| r == required_role) {
58            return true;
59        }
60        // Slow path: resolve hierarchy
61        self.resolve(user_roles).contains(required_role)
62    }
63
64    fn resolve_recursive(&self, role: &str, resolved: &mut HashSet<String>) {
65        if !resolved.insert(role.to_string()) {
66            return; // Already visited — avoid infinite loops
67        }
68        if let Some(children) = self.map.get(role) {
69            for child in children {
70                self.resolve_recursive(child, resolved);
71            }
72        }
73    }
74}
75
76/// Default role hierarchy for LaRevueGeek
77pub fn default_hierarchy() -> RoleHierarchy {
78    RoleHierarchy::new(&[
79        ("ROLE_SUPER_ADMIN", &["ROLE_ADMIN"]),
80        ("ROLE_ADMIN", &["ROLE_REDACTEUR", "ROLE_MODERATEUR"]),
81        ("ROLE_REDACTEUR", &["ROLE_USER"]),
82        ("ROLE_MODERATEUR", &["ROLE_USER"]),
83    ])
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    #[test]
91    fn super_admin_has_all_roles() {
92        let h = default_hierarchy();
93        let roles = vec!["ROLE_SUPER_ADMIN".to_string()];
94        assert!(h.has_role(&roles, "ROLE_SUPER_ADMIN"));
95        assert!(h.has_role(&roles, "ROLE_ADMIN"));
96        assert!(h.has_role(&roles, "ROLE_REDACTEUR"));
97        assert!(h.has_role(&roles, "ROLE_MODERATEUR"));
98        assert!(h.has_role(&roles, "ROLE_USER"));
99    }
100
101    #[test]
102    fn admin_does_not_have_super_admin() {
103        let h = default_hierarchy();
104        let roles = vec!["ROLE_ADMIN".to_string()];
105        assert!(h.has_role(&roles, "ROLE_ADMIN"));
106        assert!(h.has_role(&roles, "ROLE_REDACTEUR"));
107        assert!(h.has_role(&roles, "ROLE_USER"));
108        assert!(!h.has_role(&roles, "ROLE_SUPER_ADMIN"));
109    }
110
111    #[test]
112    fn basic_user_only_has_user() {
113        let h = default_hierarchy();
114        let roles = vec!["ROLE_USER".to_string()];
115        assert!(h.has_role(&roles, "ROLE_USER"));
116        assert!(!h.has_role(&roles, "ROLE_ADMIN"));
117        assert!(!h.has_role(&roles, "ROLE_SUPER_ADMIN"));
118    }
119}