This is an automated email from the ASF dual-hosted git repository.

hsluoyz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/casbin-rs.git


The following commit(s) were added to refs/heads/master by this push:
     new 3f3959b  fix: revoke copied links when deleting pattern domain roles
3f3959b is described below

commit 3f3959b56a900371c2d6f27dd82bfbabc4105d16
Author: Yang Luo <[email protected]>
AuthorDate: Fri Oct 9 22:20:48 2026 +0800

    fix: revoke copied links when deleting pattern domain roles
---
 src/rbac/default_role_manager.rs | 115 ++++++++++++++++++++++++++++++++++++---
 1 file changed, 107 insertions(+), 8 deletions(-)

diff --git a/src/rbac/default_role_manager.rs b/src/rbac/default_role_manager.rs
index 23f096f..c9aac3d 100644
--- a/src/rbac/default_role_manager.rs
+++ b/src/rbac/default_role_manager.rs
@@ -20,6 +20,7 @@ const DEFAULT_DOMAIN: &str = "DEFAULT";
 pub struct DefaultRoleManager {
     all_domains: HashMap<String, StableDiGraph<String, EdgeVariant>>,
     all_domains_indices: HashMap<String, HashMap<String, NodeIndex<u32>>>,
+    explicit_links: HashMap<String, HashSet<(String, String)>>,
     #[cfg(feature = "cached")]
     cache: DefaultCache<u64, bool>,
     max_hierarchy_level: usize,
@@ -38,6 +39,7 @@ impl DefaultRoleManager {
         DefaultRoleManager {
             all_domains: HashMap::new(),
             all_domains_indices: HashMap::new(),
+            explicit_links: HashMap::new(),
             max_hierarchy_level,
             #[cfg(feature = "cached")]
             cache: DefaultCache::new(50),
@@ -250,6 +252,57 @@ impl DefaultRoleManager {
         self.cache.clear();
     }
 
+    // a link in `domain` is still justified if it was added directly to
+    // `domain` or to a pattern domain that matches `domain`
+    fn link_is_justified(
+        &self,
+        name1: &str,
+        name2: &str,
+        domain: &str,
+    ) -> bool {
+        let key = (name1.to_owned(), name2.to_owned());
+        self.explicit_links.iter().any(|(d, links)| {
+            links.contains(&key)
+                && (d == domain
+                    || self.domain_matching_fn.is_some_and(|f| f(domain, d)))
+        })
+    }
+
+    fn remove_link_edge(
+        &mut self,
+        name1: &str,
+        name2: &str,
+        domain: &str,
+    ) -> bool {
+        use petgraph::visit::EdgeRef;
+
+        let (graph, indices) = match (
+            self.all_domains.get_mut(domain),
+            self.all_domains_indices.get(domain),
+        ) {
+            (Some(graph), Some(indices)) => (graph, indices),
+            _ => return false,
+        };
+
+        let (role1, role2) = match (indices.get(name1), indices.get(name2)) {
+            (Some(role1), Some(role2)) => (*role1, *role2),
+            _ => return false,
+        };
+
+        let edge = graph
+            .edges_connecting(role1, role2)
+            .find(|e| matches!(*e.weight(), EdgeVariant::Link))
+            .map(|e| e.id());
+
+        match edge {
+            Some(edge) => {
+                graph.remove_edge(edge);
+                true
+            }
+            None => false,
+        }
+    }
+
     fn matched_domains(&self, domain: Option<&str>) -> Vec<String> {
         let domain = domain.unwrap_or(DEFAULT_DOMAIN);
         if let Some(domain_matching_fn) = self.domain_matching_fn {
@@ -327,6 +380,7 @@ impl RoleManager for DefaultRoleManager {
     fn clear(&mut self) {
         self.all_domains_indices.clear();
         self.all_domains.clear();
+        self.explicit_links.clear();
         #[cfg(feature = "cached")]
         self.cache.clear();
     }
@@ -336,6 +390,11 @@ impl RoleManager for DefaultRoleManager {
             return;
         }
 
+        self.explicit_links
+            .entry(domain.unwrap_or(DEFAULT_DOMAIN).into())
+            .or_default()
+            .insert((name1.into(), name2.into()));
+
         let role1 = self.get_or_create_role(name1, domain);
         let role2 = self.get_or_create_role(name2, domain);
 
@@ -391,17 +450,27 @@ impl RoleManager for DefaultRoleManager {
             );
         }
 
-        let role1 = self.get_or_create_role(name1, domain);
-        let role2 = self.get_or_create_role(name2, domain);
+        let domain_name = domain.unwrap_or(DEFAULT_DOMAIN);
 
-        let graph = self
-            .all_domains
-            .get_mut(domain.unwrap_or(DEFAULT_DOMAIN))
-            .unwrap();
+        if let Some(links) = self.explicit_links.get_mut(domain_name) {
+            links.remove(&(name1.to_owned(), name2.to_owned()));
+        }
+
+        // links added to a pattern domain were copied into every matching
+        // domain, so remove those copies too unless something still grants 
them
+        let mut domains = vec![domain_name.to_owned()];
+        if domain.is_some() {
+            domains.extend(self.affected_domain_names(domain_name));
+        }
 
-        if let Some(edge_index) = graph.find_edge(role1, role2) {
-            graph.remove_edge(edge_index).unwrap();
+        let mut removed = false;
+        for d in domains {
+            if !self.link_is_justified(name1, name2, &d) {
+                removed |= self.remove_link_edge(name1, name2, &d);
+            }
+        }
 
+        if removed {
             #[cfg(feature = "cached")]
             self.cache.clear();
         }
@@ -971,4 +1040,34 @@ mod tests {
         rm.add_link("super_admin", "admin", Some("domain1"));
         assert!(rm.has_link("super_admin", "admin", Some("domain1")));
     }
+
+    #[test]
+    fn test_delete_pattern_domain_link() {
+        use crate::model::key_match;
+        let mut rm = DefaultRoleManager::new(10);
+        rm.matching_fn(None, Some(key_match));
+
+        rm.add_link("alice", "admin", Some("*"));
+        rm.add_link("bob", "admin", Some("domain2"));
+        rm.add_link("admin", "super_admin", Some("domain2"));
+        assert!(rm.has_link("alice", "admin", Some("domain2")));
+        assert!(rm.has_link("alice", "super_admin", Some("domain2")));
+
+        rm.delete_link("alice", "admin", Some("*")).unwrap();
+        assert!(!rm.has_link("alice", "admin", Some("*")));
+        assert!(!rm.has_link("alice", "admin", Some("domain2")));
+        assert!(!rm.has_link("alice", "super_admin", Some("domain2")));
+        assert!(rm.has_link("bob", "super_admin", Some("domain2")));
+
+        rm.add_link("carol", "admin", Some("*"));
+        rm.add_link("carol", "admin", Some("domain2"));
+        rm.delete_link("carol", "admin", Some("*")).unwrap();
+        assert!(rm.has_link("carol", "admin", Some("domain2")));
+        assert!(!rm.has_link("carol", "admin", Some("domain3")));
+
+        rm.add_link("dave", "admin", Some("*"));
+        rm.add_link("dave", "admin", Some("domain2"));
+        rm.delete_link("dave", "admin", Some("domain2")).unwrap();
+        assert!(rm.has_link("dave", "super_admin", Some("domain2")));
+    }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to