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.git
The following commit(s) were added to refs/heads/master by this push:
new 0a73027 fix: return errors instead of panicking on invalid patterns
in matchers
0a73027 is described below
commit 0a730271714a1998b2c481ac96cc4381364d046f
Author: Yang Luo <[email protected]>
AuthorDate: Tue Oct 20 23:04:53 2026 +0800
fix: return errors instead of panicking on invalid patterns in matchers
---
error_test.go | 33 ++++++++
util/builtin_operators.go | 187 +++++++++++++++++++++++++++--------------
util/builtin_operators_test.go | 15 ++++
3 files changed, 174 insertions(+), 61 deletions(-)
diff --git a/error_test.go b/error_test.go
index 194488a..c2f34c7 100644
--- a/error_test.go
+++ b/error_test.go
@@ -18,9 +18,11 @@
package casbin
import (
+ "strings"
"testing"
fileadapter "github.com/casbin/casbin/v3/persist/file-adapter"
+ "github.com/casbin/casbin/v3/util"
)
func TestPathError(t *testing.T) {
@@ -286,3 +288,34 @@ func TestMockAdapterErrors(t *testing.T) {
t.Log(err10.Error())
}
}
+
+func TestInvalidPatternInPolicy(t *testing.T) {
+ e, err := NewEnforcer("examples/keymatch_model.conf")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err = e.AddPolicy("alice", "/alice_data/*", "(GET"); err != nil {
+ t.Fatal(err)
+ }
+
+ _, err = e.Enforce("alice", "/alice_data/resource1", "GET")
+ if err == nil || strings.HasPrefix(err.Error(), "panic:") ||
+ !strings.Contains(err.Error(), "regexMatch: error parsing
regexp: missing closing ): `(GET`") {
+ t.Errorf("Enforce returns %v, supposed to be a regexMatch parse
error", err)
+ }
+
+ e, err = NewEnforcer("examples/rbac_with_pattern_model.conf",
"examples/rbac_with_pattern_policy.csv")
+ if err != nil {
+ t.Fatal(err)
+ }
+ e.AddNamedMatchingFunc("g2", "matchingFunc", util.KeyMatch2)
+ if _, err = e.AddNamedGroupingPolicy("g2", "/book/(1", "book_group");
err != nil {
+ t.Fatal(err)
+ }
+
+ testEnforce(t, e, "alice", "/book/1", "GET", true)
+ testEnforce(t, e, "alice", "/book/(1", "GET", true)
+ if roles, err := e.GetNamedImplicitRolesForUser("g2", "/book/(1"); err
!= nil || len(roles) != 1 {
+ t.Errorf("GetNamedImplicitRolesForUser returns %v %v, supposed
to be [book_group]", roles, err)
+ }
+}
diff --git a/util/builtin_operators.go b/util/builtin_operators.go
index cb0361e..9a846d0 100644
--- a/util/builtin_operators.go
+++ b/util/builtin_operators.go
@@ -40,8 +40,6 @@ var (
keyMatch5Re = regexp.MustCompile(`\{[^/]+\}`)
keyGet2Re1 = regexp.MustCompile(`:[^/]+`)
keyGet3Re1 = regexp.MustCompile(`\{[^/]+?\}`) // non-greedy match of
`{...}` to support multiple {} in `/.../`
- reCache = map[string]*regexp.Regexp{}
- reCacheMu = sync.RWMutex{}
)
// regexpMetaChars is exactly the set of characters that regexp.QuoteMeta
escapes.
@@ -89,17 +87,31 @@ func keyMatchShortcut(key1 string, key2 string, extraChars
string) (matched bool
// their capture groups appear (only KeyGet2/KeyGet3/KeyMatch4 use tokens).
//
// Everything in it is a pure function of key2, so it is built once per
distinct
-// key2 and reused afterwards.
+// key2 and reused afterwards. A key2 that does not compile keeps its error
here,
+// so it is reported on every call without being compiled again.
type compiledPattern struct {
re *regexp.Regexp
tokens []string
+ err error
+}
+
+func compilePattern(expr string, tokens []string) *compiledPattern {
+ re, err := regexp.Compile(expr)
+ return &compiledPattern{re: re, tokens: tokens, err: err}
+}
+
+func (p *compiledPattern) match(key1 string) (bool, error) {
+ if p.err != nil {
+ return false, p.err
+ }
+ return p.re.MatchString(key1), nil
}
// patternCache memoizes compiledPattern by raw key2, one cache per matcher
since
// each of them reads key2 in its own dialect. Only compiling the regexp used
to be
-// cached (see mustCompileOrGet), which still left every call rebuilding the
regexp
-// source with a chain of ReplaceAll passes just to look it up. Keying on key2
-// instead makes the steady state a single map read.
+// cached, which still left every call rebuilding the regexp source with a
chain of
+// ReplaceAll passes just to look it up. Keying on key2 instead makes the
steady
+// state a single map read.
//
// A policy holds a bounded set of patterns, so the cache converges to that set
// after the first pass; sync.Map suits that read-mostly shape.
@@ -125,23 +137,9 @@ var (
keyMatch5Cache patternCache
keyGet2Cache patternCache
keyGet3Cache patternCache
+ regexCache patternCache
)
-func mustCompileOrGet(key string) *regexp.Regexp {
- reCacheMu.RLock()
- re, ok := reCache[key]
- reCacheMu.RUnlock()
-
- if !ok {
- re = regexp.MustCompile(key)
- reCacheMu.Lock()
- reCache[key] = re
- reCacheMu.Unlock()
- }
-
- return re
-}
-
// validate the variadic parameter size and type as string.
func validateVariadicArgs(expectedLen int, args ...interface{}) error {
if len(args) != expectedLen {
@@ -222,12 +220,18 @@ func KeyGetFunc(args ...interface{}) (interface{}, error)
{
// KeyMatch2 determines whether key1 matches the pattern of key2 (similar to
RESTful path), key2 can contain a *.
// For example, "/foo/bar" matches "/foo/*", "/resource1" matches "/:resource".
+// An invalid pattern matches nothing; KeyMatch2Func reports it as an error.
func KeyMatch2(key1 string, key2 string) bool {
+ matched, _ := keyMatch2(key1, key2)
+ return matched
+}
+
+func keyMatch2(key1 string, key2 string) (bool, error) {
if matched, ok := keyMatchShortcut(key1, key2, ":"); ok {
- return matched
+ return matched, nil
}
- return keyMatch2Cache.get(key2, buildKeyMatch2).re.MatchString(key1)
+ return keyMatch2Cache.get(key2, buildKeyMatch2).match(key1)
}
func buildKeyMatch2(key2 string) *compiledPattern {
@@ -235,7 +239,7 @@ func buildKeyMatch2(key2 string) *compiledPattern {
key2 = keyMatch2Re.ReplaceAllString(key2, "$1[^/]+$2")
- return &compiledPattern{re: mustCompileOrGet("^" + key2 + "$")}
+ return compilePattern("^"+key2+"$", nil)
}
// KeyMatch2Func is the wrapper for KeyMatch2.
@@ -247,25 +251,38 @@ func KeyMatch2Func(args ...interface{}) (interface{},
error) {
name1 := args[0].(string)
name2 := args[1].(string)
- return KeyMatch2(name1, name2), nil
+ matched, err := keyMatch2(name1, name2)
+ if err != nil {
+ return false, fmt.Errorf("%s: %w", "keyMatch2", err)
+ }
+
+ return matched, nil
}
// KeyGet2 returns value matched pattern
// For example, "/resource1" matches "/:resource"
// if the pathVar == "resource", then "resource1" will be returned.
+// An invalid pattern returns ""; KeyGet2Func reports it as an error.
func KeyGet2(key1, key2 string, pathVar string) string {
- p := keyGet2Cache.get(key2, buildKeyGet2)
+ value, _ := keyGet(keyGet2Cache.get(key2, buildKeyGet2), key1, pathVar)
+ return value
+}
+
+func keyGet(p *compiledPattern, key1 string, pathVar string) (string, error) {
+ if p.err != nil {
+ return "", p.err
+ }
values := p.re.FindAllStringSubmatch(key1, -1)
if len(values) == 0 {
- return ""
+ return "", nil
}
for i, key := range p.tokens {
if pathVar == key {
- return values[0][i+1]
+ return values[0][i+1], nil
}
}
- return ""
+ return "", nil
}
func buildKeyGet2(key2 string) *compiledPattern {
@@ -279,7 +296,7 @@ func buildKeyGet2(key2 string) *compiledPattern {
key2 = keyGet2Re1.ReplaceAllString(key2, "$1([^/]+)$2")
- return &compiledPattern{re: mustCompileOrGet("^" + key2 + "$"), tokens:
tokens}
+ return compilePattern("^"+key2+"$", tokens)
}
// KeyGet2Func is the wrapper for KeyGet2.
@@ -292,24 +309,35 @@ func KeyGet2Func(args ...interface{}) (interface{},
error) {
name2 := args[1].(string)
key := args[2].(string)
- return KeyGet2(name1, name2, key), nil
+ value, err := keyGet(keyGet2Cache.get(name2, buildKeyGet2), name1, key)
+ if err != nil {
+ return "", fmt.Errorf("%s: %w", "keyGet2", err)
+ }
+
+ return value, nil
}
// KeyMatch3 determines whether key1 matches the pattern of key2 (similar to
RESTful path), key2 can contain a *.
// For example, "/foo/bar" matches "/foo/*", "/resource1" matches
"/{resource}".
+// An invalid pattern matches nothing; KeyMatch3Func reports it as an error.
func KeyMatch3(key1 string, key2 string) bool {
+ matched, _ := keyMatch3(key1, key2)
+ return matched
+}
+
+func keyMatch3(key1 string, key2 string) (bool, error) {
if matched, ok := keyMatchShortcut(key1, key2, ""); ok {
- return matched
+ return matched, nil
}
- return keyMatch3Cache.get(key2, buildKeyMatch3).re.MatchString(key1)
+ return keyMatch3Cache.get(key2, buildKeyMatch3).match(key1)
}
func buildKeyMatch3(key2 string) *compiledPattern {
key2 = strings.ReplaceAll(key2, "/*", "/.*")
key2 = keyMatch3Re.ReplaceAllString(key2, "$1[^/]+$2")
- return &compiledPattern{re: mustCompileOrGet("^" + key2 + "$")}
+ return compilePattern("^"+key2+"$", nil)
}
// KeyMatch3Func is the wrapper for KeyMatch3.
@@ -321,25 +349,21 @@ func KeyMatch3Func(args ...interface{}) (interface{},
error) {
name1 := args[0].(string)
name2 := args[1].(string)
- return KeyMatch3(name1, name2), nil
+ matched, err := keyMatch3(name1, name2)
+ if err != nil {
+ return false, fmt.Errorf("%s: %w", "keyMatch3", err)
+ }
+
+ return matched, nil
}
// KeyGet3 returns value matched pattern
// For example, "project/proj_project1_admin/" matches
"project/proj_{project}_admin/"
// if the pathVar == "project", then "project1" will be returned.
+// An invalid pattern returns ""; KeyGet3Func reports it as an error.
func KeyGet3(key1, key2 string, pathVar string) string {
- p := keyGet3Cache.get(key2, buildKeyGet3)
-
- values := p.re.FindAllStringSubmatch(key1, -1)
- if len(values) == 0 {
- return ""
- }
- for i, key := range p.tokens {
- if pathVar == key {
- return values[0][i+1]
- }
- }
- return ""
+ value, _ := keyGet(keyGet3Cache.get(key2, buildKeyGet3), key1, pathVar)
+ return value
}
func buildKeyGet3(key2 string) *compiledPattern {
@@ -354,7 +378,7 @@ func buildKeyGet3(key2 string) *compiledPattern {
key2 = keyGet3Re1.ReplaceAllString(key2, "$1([^/]+?)$2")
- return &compiledPattern{re: mustCompileOrGet("^" + key2 + "$"), tokens:
tokens}
+ return compilePattern("^"+key2+"$", tokens)
}
// KeyGet3Func is the wrapper for KeyGet3.
@@ -367,7 +391,12 @@ func KeyGet3Func(args ...interface{}) (interface{}, error)
{
name2 := args[1].(string)
key := args[2].(string)
- return KeyGet3(name1, name2, key), nil
+ value, err := keyGet(keyGet3Cache.get(name2, buildKeyGet3), name1, key)
+ if err != nil {
+ return "", fmt.Errorf("%s: %w", "keyGet3", err)
+ }
+
+ return value, nil
}
// KeyMatch4 determines whether key1 matches the pattern of key2 (similar to
RESTful path), key2 can contain a *.
@@ -375,16 +404,25 @@ func KeyGet3Func(args ...interface{}) (interface{},
error) {
// "/parent/123/child/123" matches "/parent/{id}/child/{id}"
// "/parent/123/child/456" does not match "/parent/{id}/child/{id}"
// But KeyMatch3 will match both.
+// An invalid pattern matches nothing; KeyMatch4Func reports it as an error.
func KeyMatch4(key1 string, key2 string) bool {
+ matched, _ := keyMatch4(key1, key2)
+ return matched
+}
+
+func keyMatch4(key1 string, key2 string) (bool, error) {
if matched, ok := keyMatchShortcut(key1, key2, ""); ok {
- return matched
+ return matched, nil
}
p := keyMatch4Cache.get(key2, buildKeyMatch4)
+ if p.err != nil {
+ return false, p.err
+ }
matches := p.re.FindStringSubmatch(key1)
if matches == nil {
- return false
+ return false, nil
}
matches = matches[1:]
@@ -399,11 +437,11 @@ func KeyMatch4(key1 string, key2 string) bool {
values[token] = matches[key]
}
if values[token] != matches[key] {
- return false
+ return false, nil
}
}
- return true
+ return true, nil
}
func buildKeyMatch4(key2 string) *compiledPattern {
@@ -416,7 +454,7 @@ func buildKeyMatch4(key2 string) *compiledPattern {
return "([^/]+)"
})
- return &compiledPattern{re: mustCompileOrGet("^" + key2 + "$"), tokens:
tokens}
+ return compilePattern("^"+key2+"$", tokens)
}
// KeyMatch4Func is the wrapper for KeyMatch4.
@@ -428,7 +466,12 @@ func KeyMatch4Func(args ...interface{}) (interface{},
error) {
name1 := args[0].(string)
name2 := args[1].(string)
- return KeyMatch4(name1, name2), nil
+ matched, err := keyMatch4(name1, name2)
+ if err != nil {
+ return false, fmt.Errorf("%s: %w", "keyMatch4", err)
+ }
+
+ return matched, nil
}
// KeyMatch5 determines whether key1 matches the pattern of key2 (similar to
RESTful path), key2 can contain a *
@@ -436,7 +479,13 @@ func KeyMatch4Func(args ...interface{}) (interface{},
error) {
// - "/foo/bar?status=1&type=2" matches "/foo/bar"
// - "/parent/child1" and "/parent/child1" matches "/parent/*"
// - "/parent/child1?status=1" matches "/parent/*".
+// An invalid pattern matches nothing; KeyMatch5Func reports it as an error.
func KeyMatch5(key1 string, key2 string) bool {
+ matched, _ := keyMatch5(key1, key2)
+ return matched
+}
+
+func keyMatch5(key1 string, key2 string) (bool, error) {
i := strings.Index(key1, "?")
if i != -1 {
@@ -446,17 +495,17 @@ func KeyMatch5(key1 string, key2 string) bool {
// After the query string is stripped, so that key1 is compared in the
same
// shape the regexp path would have matched.
if matched, ok := keyMatchShortcut(key1, key2, ""); ok {
- return matched
+ return matched, nil
}
- return keyMatch5Cache.get(key2, buildKeyMatch5).re.MatchString(key1)
+ return keyMatch5Cache.get(key2, buildKeyMatch5).match(key1)
}
func buildKeyMatch5(key2 string) *compiledPattern {
key2 = strings.ReplaceAll(key2, "/*", "/.*")
key2 = keyMatch5Re.ReplaceAllString(key2, "$1[^/]+$2")
- return &compiledPattern{re: mustCompileOrGet("^" + key2 + "$")}
+ return compilePattern("^"+key2+"$", nil)
}
// KeyMatch5Func is the wrapper for KeyMatch5.
@@ -468,12 +517,23 @@ func KeyMatch5Func(args ...interface{}) (interface{},
error) {
name1 := args[0].(string)
name2 := args[1].(string)
- return KeyMatch5(name1, name2), nil
+ matched, err := keyMatch5(name1, name2)
+ if err != nil {
+ return false, fmt.Errorf("%s: %w", "keyMatch5", err)
+ }
+
+ return matched, nil
}
// RegexMatch determines whether key1 matches the pattern of key2 in regular
expression.
+// An invalid pattern matches nothing; RegexMatchFunc reports it as an error.
func RegexMatch(key1 string, key2 string) bool {
- return mustCompileOrGet(key2).MatchString(key1)
+ matched, _ := regexCache.get(key2, buildRegexMatch).match(key1)
+ return matched
+}
+
+func buildRegexMatch(key2 string) *compiledPattern {
+ return compilePattern(key2, nil)
}
// RegexMatchFunc is the wrapper for RegexMatch.
@@ -485,7 +545,12 @@ func RegexMatchFunc(args ...interface{}) (interface{},
error) {
name1 := args[0].(string)
name2 := args[1].(string)
- return RegexMatch(name1, name2), nil
+ matched, err := regexCache.get(name2, buildRegexMatch).match(name1)
+ if err != nil {
+ return false, fmt.Errorf("%s: %w", "regexMatch", err)
+ }
+
+ return matched, nil
}
// IPMatch determines whether IP address ip1 matches the pattern of IP address
ip2, ip2 can be an IP address or a CIDR pattern.
diff --git a/util/builtin_operators_test.go b/util/builtin_operators_test.go
index a4990bb..096b394 100644
--- a/util/builtin_operators_test.go
+++ b/util/builtin_operators_test.go
@@ -295,6 +295,17 @@ func TestRegexMatch(t *testing.T) {
testRegexMatch(t, "/foo/delete/123", "/topic/delete/[0-9]+", false)
testRegexMatch(t, "/topic/delete/0", "/topic/delete/[0-9]+", true)
testRegexMatch(t, "/topic/edit/123s", "/topic/delete/[0-9]+", false)
+ testRegexMatch(t, "/topic/edit/1", "/topic/edit/[0-9", false)
+}
+
+func TestInvalidPatternMatchesNothing(t *testing.T) {
+ if KeyMatch2("/foo/(bar", "/foo/(bar") || KeyMatch3("/foo/(bar",
"/foo/(bar") ||
+ KeyMatch4("/foo/(bar", "/foo/(bar") || KeyMatch5("/foo/(bar",
"/foo/(bar") {
+ t.Error("an invalid pattern should match nothing")
+ }
+ if KeyGet2("/foo/(bar", "/:id/(bar", "id") != "" ||
KeyGet3("/foo/(bar", "/{id}/(bar", "id") != "" {
+ t.Error("an invalid pattern should get nothing")
+ }
}
func testIPMatch(t *testing.T, ip1 string, ip2 string, res bool) {
@@ -462,6 +473,8 @@ func TestRegexMatchFunc(t *testing.T) {
testRegexMatchFunc(t, false, "regexMatch: expected 2 arguments, but got
3", "/topic/create/123", "/topic/create", "/topic/update")
testRegexMatchFunc(t, false, "regexMatch: argument must be a string",
"/topic/create", false)
testRegexMatchFunc(t, true, "", "/topic/create/123", "/topic/create")
+ testRegexMatchFunc(t, false, "regexMatch: error parsing regexp: missing
closing ]: `[0-9`", "/topic/edit/1", "/topic/edit/[0-9")
+ testRegexMatchFunc(t, false, "regexMatch: error parsing regexp: missing
closing ): `(GET`", "GET", "(GET")
}
func TestKeyMatchFunc(t *testing.T) {
@@ -494,6 +507,7 @@ func TestKeyMatch2Func(t *testing.T) {
testKeyMatch2Func(t, true, "", "/foo", "/foo")
testKeyMatch2Func(t, true, "", "/foo", "/foo*")
testKeyMatch2Func(t, false, "", "/foo", "/foo/*")
+ testKeyMatch2Func(t, false, "keyMatch2: error parsing regexp: missing
closing ): `^/foo/(bar$`", "/foo/bar", "/foo/(bar")
}
func TestKeyGet2Func(t *testing.T) {
@@ -504,6 +518,7 @@ func TestKeyGet2Func(t *testing.T) {
testKeyGet2Func(t, "myid", "", "/myid/using/myresid",
"/:id/using/:resId", "id")
testKeyGet2Func(t, "myresid", "", "/myid/using/myresid",
"/:id/using/:resId", "resId")
testKeyGet2Func(t, "", "", "/myid/using/myresid", "/:id/using/:resId",
"missing")
+ testKeyGet2Func(t, "", "keyGet2: error parsing regexp: missing closing
): `^/([^/]+)/(using$`", "/myid/using", "/:id/(using", "id")
}
func TestKeyMatch3Func(t *testing.T) {
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]