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 e72f6b0 fix: match Go in regexMatch, inline comments, domain matching
and explain
e72f6b0 is described below
commit e72f6b01fd91546e75cca1e688adfcefbadcea34
Author: Yang Luo <[email protected]>
AuthorDate: Wed Oct 21 01:49:20 2026 +0800
fix: match Go in regexMatch, inline comments, domain matching and explain
---
.github/workflows/build.yml | 9 +++++++
casbin/config/config.py | 8 +++++-
casbin/core_enforcer.py | 40 ++++++++++++++++++++++++-----
casbin/util/builtin_operators.py | 2 +-
casbin/util/expression.py | 12 ++++++++-
tests/config/test_config.py | 2 +-
tests/test_conformance.py | 54 ++++++++++++++++++++++++++++++++++++++++
7 files changed, 117 insertions(+), 10 deletions(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 921e3af..3ab460b 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -18,6 +18,13 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
+ - name: Checkout conformance cases
+ uses: actions/checkout@v4
+ with:
+ repository: apache/casbin
+ path: casbin-conformance
+ sparse-checkout: conformance
+
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
@@ -31,6 +38,8 @@ jobs:
- name: Run tests
run: coverage run -m unittest discover -s tests -t tests
+ env:
+ CASBIN_CONFORMANCE_DIR: ${{ github.workspace
}}/casbin-conformance/conformance
- name: Run benchmark
run: python3 -m pytest
diff --git a/casbin/config/config.py b/casbin/config/config.py
index 74160ed..17c2873 100644
--- a/casbin/config/config.py
+++ b/casbin/config/config.py
@@ -93,7 +93,13 @@ class Config:
else:
p = line
can_write = True
- buf.append(p)
+
+ end = len(p)
+ for i, value in enumerate(p):
+ if value == self.DEFAULT_COMMENT or value ==
self.DEFAULT_COMMENT_SEM:
+ end = i
+ break
+ buf.append(p[:end])
def _write(self, section, line_num, b):
buf = "".join(b)
diff --git a/casbin/core_enforcer.py b/casbin/core_enforcer.py
index 3473309..0d4ddc8 100644
--- a/casbin/core_enforcer.py
+++ b/casbin/core_enforcer.py
@@ -17,12 +17,12 @@ import logging
import re
from functools import lru_cache
-from casbin.effect import Effector, get_effector, effect_to_bool
+from casbin.effect import Effector, AllowAndDenyEffector, get_effector,
effect_to_bool
from casbin.model import Model, FunctionMap
from casbin.persist import Adapter
from casbin.persist.adapters import FileAdapter
from casbin.rbac import default_role_manager
-from casbin.util import generate_g_function, SimpleEval, util,
generate_conditional_g_function
+from casbin.util import generate_g_function, SimpleEval, util,
generate_conditional_g_function, key_match
from casbin.util.log import configure_logging, disabled_logging
@@ -220,6 +220,28 @@ class CoreEnforcer:
else:
assertion.cond_rm =
default_role_manager.ConditionalDomainManager(10)
self.cond_rm_map[ptype] = assertion.cond_rm
+ self._register_domain_matching_func(ptype)
+
+ def _get_domain_tokens(self):
+ r_domain_token = ""
+ p_domain_token = ""
+ r_assertion = (self.model["r"] or {}).get("r")
+ if r_assertion is not None and len(r_assertion.tokens) > 1:
+ r_domain_token = r_assertion.tokens[1]
+ p_assertion = (self.model["p"] or {}).get("p")
+ if p_assertion is not None and len(p_assertion.tokens) > 1:
+ p_domain_token = p_assertion.tokens[1]
+ return r_domain_token, p_domain_token
+
+ def _register_domain_matching_func(self, ptype):
+ r_domain_token, p_domain_token = self._get_domain_tokens()
+ m_assertion = (self.model["m"] or {}).get("m")
+ if r_domain_token == "" or p_domain_token == "" or m_assertion is None:
+ return
+
+ match_fun = "keyMatch({}, {})".format(r_domain_token, p_domain_token)
+ if match_fun in m_assertion.value:
+ self.add_named_domain_matching_func(ptype, key_match)
def load_policy(self):
"""reloads the policy from file/database."""
@@ -339,6 +361,9 @@ class CoreEnforcer:
if ptype in self.rm_map.keys():
self.rm_map[ptype].add_domain_matching_func(fn)
return True
+ if ptype in self.cond_rm_map.keys():
+ self.cond_rm_map[ptype].add_domain_matching_func(fn)
+ return True
return False
@@ -411,7 +436,7 @@ class CoreEnforcer:
for key, ast in self.model["g"].items():
if len(self.rm_map) != 0:
functions[key] = generate_g_function(ast.rm)
- if len(self.cond_rm_map) != 0:
+ if ast.cond_rm is not None:
functions[key] =
generate_conditional_g_function(ast.cond_rm)
if len(rvals) != 0:
@@ -448,6 +473,7 @@ class CoreEnforcer:
policy_len = len(self.model["p"][ptype].policy)
explain_index = -1
+ allow_index = -1
if not 0 == policy_len:
for i, pvals in enumerate(self.model["p"][ptype].policy):
if len(p_tokens) != len(pvals):
@@ -485,11 +511,11 @@ class CoreEnforcer:
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 allow_index == -1 and Effector.ALLOW in policy_effects:
+ allow_index = i
if self.eft.intermediate_effect(policy_effects) !=
Effector.INDETERMINATE:
+ explain_index = i
break
else:
@@ -510,6 +536,8 @@ class CoreEnforcer:
final_effect = self.eft.final_effect(policy_effects)
result = effect_to_bool(final_effect)
+ if explain_index == -1 and final_effect == Effector.ALLOW and
isinstance(self.eft, AllowAndDenyEffector):
+ explain_index = allow_index
# Log request.
if (result and self.logger.isEnabledFor(logging.INFO)) or (
diff --git a/casbin/util/builtin_operators.py b/casbin/util/builtin_operators.py
index 99075a9..3efe5f2 100644
--- a/casbin/util/builtin_operators.py
+++ b/casbin/util/builtin_operators.py
@@ -293,7 +293,7 @@ def key_match5_func(*args) -> bool:
def regex_match(key1, key2):
"""determines whether key1 matches the pattern of key2 in regular
expression."""
- res = re.match(key2, key1)
+ res = re.search(key2, key1)
if res:
return True
else:
diff --git a/casbin/util/expression.py b/casbin/util/expression.py
index 3df8d26..37b7448 100644
--- a/casbin/util/expression.py
+++ b/casbin/util/expression.py
@@ -13,7 +13,7 @@
# limitations under the License.
from functools import lru_cache
-from simpleeval import EvalWithCompoundTypes
+from simpleeval import EvalWithCompoundTypes, NameNotDefined
import ast
@@ -51,3 +51,13 @@ class SimpleEval(EvalWithCompoundTypes):
self.names = names
return self._eval(self.ast_parsed_value)
+
+ def _eval_name(self, node):
+ try:
+ return super(SimpleEval, self)._eval_name(node)
+ except NameNotDefined:
+ if node.id == "true":
+ return True
+ if node.id == "false":
+ return False
+ raise
diff --git a/tests/config/test_config.py b/tests/config/test_config.py
index 3d9d80e..b29f58f 100644
--- a/tests/config/test_config.py
+++ b/tests/config/test_config.py
@@ -38,7 +38,7 @@ class TestConfig(TestCase):
self.assertEqual(config.get("math::math.f64"), "64.1")
# other::key test
- self.assertEqual(config.get("other::name"), "ATC自动化测试^-^&($#……#")
+ self.assertEqual(config.get("other::name"), "ATC自动化测试^-^&($")
self.assertEqual(config.get("other::key1"), "test key")
config.set("other::key1", "new test key")
diff --git a/tests/test_conformance.py b/tests/test_conformance.py
new file mode 100644
index 0000000..c462272
--- /dev/null
+++ b/tests/test_conformance.py
@@ -0,0 +1,54 @@
+# 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 json
+import os
+from unittest import TestCase, skipUnless
+
+import casbin
+
+CONFORMANCE_DIR = os.environ.get("CASBIN_CONFORMANCE_DIR")
+
+
+@skipUnless(CONFORMANCE_DIR, "CASBIN_CONFORMANCE_DIR is not set")
+class TestConformance(TestCase):
+ def test_conformance(self):
+ cases_dir = os.path.join(CONFORMANCE_DIR, "cases")
+ data_dir = os.path.join(CONFORMANCE_DIR, "data")
+ case_files = sorted(name for name in os.listdir(cases_dir) if
name.endswith(".json"))
+ self.assertTrue(case_files, "no conformance cases found in
{}".format(cases_dir))
+
+ mismatches = []
+ for case_file in case_files:
+ with open(os.path.join(cases_dir, case_file), encoding="utf-8") as
f:
+ case = json.load(f)
+ try:
+ e = casbin.Enforcer(os.path.join(data_dir, case["model"]),
os.path.join(data_dir, case["policy"]))
+ except Exception as err:
+ mismatches.append("{}: failed to load: {!r}".format(case_file,
err))
+ continue
+
+ for test in case["tests"]:
+ request = test["request"]
+ want = (test["allow"], test["explain"])
+ try:
+ allow, explain = e.enforce_ex(*request)
+ got = (allow, list(explain or []))
+ except Exception as err:
+ got = repr(err)
+ if got != want:
+ mismatches.append("{}: request {}: got {}, want
{}".format(case_file, request, got, want))
+
+ if mismatches:
+ self.fail("{} mismatches:\n{}".format(len(mismatches),
"\n".join(mismatches)))
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]