Skip to content

Commit 598d64a

Browse files
committed
fix: speed up key_match with multiple wildcards
1 parent 98725bc commit 598d64a

3 files changed

Lines changed: 81 additions & 5 deletions

File tree

‎casbin/synced_enforcer.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ def set_role_manager(self, rm):
215215
def get_adapter(self):
216216
"""gets the current adapter."""
217217
with self._rl:
218-
self._e.get_adapter()
218+
return self._e.get_adapter()
219219

220220
def set_adapter(self, adapter):
221221
"""sets the current adapter."""
@@ -712,7 +712,7 @@ def set_named_link_condition_func_params(self, ptype, user, role, *params):
712712
def is_filtered(self):
713713
"""returns true if the loaded policy has been filtered."""
714714
with self._rl:
715-
self._e.is_filtered()
715+
return self._e.is_filtered()
716716

717717
def add_policies(self, rules):
718718
"""adds authorization rules to the current policy.

‎casbin/util/builtin_operators.py‎

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import ipaddress
1616
import re
17+
from functools import lru_cache
1718
from datetime import datetime
1819
import wcmatch.glob as glob
1920

@@ -23,6 +24,72 @@
2324
KEY_MATCH5_PATTERN = re.compile(r"{[^/]+}")
2425

2526

27+
WILDCARD_PATTERN = re.compile(r"\.\*|\[\^\\?/\]\+|\.|[^\\^$.|?*+()\[\]{}]")
28+
29+
30+
def _any_char(c):
31+
return c != "\n"
32+
33+
34+
def _segment_char(c):
35+
return c != "/"
36+
37+
38+
@lru_cache(maxsize=1024)
39+
def _parse_wildcards(pattern):
40+
"""splits a path pattern made of literals, ".", "[^/]+" and two or more ".*" into (char test, repeatable) steps,
41+
returns None for any other pattern."""
42+
if pattern.count(".*") < 2:
43+
return None
44+
45+
steps = []
46+
pos = 0
47+
while pos < len(pattern):
48+
m = WILDCARD_PATTERN.match(pattern, pos)
49+
if not m:
50+
return None
51+
token = m.group()
52+
if token == ".*":
53+
steps.append((_any_char, True))
54+
elif token == ".":
55+
steps.append((_any_char, False))
56+
elif len(token) > 1:
57+
steps.append((_segment_char, False))
58+
steps.append((_segment_char, True))
59+
else:
60+
steps.append((token.__eq__, False))
61+
pos = m.end()
62+
return tuple(steps)
63+
64+
65+
def _path_match(key1, pattern):
66+
"""determines whether the whole key1 matches the path pattern. A pattern with several ".*" is matched in
67+
O(len(key1) * len(pattern)), because re.match() backtracks polynomially on it."""
68+
steps = _parse_wildcards(pattern)
69+
if steps is None:
70+
return regex_match(key1, "^" + pattern + "$")
71+
72+
end = len(steps)
73+
74+
def add(states, i):
75+
states.add(i)
76+
while i < end and steps[i][1]:
77+
i += 1
78+
states.add(i)
79+
80+
states = set()
81+
add(states, 0)
82+
for c in key1:
83+
next_states = set()
84+
for i in states:
85+
if i < end and steps[i][0](c):
86+
add(next_states, i if steps[i][1] else i + 1)
87+
if not next_states:
88+
return False
89+
states = next_states
90+
return end in states
91+
92+
2693
def key_match(key1, key2):
2794
"""determines whether key1 matches the pattern of key2 (similar to RESTful path), key2 can contain a *.
2895
For example, "/foo/bar" matches "/foo/*"
@@ -72,7 +139,7 @@ def key_match2(key1, key2):
72139
if key2 == "*":
73140
key2 = "(.*)"
74141

75-
return regex_match(key1, "^" + key2 + "$")
142+
return _path_match(key1, key2)
76143

77144

78145
def key_match2_func(*args):
@@ -114,7 +181,7 @@ def key_match3(key1, key2):
114181
key2 = key2.replace("/*", "/.*")
115182
key2 = KEY_MATCH3_PATTERN.sub(r"\g<1>[^\/]+\g<2>", key2, 0)
116183

117-
return regex_match(key1, "^" + key2 + "$")
184+
return _path_match(key1, key2)
118185

119186

120187
def key_match3_func(*args):
@@ -213,7 +280,7 @@ def key_match5(key1: str, key2: str) -> bool:
213280

214281
key2 = KEY_MATCH5_PATTERN.sub(r"[^/]+", key2, 0)
215282

216-
return regex_match(key1, "^" + key2 + "$")
283+
return _path_match(key1, key2)
217284

218285

219286
def key_match5_func(*args) -> bool:

‎tests/util/test_builtin_operators.py‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,15 @@ def test_key_match2(self):
7474

7575
self.assertFalse(util.key_match2_func("/alice/all", "/:/all"))
7676

77+
def test_key_match2_multiple_wildcards(self):
78+
self.assertTrue(util.key_match2_func("/api/a/b/c/detail", "/api/*/*/*/detail"))
79+
self.assertTrue(util.key_match2_func("/api////detail", "/api/*/*/*/detail"))
80+
self.assertTrue(util.key_match2_func("/api/a/b/c/d/1/detail", "/api/*/*/:id/detail"))
81+
self.assertFalse(util.key_match2_func("/api/a/b/detail", "/api/*/*/*/detail"))
82+
self.assertFalse(util.key_match2_func("/api/a/b/c/detail/x", "/api/*/*/*/detail"))
83+
# used to take minutes because of regex backtracking
84+
self.assertFalse(util.key_match2_func("/api/" + "/" * 5000, "/api/*/*/*/detail"))
85+
7786
def test_key_get2(self):
7887
self.assertEqual(util.key_get2("/foo", "/foo", "id"), "")
7988
self.assertEqual(util.key_get2("/foo", "/foo*", "id"), "")

0 commit comments

Comments
 (0)