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 b2a26f6  feat: add policy index for partitioned enforcement
b2a26f6 is described below

commit b2a26f6f66ce91eb9bd51ee7671a69222d8b3926
Author: Yang Luo <[email protected]>
AuthorDate: Sun Nov 1 02:40:33 2026 +0800

    feat: add policy index for partitioned enforcement
---
 casbin/__init__.py         |   1 +
 casbin/core_enforcer.py    | 110 ++++++++++++++++++++-
 casbin/model/assertion.py  |   1 +
 casbin/model/model.py      |   2 +
 casbin/model/policy.py     |  12 +++
 casbin/policy_index.py     | 105 ++++++++++++++++++++
 casbin/synced_enforcer.py  |  15 +++
 tests/test_policy_index.py | 239 +++++++++++++++++++++++++++++++++++++++++++++
 8 files changed, 484 insertions(+), 1 deletion(-)

diff --git a/casbin/__init__.py b/casbin/__init__.py
index 9c2719e..ff22611 100644
--- a/casbin/__init__.py
+++ b/casbin/__init__.py
@@ -13,6 +13,7 @@
 # limitations under the License.
 
 from .enforcer import *
+from .policy_index import PolicyIndex
 from .synced_enforcer import SyncedEnforcer
 from .distributed_enforcer import DistributedEnforcer
 from .fast_enforcer import FastEnforcer
diff --git a/casbin/core_enforcer.py b/casbin/core_enforcer.py
index 0d4ddc8..e904483 100644
--- a/casbin/core_enforcer.py
+++ b/casbin/core_enforcer.py
@@ -17,10 +17,16 @@ import logging
 import re
 from functools import lru_cache
 
-from casbin.effect import Effector, AllowAndDenyEffector, get_effector, 
effect_to_bool
+from casbin.effect import (
+    AllowAndDenyEffector,
+    Effector,
+    effect_to_bool,
+    get_effector,
+)
 from casbin.model import Model, FunctionMap
 from casbin.persist import Adapter
 from casbin.persist.adapters import FileAdapter
+from casbin.policy_index import PolicyIndex, policy_matches_bucket
 from casbin.rbac import default_role_manager
 from casbin.util import generate_g_function, SimpleEval, util, 
generate_conditional_g_function, key_match
 from casbin.util.log import configure_logging, disabled_logging
@@ -62,6 +68,8 @@ class CoreEnforcer:
 
     def __init__(self, model=None, adapter=None, enable_log=False, 
logging_config: dict = None):
         self.logger = logging.getLogger("casbin.enforcer")
+        self._policy_indexes = {}
+        self._policy_deny_cache = {}
         if isinstance(model, str):
             if isinstance(adapter, str):
                 self.init_with_file(model, adapter)
@@ -120,6 +128,7 @@ class CoreEnforcer:
         self.auto_build_role_links = True
         self.auto_notify_watcher = True
 
+        self._reset_policy_caches()
         self.init_rm_map()
 
     @staticmethod
@@ -143,6 +152,7 @@ class CoreEnforcer:
         self.model.load_model(self.model_path)
         self.model.print_model()
         self.fm = FunctionMap.load_function_map()
+        self._reset_policy_caches()
 
     def get_model(self):
         """gets the current model."""
@@ -154,6 +164,7 @@ class CoreEnforcer:
 
         self.model = m
         self.fm = FunctionMap.load_function_map()
+        self._reset_policy_caches()
 
     def get_adapter(self):
         """gets the current adapter."""
@@ -191,6 +202,82 @@ class CoreEnforcer:
         """sets the current effector."""
         self.eft = eft
 
