mirror of
https://github.com/go-admin-team/go-admin.git
synced 2026-09-25 11:31:47 +00:00
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:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
Reference in New Issue
Block a user