diff --git a/casbin/core_enforcer.py b/casbin/core_enforcer.py index 0800fb2..4ffcef2 100644 --- a/casbin/core_enforcer.py +++ b/casbin/core_enforcer.py @@ -481,8 +481,11 @@ def enforce_ex(self, *rvals): else: policy_effects.add(Effector.ALLOW) + # Update explain_index for any matching policy before checking early break condition + # to ensure explanations are captured for allow rules in deny models + explain_index = i + if self.eft.intermediate_effect(policy_effects) != Effector.INDETERMINATE: - explain_index = i break else: diff --git a/tests/test_enforcer.py b/tests/test_enforcer.py index 11af386..5ad0626 100644 --- a/tests/test_enforcer.py +++ b/tests/test_enforcer.py @@ -285,6 +285,15 @@ def test_enforce_rbac_with_deny(self): self.assertTrue(e.enforce("alice", "data2", "read")) self.assertFalse(e.enforce("alice", "data2", "write")) + def test_enforce_ex_rbac_with_deny(self): + e = self.get_enforcer( + get_examples("rbac_with_deny_model.conf"), + get_examples("rbac_with_deny_policy.csv"), + ) + # Test that enforce_ex returns explanations for both allow and deny cases + self.assertTupleEqual(e.enforce_ex("alice", "data2", "read"), (True, ["data2_admin", "data2", "read", "allow"])) + self.assertTupleEqual(e.enforce_ex("alice", "data2", "write"), (False, ["alice", "data2", "write", "deny"])) + def test_enforce_rbac_with_domains(self): e = self.get_enforcer( get_examples("rbac_with_domains_model.conf"),