mirror of
https://github.com/go-admin-team/go-admin.git
synced 2026-10-13 17:05:07 +00:00
fix🔒: bind /role-status and /job/start|remove to menus and backfill policies
These APIs now pass through AuthCheckRole but the seed data bound none of them to a menu, so a role saved through the menu tree could not be granted them. Bind them to the matching permission menus and grant existing roles that already hold those menus.
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
package version
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"runtime"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"go-admin/app/admin/models"
|
||||
"go-admin/cmd/migrate/migration"
|
||||
common "go-admin/common/models"
|
||||
)
|
||||
|
||||
// Bind the APIs that were mounted without AuthCheckRole to the menus that
|
||||
// expose them, and grant them to every role that already holds one of those
|
||||
// menus.
|
||||
//
|
||||
// /job/start, /job/remove and /role-status now go through AuthCheckRole. The
|
||||
// seed data registered them in sys_api but bound none to a menu, so a role
|
||||
// saved through the menu tree could never be granted them. Binding only
|
||||
// reaches roles saved after this migration; roles that already hold a menu
|
||||
// are granted here so an upgrade does not take the buttons away from them.
|
||||
// The admin role is let through by AuthCheckRole and needs no policy.
|
||||
//
|
||||
// Menus are found by permission, not id, for the same reason as in
|
||||
// 1786700011000. start has no menu of its own in the seed, so it follows
|
||||
// job:sysJob:edit as well as job:sysJob:start where a deployment added one.
|
||||
//
|
||||
// Ordered after 1786700003000, so the runtime models are used.
|
||||
func init() {
|
||||
_, fileName, _, _ := runtime.Caller(0)
|
||||
migration.Migrate.SetVersion(migration.GetFilename(fileName), _1786700012000BindAuthzApis)
|
||||
}
|
||||
|
||||
var authzMenuApis = []struct {
|
||||
permissions []string
|
||||
api models.SysApi
|
||||
}{
|
||||
{[]string{"admin:sysRole:update"}, models.SysApi{Title: "角色状态修改", Path: "/api/v1/role-status", Action: "PUT"}},
|
||||
{[]string{"job:sysJob:remove"}, models.SysApi{Title: "job移除", Path: "/api/v1/job/remove/:id", Action: "GET"}},
|
||||
{[]string{"job:sysJob:start", "job:sysJob:edit"}, models.SysApi{Title: "job启动", Path: "/api/v1/job/start/:id", Action: "GET"}},
|
||||
}
|
||||
|
||||
func _1786700012000BindAuthzApis(db *gorm.DB, version string) error {
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := bindAuthzApis(tx); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Create(&common.Migration{Version: version}).Error
|
||||
})
|
||||
}
|
||||
|
||||
// bindAuthzApis is split out so tests can run it without sys_migration.
|
||||
func bindAuthzApis(tx *gorm.DB) error {
|
||||
for _, b := range authzMenuApis {
|
||||
var menus []models.SysMenu
|
||||
if err := tx.Where("permission IN ?", b.permissions).Find(&menus).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if len(menus) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
var api models.SysApi
|
||||
err := tx.Where("path = ? AND action = ?", b.api.Path, b.api.Action).First(&api).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
api = b.api
|
||||
api.Type = "BUS"
|
||||
err = tx.Create(&api).Error
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i := range menus {
|
||||
if err := tx.Model(&menus[i]).Association("SysApi").Append(&api); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var roleKeys []string
|
||||
if err := tx.Model(&models.SysRole{}).
|
||||
Joins("JOIN sys_role_menu ON sys_role_menu.role_id = sys_role.role_id").
|
||||
Where("sys_role_menu.menu_id = ?", menus[i].MenuId).
|
||||
Distinct().Pluck("sys_role.role_key", &roleKeys).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, key := range roleKeys {
|
||||
if err := tx.Exec(
|
||||
"INSERT INTO casbin_rule (ptype, v0, v1, v2, v3, v4, v5) SELECT 'p', ?, ?, ?, '', '', '' WHERE NOT EXISTS (SELECT 1 FROM casbin_rule WHERE ptype='p' AND v0=? AND v1=? AND v2=?)",
|
||||
key, api.Path, api.Action, key, api.Path, api.Action,
|
||||
).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package version
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"go-admin/app/admin/models"
|
||||
)
|
||||
|
||||
const (
|
||||
roleUpdateMenuId = 226
|
||||
jobEditMenuId = 463
|
||||
jobRemoveMenuId = 464
|
||||
)
|
||||
|
||||
func TestBindAuthzApisGrantsWhatARoleAlreadyHolds(t *testing.T) {
|
||||
db := openGenMenuDB(t)
|
||||
for _, m := range []models.SysMenu{
|
||||
{MenuId: roleUpdateMenuId, MenuName: "修改角色", Permission: "admin:sysRole:update", MenuType: "F"},
|
||||
{MenuId: jobEditMenuId, MenuName: "修改定时任务", Permission: "job:sysJob:edit", MenuType: "F"},
|
||||
{MenuId: jobRemoveMenuId, MenuName: "删除定时任务", Permission: "job:sysJob:remove", MenuType: "F"},
|
||||
} {
|
||||
if err := db.Create(&m).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
addRole(t, db, 2, "roleAdmin", roleUpdateMenuId)
|
||||
addRole(t, db, 3, "jobEditor", jobEditMenuId)
|
||||
addRole(t, db, 4, "jobRemover", jobRemoveMenuId)
|
||||
addRole(t, db, 5, "clerk")
|
||||
|
||||
if err := bindAuthzApis(db); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for role, want := range map[string][]string{
|
||||
"roleAdmin": {"PUT /api/v1/role-status"},
|
||||
"jobEditor": {"GET /api/v1/job/start/:id"},
|
||||
"jobRemover": {"GET /api/v1/job/remove/:id"},
|
||||
"clerk": nil,
|
||||
} {
|
||||
if got := policies(t, db, role); !equal(got, want) {
|
||||
t.Errorf("%s holds %v, want %v", role, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBindAuthzApisReusesSeededRowAndIsIdempotent(t *testing.T) {
|
||||
db := openGenMenuDB(t)
|
||||
if err := db.Create(&models.SysMenu{MenuId: roleUpdateMenuId, Permission: "admin:sysRole:update", MenuType: "F"}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
seeded := models.SysApi{Id: 153, Path: "/api/v1/role-status", Action: "PUT"}
|
||||
if err := db.Create(&seeded).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
addRole(t, db, 2, "roleAdmin", roleUpdateMenuId)
|
||||
|
||||
for i := 0; i < 2; i++ {
|
||||
if err := bindAuthzApis(db); err != nil {
|
||||
t.Fatalf("run %d: %v", i+1, err)
|
||||
}
|
||||
}
|
||||
|
||||
var n int64
|
||||
db.Model(&models.SysApi{}).Where("path = ?", seeded.Path).Count(&n)
|
||||
if n != 1 {
|
||||
t.Errorf("%d rows for %s, want the seeded one only", n, seeded.Path)
|
||||
}
|
||||
if got := boundApis(t, db, roleUpdateMenuId); got != 1 {
|
||||
t.Errorf("menu bound to %d APIs after two runs, want 1", got)
|
||||
}
|
||||
if got := policies(t, db, "roleAdmin"); len(got) != 1 {
|
||||
t.Errorf("roleAdmin holds %v after two runs, want one policy", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBindAuthzApisSkipsMenusThatDoNotExist(t *testing.T) {
|
||||
db := openGenMenuDB(t)
|
||||
if err := bindAuthzApis(db); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var n int64
|
||||
db.Model(&models.SysApi{}).Count(&n)
|
||||
if n != 0 {
|
||||
t.Errorf("%d APIs created with no menu to bind, want 0", n)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user