mirror of
https://github.com/Buriburizaem0n/nezha_domains.git
synced 2026-09-19 17:50:12 +00:00
206 lines
7.1 KiB
Go
206 lines
7.1 KiB
Go
package controller
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/patrickmn/go-cache"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/nezhahq/nezha/model"
|
|
"github.com/nezhahq/nezha/pkg/i18n"
|
|
"github.com/nezhahq/nezha/service/singleton"
|
|
)
|
|
|
|
func setupServerGroupVisibilityFixture(t *testing.T) {
|
|
t.Helper()
|
|
|
|
originalDB := singleton.DB
|
|
originalCache := singleton.Cache
|
|
originalLoc := singleton.Loc
|
|
originalLocalizer := singleton.Localizer
|
|
originalServer := singleton.ServerShared
|
|
originalUserInfo := singleton.UserInfoMap
|
|
|
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
sqlDB, err := db.DB()
|
|
require.NoError(t, err)
|
|
require.NoError(t, db.AutoMigrate(&model.Server{}, &model.ServerGroup{}, &model.ServerGroupServer{}, &model.User{}))
|
|
|
|
singleton.DB = db
|
|
singleton.Loc = time.UTC
|
|
singleton.Cache = cache.New(time.Minute, time.Minute)
|
|
singleton.Localizer = i18n.NewLocalizer("en_US", "nezha", "translations", i18n.Translations)
|
|
singleton.UserLock.Lock()
|
|
singleton.UserInfoMap = map[uint64]model.UserInfo{
|
|
1: {Role: model.RoleAdmin},
|
|
200: {Role: model.RoleMember},
|
|
}
|
|
singleton.UserLock.Unlock()
|
|
|
|
require.NoError(t, db.Create(&model.Server{Common: model.Common{ID: 1, UserID: 1}, Name: "public", UUID: "public", HideForGuest: false}).Error)
|
|
require.NoError(t, db.Create(&model.Server{Common: model.Common{ID: 2, UserID: 1}, Name: "hidden", UUID: "hidden", HideForGuest: true}).Error)
|
|
|
|
require.NoError(t, db.Create(&model.ServerGroup{Common: model.Common{ID: 10, UserID: 1}, Name: "Public Group"}).Error)
|
|
require.NoError(t, db.Create(&model.ServerGroup{Common: model.Common{ID: 11, UserID: 1}, Name: "Empty Group"}).Error)
|
|
require.NoError(t, db.Create(&model.ServerGroupServer{Common: model.Common{UserID: 1}, ServerGroupId: 10, ServerId: 1}).Error)
|
|
|
|
singleton.ServerShared = singleton.NewServerClass()
|
|
|
|
t.Cleanup(func() {
|
|
_ = sqlDB.Close()
|
|
singleton.DB = originalDB
|
|
singleton.Cache = originalCache
|
|
singleton.Loc = originalLoc
|
|
singleton.Localizer = originalLocalizer
|
|
singleton.ServerShared = originalServer
|
|
singleton.UserLock.Lock()
|
|
singleton.UserInfoMap = originalUserInfo
|
|
singleton.UserLock.Unlock()
|
|
})
|
|
}
|
|
|
|
func newServerGroupCtx(viewer *model.User) *gin.Context {
|
|
gin.SetMode(gin.TestMode)
|
|
c, _ := gin.CreateTestContext(httptest.NewRecorder())
|
|
c.Request = httptest.NewRequest("GET", "/api/v1/server-group", nil)
|
|
if viewer != nil {
|
|
c.Set(model.CtxKeyAuthorizedUser, viewer)
|
|
}
|
|
return c
|
|
}
|
|
|
|
func collectGroupNames(items []*model.ServerGroupResponseItem) []string {
|
|
names := make([]string, 0, len(items))
|
|
for _, it := range items {
|
|
names = append(names, it.Group.Name)
|
|
}
|
|
return names
|
|
}
|
|
|
|
func TestListServerGroupGuestSkipsGroupsWithoutVisibleServers(t *testing.T) {
|
|
setupServerGroupVisibilityFixture(t)
|
|
|
|
items, err := listServerGroup(newServerGroupCtx(nil))
|
|
require.NoError(t, err)
|
|
names := collectGroupNames(items)
|
|
|
|
assert.ElementsMatch(t, []string{"Public Group"}, names,
|
|
"a group with no guest-visible servers is meaningless to a guest UI and exposing its name leaks the existence of empty/hidden-only groups")
|
|
}
|
|
|
|
func TestListServerGroupAuthenticatedMemberSeesOwnEmptyGroup(t *testing.T) {
|
|
setupServerGroupVisibilityFixture(t)
|
|
|
|
require.NoError(t, singleton.DB.Create(&model.ServerGroup{Common: model.Common{ID: 12, UserID: 200}, Name: "member empty group"}).Error)
|
|
|
|
items, err := listServerGroup(newServerGroupCtx(&model.User{
|
|
Common: model.Common{ID: 200},
|
|
Role: model.RoleMember,
|
|
}))
|
|
require.NoError(t, err)
|
|
names := collectGroupNames(items)
|
|
|
|
assert.Contains(t, names, "member empty group", "owner must still see their own empty group")
|
|
}
|
|
|
|
func TestListServerGroupAdminSeesAllGroupsIncludingEmpty(t *testing.T) {
|
|
setupServerGroupVisibilityFixture(t)
|
|
|
|
items, err := listServerGroup(newServerGroupCtx(&model.User{
|
|
Common: model.Common{ID: 1},
|
|
Role: model.RoleAdmin,
|
|
}))
|
|
require.NoError(t, err)
|
|
names := collectGroupNames(items)
|
|
|
|
assert.ElementsMatch(t, []string{"Public Group", "Empty Group"}, names,
|
|
"admin must keep full visibility, including empty groups")
|
|
}
|
|
|
|
func newServerGroupCtxWithPAT(viewer *model.User, tok *model.APIToken) *gin.Context {
|
|
c := newServerGroupCtx(viewer)
|
|
if tok != nil {
|
|
c.Set(model.CtxKeyAPIToken, tok)
|
|
}
|
|
return c
|
|
}
|
|
|
|
// PAT scoped to server_ids must hide groups whose membership is entirely
|
|
// outside the whitelist and must strip out-of-whitelist server IDs from
|
|
// remaining groups. Otherwise admin-issued limited PATs still enumerate
|
|
// every group name + server id via /api/v1/server-group.
|
|
func TestListServerGroupPATWhitelistFiltersGroupsAndServerIDs(t *testing.T) {
|
|
setupServerGroupVisibilityFixture(t)
|
|
|
|
require.NoError(t, singleton.DB.Create(&model.ServerGroupServer{
|
|
Common: model.Common{UserID: 1}, ServerGroupId: 10, ServerId: 2,
|
|
}).Error)
|
|
|
|
tok := &model.APIToken{ID: 77, UserID: 1}
|
|
tok.SetServerIDs([]uint64{1})
|
|
|
|
items, err := listServerGroup(newServerGroupCtxWithPAT(&model.User{
|
|
Common: model.Common{ID: 1}, Role: model.RoleAdmin,
|
|
}, tok))
|
|
require.NoError(t, err)
|
|
|
|
names := collectGroupNames(items)
|
|
assert.ElementsMatch(t, []string{"Public Group"}, names,
|
|
"PAT scoped to {1} must drop the empty group and not surface group names containing only server 2")
|
|
|
|
if assert.Len(t, items, 1) {
|
|
assert.ElementsMatch(t, []uint64{1}, items[0].Servers,
|
|
"server IDs outside the PAT whitelist must be redacted from the response")
|
|
}
|
|
}
|
|
|
|
func TestListServerGroupPATWithDisjointWhitelistReturnsEmpty(t *testing.T) {
|
|
setupServerGroupVisibilityFixture(t)
|
|
|
|
tok := &model.APIToken{ID: 78, UserID: 1}
|
|
tok.SetServerIDs([]uint64{9999})
|
|
|
|
items, err := listServerGroup(newServerGroupCtxWithPAT(&model.User{
|
|
Common: model.Common{ID: 1}, Role: model.RoleAdmin,
|
|
}, tok))
|
|
require.NoError(t, err)
|
|
assert.Empty(t, items, "PAT scoped to a server it cannot reach must see no groups, not all of them")
|
|
}
|
|
|
|
// batchDeleteServerGroup must refuse to delete a group whose members are not
|
|
// entirely covered by the PAT whitelist; otherwise an admin's limited PAT can
|
|
// drop groups that touch servers outside its scope.
|
|
func TestBatchDeleteServerGroupRejectsPATOutsideWhitelist(t *testing.T) {
|
|
setupServerGroupVisibilityFixture(t)
|
|
require.NoError(t, singleton.DB.Create(&model.ServerGroupServer{
|
|
Common: model.Common{UserID: 1}, ServerGroupId: 10, ServerId: 2,
|
|
}).Error)
|
|
|
|
tok := &model.APIToken{ID: 79, UserID: 1}
|
|
tok.SetServerIDs([]uint64{1})
|
|
|
|
c := newServerGroupCtxWithPAT(&model.User{
|
|
Common: model.Common{ID: 1}, Role: model.RoleAdmin,
|
|
}, tok)
|
|
body, _ := json.Marshal([]uint64{10})
|
|
c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/batch-delete/server-group", bytes.NewReader(body))
|
|
c.Request.Header.Set("Content-Type", "application/json")
|
|
|
|
_, err := batchDeleteServerGroup(c)
|
|
require.Error(t, err, "PAT scoped to {1} must not delete group 10 which still contains server 2")
|
|
|
|
var remaining int64
|
|
require.NoError(t, singleton.DB.Model(&model.ServerGroup{}).Where("id = ?", 10).Count(&remaining).Error)
|
|
assert.Equal(t, int64(1), remaining, "group 10 must remain after refused PAT delete")
|
|
}
|