Files
go-admin/common/middleware/permission.go
T
zhangwenjian 4156387eb9 fix🐛: enforce Casbin when editing another user
PUT /api/v1/sys-user sits in CasbinExclude so the profile page can reach it,
which means AuthCheckRole never runs for this route. The handler took the
target user id from the request body, so any authenticated caller could edit
another user's record - including their roleId.

The route has to stay excluded: the profile page and the admin user list share
this one endpoint, so removing the exclusion would break self-service editing
for every non-admin role. The check therefore moves into the handler: when the
target is not the caller, the request is put through Casbin explicitly.

EnforceRoleFor carries the same admin short-circuit and enforcement AuthCheckRole
uses, so a route that opts out of the middleware can still ask the same question.

Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx
2026-09-04 17:23:01 +08:00

117 lines
3.7 KiB
Go

package middleware
import (
"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"
"github.com/go-admin-team/go-admin-core/v2/sdk/api"
)
// AuthCheckRole 权限检查中间件
func AuthCheckRole() gin.HandlerFunc {
return func(c *gin.Context) {
log := api.GetRequestLogger(c)
data, _ := c.Get(jwtauth.JwtPayloadKey)
v := data.(jwtauth.MapClaims)
e := sdk.Runtime.GetCasbinByTenant(c.Request.Host)
var res, casbinExclude bool
var err error
//检查权限
if v["rolekey"] == "admin" {
res = true
c.Next()
return
}
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)
c.Next()
return
}
res, err = e.Enforce(v["rolekey"], c.Request.URL.Path, c.Request.Method)
if err != nil {
log.Errorf("AuthCheckRole error:%s method:%s path:%s", err, c.Request.Method, c.Request.URL.Path)
response.Error(c, 500, err, "")
return
}
if res {
log.Infof("isTrue: %v role: %s method: %s path: %s", res, v["rolekey"], c.Request.Method, c.Request.URL.Path)
c.Next()
} else {
log.Warnf("isTrue: %v role: %s method: %s path: %s message: %s", res, v["rolekey"], c.Request.Method, c.Request.URL.Path, "当前request无权限,请管理员确认!")
c.JSON(http.StatusOK, gin.H{
"code": 403,
"msg": "对不起,您没有该接口访问权限,请联系管理员",
})
c.Abort()
return
}
}
}
// EnforceRoleFor reports whether the caller's role has explicit Casbin
// permission to act on path with method.
//
// AuthCheckRole never calls Enforce for a route CasbinExclude lists - that
// is the whole point of the list. A handler on such a route can still need
// the real answer for part of what it does: sys_user.go's Update shares its
// excluded route between the personal-center screen editing the caller's own
// record (which is why the route is excluded at all) and an admin editing
// someone else's, and only the second case is meant to require a policy
// grant. That handler asks here instead of assuming the middleware already
// checked.
func EnforceRoleFor(c *gin.Context, path, method string) (bool, error) {
data, ok := c.Get(jwtauth.JwtPayloadKey)
if !ok {
return false, nil
}
v, ok := data.(jwtauth.MapClaims)
if !ok {
return false, nil
}
if v["rolekey"] == "admin" {
return true, nil
}
e := sdk.Runtime.GetCasbinByTenant(c.Request.Host)
return e.Enforce(v["rolekey"], path, method)
}
// 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
}