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-pycasbin.git


The following commit(s) were added to refs/heads/master by this push:
     new 12edbf9  fix: keep rm and cond_rm usable after policy reload
12edbf9 is described below

commit 12edbf95f7a7b24215be1eb9a7898854262e0cd0
Author: Yang Luo <[email protected]>
AuthorDate: Tue Aug 4 14:28:44 2026 +0800

    fix: keep rm and cond_rm usable after policy reload
---
 casbin/async_internal_enforcer.py |  7 ++++-
 casbin/model/assertion.py         | 15 +++++++---
 tests/test_enforcer.py            | 62 +++++++++++++++++++++++++++++++++++++++
 3 files changed, 79 insertions(+), 5 deletions(-)

diff --git a/casbin/async_internal_enforcer.py 
b/casbin/async_internal_enforcer.py
index 0c65728..275a0ae 100644
--- a/casbin/async_internal_enforcer.py
+++ b/casbin/async_internal_enforcer.py
@@ -62,8 +62,13 @@ class AsyncInternalEnforcer(CoreEnforcer):
                 need_to_rebuild = True
                 for rm in self.rm_map.values():
                     rm.clear()
+                if len(self.rm_map) != 0:
+                    new_model.build_role_links(self.rm_map)
 
-                new_model.build_role_links(self.rm_map)
+                for crm in self.cond_rm_map.values():
+                    crm.clear()
+                if len(self.cond_rm_map) != 0:
+                    new_model.build_conditional_role_links(self.cond_rm_map)
 
             self.model = new_model
 
diff --git a/casbin/model/assertion.py b/casbin/model/assertion.py
index 88096a0..f92da8d 100644
--- a/casbin/model/assertion.py
+++ b/casbin/model/assertion.py
@@ -12,8 +12,9 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-import logging
 import copy
+import logging
+
 from casbin.model.policy_op import PolicyOp
 
 
@@ -111,15 +112,21 @@ class Assertion:
             self.cond_rm.set_domain_link_condition_func_params(rule[0], 
rule[1], domain, *rule[len(self.tokens) :])
 
     def __deepcopy__(self, memo):
-        """Custom deepcopy implementation that excludes rm and cond_rm 
attributes.
-        This stems from an issue with the watcher concurrent reloading causing 
an edge case of deepcopy error.
+        """Custom deepcopy implementation that copies policy data only,
+        sharing rm and cond_rm by reference instead of cloning them.
+
+        The role managers are live, shared, cyclic object graphs: deep-copying 
them
+        races with concurrent add_link() calls from another thread (as happens 
when a
+        watcher reloads the policy), and the copy is discarded by 
build_role_links()
+        anyway. This matches casbin-go, whose Assertion.copy() also carries RM 
and
+        CondRM over by reference.
         """
         cls = self.__class__
         result = cls.__new__(cls)
         memo[id(self)] = result
         for k, v in self.__dict__.items():
             if k in ("rm", "cond_rm"):
-                setattr(result, k, None)
+                setattr(result, k, v)
             else:
                 setattr(result, k, copy.deepcopy(v, memo))
         return result
diff --git a/tests/test_enforcer.py b/tests/test_enforcer.py
index 5ad0626..d1b5f32 100644
--- a/tests/test_enforcer.py
+++ b/tests/test_enforcer.py
@@ -12,6 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import copy
 import os
 import time
 from unittest import TestCase, IsolatedAsyncioTestCase
@@ -1041,3 +1042,64 @@ class TestConfigAsync(IsolatedAsyncioTestCase):
         self.assertTrue(e.enforce("alice", "data2", "read"))
         self.assertTrue(e.enforce("alice", "data3", "scribble"))
         self.assertFalse(e.enforce("alice", "data4", "scribble"))
+
+    async def test_load_policy_rebuilds_conditional_role_links(self):
+        e = self.get_enforcer(
+            get_examples("rbac_with_temporal_roles_model.conf"),
+            get_examples("rbac_with_temporal_roles_policy.csv"),
+        )
+        await e.load_policy()
+
+        ast = e.model.model["g"]["g"]
+        self.assertIsNotNone(ast.cond_rm)
+        self.assertIs(ast.cond_rm, e.cond_rm_map["g"])
+
+        e.add_named_link_condition_func("g", "alice", "data2_admin", 
util.time_match_func)
+        self.assertTrue(e.enforce("alice", "data1", "read"))
+        self.assertFalse(e.enforce("alice", "data2", "read"))
+
+
+class TestModelDeepCopy(TestCase):
+    """Assertion.__deepcopy__ must not clone the live role managers.
+
+    They are shared, cyclic object graphs; deep-copying them races with 
concurrent
+    add_link() calls and the copy would be thrown away by build_role_links() 
anyway.
+    """
+
+    def test_deepcopy_shares_role_managers(self):
+        e = casbin.Enforcer(
+            get_examples("rbac_model.conf"),
+            get_examples("rbac_policy.csv"),
+        )
+        ast = e.model.model["g"]["g"]
+
+        new_model = copy.deepcopy(e.model)
+        new_ast = new_model.model["g"]["g"]
+
+        self.assertIs(new_ast.rm, ast.rm)
+        self.assertIs(new_ast.cond_rm, ast.cond_rm)
+        # policy data is still copied, not shared
+        self.assertIsNot(new_ast.policy, ast.policy)
+        self.assertEqual(new_ast.policy, ast.policy)
+
+    def test_load_policy_keeps_role_manager(self):
+        e = casbin.Enforcer(
+            get_examples("rbac_model.conf"),
+            get_examples("rbac_policy.csv"),
+        )
+        e.load_policy()
+
+        self.assertIs(e.model.model["g"]["g"].rm, e.rm_map["g"])
+        self.assertTrue(e.enforce("alice", "data2", "read"))
+
+    def test_load_policy_without_auto_build_role_links(self):
+        e = casbin.Enforcer(
+            get_examples("rbac_model.conf"),
+            get_examples("rbac_policy.csv"),
+        )
+        e.enable_auto_build_role_links(False)
+        e.load_policy()
+
+        # the role manager must still be usable even though nothing rebuilt 
the links
+        self.assertIsNotNone(e.model.model["g"]["g"].rm)
+        self.assertTrue(e.enforce("alice", "data2", "read"))


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

Reply via email to