vlieven opened a new issue, #435:
URL: https://github.com/apache/casbin-pycasbin/issues/435

   ## Description
   
   `RoleManager.add_link` only loops over `all_roles `when a matching function 
is configured:
   
   ```python
   if self.matching_func != None:
       for r in self.all_roles.values():
           ...
   ```
   
   `RoleManager.delete_link` has the same loop, but without that check:
   
   ```python
   for r in self.all_roles.values():
       if r.name != user.name and self._matching_fn(user.name, r.name, 
MatchOrder.PATTERN_STR):
           r.remove_role(role)
       if r.name != role.name and self._matching_fn(role.name, r.name, 
MatchOrder.PATTERN_STR):
           role.remove_role(r)
   ```
   
   When `matching_func` is `None`, every `_matching_fn` call ends up in 
`match_error_handler(None, ...)`. That raises a TypeError, which the bare 
except swallows and turns into False. The result is correct, but removing a 
single link costs two raised and caught exceptions per role.
   
   This shows up when you combine `add_named_matching_func("g", None)` with a 
domain matching function, for example to use `*` as a domain wildcard (`g, 
alice, admin, *`, as described in the RBAC with pattern docs). Disabling the 
role matcher is needed there, because `DomainManagerBase` installs an equality 
lambda by default. That lambda makes `add_link` scan all roles on every insert, 
which makes rebuilding the per-domain role managers quadratic. But a wildcard 
link is present in every cached domain's role manager, so removing it runs the 
unguarded loop once per domain.
   
   ## Reproduction
   
   ```python
   import time
   
   import casbin
   from casbin.model import Model
   from casbin.util import key_match
   
   MODEL = """
   [request_definition]
   r = sub, dom, obj, act
   
   [policy_definition]
   p = sub, act
   
   [role_definition]
   g = _, _, _
   
   [policy_effect]
   e = some(where (p.eft == allow))
   
   [matchers]
   m = g(r.sub, p.sub, r.obj) && r.act == p.act
   """
   
   model = Model()
   model.load_model_from_text(MODEL)
   e = casbin.Enforcer(model)
   e.add_named_matching_func("g", None)
   e.add_named_domain_matching_func("g", key_match)
   
   e.add_policy("reader", "read")
   e.add_named_grouping_policies(
       "g",
       [[f"group{i % 30}", "reader", f"resource{i}"] for i in range(1000)]
       + [[f"user{i}", f"group{i % 30}", "*"] for i in range(450)],
   )
   
   # Warm up the per-domain role managers
   for i in range(1000):
       e.enforce("user0", "", f"resource{i}", "read")
   
   start = time.perf_counter()
   e.remove_named_grouping_policy("g", "user1", "group1", "*")
   print(f"remove_named_grouping_policy took {(time.perf_counter() - start) * 
1000:.0f} ms")
   ```
   
   On pycasbin 2.8.0 with Python 3.13, removing one link takes about 356 ms. 
With the guard described below, it takes about 3 ms. With a SyncedEnforcer, 
this runs while holding the write lock, so every enforce call waits for it.
   
   ## Expected behavior
   
   `delete_link` skips the pattern loop when no matching function is set, the 
same way `add_link` does.
   
   ## Proposed fix
   
   Wrap the loop in `delete_link` in the same check `add_link` uses:
   
   ```python
   if self.matching_func is not None:
       for r in self.all_roles.values():
           ...
   ```
   
   ## Environment
   
   - pycasbin 2.8.0 (the code is the same on master)
   - Python 3.13


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to