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 b139a8a fix: speed up bulk policy loading, quiet rule logs and cap
domain cache
b139a8a is described below
commit b139a8ab60aab3dbbfca491b98822ad594387fb4
Author: Yang Luo <[email protected]>
AuthorDate: Sun Nov 1 01:54:07 2026 +0800
fix: speed up bulk policy loading, quiet rule logs and cap domain cache
---
casbin/model/assertion.py | 2 +-
casbin/model/policy.py | 49 +++++++--
casbin/rbac/default_role_manager/role_manager.py | 35 +++++--
tests/model/test_policy.py | 120 +++++++++++++++++++++++
tests/rbac/test_role_manager.py | 29 ++++++
tests/test_enforcer.py | 29 ++++++
6 files changed, 244 insertions(+), 20 deletions(-)
diff --git a/casbin/model/assertion.py b/casbin/model/assertion.py
index f92da8d..408e369 100644
--- a/casbin/model/assertion.py
+++ b/casbin/model/assertion.py
@@ -46,7 +46,7 @@ class Assertion:
self.rm.add_link(*rule[:count])
- self.logger.info("Role links for: {}".format(self.key))
+ self.logger.debug(f"Role links for: {self.key}")
self.rm.print_roles()
def build_incremental_role_links(self, rm, op, rules):
diff --git a/casbin/model/policy.py b/casbin/model/policy.py
index 9c57cb3..65882b8 100644
--- a/casbin/model/policy.py
+++ b/casbin/model/policy.py
@@ -68,15 +68,15 @@ class Policy:
ast.build_conditional_role_links(cond_rm)
def print_policy(self):
- """Log using info"""
+ """Logs the policy at debug level."""
- self.logger.info("Policy:")
+ self.logger.debug("Policy:")
for sec in ["p", "g"]:
if sec not in self.keys():
continue
for key, ast in self[sec].items():
- self.logger.info("{} : {} : {}".format(key, ast.value,
ast.policy))
+ self.logger.debug(f"{key} : {ast.value} : {ast.policy}")
def clear_policy(self):
"""clears all current policy."""
@@ -156,15 +156,24 @@ class Policy:
return True
def add_policies(self, sec, ptype, rules):
- """adds policy rules to the model."""
+ """adds policy rules to the model in one pass.
+
+ Adapters loading many rules should use this instead of calling
add_policy in a loop.
+ """
+ assertion = self[sec][ptype]
+ rules = list(rules)
+ policy_keys = {tuple(rule) for rule in assertion.policy}
for rule in rules:
- if self.has_policy(sec, ptype, rule):
+ if tuple(rule) in policy_keys:
return False
- for rule in rules:
- self.add_policy(sec, ptype, rule)
+ if sec == "p" and assertion.priority_index >= 0:
+ for rule in rules:
+ self.add_policy(sec, ptype, rule)
+ return True
+ self._append_policies(assertion, rules, policy_keys)
return True
def add_policies_ex(self, sec, ptype, rules):
@@ -173,11 +182,33 @@ class Policy:
If the rule already exists, the rule will not be added.
But unlike add_policies, other non-existent rules are added instead of
returning false directly.
"""
+ assertion = self[sec][ptype]
+ rules = list(rules)
+ policy_keys = {tuple(rule) for rule in assertion.policy}
+
+ if sec == "p" and assertion.priority_index >= 0:
+ rules_added = False
+ for rule in rules:
+ key = tuple(rule)
+ if key in policy_keys:
+ continue
+ self.add_policy(sec, ptype, rule)
+ policy_keys.add(key)
+ rules_added = True
+ return rules_added
+
+ return self._append_policies(assertion, rules, policy_keys)
+
+ @staticmethod
+ def _append_policies(assertion, rules, policy_keys):
rules_added = False
for rule in rules:
- if self.has_policy(sec, ptype, rule):
+ key = tuple(rule)
+ if key in policy_keys:
continue
- self.add_policy(sec, ptype, rule)
+ policy_keys.add(key)
+ assertion.policy.append(rule)
+ assertion.policy_map[DEFAULT_SEP.join(rule)] =
len(assertion.policy) - 1
rules_added = True
return rules_added
diff --git a/casbin/rbac/default_role_manager/role_manager.py
b/casbin/rbac/default_role_manager/role_manager.py
index 84a91b3..183c1b8 100644
--- a/casbin/rbac/default_role_manager/role_manager.py
+++ b/casbin/rbac/default_role_manager/role_manager.py
@@ -13,7 +13,7 @@
# limitations under the License.
import logging
-from collections import namedtuple
+from collections import OrderedDict, namedtuple
from enum import Enum
from casbin.rbac import RoleManager as RM
@@ -216,7 +216,7 @@ class RoleManager(RM):
return ", ".join(line)
def print_roles(self):
- self.logger.info(self.to_string())
+ self.logger.debug(self.to_string())
class DomainManagerBase(RM):
@@ -297,19 +297,34 @@ class DomainManagerBase(RM):
class DomainManager(DomainManagerBase):
- def __init__(self, max_hierarchy_level=10):
+ def __init__(self, max_hierarchy_level=10, max_cached_role_managers=1000):
+ """Caches one role manager per domain.
+
+ max_cached_role_managers limits the cache; the least recently used role
+ manager is evicted when the limit is exceeded. Pass 0 or None for no
limit.
+ ConditionalDomainManager does not use this limit because its link
+ conditions live in the role managers.
+ """
super().__init__(max_hierarchy_level)
- self.rm_map = dict() # type: dict[str, RoleManager]
+ self.max_cached_role_managers = max_cached_role_managers
+ self.rm_map = OrderedDict() # type: OrderedDict[str, RoleManager]
def _rebuild(self):
- self.rm_map = dict()
+ self.rm_map = OrderedDict()
def _get_role_manager(self, *domain):
domain1 = self._get_domain(*domain)
- if domain1 not in self.rm_map:
- self.rm_map[domain1] = super()._get_role_manager(*domain)
+ rm = self.rm_map.get(domain1)
+ if rm is None:
+ rm = super()._get_role_manager(*domain)
+ self.rm_map[domain1] = rm
+ if self.max_cached_role_managers and self.max_cached_role_managers
> 0:
+ while len(self.rm_map) > self.max_cached_role_managers:
+ self.rm_map.popitem(last=False)
+ else:
+ self.rm_map.move_to_end(domain1)
- return self.rm_map[domain1]
+ return rm
def _affected_role_managers(self, *domain):
domain_pattern = self._get_domain(*domain)
@@ -336,7 +351,7 @@ class DomainManager(DomainManagerBase):
def clear(self):
super().clear()
- self.rm_map = dict()
+ self.rm_map = OrderedDict()
def add_link(self, name1, name2, *domain):
super().add_link(name1, name2, *domain)
@@ -360,7 +375,7 @@ class DomainManager(DomainManagerBase):
def print_roles(self):
for domain, rm in self.rm_map.items():
line = rm.to_string()
- self.logger.info(f"{domain}: {line}")
+ self.logger.debug(f"{domain}: {line}")
def match_error_handler(fn, key1, key2):
diff --git a/tests/model/test_policy.py b/tests/model/test_policy.py
index b4d1cbf..62e1979 100644
--- a/tests/model/test_policy.py
+++ b/tests/model/test_policy.py
@@ -173,6 +173,126 @@ class TestPolicy(TestCase):
for new_rule in new_rules:
self.assertTrue(m.has_policy("p", "p", new_rule))
+ def test_add_policies_bulk(self):
+ m = Model()
+ m.load_model(get_examples("basic_model.conf"))
+
+ rules = [[f"user{i}", f"data{i}", "read"] for i in range(1000)]
+ self.assertTrue(m.add_policies("p", "p", rules))
+ self.assertEqual(m.get_policy("p", "p"), rules)
+ self.assertEqual(m["p"]["p"].policy_map["user999,data999,read"], 999)
+
+ self.assertFalse(
+ m.add_policies(
+ "p", "p", [["new", "data", "read"], ["user0", "data0", "read"]]
+ )
+ )
+ self.assertEqual(len(m.get_policy("p", "p")), 1000)
+ self.assertFalse(m.has_policy("p", "p", ["new", "data", "read"]))
+
+ self.assertTrue(
+ m.add_policies("p", "p", [["dup", "data", "read"], ["dup", "data",
"read"]])
+ )
+ self.assertEqual(len(m.get_policy("p", "p")), 1001)
+ self.assertTrue(m.has_policy("p", "p", ["dup", "data", "read"]))
+
+ def test_add_policies_accepts_iterators(self):
+ m = Model()
+ m.load_model(get_examples("basic_model.conf"))
+
+ rules = ([f"user{i}", f"data{i}", "read"] for i in range(3))
+ self.assertTrue(m.add_policies("p", "p", rules))
+ self.assertEqual(
+ m.get_policy("p", "p"),
+ [
+ ["user0", "data0", "read"],
+ ["user1", "data1", "read"],
+ ["user2", "data2", "read"],
+ ],
+ )
+
+ def test_add_policies_ex_bulk(self):
+ m = Model()
+ m.load_model(get_examples("basic_model.conf"))
+
+ m.add_policy("p", "p", ["user0", "data0", "read"])
+ rules = [
+ ["user0", "data0", "read"],
+ ["user1", "data1", "read"],
+ ["user1", "data1", "read"],
+ ["user2", "data2", "read"],
+ ]
+ self.assertTrue(m.add_policies_ex("p", "p", rules))
+ self.assertEqual(
+ m.get_policy("p", "p"),
+ [
+ ["user0", "data0", "read"],
+ ["user1", "data1", "read"],
+ ["user2", "data2", "read"],
+ ],
+ )
+
+ self.assertFalse(m.add_policies_ex("p", "p", [["user0", "data0",
"read"]]))
+
+ def test_add_policies_distinguishes_comma_in_rule(self):
+ m = Model()
+ m.load_model(get_examples("basic_model.conf"))
+
+ m.add_policy("p", "p", ["a,b", "read"])
+ self.assertTrue(m.add_policies_ex("p", "p", [["a", "b", "read"]]))
+ self.assertEqual(
+ m.get_policy("p", "p"),
+ [["a,b", "read"], ["a", "b", "read"]],
+ )
+
+ def test_add_policies_priority(self):
+ rules = [
+ ["2", "admin", "data2", "read", "allow"],
+ ["1", "admin", "data1", "read", "allow"],
+ ["3", "admin", "data3", "read", "allow"],
+ ]
+
+ sequential = Model()
+ sequential.load_model(get_examples("priority_model_explicit.conf"))
+ sequential.sort_policies_by_priority()
+ for rule in rules:
+ sequential.add_policy("p", "p", rule)
+
+ bulk = Model()
+ bulk.load_model(get_examples("priority_model_explicit.conf"))
+ bulk.sort_policies_by_priority()
+ self.assertTrue(bulk.add_policies("p", "p", rules))
+
+ self.assertEqual(bulk.get_policy("p", "p"), sequential.get_policy("p",
"p"))
+ self.assertEqual(
+ bulk.get_policy("p", "p"),
+ [
+ ["1", "admin", "data1", "read", "allow"],
+ ["2", "admin", "data2", "read", "allow"],
+ ["3", "admin", "data3", "read", "allow"],
+ ],
+ )
+
+ self.assertTrue(
+ bulk.add_policies_ex(
+ "p",
+ "p",
+ [
+ ["2", "admin", "data2", "read", "allow"],
+ ["0", "admin", "data0", "read", "allow"],
+ ],
+ )
+ )
+ self.assertEqual(
+ bulk.get_policy("p", "p"),
+ [
+ ["0", "admin", "data0", "read", "allow"],
+ ["1", "admin", "data1", "read", "allow"],
+ ["2", "admin", "data2", "read", "allow"],
+ ["3", "admin", "data3", "read", "allow"],
+ ],
+ )
+
def test_remove_policy(self):
m = Model()
m.load_model(get_examples("basic_model.conf"))
diff --git a/tests/rbac/test_role_manager.py b/tests/rbac/test_role_manager.py
index 40c2c1f..9c53f08 100644
--- a/tests/rbac/test_role_manager.py
+++ b/tests/rbac/test_role_manager.py
@@ -266,6 +266,35 @@ class TestDomainManager(TestRoleManager):
def get_role_manager(self):
return default_role_manager.DomainManager(max_hierarchy_level=10)
+ def test_domain_role_manager_cache_limit(self):
+ rm = default_role_manager.DomainManager(
+ max_hierarchy_level=10, max_cached_role_managers=2
+ )
+ rm.add_link("u1", "g1", "domain1")
+ rm.add_link("u2", "g2", "domain2")
+ rm.add_link("u3", "g3", "domain3")
+
+ self.assertTrue(rm.has_link("u1", "g1", "domain1"))
+ self.assertTrue(rm.has_link("u2", "g2", "domain2"))
+ self.assertTrue(rm.has_link("u3", "g3", "domain3"))
+ self.assertEqual(len(rm.rm_map), 2)
+ self.assertEqual(list(rm.rm_map.keys()), ["domain2", "domain3"])
+
+ self.assertTrue(rm.has_link("u1", "g1", "domain1"))
+ self.assertEqual(len(rm.rm_map), 2)
+ self.assertEqual(list(rm.rm_map.keys()), ["domain3", "domain1"])
+
+ def test_domain_role_manager_cache_unbounded(self):
+ rm = default_role_manager.DomainManager(
+ max_hierarchy_level=10, max_cached_role_managers=0
+ )
+ for i in range(5):
+ rm.add_link(f"u{i}", f"g{i}", f"domain{i}")
+
+ for i in range(5):
+ self.assertTrue(rm.has_link(f"u{i}", f"g{i}", f"domain{i}"))
+ self.assertEqual(len(rm.rm_map), 5)
+
def test_domain_role(self):
rm = self.get_role_manager()
rm.add_link("u1", "g1", "domain1")
diff --git a/tests/test_enforcer.py b/tests/test_enforcer.py
index c295283..c05059f 100644
--- a/tests/test_enforcer.py
+++ b/tests/test_enforcer.py
@@ -14,6 +14,7 @@
import atexit
import copy
+import logging
import os
import shutil
import tempfile
@@ -1139,3 +1140,31 @@ class TestModelDeepCopy(TestCase):
# 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"))
+
+
+class TestLogging(TestCaseBase):
+ def test_load_policy_logs_rules_at_debug(self):
+ e = self.get_enforcer(
+ get_examples("rbac_model.conf"),
+ get_examples("rbac_policy.csv"),
+ )
+ policy_logger = logging.getLogger("casbin.policy")
+ role_logger = logging.getLogger("casbin.role")
+ policy_disabled = policy_logger.disabled
+ role_disabled = role_logger.disabled
+ policy_logger.disabled = False
+ role_logger.disabled = False
+
+ try:
+ with self.assertLogs("casbin.policy", level="DEBUG") as captured:
+ e.load_policy()
+ self.assertFalse(any(record.levelno >= logging.INFO for record in
captured.records))
+ self.assertTrue(any("alice" in record.getMessage() for record in
captured.records))
+
+ with self.assertLogs("casbin.role", level="DEBUG") as captured:
+ e.load_policy()
+ self.assertFalse(any(record.levelno >= logging.INFO for record in
captured.records))
+ self.assertTrue(any("alice" in record.getMessage() for record in
captured.records))
+ finally:
+ policy_logger.disabled = policy_disabled
+ role_logger.disabled = role_disabled
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]