diff --git a/casbin/util/builtin_operators.py b/casbin/util/builtin_operators.py index e93b88f9..cf478864 100644 --- a/casbin/util/builtin_operators.py +++ b/casbin/util/builtin_operators.py @@ -5,6 +5,7 @@ KEY_MATCH2_PATTERN = re.compile(r"(.*?):[^\/]+(.*?)") KEY_MATCH3_PATTERN = re.compile(r"(.*?){[^\/]+}(.*?)") +KEY_MATCH4_PATTERN = re.compile(r"{([^/]+)}") def key_match(key1, key2): @@ -68,6 +69,55 @@ def key_match3_func(*args): return key_match3(name1, name2) +def key_match4(key1: str, key2: str) -> bool: + """ + key_match4 determines whether key1 matches the pattern of key2 (similar to RESTful path), key2 can contain a *. + Besides what key_match3 does, key_match4 can also match repeated patterns: + "/parent/123/child/123" matches "/parent/{id}/child/{id}" + "/parent/123/child/456" does not match "/parent/{id}/child/{id}" + But key_match3 will match both. + """ + key2 = key2.replace("/*", "/.*") + + tokens: [str] = [] + + def repl(matchobj): + tokens.append(matchobj.group(1)) + return "([^/]+)" + + key2 = KEY_MATCH4_PATTERN.sub(repl, key2) + + regexp = re.compile("^" + key2 + "$") + matches = regexp.match(key1) + + if matches is None: + return False + if len(tokens) != len(matches.groups()): + raise Exception("KeyMatch4: number of tokens is not equal to number of values") + + tokens_matches = dict() + + for i in range(len(tokens)): + token, match = tokens[i], matches.groups()[i] + + if token not in tokens_matches.keys(): + tokens_matches[token] = match + else: + if tokens_matches[token] != match: + return False + return True + + +def key_match4_func(*args) -> bool: + """ + key_match4_func is the wrapper for key_match4. + """ + name1 = args[0] + name2 = args[1] + + return key_match4(name1, name2) + + def regex_match(key1, key2): """determines whether key1 matches the pattern of key2 in regular expression.""" diff --git a/tests/util/test_builtin_operators.py b/tests/util/test_builtin_operators.py index aa9bb132..11c3f512 100644 --- a/tests/util/test_builtin_operators.py +++ b/tests/util/test_builtin_operators.py @@ -87,6 +87,55 @@ def test_key_match3(self): util.key_match3_func("/myid/using/myresid", "/{id/using/{resId}") ) + def test_key_match4(self): + self.assertTrue( + util.key_match4_func("/parent/123/child/123", "/parent/{id}/child/{id}") + ) + self.assertFalse( + util.key_match4_func("/parent/123/child/456", "/parent/{id}/child/{id}") + ) + + self.assertTrue( + util.key_match4_func( + "/parent/123/child/123", "/parent/{id}/child/{another_id}" + ) + ) + self.assertTrue( + util.key_match4_func( + "/parent/123/child/456", "/parent/{id}/child/{another_id}" + ) + ) + + self.assertTrue( + util.key_match4_func( + "/parent/123/child/456", "/parent/{id}/child/{another_id}" + ) + ) + self.assertFalse( + util.key_match4_func( + "/parent/123/child/123/book/456", "/parent/{id}/child/{id}/book/{id}" + ) + ) + self.assertFalse( + util.key_match4_func( + "/parent/123/child/456/book/123", "/parent/{id}/child/{id}/book/{id}" + ) + ) + self.assertFalse( + util.key_match4_func( + "/parent/123/child/456/book/", "/parent/{id}/child/{id}/book/{id}" + ) + ) + self.assertFalse( + util.key_match4_func( + "/parent/123/child/456", "/parent/{id}/child/{id}/book/{id}" + ) + ) + + self.assertFalse( + util.key_match4_func("/parent/123/child/123", "/parent/{i/d}/child/{i/d}") + ) + def test_regex_match(self): self.assertTrue(util.regex_match_func("/topic/create", "/topic/create")) self.assertTrue(util.regex_match_func("/topic/create/123", "/topic/create"))