+    def set_policy_index(self, ptype, index):
+        """partitions the policy rules of ptype with a PolicyIndex."""
+        if not isinstance(index, PolicyIndex):
+            raise TypeError("index must be a PolicyIndex")
+        if "p" not in self.model.keys() or ptype not in self.model["p"]:
+            raise ValueError(f"policy type {ptype!r} does not exist")
+        self._policy_indexes[ptype] = index
+
+    def set_policy_index_by_token(self, ptype, request_token, policy_token):
+        """partitions ptype's rules by the string value of two tokens.
+
+        It uses the default request type "r"; use set_policy_index with a
+        custom function when enforcing through an EnforceContext.
+        """
+        if "r" not in self.model.keys() or "r" not in self.model["r"]:
+            raise ValueError('request type "r" does not exist')
+        request_tokens = self.model["r"]["r"].tokens
+        if request_token not in request_tokens:
+            raise ValueError(f"request token {request_token!r} does not exist")
+        request_index = request_tokens.index(request_token)
+
+        if "p" not in self.model.keys() or ptype not in self.model["p"]:
+            raise ValueError(f"policy type {ptype!r} does not exist")
+        policy_tokens = self.model["p"][ptype].tokens
+        if policy_token not in policy_tokens:
+            raise ValueError(f"policy token {policy_token!r} does not exist")
+        policy_index = policy_tokens.index(policy_token)
+
+        def request_key(rvals, index=request_index):
+            if index >= len(rvals):
+                return "", False
+            value = rvals[index]
+            if isinstance(value, str):
+                return value, True
+            return "", False
+
+        def policy_key(rule, index=policy_index):
+            if index >= len(rule):
+                return ""
+            return rule[index]
+
+        self.set_policy_index(ptype, PolicyIndex(request_key, policy_key))
+
+    def remove_policy_index(self, ptype):
+        """removes the policy partition for ptype."""
+        self._policy_indexes.pop(ptype, None)
+
+    def _reset_policy_caches(self):
+        for index in self._policy_indexes.values():
+            index.reset()
+        self._policy_deny_cache.clear()
+
+    def _policy_has_deny(self, ptype, p_tokens):
+        assertion = self.model["p"][ptype]
+        cached = self._policy_deny_cache.get(ptype)
+        if cached is not None and cached[0] == assertion.policy_version and 
cached[1] == len(assertion.policy):
+            return cached[2], cached[3]
+
+        eft_token = ptype + "_eft"
+        eft_index = p_tokens.index(eft_token) if eft_token in p_tokens else -1
+        has_deny = False
+        all_valid = True
+        for rule in assertion.policy:
+            if len(rule) != len(p_tokens):
+                all_valid = False
+            if eft_index != -1 and eft_index < len(rule) and rule[eft_index] 
== "deny":
+                has_deny = True
+
+        self._policy_deny_cache[ptype] = (
+            assertion.policy_version,
+            len(assertion.policy),
+            has_deny,
+            all_valid,
+        )
+        return has_deny, all_valid
+
     def clear_policy(self):
         """clears all policy."""
 
@@ -271,6 +358,7 @@ class CoreEnforcer:
                     new_model.build_conditional_role_links(self.cond_rm_map)
 
             self.model = new_model
+            self._reset_policy_caches()
 
         except Exception as e:
             if self.auto_build_role_links and need_to_rebuild:
@@ -472,6 +560,19 @@ class CoreEnforcer:
 
         policy_len = len(self.model["p"][ptype].policy)
 
+        policy_index = self._policy_indexes.get(ptype)
+        bucket_ids = None
+        request_bucket = 0
+        use_policy_index = False
+        if policy_index is not None:
+            bucket_ids = policy_index.ensure_built(self.model["p"][ptype])
+            request_bucket, use_policy_index = 
policy_index.request_bucket(rvals)
+
+        shortcut_allow_and_deny = False
+        if policy_len != 0 and type(self.eft) is AllowAndDenyEffector:
+            has_deny, all_valid = self._policy_has_deny(ptype, p_tokens)
+            shortcut_allow_and_deny = all_valid and not has_deny
+
         explain_index = -1
         allow_index = -1
         if not 0 == policy_len:
@@ -479,6 +580,9 @@ class CoreEnforcer:
                 if len(p_tokens) != len(pvals):
                     raise RuntimeError("invalid policy size")
 
+                if use_policy_index and not policy_matches_bucket(bucket_ids, 
i, request_bucket):
+                    continue
+
                 p_parameters = dict(zip(p_tokens, pvals))
                 parameters = dict(r_parameters, **p_parameters)
 
@@ -514,6 +618,10 @@ class CoreEnforcer:
                 if allow_index == -1 and Effector.ALLOW in policy_effects:
                     allow_index = i
 
+                if shortcut_allow_and_deny and Effector.ALLOW in 
policy_effects:
+                    explain_index = i
+                    break
+
                 if self.eft.intermediate_effect(policy_effects) != 
Effector.INDETERMINATE:
                     explain_index = i
                     break
