refactor(auth): 移除 RO/RW 权限机制并完善配置历史

- 删除 GrantPermissons 与 buildPermissionCode,权限只判断超管、组管理员和应用负责人
- GetHistory 增加权限校验,避免越权读取配置历史
- 配置历史改为分页查询,前端历史弹窗增加分页并处理删除后的页码回退
This commit is contained in:
2026-09-16 21:11:09 +08:00
parent af3cf2f6e6
commit 1f8cae5a8b
8 changed files with 217 additions and 54 deletions
+46 -14
View File
@@ -149,7 +149,7 @@ func (h *ConfigHandler) GetById(c *gin.Context) {
} }
userInfo := middleware.GetUserInfo(c) 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()}) c.JSON(http.StatusForbidden, model.WrapperResponse{Code: 403, Msg: err.Error()})
return return
} }
@@ -183,7 +183,7 @@ func (h *ConfigHandler) Add(c *gin.Context) {
c.JSON(http.StatusOK, model.WrapperResponse{Code: 400, Msg: "全局配置需要指定所属业务组"}) c.JSON(http.StatusOK, model.WrapperResponse{Code: 400, Msg: "全局配置需要指定所属业务组"})
return 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()}) c.JSON(http.StatusForbidden, model.WrapperResponse{Code: 403, Msg: err.Error()})
return return
} }
@@ -264,7 +264,7 @@ func (h *ConfigHandler) Update(c *gin.Context) {
} }
userInfo := middleware.GetUserInfo(c) 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()}) c.JSON(http.StatusForbidden, model.WrapperResponse{Code: 403, Msg: err.Error()})
return return
} }
@@ -311,7 +311,7 @@ func (h *ConfigHandler) Delete(c *gin.Context) {
} }
userInfo := middleware.GetUserInfo(c) 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()}) c.JSON(http.StatusForbidden, model.WrapperResponse{Code: 403, Msg: err.Error()})
return return
} }
@@ -335,19 +335,51 @@ func (h *ConfigHandler) Delete(c *gin.Context) {
func (h *ConfigHandler) GetHistory(c *gin.Context) { func (h *ConfigHandler) GetHistory(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id")) 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 config model.AppConfig
var currentConfig model.AppConfig has, _ := dao.Engine.ID(id).Get(&config)
if has, _ := dao.Engine.ID(histories[0].OriginId).Get(&currentConfig); has {
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 { for i := range histories {
histories[i].ActiveContents = currentConfig.Contents 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 手动删除单条配置历史 // DeleteHistory 手动删除单条配置历史
@@ -363,7 +395,7 @@ func (h *ConfigHandler) DeleteHistory(c *gin.Context) {
userInfo := middleware.GetUserInfo(c) userInfo := middleware.GetUserInfo(c)
var config model.AppConfig var config model.AppConfig
if has, _ := dao.Engine.ID(history.OriginId).Get(&config); has { 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()}) c.JSON(http.StatusForbidden, model.WrapperResponse{Code: 403, Msg: err.Error()})
return return
} }
@@ -394,7 +426,7 @@ func (h *ConfigHandler) Rollback(c *gin.Context) {
var config model.AppConfig var config model.AppConfig
if has, _ := dao.Engine.ID(history.OriginId).Get(&config); has { if has, _ := dao.Engine.ID(history.OriginId).Get(&config); has {
userInfo := middleware.GetUserInfo(c) 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()}) c.JSON(http.StatusForbidden, model.WrapperResponse{Code: 403, Msg: err.Error()})
return return
} }
-1
View File
@@ -28,7 +28,6 @@ type LoginUserInfo struct {
Type string `json:"type"` Type string `json:"type"`
GroupId *int `json:"groupId"` GroupId *int `json:"groupId"`
GrantAppIds []int `json:"grantAppIds"` GrantAppIds []int `json:"grantAppIds"`
GrantPermissons []string `json:"grantPermissons"`
GroupMaster bool `json:"groupMaster"` GroupMaster bool `json:"groupMaster"`
} }
-7
View File
@@ -50,7 +50,6 @@ func (s *AuthService) Login(userName, password string) (string, *model.LoginUser
Type: user.Type, Type: user.Type,
GroupId: user.GroupId, GroupId: user.GroupId,
GrantAppIds: make([]int, 0), GrantAppIds: make([]int, 0),
GrantPermissons: make([]string, 0),
} }
// load permissions // load permissions
@@ -125,7 +124,6 @@ func (s *AuthService) ValidateToken(tokenString string) (*model.LoginUserInfo, e
Type: user.Type, Type: user.Type,
GroupId: user.GroupId, GroupId: user.GroupId,
GrantAppIds: make([]int, 0), GrantAppIds: make([]int, 0),
GrantPermissons: make([]string, 0),
} }
s.loadPermissions(userInfo, user.Id) s.loadPermissions(userInfo, user.Id)
return userInfo, nil return userInfo, nil
@@ -146,10 +144,5 @@ func (s *AuthService) loadPermissions(userInfo *model.LoginUserInfo, userId int)
} }
for _, app := range apps { for _, app := range apps {
userInfo.GrantAppIds = append(userInfo.GrantAppIds, app.Id) 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
}
+4 -5
View File
@@ -18,19 +18,18 @@ func (s *PermissionService) RequireSuperAdmin(userInfo *model.LoginUserInfo) err
return nil 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() { if userInfo.IsSuperAdmin() {
return nil return nil
} }
if groupId != nil && userInfo.GroupId != nil && *groupId == *userInfo.GroupId { if userInfo.IsGroupAdmin() && groupId != nil && userInfo.GroupId != nil && *groupId == *userInfo.GroupId {
return nil return nil
} }
if appId == nil || *appId == 0 { if appId == nil || *appId == 0 {
return errors.New("你没有全局配置权限") return errors.New("你没有全局配置权限")
} }
permCode := buildPermissionCode(*appId, operate) for _, id := range userInfo.GrantAppIds {
for _, p := range userInfo.GrantPermissons { if id == *appId {
if p == permCode {
return nil return nil
} }
} }
@@ -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)
}
})
}
}
+3 -2
View File
@@ -38,10 +38,11 @@ export function deleteConfig(id) {
}) })
} }
export function getConfigHistory(id) { export function getConfigHistory(id, params) {
return request({ return request({
url: `/admin/config/configHistories/${id}`, url: `/admin/config/configHistories/${id}`,
method: 'get' method: 'get',
params
}) })
} }
+28 -5
View File
@@ -86,6 +86,15 @@
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<el-pagination
style="margin-top: 10px; text-align: right"
@current-change="handleHistoryPageChange"
:current-page="historyQuery.pageNo"
:page-size="historyQuery.pageSize"
:total="historyTotal"
layout="total, prev, pager, next"
small
></el-pagination>
</el-dialog> </el-dialog>
<el-dialog title="历史版本对比" :visible.sync="contentVisible" width="1200px" append-to-body> <el-dialog title="历史版本对比" :visible.sync="contentVisible" width="1200px" append-to-body>
@@ -247,6 +256,8 @@ export default {
apps: [], apps: [],
historyVisible: false, historyVisible: false,
historyList: [], historyList: [],
historyTotal: 0,
historyQuery: { pageNo: 1, pageSize: 10 },
currentConfig: null, currentConfig: null,
contentVisible: false, contentVisible: false,
currentHistory: {}, currentHistory: {},
@@ -316,19 +327,31 @@ export default {
}, },
async showHistory(row) { async showHistory(row) {
this.currentConfig = row this.currentConfig = row
try { this.historyQuery.pageNo = 1
const res = await getConfigHistory(row.id) await this.loadHistory()
this.historyList = res.data || []
this.historyVisible = true this.historyVisible = true
},
async loadHistory() {
if (!this.currentConfig) return
try {
const res = await getConfigHistory(this.currentConfig.id, this.historyQuery)
this.historyList = res.data || []
this.historyTotal = res.total || 0
} catch (e) {} } catch (e) {}
}, },
handleHistoryPageChange(page) {
this.historyQuery.pageNo = page
this.loadHistory()
},
async handleDeleteHistory(row) { async handleDeleteHistory(row) {
try { try {
await this.$confirm('确定删除这条历史记录?删除后不可恢复。') await this.$confirm('确定删除这条历史记录?删除后不可恢复。')
await deleteConfigHistory(row.id) await deleteConfigHistory(row.id)
this.$message.success('删除成功') this.$message.success('删除成功')
const res = await getConfigHistory(row.originId) if (this.historyList.length === 1 && this.historyQuery.pageNo > 1) {
this.historyList = res.data || [] this.historyQuery.pageNo--
}
await this.loadHistory()
} catch (e) {} } catch (e) {}
}, },
async viewHistoryContent(row) { async viewHistoryContent(row) {
+28 -5
View File
@@ -81,6 +81,15 @@
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<el-pagination
style="margin-top: 10px; text-align: right"
@current-change="handleHistoryPageChange"
:current-page="historyQuery.pageNo"
:page-size="historyQuery.pageSize"
:total="historyTotal"
layout="total, prev, pager, next"
small
></el-pagination>
</el-dialog> </el-dialog>
<el-dialog title="历史版本对比" :visible.sync="contentVisible" width="1200px" append-to-body> <el-dialog title="历史版本对比" :visible.sync="contentVisible" width="1200px" append-to-body>
@@ -241,6 +250,8 @@ export default {
profiles: [], profiles: [],
historyVisible: false, historyVisible: false,
historyList: [], historyList: [],
historyTotal: 0,
historyQuery: { pageNo: 1, pageSize: 10 },
currentConfig: null, currentConfig: null,
contentVisible: false, contentVisible: false,
currentHistory: {}, currentHistory: {},
@@ -302,19 +313,31 @@ export default {
}, },
async showHistory(row) { async showHistory(row) {
this.currentConfig = row this.currentConfig = row
try { this.historyQuery.pageNo = 1
const res = await getConfigHistory(row.id) await this.loadHistory()
this.historyList = res.data || []
this.historyVisible = true this.historyVisible = true
},
async loadHistory() {
if (!this.currentConfig) return
try {
const res = await getConfigHistory(this.currentConfig.id, this.historyQuery)
this.historyList = res.data || []
this.historyTotal = res.total || 0
} catch (e) {} } catch (e) {}
}, },
handleHistoryPageChange(page) {
this.historyQuery.pageNo = page
this.loadHistory()
},
async handleDeleteHistory(row) { async handleDeleteHistory(row) {
try { try {
await this.$confirm('确定删除这条历史记录?删除后不可恢复。') await this.$confirm('确定删除这条历史记录?删除后不可恢复。')
await deleteConfigHistory(row.id) await deleteConfigHistory(row.id)
this.$message.success('删除成功') this.$message.success('删除成功')
const res = await getConfigHistory(row.originId) if (this.historyList.length === 1 && this.historyQuery.pageNo > 1) {
this.historyList = res.data || [] this.historyQuery.pageNo--
}
await this.loadHistory()
} catch (e) {} } catch (e) {}
}, },
async viewHistoryContent(row) { async viewHistoryContent(row) {