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 c3d23f4 fix: look up pattern tokens by group name in KeyMatch4 and
KeyGet2/3 (#1767)
c3d23f4 is described below
commit c3d23f48e6939bae5497d3d07b0ffcce11b0928e
Author: Arshil <[email protected]>
AuthorDate: Sat Oct 31 07:22:23 2026 +0530
fix: look up pattern tokens by group name in KeyMatch4 and KeyGet2/3 (#1767)
---
error_test.go | 22 ++++++++++++++
util/builtin_operators.go | 65 +++++++++++++++++++++++++++++++-----------
util/builtin_operators_test.go | 16 ++++++++++-
3 files changed, 86 insertions(+), 17 deletions(-)
diff --git a/error_test.go b/error_test.go
index c2f34c7..5ee2d40 100644
--- a/error_test.go
+++ b/error_test.go
@@ -319,3 +319,25 @@ func TestInvalidPatternInPolicy(t *testing.T) {
t.Errorf("GetNamedImplicitRolesForUser returns %v %v, supposed
to be [book_group]", roles, err)
}
}
+
+func TestPatternWithCaptureGroupInPolicy(t *testing.T) {
+ e, err := NewEnforcer("examples/rbac_with_pattern_model.conf")
+ if err != nil {
+ t.Fatal(err)
+ }
+ e.AddNamedMatchingFunc("g2", "matchingFunc", util.KeyMatch4)
+ if _, err = e.AddPolicy("alice", "book_group", "GET"); err != nil {
+ t.Fatal(err)
+ }
+ if _, err = e.AddNamedGroupingPolicy("g2",
"/(book|pen)/{id}/copy/{id}", "book_group"); err != nil {
+ t.Fatal(err)
+ }
+
+ testEnforce(t, e, "alice", "/book/1/copy/1", "GET", true)
+ testEnforce(t, e, "alice", "/pen/1/copy/1", "GET", true)
+ testEnforce(t, e, "alice", "/book/1/copy/2", "GET", false)
+ testEnforce(t, e, "alice", "/cup/1/copy/1", "GET", false)
+ if roles, err := e.GetNamedImplicitRolesForUser("g2",
"/book/1/copy/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 9a846d0..87ed875 100644
--- a/util/builtin_operators.go
+++ b/util/builtin_operators.go
@@ -22,6 +22,7 @@ import (
"fmt"
"net"
"regexp"
+ "strconv"
"strings"
"sync"
"time"
@@ -84,7 +85,11 @@ func keyMatchShortcut(key1 string, key2 string, extraChars
string) (matched bool
// compiledPattern is the fully prepared form of a matcher's key2: the regexp
that
// key1 is tested against, plus the names of the pattern variables in the order
-// their capture groups appear (only KeyGet2/KeyGet3/KeyMatch4 use tokens).
+// they appear and the submatch index of each one's capture group (only
+// KeyGet2/KeyGet3/KeyMatch4 use tokens).
+//
+// The indices are looked up by group name rather than assumed to be 1..n,
because
+// key2 can carry capture groups of its own, e.g. "/(a|b)/{id}".
//
// Everything in it is a pure function of key2, so it is built once per
distinct
// key2 and reused afterwards. A key2 that does not compile keeps its error
here,
@@ -92,12 +97,43 @@ func keyMatchShortcut(key1 string, key2 string, extraChars
string) (matched bool
type compiledPattern struct {
re *regexp.Regexp
tokens []string
+ groups []int
err error
}
+// tokenGroup returns the named capture group that stands in for the i-th
pattern
+// variable; compilePattern finds it again by tokenGroupName.
+func tokenGroup(i int, expr string) string {
+ return "(?P<" + tokenGroupName(i) + ">" + expr + ")"
+}
+
+func tokenGroupName(i int) string {
+ return "casbinToken" + strconv.Itoa(i)
+}
+
func compilePattern(expr string, tokens []string) *compiledPattern {
re, err := regexp.Compile(expr)
- return &compiledPattern{re: re, tokens: tokens, err: err}
+ if err != nil {
+ return &compiledPattern{tokens: tokens, err: err}
+ }
+
+ groups := make([]int, len(tokens))
+ for i := range tokens {
+ groups[i] = re.SubexpIndex(tokenGroupName(i))
+ }
+
+ return &compiledPattern{re: re, tokens: tokens, groups: groups}
+}
+
+// replaceTokens replaces every match of tokenRe in key2 with a numbered
+// tokenGroup around expr.
+func replaceTokens(key2 string, tokenRe *regexp.Regexp, expr string) string {
+ i := 0
+ return tokenRe.ReplaceAllStringFunc(key2, func(string) string {
+ group := tokenGroup(i, expr)
+ i++
+ return group
+ })
}
func (p *compiledPattern) match(key1 string) (bool, error) {
@@ -273,13 +309,13 @@ func keyGet(p *compiledPattern, key1 string, pathVar
string) (string, error) {
return "", p.err
}
- values := p.re.FindAllStringSubmatch(key1, -1)
- if len(values) == 0 {
+ values := p.re.FindStringSubmatch(key1)
+ if values == nil {
return "", nil
}
for i, key := range p.tokens {
if pathVar == key {
- return values[0][i+1], nil
+ return values[p.groups[i]], nil
}
}
return "", nil
@@ -294,7 +330,7 @@ func buildKeyGet2(key2 string) *compiledPattern {
tokens[i] = token[1:]
}
- key2 = keyGet2Re1.ReplaceAllString(key2, "$1([^/]+)$2")
+ key2 = replaceTokens(key2, keyGet2Re1, "[^/]+")
return compilePattern("^"+key2+"$", tokens)
}
@@ -376,7 +412,7 @@ func buildKeyGet3(key2 string) *compiledPattern {
tokens[i] = token[1 : len(token)-1]
}
- key2 = keyGet3Re1.ReplaceAllString(key2, "$1([^/]+?)$2")
+ key2 = replaceTokens(key2, keyGet3Re1, "[^/]+?")
return compilePattern("^"+key2+"$", tokens)
}
@@ -424,19 +460,15 @@ func keyMatch4(key1 string, key2 string) (bool, error) {
if matches == nil {
return false, nil
}
- matches = matches[1:]
-
- if len(p.tokens) != len(matches) {
- panic(errors.New("KeyMatch4: number of tokens is not equal to
number of values"))
- }
values := map[string]string{}
- for key, token := range p.tokens {
+ for i, token := range p.tokens {
+ match := matches[p.groups[i]]
if _, ok := values[token]; !ok {
- values[token] = matches[key]
+ values[token] = match
}
- if values[token] != matches[key] {
+ if values[token] != match {
return false, nil
}
}
@@ -450,8 +482,9 @@ func buildKeyMatch4(key2 string) *compiledPattern {
tokens := []string{}
key2 = keyMatch4Re.ReplaceAllStringFunc(key2, func(s string) string {
+ group := tokenGroup(len(tokens), "[^/]+")
tokens = append(tokens, s[1:len(s)-1])
- return "([^/]+)"
+ return group
})
return compilePattern("^"+key2+"$", tokens)
diff --git a/util/builtin_operators_test.go b/util/builtin_operators_test.go
index 096b394..c9445d3 100644
--- a/util/builtin_operators_test.go
+++ b/util/builtin_operators_test.go
@@ -160,6 +160,11 @@ func TestKeyGet2(t *testing.T) {
testKeyGet2(t, "/alice/all", "/:id", "id", "")
testKeyGet2(t, "/alice/all", "/:/all", "", "")
+
+ // Capture groups of the pattern's own must not shift which value a
token gets.
+ testKeyGet2(t, "/a/x/42", "/(a|b)/x/:id", "id", "42")
+ testKeyGet2(t, "/42/a", "/:id/(a|b)", "id", "42")
+ testKeyGet2(t, "/b/7/c/9", "/(a|b)/:x/(c|d)/:y", "y", "9")
}
func testKeyMatch3(t *testing.T, key1 string, key2 string, res bool) {
@@ -247,6 +252,10 @@ func TestKeyGet3(t *testing.T) {
"gn", "group_name")
testKeyGet3(t, "/api/group1_group_name/project1_admin/info",
"/api/{g}_{gn}/{proj}_admin/info",
"proj", "project1")
+
+ testKeyGet3(t, "/a/x/42", "/(a|b)/x/{id}", "id", "42")
+ testKeyGet3(t, "/42/a", "/{id}/(a|b)", "id", "42")
+ testKeyGet3(t, "/b/7/c/9", "/(a|b)/{x}/(c|d)/{y}", "y", "9")
}
func testKeyMatch4(t *testing.T, key1 string, key2 string, res bool) {
@@ -273,6 +282,11 @@ func TestKeyMatch4(t *testing.T) {
testKeyMatch4(t, "/parent/123/child/456",
"/parent/{id}/child/{id}/book/{id}", false)
testKeyMatch4(t, "/parent/123/child/123", "/parent/{i/d}/child/{i/d}",
false)
+
+ testKeyMatch4(t, "/a/123/child/123", "/(a|b)/{id}/child/{id}", true)
+ testKeyMatch4(t, "/a/123/child/456", "/(a|b)/{id}/child/{id}", false)
+ testKeyMatch4(t, "/c/123/child/123", "/(a|b)/{id}/child/{id}", false)
+ testKeyMatch4(t, "/123/x/123", "/{id}/(x|y)/{id}", true)
}
func testRegexMatch(t *testing.T, key1 string, key2 string, res bool) {
@@ -518,7 +532,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")
+ testKeyGet2Func(t, "", "keyGet2: error parsing regexp: missing closing
): `^/(?P<casbinToken0>[^/]+)/(using$`", "/myid/using", "/:id/(using", "id")
}
func TestKeyMatch3Func(t *testing.T) {
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]