diff --git a/casbin/model/assertion.py b/casbin/model/assertion.py
index 408e369..3835333 100644
--- a/casbin/model/assertion.py
+++ b/casbin/model/assertion.py
@@ -26,6 +26,7 @@ class Assertion:
         self.tokens = []
         self.params_tokens = []
         self.policy = []
+        self.policy_version = 0
         self.rm = None
         self.cond_rm = None
         self.priority_index: int = -1
diff --git a/casbin/model/model.py b/casbin/model/model.py
index e75572c..681af38 100644
--- a/casbin/model/model.py
+++ b/casbin/model/model.py
@@ -135,6 +135,7 @@ class Model(Policy):
 
             for i, policy in enumerate(assertion.policy):
                 assertion.policy_map[",".join(policy)] = i
+            assertion.policy_version += 1
 
         return None
 
@@ -162,6 +163,7 @@ class Model(Policy):
             assertion.policy = sorted(assertion.policy, key=compare_policy)
             for i, policy in enumerate(assertion.policy):
                 assertion.policy_map[",".join(policy)] = i
+            assertion.policy_version += 1
 
     def get_subject_hierarchy_map(self, policies):
         """
diff --git a/casbin/model/policy.py b/casbin/model/policy.py
index 65882b8..4437344 100644
--- a/casbin/model/policy.py
+++ b/casbin/model/policy.py
@@ -87,6 +87,7 @@ class Policy:
 
             for key in self[sec].keys():
                 self[sec][key].policy = []
+                self[sec][key].policy_version += 1
 
     def get_policy(self, sec, ptype):
         """gets all rules in a policy.
@@ -153,6 +154,7 @@ class Policy:
                 print(e)
 
         assertion.policy_map[DEFAULT_SEP.join(rule)] = len(assertion.policy) - 
1
+        assertion.policy_version += 1
         return True
 
     def add_policies(self, sec, ptype, rules):
@@ -210,6 +212,8 @@ class Policy:
             assertion.policy.append(rule)
             assertion.policy_map[DEFAULT_SEP.join(rule)] = 
len(assertion.policy) - 1
             rules_added = True
+        if rules_added:
+            assertion.policy_version += 1
         return rules_added
 
     def update_policy(self, sec, ptype, old_rule, new_rule):
@@ -236,6 +240,7 @@ class Policy:
         else:
             ast.policy[rule_index] = new_rule
 
+        ast.policy_version += 1
         return True
 
     def update_policies(self, sec, ptype, old_rules, new_rules):
@@ -268,6 +273,7 @@ class Policy:
             for idx, old_rule, new_rule in zip(old_rules_index, old_rules, 
new_rules):
                 ast.policy[idx] = new_rule
 
+        ast.policy_version += 1
         return True
 
     def remove_policy(self, sec, ptype, rule):
@@ -276,6 +282,7 @@ class Policy:
             return False
 
         self[sec][ptype].policy.remove(rule)
+        self[sec][ptype].policy_version += 1
 
         return rule not in self[sec][ptype].policy
 
@@ -289,6 +296,7 @@ class Policy:
             if rule in self[sec][ptype].policy:
                 return False
 
+        self[sec][ptype].policy_version += 1
         return True
 
     def remove_policies_with_effected(self, sec, ptype, rules):
@@ -321,6 +329,8 @@ class Policy:
                 tmp.append(rule)
 
         self[sec][ptype].policy = tmp
+        if effects:
+            self[sec][ptype].policy_version += 1
 
         return effects
 
@@ -341,6 +351,8 @@ class Policy:
                 tmp.append(rule)
 
         self[sec][ptype].policy = tmp
+        if res:
+            self[sec][ptype].policy_version += 1
 
         return res
 
