From 1f8cae5a8b6cbc39b4002c7c822624e88c4c8b03 Mon Sep 17 00:00:00 2001 From: Odboy Date: Wed, 16 Sep 2026 21:11:09 +0800 Subject: [PATCH] =?UTF-8?q?refactor(auth):=20=E7=A7=BB=E9=99=A4=20RO/RW=20?= =?UTF-8?q?=E6=9D=83=E9=99=90=E6=9C=BA=E5=88=B6=E5=B9=B6=E5=AE=8C=E5=96=84?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E5=8E=86=E5=8F=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除 GrantPermissons 与 buildPermissionCode,权限只判断超管、组管理员和应用负责人 - GetHistory 增加权限校验,避免越权读取配置历史 - 配置历史改为分页查询,前端历史弹窗增加分页并处理删除后的页码回退 --- server/internal/handler/config.go | 62 +++++++++++---- server/internal/model/user.go | 13 ++- server/internal/service/auth.go | 27 +++---- server/internal/service/permission.go | 9 +-- server/internal/service/permission_test.go | 93 ++++++++++++++++++++++ web/src/api/config.js | 5 +- web/src/views/config/AllConfigList.vue | 31 +++++++- web/src/views/config/ConfigList.vue | 31 +++++++- 8 files changed, 217 insertions(+), 54 deletions(-) create mode 100644 server/internal/service/permission_test.go diff --git a/server/internal/handler/config.go b/server/internal/handler/config.go index 0daa12d..b0cce72 100644 --- a/server/internal/handler/config.go +++ b/server/internal/handler/config.go @@ -149,7 +149,7 @@ func (h *ConfigHandler) GetById(c *gin.Context) { } userInfo := middleware.GetUserInfo(c) - if err := h.permService.RequirePermission(userInfo, config.GroupId, config.AppId, "RO"); err != nil { + if err := h.permService.RequirePermission(userInfo, config.GroupId, config.AppId); err != nil { c.JSON(http.StatusForbidden, model.WrapperResponse{Code: 403, Msg: err.Error()}) return } @@ -183,7 +183,7 @@ func (h *ConfigHandler) Add(c *gin.Context) { c.JSON(http.StatusOK, model.WrapperResponse{Code: 400, Msg: "全局配置需要指定所属业务组"}) return } - if err := h.permService.RequirePermission(userInfo, req.GroupId, nil, "RW"); err != nil { + if err := h.permService.RequirePermission(userInfo, req.GroupId, nil); err != nil { c.JSON(http.StatusForbidden, model.WrapperResponse{Code: 403, Msg: err.Error()}) return } @@ -264,7 +264,7 @@ func (h *ConfigHandler) Update(c *gin.Context) { } userInfo := middleware.GetUserInfo(c) - if err := h.permService.RequirePermission(userInfo, config.GroupId, config.AppId, "RW"); err != nil { + if err := h.permService.RequirePermission(userInfo, config.GroupId, config.AppId); err != nil { c.JSON(http.StatusForbidden, model.WrapperResponse{Code: 403, Msg: err.Error()}) return } @@ -311,7 +311,7 @@ func (h *ConfigHandler) Delete(c *gin.Context) { } userInfo := middleware.GetUserInfo(c) - if err := h.permService.RequirePermission(userInfo, config.GroupId, config.AppId, "RW"); err != nil { + if err := h.permService.RequirePermission(userInfo, config.GroupId, config.AppId); err != nil { c.JSON(http.StatusForbidden, model.WrapperResponse{Code: 403, Msg: err.Error()}) return } @@ -335,19 +335,51 @@ func (h *ConfigHandler) Delete(c *gin.Context) { func (h *ConfigHandler) GetHistory(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) - var histories []model.AppConfigHistory - dao.Engine.Where("origin_id = ?", id).OrderBy("id desc").Find(&histories) - if len(histories) > 0 { - var currentConfig model.AppConfig - if has, _ := dao.Engine.ID(histories[0].OriginId).Get(¤tConfig); has { - for i := range histories { - histories[i].ActiveContents = currentConfig.Contents - } + var config model.AppConfig + has, _ := dao.Engine.ID(id).Get(&config) + + userInfo := middleware.GetUserInfo(c) + if has { + if err := h.permService.RequirePermission(userInfo, config.GroupId, config.AppId); err != nil { + c.JSON(http.StatusForbidden, model.WrapperResponse{Code: 403, Msg: err.Error()}) + return + } + } else if !userInfo.IsSuperAdmin() { + // 配置已被物理删除时,只有超级管理员可以查看残留历史 + c.JSON(http.StatusForbidden, model.WrapperResponse{Code: 403, Msg: "只有超级管理员才能查看该配置历史"}) + return + } + + pageNo, _ := strconv.Atoi(c.DefaultQuery("pageNo", "1")) + pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10")) + if pageNo < 1 { + pageNo = 1 + } + if pageSize < 1 { + pageSize = 10 + } + if pageSize > 200 { + pageSize = 200 + } + + histories := make([]model.AppConfigHistory, 0) + total, _ := dao.Engine.Where("origin_id = ?", id).Count(&model.AppConfigHistory{}) + dao.Engine.Where("origin_id = ?", id).OrderBy("id desc"). + Limit(pageSize, (pageNo-1)*pageSize).Find(&histories) + + if has { + for i := range histories { + histories[i].ActiveContents = config.Contents } } - c.JSON(http.StatusOK, model.WrapperResponse{Code: 0, Data: histories}) + c.JSON(http.StatusOK, model.PageResult{ + PageNo: pageNo, + PageSize: pageSize, + Total: total, + Data: histories, + }) } // DeleteHistory 手动删除单条配置历史 @@ -363,7 +395,7 @@ func (h *ConfigHandler) DeleteHistory(c *gin.Context) { userInfo := middleware.GetUserInfo(c) var config model.AppConfig if has, _ := dao.Engine.ID(history.OriginId).Get(&config); has { - if err := h.permService.RequirePermission(userInfo, config.GroupId, config.AppId, "RW"); err != nil { + if err := h.permService.RequirePermission(userInfo, config.GroupId, config.AppId); err != nil { c.JSON(http.StatusForbidden, model.WrapperResponse{Code: 403, Msg: err.Error()}) return } @@ -394,7 +426,7 @@ func (h *ConfigHandler) Rollback(c *gin.Context) { var config model.AppConfig if has, _ := dao.Engine.ID(history.OriginId).Get(&config); has { userInfo := middleware.GetUserInfo(c) - if err := h.permService.RequirePermission(userInfo, config.GroupId, config.AppId, "RW"); err != nil { + if err := h.permService.RequirePermission(userInfo, config.GroupId, config.AppId); err != nil { c.JSON(http.StatusForbidden, model.WrapperResponse{Code: 403, Msg: err.Error()}) return } diff --git a/server/internal/model/user.go b/server/internal/model/user.go index 0dbb9c1..b634bd0 100644 --- a/server/internal/model/user.go +++ b/server/internal/model/user.go @@ -23,13 +23,12 @@ func (User) TableName() string { } type LoginUserInfo struct { - Id string `json:"id"` - Name string `json:"name"` - Type string `json:"type"` - GroupId *int `json:"groupId"` - GrantAppIds []int `json:"grantAppIds"` - GrantPermissons []string `json:"grantPermissons"` - GroupMaster bool `json:"groupMaster"` + Id string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + GroupId *int `json:"groupId"` + GrantAppIds []int `json:"grantAppIds"` + GroupMaster bool `json:"groupMaster"` } func (u *LoginUserInfo) IsSuperAdmin() bool { diff --git a/server/internal/service/auth.go b/server/internal/service/auth.go index 3d9973d..4027e2a 100644 --- a/server/internal/service/auth.go +++ b/server/internal/service/auth.go @@ -45,12 +45,11 @@ func (s *AuthService) Login(userName, password string) (string, *model.LoginUser } userInfo := &model.LoginUserInfo{ - Id: strconv.Itoa(user.Id), - Name: user.Name, - Type: user.Type, - GroupId: user.GroupId, - GrantAppIds: make([]int, 0), - GrantPermissons: make([]string, 0), + Id: strconv.Itoa(user.Id), + Name: user.Name, + Type: user.Type, + GroupId: user.GroupId, + GrantAppIds: make([]int, 0), } // load permissions @@ -120,12 +119,11 @@ func (s *AuthService) ValidateToken(tokenString string) (*model.LoginUserInfo, e } userInfo := &model.LoginUserInfo{ - Id: strconv.Itoa(user.Id), - Name: user.Name, - Type: user.Type, - GroupId: user.GroupId, - GrantAppIds: make([]int, 0), - GrantPermissons: make([]string, 0), + Id: strconv.Itoa(user.Id), + Name: user.Name, + Type: user.Type, + GroupId: user.GroupId, + GrantAppIds: make([]int, 0), } s.loadPermissions(userInfo, user.Id) return userInfo, nil @@ -146,10 +144,5 @@ func (s *AuthService) loadPermissions(userInfo *model.LoginUserInfo, userId int) } for _, app := range apps { userInfo.GrantAppIds = append(userInfo.GrantAppIds, app.Id) - userInfo.GrantPermissons = append(userInfo.GrantPermissons, buildPermissionCode(app.Id, "RW")) } } - -func buildPermissionCode(appId int, operate string) string { - return strconv.Itoa(appId) + ":" + operate -} diff --git a/server/internal/service/permission.go b/server/internal/service/permission.go index 024f6e5..b2f30e0 100644 --- a/server/internal/service/permission.go +++ b/server/internal/service/permission.go @@ -18,19 +18,18 @@ func (s *PermissionService) RequireSuperAdmin(userInfo *model.LoginUserInfo) err return nil } -func (s *PermissionService) RequirePermission(userInfo *model.LoginUserInfo, groupId *int, appId *int, operate string) error { +func (s *PermissionService) RequirePermission(userInfo *model.LoginUserInfo, groupId *int, appId *int) error { if userInfo.IsSuperAdmin() { return nil } - if groupId != nil && userInfo.GroupId != nil && *groupId == *userInfo.GroupId { + if userInfo.IsGroupAdmin() && groupId != nil && userInfo.GroupId != nil && *groupId == *userInfo.GroupId { return nil } if appId == nil || *appId == 0 { return errors.New("你没有全局配置权限") } - permCode := buildPermissionCode(*appId, operate) - for _, p := range userInfo.GrantPermissons { - if p == permCode { + for _, id := range userInfo.GrantAppIds { + if id == *appId { return nil } } diff --git a/server/internal/service/permission_test.go b/server/internal/service/permission_test.go new file mode 100644 index 0000000..7f2348e --- /dev/null +++ b/server/internal/service/permission_test.go @@ -0,0 +1,93 @@ +package service + +import ( + "testing" + + "kenaito-conf/server/internal/model" +) + +func TestRequirePermission(t *testing.T) { + svc := NewPermissionService() + groupID := 1 + otherGroupID := 2 + appID := 10 + + tests := []struct { + name string + user *model.LoginUserInfo + groupId *int + appId *int + wantErr bool + }{ + { + name: "super admin can access anything", + user: &model.LoginUserInfo{Type: "superAdmin"}, + groupId: &groupID, + appId: &appID, + }, + { + name: "group admin can access own group", + user: &model.LoginUserInfo{Type: "groupAdmin", GroupId: &groupID}, + groupId: &groupID, + appId: &appID, + }, + { + name: "group admin cannot access other group", + user: &model.LoginUserInfo{Type: "groupAdmin", GroupId: &groupID}, + groupId: &otherGroupID, + appId: &appID, + wantErr: true, + }, + { + name: "owner can access app", + user: &model.LoginUserInfo{Type: "user", GroupId: &groupID, GrantAppIds: []int{appID}}, + groupId: &groupID, + appId: &appID, + }, + { + name: "owner can access app across group", + user: &model.LoginUserInfo{Type: "user", GroupId: &otherGroupID, GrantAppIds: []int{appID}}, + groupId: &groupID, + appId: &appID, + }, + { + name: "normal group member cannot access app without ownership", + user: &model.LoginUserInfo{Type: "user", GroupId: &groupID}, + groupId: &groupID, + appId: &appID, + wantErr: true, + }, + { + name: "normal user cannot access global config", + user: &model.LoginUserInfo{Type: "user", GroupId: &groupID}, + groupId: &groupID, + appId: nil, + wantErr: true, + }, + { + name: "group admin can access own global config", + user: &model.LoginUserInfo{Type: "groupAdmin", GroupId: &groupID}, + groupId: &groupID, + appId: nil, + }, + { + name: "group admin cannot access other global config", + user: &model.LoginUserInfo{Type: "groupAdmin", GroupId: &groupID}, + groupId: &otherGroupID, + appId: nil, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := svc.RequirePermission(tt.user, tt.groupId, tt.appId) + if tt.wantErr && err == nil { + t.Fatalf("expected an error, got nil") + } + if !tt.wantErr && err != nil { + t.Fatalf("unexpected error: %v", err) + } + }) + } +} diff --git a/web/src/api/config.js b/web/src/api/config.js index f39405c..6882ae8 100644 --- a/web/src/api/config.js +++ b/web/src/api/config.js @@ -38,10 +38,11 @@ export function deleteConfig(id) { }) } -export function getConfigHistory(id) { +export function getConfigHistory(id, params) { return request({ url: `/admin/config/configHistories/${id}`, - method: 'get' + method: 'get', + params }) } diff --git a/web/src/views/config/AllConfigList.vue b/web/src/views/config/AllConfigList.vue index 9a04936..965a905 100644 --- a/web/src/views/config/AllConfigList.vue +++ b/web/src/views/config/AllConfigList.vue @@ -86,6 +86,15 @@ + @@ -247,6 +256,8 @@ export default { apps: [], historyVisible: false, historyList: [], + historyTotal: 0, + historyQuery: { pageNo: 1, pageSize: 10 }, currentConfig: null, contentVisible: false, currentHistory: {}, @@ -316,19 +327,31 @@ export default { }, async showHistory(row) { this.currentConfig = row + this.historyQuery.pageNo = 1 + await this.loadHistory() + this.historyVisible = true + }, + async loadHistory() { + if (!this.currentConfig) return try { - const res = await getConfigHistory(row.id) + const res = await getConfigHistory(this.currentConfig.id, this.historyQuery) this.historyList = res.data || [] - this.historyVisible = true + this.historyTotal = res.total || 0 } catch (e) {} }, + handleHistoryPageChange(page) { + this.historyQuery.pageNo = page + this.loadHistory() + }, async handleDeleteHistory(row) { try { await this.$confirm('确定删除这条历史记录?删除后不可恢复。') await deleteConfigHistory(row.id) this.$message.success('删除成功') - const res = await getConfigHistory(row.originId) - this.historyList = res.data || [] + if (this.historyList.length === 1 && this.historyQuery.pageNo > 1) { + this.historyQuery.pageNo-- + } + await this.loadHistory() } catch (e) {} }, async viewHistoryContent(row) { diff --git a/web/src/views/config/ConfigList.vue b/web/src/views/config/ConfigList.vue index aa3e2c2..60ee422 100644 --- a/web/src/views/config/ConfigList.vue +++ b/web/src/views/config/ConfigList.vue @@ -81,6 +81,15 @@ + @@ -241,6 +250,8 @@ export default { profiles: [], historyVisible: false, historyList: [], + historyTotal: 0, + historyQuery: { pageNo: 1, pageSize: 10 }, currentConfig: null, contentVisible: false, currentHistory: {}, @@ -302,19 +313,31 @@ export default { }, async showHistory(row) { this.currentConfig = row + this.historyQuery.pageNo = 1 + await this.loadHistory() + this.historyVisible = true + }, + async loadHistory() { + if (!this.currentConfig) return try { - const res = await getConfigHistory(row.id) + const res = await getConfigHistory(this.currentConfig.id, this.historyQuery) this.historyList = res.data || [] - this.historyVisible = true + this.historyTotal = res.total || 0 } catch (e) {} }, + handleHistoryPageChange(page) { + this.historyQuery.pageNo = page + this.loadHistory() + }, async handleDeleteHistory(row) { try { await this.$confirm('确定删除这条历史记录?删除后不可恢复。') await deleteConfigHistory(row.id) this.$message.success('删除成功') - const res = await getConfigHistory(row.originId) - this.historyList = res.data || [] + if (this.historyList.length === 1 && this.historyQuery.pageNo > 1) { + this.historyQuery.pageNo-- + } + await this.loadHistory() } catch (e) {} }, async viewHistoryContent(row) {