From 060b6cfd64a0df0f409dbc4d6217bda14341d06b Mon Sep 17 00:00:00 2001 From: zhangwenjian Date: Sat, 5 Sep 2026 11:08:02 +0800 Subject: [PATCH] =?UTF-8?q?fix=F0=9F=90=9B:=20grant=20an=20application's?= =?UTF-8?q?=20apis=20even=20when=20it=20registers=20no=20menus?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit grantToAdminRole does two independent things - it grants the menus to the admin role and writes a casbin rule per api - and SeedMenus skipped the whole call whenever the menu list came back empty. An application is free to register apis with no menus: endpoints another service calls, a webhook, a UI mounted somewhere else. Those installs wrote their sys_api rows and then no casbin rule for any of them, so every one of those endpoints was denied to everyone, admin included - from a migration that reported success and left rows in the table to prove it had run. There is nothing to look at afterwards that says what went wrong. Guard on both lists instead, so nothing registered stays a no-op and apis alone still get granted. Claude-Session: https://claude.ai/code/session_01HPTAw8b8tAdFNFn8rKdPYx --- app/admin/service/seed.go | 8 +++++- app/admin/service/seed_test.go | 52 ++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/app/admin/service/seed.go b/app/admin/service/seed.go index e61453c4..3848dabf 100644 --- a/app/admin/service/seed.go +++ b/app/admin/service/seed.go @@ -67,7 +67,13 @@ func (adminSeeder) SeedMenus(tx *gorm.DB, appCode string, menus []seed.MenuSpec, return fmt.Errorf("seed: app %q: menus: %w", appCode, err) } - if len(menuIDs) == 0 { + // Not `len(menuIDs) == 0`: grantToAdminRole grants two independent + // things, and an application is free to register apis without menus - + // endpoints another service calls, or a UI mounted somewhere else. + // Skipping the whole call on an empty menu list wrote the sys_api rows + // and then no casbin rule for them, so those endpoints were denied to + // everyone, admin included, with a migration that reported success. + if len(menuIDs) == 0 && len(apiRows) == 0 { return nil } return grantToAdminRole(tx, menuIDs, apiRows) diff --git a/app/admin/service/seed_test.go b/app/admin/service/seed_test.go index 0e5558aa..fe7a9845 100644 --- a/app/admin/service/seed_test.go +++ b/app/admin/service/seed_test.go @@ -237,3 +237,55 @@ func TestSeederIsRegistered(t *testing.T) { t.Fatalf("SeedMenus through the package-level entry point: %v", err) } } + +// An application is free to register apis with no menus at all - endpoints +// another service calls, or a UI mounted somewhere else. Skipping +// grantToAdminRole on an empty menu list wrote the sys_api rows and then no +// casbin rule for them, so every one of those endpoints was denied to +// everyone including admin, from a migration that reported success. +func TestSeedMenusGrantsApisWhenThereAreNoMenus(t *testing.T) { + db := newSeedTestDB(t) + role := seedAdminRole(t, db) + + apis := []seed.ApiSpec{ + {Code: "hook", Title: "Inbound hook", Path: "/api/v1/hook", Method: "POST", Handle: "hook.Receive"}, + {Code: "sync", Title: "Sync", Path: "/api/v1/sync", Method: "GET", Handle: "hook.Sync"}, + } + if err := (adminSeeder{}).SeedMenus(db, "hooks", nil, apis); err != nil { + t.Fatalf("SeedMenus: %v", err) + } + + var apiCount int64 + db.Model(&models.SysApi{}).Where("app_code = ?", "hooks").Count(&apiCount) + if apiCount != int64(len(apis)) { + t.Fatalf("sys_api rows = %d, want %d", apiCount, len(apis)) + } + + for _, a := range apis { + var n int64 + db.Table("casbin_rule"). + Where("ptype = 'p' AND v0 = ? AND v1 = ? AND v2 = ?", role.RoleKey, a.Path, a.Method). + Count(&n) + if n != 1 { + t.Errorf("casbin_rule for %s %s = %d rows, want 1: the endpoint is denied to admin", a.Method, a.Path, n) + } + } +} + +// The other half of the same guard: nothing registered at all must stay a +// no-op rather than start touching sys_role_menu or casbin_rule. +func TestSeedMenusWithNothingRegisteredWritesNothing(t *testing.T) { + db := newSeedTestDB(t) + seedAdminRole(t, db) + + if err := (adminSeeder{}).SeedMenus(db, "empty", nil, nil); err != nil { + t.Fatalf("SeedMenus: %v", err) + } + for _, table := range []string{"casbin_rule", "sys_role_menu"} { + var n int64 + db.Table(table).Count(&n) + if n != 0 { + t.Errorf("%s has %d rows, want 0", table, n) + } + } +}