diff --git a/casbin/policy_index.py b/casbin/policy_index.py
new file mode 100644
index 0000000..e77197d
--- /dev/null
+++ b/casbin/policy_index.py
@@ -0,0 +1,105 @@
+# Copyright 2026 The casbin Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import threading
+
+_WILDCARD_POLICY_BUCKET = 2**32 - 1
+_MISSING_POLICY_BUCKET = 2**32 - 2
+
+
+class PolicyIndex:
+    """PolicyIndex partitions the policy rules of one policy type so that
+    enforce() only evaluates the rules in the request's partition.
+
+    request_key receives the request values, with an optional EnforceContext
+    removed, and returns ``(key, True)``. Returning ``(key, False)`` disables
+    the index for that request, so every rule is evaluated.
+
+    policy_key returns the partition key for one policy rule. The key ``"*"``
+    puts the rule in every partition.
+
+    enforce() skips the matcher of a rule whose key differs from the request
+    key. The caller must guarantee that such a rule can never match the
+    request; otherwise the decision changes. This includes matchers that
+    evaluate policy fields with eval(). Rules are still merged by the
+    configured effector, so deny rules must be reachable through the request
+    key or ``"*"``.
+    """
+
+    def __init__(self, request_key, policy_key):
+        if not callable(request_key) or not callable(policy_key):
+            raise TypeError("request_key and policy_key must be callable")
+
+        self.request_key = request_key
+        self.policy_key = policy_key
+        self._lock = threading.Lock()
+        self._built = False
+        self._version = 0
+        self._bucket_ids = []
+        self._names = {}
+
+    def ensure_built(self, assertion):
+        with self._lock:
+            if (
+                self._built
+                and self._version == assertion.policy_version
+                and len(self._bucket_ids) == len(assertion.policy)
+            ):
+                return self._bucket_ids
+
+            names = {}
+            bucket_ids = []
+            next_id = 0
+            for rule in assertion.policy:
+                key = self.policy_key(rule)
+                if key == "*":
+                    bucket_ids.append(_WILDCARD_POLICY_BUCKET)
+                    continue
+
+                bucket_id = names.get(key)
+                if bucket_id is None:
+                    bucket_id = next_id
+                    names[key] = bucket_id
+                    next_id += 1
+                bucket_ids.append(bucket_id)
+
+            self._bucket_ids = bucket_ids
+            self._names = names
+            self._version = assertion.policy_version
+            self._built = True
+            return bucket_ids
+
+    def request_bucket(self, rvals):
+        key, ok = self.request_key(rvals)
+        if not ok:
+            return 0, False
+
+        with self._lock:
+            bucket_id = self._names.get(key)
+        if bucket_id is None:
+            return _MISSING_POLICY_BUCKET, True
+        return bucket_id, True
+
+    def reset(self):
+        with self._lock:
+            self._built = False
+            self._bucket_ids = []
+            self._names = {}
+
+
+def policy_matches_bucket(bucket_ids, policy_index, bucket):
+    if policy_index < 0 or policy_index >= len(bucket_ids):
+        return True
+    bucket_id = bucket_ids[policy_index]
+    return bucket_id == _WILDCARD_POLICY_BUCKET or bucket_id == bucket
diff --git a/casbin/synced_enforcer.py b/casbin/synced_enforcer.py
index bfd714a..c51f7b7 100644
--- a/casbin/synced_enforcer.py
+++ b/casbin/synced_enforcer.py
@@ -819,6 +819,21 @@ class SyncedEnforcer:
         assertion = self._e.model["p"][ptype]
         assertion.field_index_map[field] = index
 
