优化结构
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
platformmodule "kra/pkg/module"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// EnsureAdminSurface upgrades an already initialized installation with a
|
||||
// module's current menus, API metadata, root menu links, and root policies.
|
||||
// It intentionally does nothing before first-install seed data exists.
|
||||
func EnsureAdminSurface(db *gorm.DB, surface platformmodule.Surface, rootAuthorityID uint) error {
|
||||
if db == nil || !db.Migrator().HasTable(&menuPO{}) || !db.Migrator().HasTable(&apiPO{}) {
|
||||
return nil
|
||||
}
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
parentIDs := map[string]uint{}
|
||||
for _, item := range surface.Menus {
|
||||
if item.ParentName == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := parentIDs[item.ParentName]; ok {
|
||||
continue
|
||||
}
|
||||
var parent menuPO
|
||||
if err := tx.Where("name = ?", item.ParentName).First(&parent).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
parentIDs[item.ParentName] = parent.ID
|
||||
}
|
||||
|
||||
menus := make([]menuPO, 0, len(surface.Menus))
|
||||
for _, item := range surface.Menus {
|
||||
menu := menuPO{
|
||||
MenuLevel: 1, ParentID: parentIDs[item.ParentName], Path: item.Path, Name: item.Name,
|
||||
Component: item.Component, Title: item.Title, Icon: item.Icon, Sort: item.Sort,
|
||||
}
|
||||
if item.ParentName == "" {
|
||||
menu.MenuLevel = 0
|
||||
}
|
||||
var current menuPO
|
||||
err := tx.Where("name = ?", item.Name).First(¤t).Error
|
||||
switch {
|
||||
case errors.Is(err, gorm.ErrRecordNotFound):
|
||||
if err = tx.Create(&menu).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
case err != nil:
|
||||
return err
|
||||
default:
|
||||
if err = tx.Model(¤t).Updates(map[string]any{
|
||||
"menu_level": menu.MenuLevel, "parent_id": menu.ParentID, "path": menu.Path,
|
||||
"component": menu.Component, "title": menu.Title, "icon": menu.Icon, "sort": menu.Sort,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
menu.ID = current.ID
|
||||
}
|
||||
menus = append(menus, menu)
|
||||
}
|
||||
|
||||
apis := make([]apiPO, 0, len(surface.APIs))
|
||||
for _, item := range surface.APIs {
|
||||
api := apiPO{Path: item.Path, Method: strings.ToUpper(item.Method), APIGroup: item.Group, Description: item.Description}
|
||||
var current apiPO
|
||||
err := tx.Where("path = ? AND method = ?", api.Path, api.Method).First(¤t).Error
|
||||
switch {
|
||||
case errors.Is(err, gorm.ErrRecordNotFound):
|
||||
if err = tx.Create(&api).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
case err != nil:
|
||||
return err
|
||||
default:
|
||||
if err = tx.Model(¤t).Updates(map[string]any{"api_group": api.APIGroup, "description": api.Description}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
api.ID = current.ID
|
||||
}
|
||||
apis = append(apis, api)
|
||||
}
|
||||
|
||||
if rootAuthorityID == 0 || !tx.Migrator().HasTable(&authorityPO{}) || !tx.Migrator().HasTable(&authorityMenuPO{}) || !tx.Migrator().HasTable(&casbinRulePO{}) {
|
||||
return nil
|
||||
}
|
||||
var root authorityPO
|
||||
if err := tx.Where("authority_id = ?", rootAuthorityID).First(&root).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
for _, menu := range menus {
|
||||
link := authorityMenuPO{SysAuthorityAuthorityID: rootAuthorityID, SysBaseMenuID: menu.ID}
|
||||
var count int64
|
||||
if err := tx.Model(&authorityMenuPO{}).
|
||||
Where("sys_authority_authority_id = ? AND sys_base_menu_id = ?", rootAuthorityID, menu.ID).
|
||||
Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
if err := tx.Create(&link).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, api := range apis {
|
||||
exists, err := policyExists(tx, rootAuthorityID, api.Path, api.Method)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
rule := newPolicyRule(rootAuthorityID, api.Path, api.Method)
|
||||
if err := tx.Create(&rule).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// InheritAPIPolicy grants a new route only to authorities that already own
|
||||
// the supplied predecessor route. This preserves existing read-access intent
|
||||
// without broadening unrelated write permissions.
|
||||
func InheritAPIPolicy(db *gorm.DB, sourcePath, sourceMethod, targetPath, targetMethod string) error {
|
||||
if db == nil || !db.Migrator().HasTable(&casbinRulePO{}) {
|
||||
return nil
|
||||
}
|
||||
sourceMethod = strings.ToUpper(sourceMethod)
|
||||
targetMethod = strings.ToUpper(targetMethod)
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
var rows []casbinRulePO
|
||||
if err := policyScope(tx).Where("v1 = ? AND v2 = ?", sourcePath, sourceMethod).Find(&rows).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, row := range rows {
|
||||
parsed, err := strconv.ParseUint(row.V0, 10, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
authorityID := uint(parsed)
|
||||
exists, err := policyExists(tx, authorityID, targetPath, targetMethod)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
rule := newPolicyRule(authorityID, targetPath, targetMethod)
|
||||
if err := tx.Create(&rule).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// GrantMenuAPIs makes a visible administration menu usable by granting only
|
||||
// its explicitly supplied API set to authorities that already own the menu.
|
||||
func GrantMenuAPIs(db *gorm.DB, menuName string, apis []platformmodule.API) error {
|
||||
if db == nil || !db.Migrator().HasTable(&menuPO{}) || !db.Migrator().HasTable(&authorityMenuPO{}) || !db.Migrator().HasTable(&casbinRulePO{}) {
|
||||
return nil
|
||||
}
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
var menu menuPO
|
||||
if err := tx.Where("name = ?", menuName).First(&menu).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
var links []authorityMenuPO
|
||||
if err := tx.Where("sys_base_menu_id = ?", menu.ID).Find(&links).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, link := range links {
|
||||
for _, api := range apis {
|
||||
method := strings.ToUpper(api.Method)
|
||||
exists, err := policyExists(tx, link.SysAuthorityAuthorityID, api.Path, method)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
rule := newPolicyRule(link.SysAuthorityAuthorityID, api.Path, method)
|
||||
if err := tx.Create(&rule).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
platformmodule "kra/pkg/module"
|
||||
)
|
||||
|
||||
func TestEnsureAdminSurfaceAndPolicyInheritance(t *testing.T) {
|
||||
db, err := openWithDriver("sqlite", "file:"+t.Name()+"?mode=memory&cache=shared")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = migrateAll(db); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
parent := menuPO{Name: "extensions", Path: "extensions", Title: "扩展功能"}
|
||||
if err = db.Create(&parent).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rootParent := uint(0)
|
||||
if err = db.Create(&[]authorityPO{
|
||||
{AuthorityID: 888, AuthorityName: "root", ParentID: &rootParent},
|
||||
{AuthorityID: 999, AuthorityName: "operator", ParentID: &rootParent},
|
||||
}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = db.Create(&[]casbinRulePO{
|
||||
newPolicyRule(999, "/payment/orders", "GET"),
|
||||
newPolicyRule(999, "/payment/orders/:provider/:tradeNo", "GET"),
|
||||
}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
surface := platformmodule.Surface{
|
||||
Menus: []platformmodule.Menu{
|
||||
{Name: "paymentOrders", Path: "paymentOrders", ParentName: "extensions", Component: "view/payment/orders.vue", Title: "支付订单", Sort: 6},
|
||||
{Name: "paymentConfig", Path: "paymentConfig", ParentName: "extensions", Component: "view/payment/config.vue", Title: "支付配置", Sort: 7},
|
||||
},
|
||||
APIs: []platformmodule.API{
|
||||
{Path: "/payment/orders", Method: "GET", Group: "支付", Description: "订单列表"},
|
||||
{Path: "/payment/orders/summary", Method: "GET", Group: "支付", Description: "订单汇总"},
|
||||
{Path: "/payment/orders/:provider/:tradeNo", Method: "GET", Group: "支付", Description: "订单详情"},
|
||||
{Path: "/payment/orders/:provider/:tradeNo/events", Method: "GET", Group: "支付", Description: "订单事件"},
|
||||
{Path: "/payment/refund", Method: "POST", Group: "支付", Description: "退款"},
|
||||
},
|
||||
}
|
||||
for attempt := 0; attempt < 2; attempt++ {
|
||||
if err = EnsureAdminSurface(db, surface, 888); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var paymentMenu menuPO
|
||||
if err = db.Where("name = ?", "paymentOrders").First(&paymentMenu).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var linkCount int64
|
||||
if err = db.Model(&authorityMenuPO{}).Where("sys_authority_authority_id = ? AND sys_base_menu_id = ?", 999, paymentMenu.ID).Count(&linkCount).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if linkCount == 0 {
|
||||
if err = db.Create(&authorityMenuPO{SysAuthorityAuthorityID: 999, SysBaseMenuID: paymentMenu.ID}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err = InheritAPIPolicy(db, "/payment/orders", "GET", "/payment/orders/summary", "GET"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = InheritAPIPolicy(db, "/payment/orders/:provider/:tradeNo", "GET", "/payment/orders/:provider/:tradeNo/events", "GET"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = GrantMenuAPIs(db, "paymentOrders", surface.APIs[:4]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
for _, test := range []struct {
|
||||
authority uint
|
||||
path string
|
||||
method string
|
||||
want bool
|
||||
}{
|
||||
{888, "/payment/orders", "GET", true},
|
||||
{888, "/payment/refund", "POST", true},
|
||||
{999, "/payment/orders/summary", "GET", true},
|
||||
{999, "/payment/orders", "GET", true},
|
||||
{999, "/payment/orders/:provider/:tradeNo/events", "GET", true},
|
||||
{999, "/payment/refund", "POST", false},
|
||||
} {
|
||||
exists, checkErr := policyExists(db, test.authority, test.path, test.method)
|
||||
if checkErr != nil {
|
||||
t.Fatal(checkErr)
|
||||
}
|
||||
if exists != test.want {
|
||||
t.Fatalf("policy %d %s %s = %v, want %v", test.authority, test.method, test.path, exists, test.want)
|
||||
}
|
||||
}
|
||||
var apiCount int64
|
||||
if err = db.Model(&apiPO{}).Where("path LIKE ?", "/payment/%").Count(&apiCount).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if apiCount != int64(len(surface.APIs)) {
|
||||
t.Fatalf("payment APIs = %d, want %d", apiCount, len(surface.APIs))
|
||||
}
|
||||
var rootLinks int64
|
||||
if err = db.Model(&authorityMenuPO{}).Where("sys_authority_authority_id = ?", 888).Count(&rootLinks).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if rootLinks != int64(len(surface.Menus)) {
|
||||
t.Fatalf("root menu links = %d, want %d", rootLinks, len(surface.Menus))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user