diff --git a/cmd/dashboard/controller/alertrule.go b/cmd/dashboard/controller/alertrule.go index 84b957c8..0d009252 100644 --- a/cmd/dashboard/controller/alertrule.go +++ b/cmd/dashboard/controller/alertrule.go @@ -194,8 +194,6 @@ func validateRule(c *gin.Context, r *model.AlertRule) error { return singleton.Localizer.ErrorT("need to configure at least a single rule") } - // Trigger task IDs are user-controlled; validate them here so alerts cannot - // reference another user's cron and later execute it from the sentinel path. if !singleton.CronShared.CheckPermission(c, slices.Values(r.FailTriggerTasks)) { return singleton.Localizer.ErrorT("permission denied") } @@ -203,5 +201,9 @@ func validateRule(c *gin.Context, r *model.AlertRule) error { return singleton.Localizer.ErrorT("permission denied") } + if err := assertOwnsNotificationGroup(c, r.NotificationGroupID); err != nil { + return err + } + return nil } diff --git a/cmd/dashboard/controller/cron.go b/cmd/dashboard/controller/cron.go index e715c534..abdf9a94 100644 --- a/cmd/dashboard/controller/cron.go +++ b/cmd/dashboard/controller/cron.go @@ -54,6 +54,10 @@ func createCron(c *gin.Context) (uint64, error) { return 0, singleton.Localizer.ErrorT("permission denied") } + if err := assertOwnsNotificationGroup(c, cf.NotificationGroupID); err != nil { + return 0, err + } + cr.UserID = getUid(c) cr.TaskType = cf.TaskType cr.Name = cf.Name @@ -68,7 +72,6 @@ func createCron(c *gin.Context) (uint64, error) { return 0, singleton.Localizer.ErrorT("scheduled tasks cannot be triggered by alarms") } - // 对于计划任务类型,需要更新CronJob var err error if cf.TaskType == model.CronTypeCronTask { if cr.CronJobID, err = singleton.CronShared.AddFunc(cr.Scheduler, singleton.CronTrigger(&cr)); err != nil { @@ -112,6 +115,10 @@ func updateCron(c *gin.Context) (any, error) { return 0, singleton.Localizer.ErrorT("permission denied") } + if err := assertOwnsNotificationGroup(c, cf.NotificationGroupID); err != nil { + return nil, err + } + var cr model.Cron if err := singleton.DB.First(&cr, id).Error; err != nil { return nil, singleton.Localizer.ErrorT("task id %d does not exist", id) diff --git a/cmd/dashboard/controller/nat.go b/cmd/dashboard/controller/nat.go index a6d6e707..bc69a38b 100644 --- a/cmd/dashboard/controller/nat.go +++ b/cmd/dashboard/controller/nat.go @@ -53,10 +53,15 @@ func createNAT(c *gin.Context) (uint64, error) { return 0, err } - if server, ok := singleton.ServerShared.Get(nf.ServerID); ok { - if !server.HasPermission(c) { - return 0, singleton.Localizer.ErrorT("permission denied") - } + if nf.ServerID == 0 { + return 0, singleton.Localizer.ErrorT("have invalid server id") + } + server, ok := singleton.ServerShared.Get(nf.ServerID) + if !ok { + return 0, singleton.Localizer.ErrorT("have invalid server id") + } + if !server.HasPermission(c) { + return 0, singleton.Localizer.ErrorT("permission denied") } uid := getUid(c) @@ -101,10 +106,15 @@ func updateNAT(c *gin.Context) (any, error) { return nil, err } - if server, ok := singleton.ServerShared.Get(nf.ServerID); ok { - if !server.HasPermission(c) { - return nil, singleton.Localizer.ErrorT("permission denied") - } + if nf.ServerID == 0 { + return nil, singleton.Localizer.ErrorT("have invalid server id") + } + server, ok := singleton.ServerShared.Get(nf.ServerID) + if !ok { + return nil, singleton.Localizer.ErrorT("have invalid server id") + } + if !server.HasPermission(c) { + return nil, singleton.Localizer.ErrorT("permission denied") } var n model.NAT diff --git a/cmd/dashboard/controller/permissions.go b/cmd/dashboard/controller/permissions.go new file mode 100644 index 00000000..5a1d33cc --- /dev/null +++ b/cmd/dashboard/controller/permissions.go @@ -0,0 +1,51 @@ +package controller + +import ( + "github.com/gin-gonic/gin" + + "github.com/nezhahq/nezha/model" + "github.com/nezhahq/nezha/service/singleton" +) + +func callerIsAdmin(c *gin.Context) bool { + auth, ok := c.Get(model.CtxKeyAuthorizedUser) + if !ok { + return false + } + user, ok := auth.(*model.User) + if !ok || user == nil { + return false + } + return user.Role.IsAdmin() +} + +func userCanViewServer(c *gin.Context, server *model.Server) bool { + if server == nil { + return false + } + if callerIsAdmin(c) { + return true + } + if _, isMember := c.Get(model.CtxKeyAuthorizedUser); isMember { + if server.HasPermission(c) { + return true + } + return !server.HideForGuest + } + return !server.HideForGuest +} + +func assertOwnsNotificationGroup(c *gin.Context, groupID uint64) error { + if groupID == 0 { + return nil + } + + var ng model.NotificationGroup + if err := singleton.DB.First(&ng, groupID).Error; err != nil { + return singleton.Localizer.ErrorT("notification group id %d does not exist", groupID) + } + if !ng.HasPermission(c) { + return singleton.Localizer.ErrorT("permission denied") + } + return nil +} diff --git a/cmd/dashboard/controller/server.go b/cmd/dashboard/controller/server.go index 880db1f6..ec9bad59 100644 --- a/cmd/dashboard/controller/server.go +++ b/cmd/dashboard/controller/server.go @@ -336,6 +336,10 @@ func batchMoveServer(c *gin.Context) (any, error) { return nil, singleton.Localizer.ErrorT("user id is required") } + if !callerIsAdmin(c) && moveForm.ToUser != getUid(c) { + return nil, singleton.Localizer.ErrorT("permission denied") + } + singleton.UserLock.RLock() defer singleton.UserLock.RUnlock() if _, ok := singleton.UserInfoMap[moveForm.ToUser]; !ok { @@ -412,10 +416,10 @@ func getServerMetrics(c *gin.Context) (*model.ServerMetricsResponse, error) { return nil, singleton.Localizer.ErrorT("server not found") } - _, isMember := c.Get(model.CtxKeyAuthorizedUser) - if server.HideForGuest && !isMember { + if !userCanViewServer(c, server) { return nil, singleton.Localizer.ErrorT("unauthorized") } + _, isMember := c.Get(model.CtxKeyAuthorizedUser) metricName := c.Query("metric") metricType, ok := serverMetricMap[metricName] diff --git a/cmd/dashboard/controller/service.go b/cmd/dashboard/controller/service.go index 0e953686..cf7d03e3 100644 --- a/cmd/dashboard/controller/service.go +++ b/cmd/dashboard/controller/service.go @@ -110,7 +110,7 @@ func getServiceHistory(c *gin.Context) (*model.ServiceHistoryResponse, error) { } if !singleton.TSDBEnabled() { - return queryServiceHistoryFromDB(serviceID, period, response) + return queryServiceHistoryFromDB(c, serviceID, period, response) } result, err := singleton.TSDBShared.QueryServiceHistory(serviceID, period) @@ -120,17 +120,21 @@ func getServiceHistory(c *gin.Context) (*model.ServiceHistoryResponse, error) { serverMap := singleton.ServerShared.GetList() + filtered := result.Servers[:0] for i := range result.Servers { - if server, ok := serverMap[result.Servers[i].ServerID]; ok { - result.Servers[i].ServerName = server.Name + server, ok := serverMap[result.Servers[i].ServerID] + if !ok || !userCanViewServer(c, server) { + continue } + result.Servers[i].ServerName = server.Name + filtered = append(filtered, result.Servers[i]) } - response.Servers = result.Servers + response.Servers = filtered return response, nil } -func queryServiceHistoryFromDB(serviceID uint64, period tsdb.QueryPeriod, response *model.ServiceHistoryResponse) (*model.ServiceHistoryResponse, error) { +func queryServiceHistoryFromDB(c *gin.Context, serviceID uint64, period tsdb.QueryPeriod, response *model.ServiceHistoryResponse) (*model.ServiceHistoryResponse, error) { since := time.Now().Add(-period.Duration()) var histories []model.ServiceHistory @@ -146,11 +150,13 @@ func queryServiceHistoryFromDB(serviceID uint64, period tsdb.QueryPeriod, respon } for serverID, records := range grouped { - stats := model.ServerServiceStats{ - ServerID: serverID, + server, ok := serverMap[serverID] + if !ok || !userCanViewServer(c, server) { + continue } - if server, ok := serverMap[serverID]; ok { - stats.ServerName = server.Name + stats := model.ServerServiceStats{ + ServerID: serverID, + ServerName: server.Name, } var totalDelay float64 @@ -216,12 +222,10 @@ func listServerServices(c *gin.Context) ([]*model.ServiceInfos, error) { return nil, singleton.Localizer.ErrorT("server not found") } - _, isMember := c.Get(model.CtxKeyAuthorizedUser) - authorized := isMember - - if server.HideForGuest && !authorized { + if !userCanViewServer(c, server) { return nil, singleton.Localizer.ErrorT("unauthorized") } + _, isMember := c.Get(model.CtxKeyAuthorizedUser) // 解析时间范围 periodStr := c.DefaultQuery("period", "1d") @@ -373,16 +377,13 @@ func listServerWithServices(c *gin.Context) ([]uint64, error) { } } - _, isMember := c.Get(model.CtxKeyAuthorizedUser) - authorized := isMember - var ret []uint64 for id := range serverIDSet { server, ok := serverMap[id] if !ok || server == nil { continue } - if !server.HideForGuest || authorized { + if userCanViewServer(c, server) { ret = append(ret, id) } } @@ -545,8 +546,6 @@ func validateServers(c *gin.Context, ss *model.Service) error { return singleton.Localizer.ErrorT("permission denied") } - // Trigger task IDs are user-controlled; validate them here so services cannot - // reference another user's cron and later execute it from the sentinel path. if !singleton.CronShared.CheckPermission(c, slices.Values(ss.FailTriggerTasks)) { return singleton.Localizer.ErrorT("permission denied") } @@ -554,5 +553,9 @@ func validateServers(c *gin.Context, ss *model.Service) error { return singleton.Localizer.ErrorT("permission denied") } + if err := assertOwnsNotificationGroup(c, ss.NotificationGroupID); err != nil { + return err + } + return nil }