package model import ( "time" "github.com/gin-gonic/gin" "github.com/goccy/go-json" "github.com/robfig/cron/v3" "gorm.io/gorm" ) const ( CronCoverIgnoreAll = iota CronCoverAll CronCoverAlertTrigger CronTypeCronTask = 0 CronTypeTriggerTask = 1 ) type Cron struct { Common Name string `json:"name"` TaskType uint8 `gorm:"default:0" json:"task_type"` // 0:计划任务 1:触发任务 Scheduler string `json:"scheduler"` // 分钟 小时 天 月 星期 Command string `json:"command,omitempty"` Servers []uint64 `gorm:"-" json:"servers"` PushSuccessful bool `json:"push_successful,omitempty"` // 推送成功的通知 NotificationGroupID uint64 `json:"notification_group_id"` // 指定通知方式的分组 LastExecutedAt time.Time `json:"last_executed_at,omitempty"` // 最后一次执行时间 LastResult bool `json:"last_result,omitempty"` // 最后一次执行结果 Cover uint8 `json:"cover"` // 计划任务覆盖范围 (0:仅覆盖特定服务器 1:仅忽略特定服务器 2:由触发该计划任务的服务器执行) CronJobID cron.EntryID `gorm:"-" json:"cron_job_id,omitempty"` ServersRaw string `json:"-"` } func (c *Cron) BeforeSave(tx *gorm.DB) error { if data, err := json.Marshal(c.Servers); err != nil { return err } else { c.ServersRaw = string(data) } return nil } func (c *Cron) AfterFind(tx *gorm.DB) error { return json.Unmarshal([]byte(c.ServersRaw), &c.Servers) } // HasPermission 扩展默认的 owner/admin 检查,使得 PAT 的 server_ids 白名单 // 同样能收窄 cron 的列出、触发、删除路径。 // // 语义按 Cover 字段分流,与 dispatch 入口(CronTrigger)的 fan-out 规则严格 // 对齐——Servers 字段在不同 Cover 下含义完全相反: // // - CronCoverIgnoreAll:Servers 是 allow-list;必须每个 server 都落在 PAT // 白名单内。空 allow-list 是「matches nothing」的退化形态,安全。 // - CronCoverAlertTrigger:Servers 是触发服务器 allow-list;与上同。 // - CronCoverAll:Servers 是 deny-list。dispatch 时 fan out 到 owner 的 // 全部 server 再减去这个 deny-list。受限 PAT 必须保证 deny-list 已经覆 // 盖 owner 在白名单外的所有 servers——否则 CronTrigger 会把任务发到 // PAT 没权限的 server 上。本方法和 controller 写侧 guard // rejectImplicitCoverForLimitedPAT* / 运行时 guard // enforcePATCronDispatchScope 共用 DenyListSafeForLimitedPAT,避免列表 // 视图把越界历史/旁路写入行漏给受限 PAT。 func (c *Cron) HasPermission(ctx *gin.Context) bool { if !c.Common.HasPermission(ctx) { return false } v, ok := ctx.Get(CtxKeyAPIToken) if !ok { return true } tok, _ := v.(APITokenAccessor) if tok == nil { return true } switch c.Cover { case CronCoverAll: return DenyListSafeForLimitedPAT(tok, c.GetUserID(), c.Servers) default: for _, id := range c.Servers { if !tok.CanAccessServer(id) { return false } } return true } }