From 836f1db4b7317b7ec5d0b78dc0d4e6d87ed9a175 Mon Sep 17 00:00:00 2001 From: naiba Date: Tue, 2 Jun 2026 03:31:36 +0000 Subject: [PATCH] feat(service): replace EnableShowInService with HideForGuest Mirror the server HideForGuest flag on services: rename the field (dropping the gorm default), invert userCanViewService so a service is visible to guests by default and hidden only when HideForGuest is set, and flip the public-stats filter in servicesentinel accordingly. Update the visibility and permission-matrix tests to the new semantics. --- .../controller/permission_matrix_test.go | 4 +-- cmd/dashboard/controller/permissions.go | 20 +++++++------- cmd/dashboard/controller/service.go | 4 +-- .../controller/service_visibility_test.go | 26 ++++++++++--------- model/service.go | 2 +- model/service_api.go | 2 +- 6 files changed, 29 insertions(+), 29 deletions(-) diff --git a/cmd/dashboard/controller/permission_matrix_test.go b/cmd/dashboard/controller/permission_matrix_test.go index 68f0f047..e70361e9 100644 --- a/cmd/dashboard/controller/permission_matrix_test.go +++ b/cmd/dashboard/controller/permission_matrix_test.go @@ -261,8 +261,8 @@ func TestShowServiceFiltersCycleTransferStatsLikeServerList(t *testing.T) { assert.NoError(t, singleton.DB.Create(&model.Server{Common: model.Common{ID: 3, UserID: 200}, Name: "hidden member server", UUID: "hidden-member-server", HideForGuest: true}).Error) singleton.ServerShared = singleton.NewServerClass() - assert.NoError(t, singleton.DB.Create(&model.Service{Common: model.Common{ID: 10, UserID: 1}, Name: "shown service", EnableShowInService: true}).Error) - assert.NoError(t, singleton.DB.Create(&model.Service{Common: model.Common{ID: 11, UserID: 1}, Name: "hidden service"}).Error) + assert.NoError(t, singleton.DB.Create(&model.Service{Common: model.Common{ID: 10, UserID: 1}, Name: "shown service"}).Error) + assert.NoError(t, singleton.DB.Create(&model.Service{Common: model.Common{ID: 11, UserID: 1}, Name: "hidden service", HideForGuest: true}).Error) originalServiceSentinel := singleton.ServiceSentinelShared serviceSentinel, err := singleton.NewServiceSentinel(make(chan *model.Service, 2)) diff --git a/cmd/dashboard/controller/permissions.go b/cmd/dashboard/controller/permissions.go index 9fa2c322..4ff3bfa7 100644 --- a/cmd/dashboard/controller/permissions.go +++ b/cmd/dashboard/controller/permissions.go @@ -489,18 +489,16 @@ func userCanViewService(c *gin.Context, service *model.Service) bool { if service == nil { return false } - // EnableShowInService 是显式公开旗标:guest 都可看,PAT 白名单不收窄 - // 公开视图(公开 service 本来就不绑特定 server)。其它分支才走 PAT。 - if service.EnableShowInService { - return true + // HideForGuest 默认公开,置 true 才对 guest 隐藏,语义与 Server.HideForGuest 对齐。 + if service.HideForGuest { + if _, isMember := c.Get(model.CtxKeyAuthorizedUser); !isMember { + return false + } + // 必须先让 Service.HasPermission 跑 PAT 白名单收口,再让 admin 在无 PAT 请求上 + // 短路放行,否则 admin 自签的受限 PAT 会被早返回绕过 list/history 的 PAT 边界。 + return service.HasPermission(c) } - if _, isMember := c.Get(model.CtxKeyAuthorizedUser); !isMember { - return false - } - // 关键:必须先让 Service.HasPermission 跑 PAT 白名单收口,再让 admin - // 身份在没有 PAT 的请求上短路放行。否则 admin 自己签发的 server_ids - // 受限 PAT 会被 admin 早返回直接放过,绕过 list/history 入口的 PAT 边界。 - return service.HasPermission(c) + return true } func assertOwnsNotificationGroup(c *gin.Context, groupID uint64) error { diff --git a/cmd/dashboard/controller/service.go b/cmd/dashboard/controller/service.go index a2323573..ce5937d7 100644 --- a/cmd/dashboard/controller/service.go +++ b/cmd/dashboard/controller/service.go @@ -489,7 +489,7 @@ func createService(c *gin.Context) (uint64, error) { m.LatencyNotify = mf.LatencyNotify m.MinLatency = mf.MinLatency m.MaxLatency = mf.MaxLatency - m.EnableShowInService = mf.EnableShowInService + m.HideForGuest = mf.HideForGuest m.EnableTriggerTask = mf.EnableTriggerTask m.RecoverTriggerTasks = mf.RecoverTriggerTasks m.FailTriggerTasks = mf.FailTriggerTasks @@ -558,7 +558,7 @@ func updateService(c *gin.Context) (any, error) { m.LatencyNotify = mf.LatencyNotify m.MinLatency = mf.MinLatency m.MaxLatency = mf.MaxLatency - m.EnableShowInService = mf.EnableShowInService + m.HideForGuest = mf.HideForGuest m.EnableTriggerTask = mf.EnableTriggerTask m.RecoverTriggerTasks = mf.RecoverTriggerTasks m.FailTriggerTasks = mf.FailTriggerTasks diff --git a/cmd/dashboard/controller/service_visibility_test.go b/cmd/dashboard/controller/service_visibility_test.go index 5ee44433..5bbdaded 100644 --- a/cmd/dashboard/controller/service_visibility_test.go +++ b/cmd/dashboard/controller/service_visibility_test.go @@ -20,29 +20,29 @@ func newServiceVisibilityCtx(viewer *model.User) *gin.Context { } func TestUserCanViewServiceVisibleServiceIsPublic(t *testing.T) { - visible := &model.Service{Common: model.Common{ID: 1, UserID: 100}, EnableShowInService: true} - assert.True(t, userCanViewService(newServiceVisibilityCtx(nil), visible), "guest must see EnableShowInService=true regardless of owner") + visible := &model.Service{Common: model.Common{ID: 1, UserID: 100}, HideForGuest: false} + assert.True(t, userCanViewService(newServiceVisibilityCtx(nil), visible), "guest must see HideForGuest=false regardless of owner") } func TestUserCanViewServiceHiddenServiceRejectsGuest(t *testing.T) { - hidden := &model.Service{Common: model.Common{ID: 1, UserID: 100}, EnableShowInService: false} + hidden := &model.Service{Common: model.Common{ID: 1, UserID: 100}, HideForGuest: true} assert.False(t, userCanViewService(newServiceVisibilityCtx(nil), hidden), "guest must NOT see hidden service via per-server / per-id sideband endpoints") } func TestUserCanViewServiceHiddenServiceRejectsForeignMember(t *testing.T) { - hidden := &model.Service{Common: model.Common{ID: 1, UserID: 100}, EnableShowInService: false} + hidden := &model.Service{Common: model.Common{ID: 1, UserID: 100}, HideForGuest: true} foreign := &model.User{Common: model.Common{ID: 200}, Role: model.RoleMember} assert.False(t, userCanViewService(newServiceVisibilityCtx(foreign), hidden), "foreign member must NOT see another user's hidden service") } func TestUserCanViewServiceHiddenServiceAllowsOwner(t *testing.T) { - hidden := &model.Service{Common: model.Common{ID: 1, UserID: 100}, EnableShowInService: false} + hidden := &model.Service{Common: model.Common{ID: 1, UserID: 100}, HideForGuest: true} owner := &model.User{Common: model.Common{ID: 100}, Role: model.RoleMember} assert.True(t, userCanViewService(newServiceVisibilityCtx(owner), hidden), "owner must still see their own hidden service") } func TestUserCanViewServiceHiddenServiceAllowsAdmin(t *testing.T) { - hidden := &model.Service{Common: model.Common{ID: 1, UserID: 100}, EnableShowInService: false} + hidden := &model.Service{Common: model.Common{ID: 1, UserID: 100}, HideForGuest: true} admin := &model.User{Common: model.Common{ID: 1}, Role: model.RoleAdmin} assert.True(t, userCanViewService(newServiceVisibilityCtx(admin), hidden), "admin must be able to see any hidden service") } @@ -52,9 +52,10 @@ func TestUserCanViewServiceHiddenServiceAllowsAdmin(t *testing.T) { // 保持对称,避免 hidden service 通过 admin 早返回泄漏给受限 PAT。 func TestUserCanViewServiceLimitedPATShouldDenyAdminWhenOutsideWhitelist(t *testing.T) { hidden := &model.Service{ - Common: model.Common{ID: 1, UserID: 100}, - Cover: model.ServiceCoverIgnoreAll, - SkipServers: map[uint64]bool{2: true}, + Common: model.Common{ID: 1, UserID: 100}, + Cover: model.ServiceCoverIgnoreAll, + SkipServers: map[uint64]bool{2: true}, + HideForGuest: true, } admin := &model.User{Common: model.Common{ID: 1}, Role: model.RoleAdmin} tok := &model.APIToken{ID: 7, UserID: 1} @@ -69,9 +70,10 @@ func TestUserCanViewServiceLimitedPATShouldDenyAdminWhenOutsideWhitelist(t *test func TestUserCanViewServiceLimitedPATAllowsAdminInsideWhitelist(t *testing.T) { visible := &model.Service{ - Common: model.Common{ID: 2, UserID: 100}, - Cover: model.ServiceCoverIgnoreAll, - SkipServers: map[uint64]bool{1: true}, + Common: model.Common{ID: 2, UserID: 100}, + Cover: model.ServiceCoverIgnoreAll, + SkipServers: map[uint64]bool{1: true}, + HideForGuest: true, } admin := &model.User{Common: model.Common{ID: 1}, Role: model.RoleAdmin} tok := &model.APIToken{ID: 7, UserID: 1} diff --git a/model/service.go b/model/service.go index cac80ac0..701bedb8 100644 --- a/model/service.go +++ b/model/service.go @@ -216,7 +216,7 @@ type Service struct { Cover uint8 `json:"cover"` EnableTriggerTask bool `gorm:"default: false" json:"enable_trigger_task,omitempty"` - EnableShowInService bool `gorm:"default: false" json:"enable_show_in_service,omitempty"` + HideForGuest bool `json:"hide_for_guest,omitempty"` // 对游客隐藏 FailTriggerTasksRaw string `gorm:"default:'[]'" json:"-"` RecoverTriggerTasksRaw string `gorm:"default:'[]'" json:"-"` diff --git a/model/service_api.go b/model/service_api.go index b314fc12..354b761a 100644 --- a/model/service_api.go +++ b/model/service_api.go @@ -14,7 +14,7 @@ type ServiceForm struct { MaxLatency float32 `json:"max_latency,omitempty" default:"0.0"` LatencyNotify bool `json:"latency_notify,omitempty" validate:"optional"` EnableTriggerTask bool `json:"enable_trigger_task,omitempty" validate:"optional"` - EnableShowInService bool `json:"enable_show_in_service,omitempty" validate:"optional"` + HideForGuest bool `json:"hide_for_guest,omitempty" validate:"optional"` FailTriggerTasks []uint64 `json:"fail_trigger_tasks,omitempty"` RecoverTriggerTasks []uint64 `json:"recover_trigger_tasks,omitempty"` SkipServers map[uint64]bool `json:"skip_servers,omitempty"`