完善平台角色菜单树与授权登录导航

This commit is contained in:
czl231
2026-08-17 21:56:22 +08:00
parent c7449cb38d
commit d6bb3bd43e
22 changed files with 739 additions and 17 deletions

View File

@@ -59,6 +59,20 @@ func platformMenuAllowsRequest(menus []platformbase.Menu, requestPath, method st
return false
}
// isPlatformSelfMenuListRequest 判断是否为当前账号读取自身授权导航的基础请求。
// 该接口的响应仍会按当前角色过滤,不授予菜单配置详情或写权限。
func isPlatformSelfMenuListRequest(requestPath, method string) bool {
if method != "GET" {
return false
}
marker := "/platform/v1/"
index := strings.Index(requestPath, marker)
if index < 0 {
return false
}
return strings.Trim(requestPath[index+len(marker):], "/") == "platform_menu"
}
func platformRouteMenuIdentity(resource string) string {
switch {
case resource == "dashboard":
@@ -114,7 +128,17 @@ func RequirePlatformMenuAccess() gin.HandlerFunc {
return
}
menus, err := platformbase.LoadPlatformMenus(claims.Role)
if err != nil || !platformMenuAllowsRequest(menus, ctx.Request.URL.Path, ctx.Request.Method) ||
if err != nil {
infra.Response.Error(ctx, errcode.ErrPermissionDenied)
ctx.Abort()
return
}
if isPlatformSelfMenuListRequest(ctx.Request.URL.Path, ctx.Request.Method) {
ctx.Set(platformMenusContextKey, menus)
ctx.Next()
return
}
if !platformMenuAllowsRequest(menus, ctx.Request.URL.Path, ctx.Request.Method) ||
!platformScopedRequestAllowed(ctx, menus) {
infra.Response.Error(ctx, errcode.ErrPermissionDenied)
ctx.Abort()

View File

@@ -6,6 +6,20 @@ import (
platformbase "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/platform"
)
// TestPlatformSelfMenuListRequest 保障普通账号只能免菜单能力读取自己的导航列表。
func TestPlatformSelfMenuListRequest(t *testing.T) {
path := "/heqi/platform/v1/platform_menu"
if !isPlatformSelfMenuListRequest(path, "GET") {
t.Fatal("平台账号应可读取自己的授权菜单列表")
}
if isPlatformSelfMenuListRequest(path, "POST") {
t.Fatal("菜单写请求不得通过自身导航读取规则放行")
}
if isPlatformSelfMenuListRequest(path+"/platform_menu", "GET") {
t.Fatal("菜单详情不得通过自身导航读取规则放行")
}
}
func TestSecondLevelMenuPermissionDoesNotGrantSibling(t *testing.T) {
menus := []platformbase.Menu{{Identity: "delivery_basic"}}
if !platformMenuAllowsPath(menus, "/heqi/platform/v1/delivery_basic") {

View File

@@ -18,7 +18,7 @@ type platformRoleMenusRequest struct {
MenuIdentities []string `json:"menu_identities"`
}
// ReplacePlatformRoleMenus replaces every menu assignment for a role atomically.
// ReplacePlatformRoleMenus 以事务整体替换角色的叶子菜单权限。
func ReplacePlatformRoleMenus(ctx *gin.Context) {
if !common.RequirePlatformRoot(ctx) {
return
@@ -42,7 +42,8 @@ func ReplacePlatformRoleMenus(ctx *gin.Context) {
if !ok {
return gorm.ErrRecordNotFound
}
if menu.ParentIdentity != "" {
// 有子菜单的父节点仅用于导航聚合;无子菜单的顶级页面仍是有效业务权限。
if platformMenuIsAssignable(menu) {
menuIdentities[menu.Identity] = struct{}{}
}
}
@@ -71,7 +72,7 @@ func ReplacePlatformRoleMenus(ctx *gin.Context) {
infra.Response.Success(ctx, gin.H{"updated": true})
}
// ListPlatformRoleMenuIdentities returns the current assignment for the role editor.
// ListPlatformRoleMenuIdentities 返回角色编辑器当前保存的叶子菜单标识。
func ListPlatformRoleMenuIdentities(ctx *gin.Context) {
if !common.RequirePlatformRoot(ctx) {
return
@@ -90,3 +91,16 @@ func ListPlatformRoleMenuIdentities(ctx *gin.Context) {
}
infra.Response.Success(ctx, gin.H{"menu_identities": identities})
}
// platformMenuIsAssignable 区分导航分组和可持久化的业务页面权限。
func platformMenuIsAssignable(menu platformbase.Menu) bool {
if menu.Status != common.StatusEnable {
return false
}
for _, candidate := range platformbase.AllPlatformMenus() {
if candidate.ParentIdentity == menu.Identity {
return false
}
}
return true
}

View File

@@ -0,0 +1,28 @@
package platform
import (
"testing"
platformbase "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/platform"
)
// TestPlatformMenuIsAssignable 验证导航父级不落库,独立顶级页面和普通叶子可以保存。
func TestPlatformMenuIsAssignable(t *testing.T) {
tests := []struct {
identity string
want bool
}{
{identity: "organization", want: false},
{identity: "gas_basic", want: true},
{identity: "dashboard_overview", want: true},
}
for _, test := range tests {
menu, ok := platformbase.FindPlatformMenu(test.identity)
if !ok {
t.Fatalf("测试菜单不存在:%s", test.identity)
}
if got := platformMenuIsAssignable(menu); got != test.want {
t.Fatalf("菜单 %s 可分配状态错误got=%v want=%v", test.identity, got, test.want)
}
}
}