fix(controller): enforce ownership on notification group, NAT server, and batch move

Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
This commit is contained in:
naiba
2026-05-17 10:24:19 +08:00
co-authored by naiba/CloudCode
parent 423175a425
commit a5995ce045
6 changed files with 109 additions and 32 deletions
+4 -2
View File
@@ -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
}
+8 -1
View File
@@ -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)
+18 -8
View File
@@ -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
+51
View File
@@ -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
}
+6 -2
View File
@@ -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]
+22 -19
View File
@@ -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
}