kra-new/internal/data/system/surface_upgrade.go

201 lines
6.0 KiB
Go

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(&current).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(&current).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(&current).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(&current).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
})
}