From 22716e90c1ad193106b9b550675eaf54f2bd71e2 Mon Sep 17 00:00:00 2001 From: zhangwenjian Date: Sat, 5 Sep 2026 21:24:25 +0800 Subject: [PATCH] =?UTF-8?q?fix=F0=9F=90=9B:=20/getinfo=20cannot=20be=20sco?= =?UTF-8?q?ped=20by=20a=20data=20permission=20it=20never=20receives?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Logging in on a deployment with enabledp: true ends on the login page. The login itself succeeds - sys_login_log records it - and then /api/v1/getinfo answers 401 "登录失败", which sends the browser straight back. The query behind it reads: SELECT * FROM sys_user WHERE sys_user.user_id = 1 AND 1 = 0 AND deleted_at = 0 The 1 = 0 comes from the data-permission scope. GetInfo asked for a permission with GetPermissionFromContext, but the group this route sits in installs only the JWT middleware - no PermissionAction - so nothing ever put one in the context and what came back was the zero value. An unset scope is not one of the five recognised ones, and since unknown scopes began failing closed rather than silently matching every row, that zero value now means "match nothing". The route was working by accident before, and only on deployments that enable data permissions: the repository default is enabledp: false, where Permission returns the query untouched. That is why the local suite and CI are both green and the demo site is not. Two different faults, so two different fixes: /getinfo reads the caller's own row - the id comes from the token. A data scope answers "whose rows may this user see", so there is nothing left for it to restrict, and applying one is not a stricter version of the query but a broken one: DataScopeSelf matches on create_by, and an account is created by whoever added it, so a scoped self-read would 401 every user who did not create their own account. It now goes through GetSelf, which does no scoping at all - which is how GetProfile has always read the same row. /sys-api is the opposite case. Its three handlers do read the permission, and they are listing and updating other people's rows, so the middleware belongs there and was simply missing. Added. Those four endpoints were found by checking every handler that reads the permission against the group it is registered on. The check reports four before this commit and none after. No test. Both paths need a *gorm.DB with sys_user and sys_role rows before they reach the line that matters, and this repository's CI has no database - `make build` is CGO_ENABLED=0 with no sqlite tag. What can be tested is the shape of the mistake rather than its effect, and that belongs in tools/checksilent as a rule of its own; it is not in this commit because a site that cannot be logged into should not wait for it. --- app/admin/apis/sys_user.go | 10 ++++++++-- app/admin/router/sys_api.go | 6 +++++- app/admin/service/sys_user.go | 24 ++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/app/admin/apis/sys_user.go b/app/admin/apis/sys_user.go index 07b8c6dd..5d595dea 100644 --- a/app/admin/apis/sys_user.go +++ b/app/admin/apis/sys_user.go @@ -444,7 +444,6 @@ func (e SysUser) GetInfo(c *gin.Context) { e.Error(500, err, err.Error()) return } - p := actions.GetPermissionFromContext(c) var roles = make([]string, 1) roles[0] = user.GetRoleName(c) var permissions = make([]string, 1) @@ -464,7 +463,14 @@ func (e SysUser) GetInfo(c *gin.Context) { } sysUser := models.SysUser{} req.Id = user.GetUserId(c) - err = s.Get(&req, p, &sysUser) + // Unscoped on purpose: the id is the caller's own, taken from the token. + // This used to go through Get with whatever GetPermissionFromContext + // returned - and this route installs no PermissionAction, so that was the + // zero value. An unset scope is not a recognised one, so once unknown + // scopes started failing closed rather than silently matching everything, + // every login on a deployment with enabledp: true ended here with a 401 + // and the browser went straight back to the login page. + err = s.GetSelf(&req, &sysUser) if err != nil { e.Error(http.StatusUnauthorized, err, "登录失败") return diff --git a/app/admin/router/sys_api.go b/app/admin/router/sys_api.go index 1eba4c38..39586cf2 100644 --- a/app/admin/router/sys_api.go +++ b/app/admin/router/sys_api.go @@ -5,6 +5,7 @@ import ( jwt "github.com/go-admin-team/go-admin-core/v2/jwtauth" "go-admin/app/admin/apis" + "go-admin/common/actions" "go-admin/common/middleware" ) @@ -15,7 +16,10 @@ func init() { // registerSysApiRouter func registerSysApiRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) { api := apis.SysApi{} - r := v1.Group("/sys-api").Use(authMiddleware.MiddlewareFunc()).Use(middleware.AuthCheckRole()) + // PermissionAction is not optional here: all three handlers below read the + // data permission out of the context, and without it they read the zero + // value - an unset scope, which Permission now fails closed on. + r := v1.Group("/sys-api").Use(authMiddleware.MiddlewareFunc()).Use(middleware.AuthCheckRole()).Use(actions.PermissionAction()) { r.GET("", api.GetPage) r.GET("/:id", api.Get) diff --git a/app/admin/service/sys_user.go b/app/admin/service/sys_user.go index 839a9b3b..320be907 100644 --- a/app/admin/service/sys_user.go +++ b/app/admin/service/sys_user.go @@ -38,6 +38,30 @@ func (e *SysUser) GetPage(c *dto.SysUserGetPageReq, p *actions.DataPermission, l return nil } +// GetSelf 获取调用者自己的 SysUser 对象,不套数据权限 +// +// The data scope answers "whose rows may this user see"; the caller here is +// reading their own, and the id comes from the token, so there is nothing left +// for a scope to restrict. Applying one is not a stricter version of this +// query - it is a broken one. DataScopeSelf matches on create_by, and a user +// account is created by whoever added it, so a scoped self-read would fail for +// every user who did not create their own account. +// +// GetProfile has always read the same row this way, with no scope at all. +func (e *SysUser) GetSelf(d *dto.SysUserById, model *models.SysUser) error { + err := e.Orm.First(model, d.GetId()).Error + if err != nil && errors.Is(err, gorm.ErrRecordNotFound) { + err = errors.New("查看对象不存在或无权查看") + e.Log.Errorf("db error: %s", err) + return err + } + if err != nil { + e.Log.Errorf("db error: %s", err) + return err + } + return nil +} + // Get 获取SysUser对象 func (e *SysUser) Get(d *dto.SysUserById, p *actions.DataPermission, model *models.SysUser) error { var data models.SysUser