+    def set_policy_index(self, ptype, index):
+        """partitions the policy rules of ptype with a PolicyIndex."""
+        with self._wl:
+            return self._e.set_policy_index(ptype, index)
+
+    def set_policy_index_by_token(self, ptype, request_token, policy_token):
+        """partitions ptype's rules by the string value of two tokens."""
+        with self._wl:
+            return self._e.set_policy_index_by_token(ptype, request_token, 
policy_token)
+
+    def remove_policy_index(self, ptype):
+        """removes the policy partition for ptype."""
+        with self._wl:
+            return self._e.remove_policy_index(ptype)
+
     def get_all_roles_by_domain(self, domain):
         """gets all roles associated with the domain.
         note: Not applicable to Domains with inheritance relationship  
(implicit roles)"""
diff --git a/tests/test_policy_index.py b/tests/test_policy_index.py
new file mode 100644
index 0000000..ecd83bf
--- /dev/null
+++ b/tests/test_policy_index.py
@@ -0,0 +1,239 @@
+# Copyright 2026 The casbin Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from unittest import TestCase
+
+import casbin
+
+ALLOW_AND_DENY = "some(where (p.eft == allow)) && !some(where (p.eft == deny))"
+
+
+def new_enforcer(effect=ALLOW_AND_DENY, with_eft=True):
+    model = casbin.Model()
+    model.add_def("r", "r", "sub, obj_type, obj, act")
+    if with_eft:
+        model.add_def("p", "p", "sub, obj_type, obj, act, eft")
+    else:
+        model.add_def("p", "p", "sub, obj_type, obj, act")
+    model.add_def("g", "g", "_, _")
+    model.add_def("e", "e", effect)
+    model.add_def(
+        "m",
+        "m",
+        "g(r.sub, p.sub) && matchObj(p.obj_type, p.obj, r.obj_type, r.obj) && 
r.act == p.act",
+    )
+
+    enforcer = casbin.Enforcer(model)
+    calls = [0]
+
+    def match_obj(p_type, p_obj, r_type, r_obj):
+        calls[0] += 1
+        return (p_type == "*" or p_type == r_type) and p_obj == r_obj
+
+    enforcer.add_function("matchObj", match_obj)
+    return enforcer, calls
+
+
+def add_policies(enforcer, with_eft=True, with_deny=True):
+    rules = [
+        ["alice", "doc", "doc1", "read"],
+        ["alice", "doc", "doc2", "read"],
+        ["alice", "img", "img1", "read"],
+        ["alice", "*", "video1", "read"],
+    ]
+    if with_eft:
+        rules = [rule + ["allow"] for rule in rules]
+    enforcer.add_policies(rules)
+    if with_deny:
+        enforcer.add_policy("alice", "doc", "doc3", "read", "deny")
+
+
+def set_object_type_index(enforcer):
+    enforcer.set_policy_index(
+        "p",
+        casbin.PolicyIndex(
+            request_key=lambda rvals: (rvals[1], True),
+            policy_key=lambda rule: rule[1],
+        ),
+    )
+
+
+class PolicyIndexTest(TestCase):
+    def test_matches_unindexed_decisions(self):
+        indexed, _ = new_enforcer()
+        add_policies(indexed)
+        set_object_type_index(indexed)
+
+        unindexed, _ = new_enforcer()
+        add_policies(unindexed)
+
+        indexed.add_grouping_policy("bob", "alice")
+        unindexed.add_grouping_policy("bob", "alice")
+
+        requests = [
+            ("alice", "doc", "doc1", "read"),
+            ("alice", "doc", "doc2", "read"),
+            ("alice", "doc", "doc3", "read"),
+            ("alice", "img", "img1", "read"),
+            ("alice", "audio", "video1", "read"),
+            ("alice", "audio", "missing", "read"),
+            ("alice", "doc", "doc1", "write"),
+            ("bob", "doc", "doc1", "read"),
+            ("bob", "img", "img1", "read"),
+            ("bob", "audio", "video1", "read"),
+        ]
+        for request in requests:
+            indexed_result, indexed_explain = indexed.enforce_ex(*request)
+            unindexed_result, unindexed_explain = 
unindexed.enforce_ex(*request)
+            self.assertEqual(indexed_result, unindexed_result, request)
+            self.assertEqual(indexed_explain, unindexed_explain, request)
+
+    def test_skips_unrelated_rules(self):
+        enforcer, calls = new_enforcer()
+        add_policies(enforcer)
+        set_object_type_index(enforcer)
+
+        for request, expected_calls in [
+            (("alice", "doc", "doc1", "read"), 4),
+            (("alice", "img", "img1", "read"), 2),
+            (("alice", "audio", "video1", "read"), 1),
+        ]:
+            calls[0] = 0
+            enforcer.enforce(*request)
+            self.assertEqual(calls[0], expected_calls, request)
+
+        enforcer.remove_policy_index("p")
+        calls[0] = 0
+        enforcer.enforce("alice", "doc", "doc1", "read")
+        self.assertEqual(calls[0], 5)
+
+    def test_rebuilds_after_policy_changes(self):
+        enforcer, calls = new_enforcer()
+        add_policies(enforcer)
+        set_object_type_index(enforcer)
+
+        self.assertFalse(enforcer.enforce("alice", "doc", "doc4", "read"))
+
+        enforcer.add_policy("alice", "doc", "doc4", "read", "allow")
+        calls[0] = 0
+        self.assertTrue(enforcer.enforce("alice", "doc", "doc4", "read"))
+        self.assertEqual(calls[0], 5)
+
+        enforcer.remove_policy("alice", "doc", "doc4", "read", "allow")
+        self.assertFalse(enforcer.enforce("alice", "doc", "doc4", "read"))
+
+        old_rule = ["alice", "img", "img1", "read", "allow"]
+        new_rule = ["alice", "doc", "doc4", "read", "allow"]
+        self.assertTrue(enforcer.update_policy(old_rule, new_rule))
+        self.assertFalse(enforcer.enforce("alice", "img", "img1", "read"))
+        self.assertTrue(enforcer.enforce("alice", "doc", "doc4", "read"))
+
+        enforcer.clear_policy()
+        self.assertFalse(enforcer.enforce("alice", "doc", "doc1", "read"))
+        enforcer.add_policy("alice", "doc", "doc1", "read", "allow")
+        self.assertTrue(enforcer.enforce("alice", "doc", "doc1", "read"))
+
+    def test_disabled_for_unpartitioned_request(self):
+        enforcer, calls = new_enforcer()
+        add_policies(enforcer)
+        enforcer.set_policy_index(
+            "p",
+            casbin.PolicyIndex(
+                request_key=lambda rvals: ("", False),
+                policy_key=lambda rule: rule[1],
+            ),
+        )
+
+        calls[0] = 0
+        enforcer.enforce("alice", "doc", "doc1", "read")
+        self.assertEqual(calls[0], 5)
+
+    def test_validation(self):
+        enforcer, _ = new_enforcer()
+
+        with self.assertRaises(TypeError):
+            enforcer.set_policy_index("p", ("not", "an", "index"))
+        with self.assertRaises(ValueError):
+            set_object_type_index_to_missing_type(enforcer)
+        with self.assertRaises(ValueError):
+            enforcer.set_policy_index_by_token("p", "r_missing", "p_obj_type")
+        with self.assertRaises(ValueError):
+            enforcer.set_policy_index_by_token("p", "r_obj_type", "p_missing")
+
+    def test_by_token(self):
+        enforcer, calls = new_enforcer()
+        add_policies(enforcer)
+        enforcer.set_policy_index_by_token("p", "r_obj_type", "p_obj_type")
+
+        calls[0] = 0
+        self.assertTrue(enforcer.enforce("alice", "doc", "doc1", "read"))
+        self.assertEqual(calls[0], 4)
+
+    def test_allow_and_deny_short_circuits_without_deny_rules(self):
+        for with_eft in [False, True]:
+            enforcer, calls = new_enforcer(with_eft=with_eft)
+            add_policies(enforcer, with_eft=with_eft, with_deny=False)
+
+            calls[0] = 0
+            self.assertTrue(enforcer.enforce("alice", "doc", "doc1", "read"))
+            self.assertEqual(calls[0], 1, with_eft)
+
+    def test_allow_and_deny_still_evaluates_when_deny_exists(self):
+        enforcer, calls = new_enforcer()
+        add_policies(enforcer)
+
+        calls[0] = 0
+        self.assertTrue(enforcer.enforce("alice", "doc", "doc1", "read"))
+        self.assertEqual(calls[0], 5)
+
+    def test_resets_when_model_changes(self):
+        enforcer, calls = new_enforcer(with_eft=False)
+        enforcer.add_policy("alice", "doc", "doc1", "read")
+        enforcer.add_policy("alice", "img", "img1", "read")
+        set_object_type_index(enforcer)
+
+        self.assertTrue(enforcer.enforce("alice", "img", "img1", "read"))
+
+        replacement = casbin.Model()
+        replacement.add_def("r", "r", "sub, obj_type, obj, act")
+        replacement.add_def("p", "p", "sub, obj_type, obj, act")
+        replacement.add_def("g", "g", "_, _")
+        replacement.add_def("e", "e", ALLOW_AND_DENY)
+        replacement.add_def(
+            "m",
+            "m",
+            "g(r.sub, p.sub) && matchObj(p.obj_type, p.obj, r.obj_type, r.obj) 
&& r.act == p.act",
+        )
+        replacement.add_policy("p", "p", ["alice", "img", "img1", "read"])
+        replacement.add_policy("p", "p", ["alice", "doc", "doc1", "read"])
+
+        enforcer.set_model(replacement)
+
+        def match_obj(p_type, p_obj, r_type, r_obj):
+            calls[0] += 1
+            return (p_type == "*" or p_type == r_type) and p_obj == r_obj
+
+        enforcer.add_function("matchObj", match_obj)
+
+        self.assertTrue(enforcer.enforce("alice", "img", "img1", "read"))
+
+
+def set_object_type_index_to_missing_type(enforcer):
+    enforcer.set_policy_index(
+        "missing",
+        casbin.PolicyIndex(
+            request_key=lambda rvals: ("", True),
+            policy_key=lambda rule: rule[0],
+        ),
+    )


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

Reply via email to