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]

Reply via email to