Merge pull request #885 from go-admin-team/fix/casbin-tenant-and-pattern-cache

fix: key the casbin enforcer by tenant, and stop recompiling patterns in the exclusion scan
This commit is contained in:
wenjianzhang
2026-08-31 15:01:42 +08:00
committed by GitHub
5 changed files with 121 additions and 10 deletions
+5 -1
View File
@@ -63,7 +63,11 @@ func setupSimpleDatabase(host string, c *toolsConfig.Database) {
log.Info(pkg.Green(c.Driver + " connect success !"))
}
e := mycasbin.Setup(db, "")
// Keyed by host, matching the database this enforcer reads from. Passing
// the same key for every host would hand each one the enforcer built from
// whichever database was configured first, and the rest would be decided
// by a casbin_rule table that is not theirs.
e := mycasbin.Setup(db, host)
sdk.Runtime.SetDbByTenant(host, db)
sdk.Runtime.SetCasbinByTenant(host, e)
+34 -6
View File
@@ -1,10 +1,11 @@
package middleware
import (
"github.com/casbin/casbin/v3/util"
"fmt"
"net/http"
"github.com/gin-gonic/gin"
mycasbin "github.com/go-admin-team/go-admin-core/v2/casbin"
"github.com/go-admin-team/go-admin-core/v2/jwtauth"
"github.com/go-admin-team/go-admin-core/v2/response"
"github.com/go-admin-team/go-admin-core/v2/sdk"
@@ -26,11 +27,9 @@ func AuthCheckRole() gin.HandlerFunc {
c.Next()
return
}
for _, i := range CasbinExclude {
if util.KeyMatch2(c.Request.URL.Path, i.Url) && c.Request.Method == i.Method {
casbinExclude = true
break
}
casbinExclude, err = excludedFromCasbin(c.Request.Method, c.Request.URL.Path)
if err != nil {
log.Errorf("AuthCheckRole: %s", err)
}
if casbinExclude {
log.Infof("Casbin exclusion, no validation method:%s path:%s", c.Request.Method, c.Request.URL.Path)
@@ -59,3 +58,32 @@ func AuthCheckRole() gin.HandlerFunc {
}
}
// excludedFromCasbin reports whether the route skips the permission check.
//
// It runs for every non-admin request, so the order matters: the method rules
// out most entries with a string compare, where the path test costs a pattern
// match. mycasbin.KeyMatch2 answers what casbin's util.KeyMatch2 answers
// without recompiling the pattern every time, which is what made this loop
// expensive - about 2,500 allocations per request against a 32-entry list.
//
// A pattern that will not compile is a bug in CasbinExclude rather than in the
// request, so the entry is skipped and the scan continues; the error comes
// back for the caller to log.
func excludedFromCasbin(method, path string) (bool, error) {
var bad error
for _, i := range CasbinExclude {
if method != i.Method {
continue
}
ok, err := mycasbin.KeyMatch2(path, i.Url)
if err != nil {
bad = fmt.Errorf("CasbinExclude entry %q is not a valid pattern: %w", i.Url, err)
continue
}
if ok {
return true, bad
}
}
return false, bad
}
+79
View File
@@ -0,0 +1,79 @@
package middleware
import "testing"
// excluded is excludedFromCasbin with the error dropped: these tests are about
// the answer and its cost, and CasbinExclude has no malformed entry to report.
func excluded(t testing.TB, path, method string) bool {
t.Helper()
ok, err := excludedFromCasbin(method, path)
if err != nil {
t.Fatalf("CasbinExclude holds a pattern that will not compile: %s", err)
}
return ok
}
// TestCasbinExcludeScanMatches pins the behaviour the scan has to keep: an
// excluded route is recognised, a protected one is not, and the method has to
// agree.
func TestCasbinExcludeScanMatches(t *testing.T) {
cases := []struct {
path, method string
want bool
}{
{"/api/v1/health", "GET", true},
{"/api/v1/login", "POST", true},
{"/api/v1/roleMenuTreeselect/12", "GET", true},
{"/api/v1/dept", "GET", false},
{"/api/v1/sys-user", "GET", false},
// Same path, wrong method: sys-user is excluded for PUT only.
{"/api/v1/sys-user", "PUT", true},
{"/api/v1/health", "POST", false},
}
for _, c := range cases {
if got := excluded(t, c.path, c.method); got != c.want {
t.Errorf("excludedFromCasbin(%s %s) = %v, want %v", c.method, c.path, got, c.want)
}
}
}
// TestCasbinExcludeScanAllocationBudget is what keeps the scan cheap.
//
// The list is walked per request with a pattern match per entry, and
// casbin's util.KeyMatch2 compiles a regexp on every call - the whole scan
// cost about 2,566 allocations that way. Going back to it fails this test.
//
// Allocation counts are deterministic across machines; wall-clock is not.
func TestCasbinExcludeScanAllocationBudget(t *testing.T) {
// A protected route, so the scan runs to the end without an early match -
// the case every authenticated business request hits.
const path, method = "/api/v1/dept", "GET"
if excluded(t, path, method) {
t.Fatalf("setup failed: %s is in the exclusion list", path)
}
// The budget covers the GET entries that carry a path parameter, which
// still need a match. Measured at 0 for the cached matcher; the headroom
// is for entries being added to the list.
const budget = 64
got := testing.AllocsPerRun(100, func() {
_, _ = excludedFromCasbin(method, path)
})
if got > budget {
t.Errorf("scanning CasbinExclude allocates %.0f times, budget is %d\n"+
"casbin's util.KeyMatch2 costs about 2566 here; use mycasbin.KeyMatch2",
got, budget)
}
}
// BenchmarkCasbinExcludeScan reports what the scan adds to a request.
func BenchmarkCasbinExcludeScan(b *testing.B) {
b.ReportAllocs()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_, _ = excludedFromCasbin("GET", "/api/v1/dept")
}
})
}
+1 -1
View File
@@ -11,7 +11,7 @@ require (
github.com/casbin/casbin/v3 v3.8.1
github.com/gin-gonic/gin v1.12.0
github.com/glebarez/sqlite v1.11.0
github.com/go-admin-team/go-admin-core/v2 v2.2.0
github.com/go-admin-team/go-admin-core/v2 v2.3.0
github.com/google/uuid v1.6.0
github.com/huaweicloud/huaweicloud-sdk-go-obs v3.26.6+incompatible
github.com/mssola/user_agent v0.6.0
+2 -2
View File
@@ -145,8 +145,8 @@ github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
github.com/go-admin-team/go-admin-core/v2 v2.2.0 h1:8aJk9q5RQ+T0a6gs95Ay93qzGUc5ilXv90RbXPiwnEI=
github.com/go-admin-team/go-admin-core/v2 v2.2.0/go.mod h1:YiJr2+vqC9qV5AoGeL+1W55h3XZ99CB5xWnP3Wo8c5g=
github.com/go-admin-team/go-admin-core/v2 v2.3.0 h1:P2Po+jsJByzv1y2cN8eEGfFOutWSYNzQTJxLGAFmHkE=
github.com/go-admin-team/go-admin-core/v2 v2.3.0/go.mod h1:YiJr2+vqC9qV5AoGeL+1W55h3XZ99CB5xWnP3Wo8c5g=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o=