Cron 执行身份边界(INV-CRON-10)

wesgine 定时任务子系统中 Actor 边界的设计,确保定时任务只能由所有者操作和触发。


核心问题

Actor 边界的第四个面,也是唯一一个泄漏的不是数据而是执行身份的面。

executeJob 把 entry.Actor 抄进 JobRequest,job 以所有者身份跑——所以一次不查主的 Trigger 不是"改了别人的配置",是以别人的身份跑代码。

后果:


两个单点

jobVisible(可见性)

func jobVisible(entry CronEntry, actor string) bool {
    if actor == "" { return true }  // admin 视图
    if entry.Actor == "" { return false } // 无主仅 admin 可见
    return entry.Actor == actor
}

ownedJob(by-ID 解析)

行身份是 (id, actor):

SELECT * FROM wes_cron_jobs WHERE id = ? AND actor = ?

不存在与不属于你返回同一个 wcron.ErrJobNotFound——不给 ID 探针。


Manager 方法全收 actor

wcron.Manager 的每个方法都收 actor 参数:

type Manager interface {
    List(ctx, actor) ([]Entry, error)
    Get(ctx, id, actor) (*Entry, error)
    Add(ctx, actor, spec) (string, error)
    Update(ctx, id, actor, patch) error
    Remove(ctx, id, actor) error
    Trigger(ctx, id, actor) error
    Enable(ctx, id, actor) error
    Disable(ctx, id, actor) error
}

少一个就是一条完整越权路径,而它编译通过、行为看起来"正常"。


工具侧 actor 来源

模型侧 cron_manage 工具:

actionactor 来源说明
addtc.ActorjobInput 无 Actor 字段,不可冒名
updatetc.Actor缺 actor 时只需一个 ID 即可越权
removetc.Actor同上
triggertc.Actor同上

HTTP 侧 /cron/* 一律 actorFromRequestOrContext(req)。


无主 Job 处理

来源

actor 列是 DEFAULT '' 后加的,INV-CRON-10 生效前写入的每一行都无主。

读侧 fail-closed,ListDue 不带 actor

那些行:

"能跑、关不掉、查不到"比"看不到"坏得多。

ClaimOwnerless 认领

func (m *Manager) ClaimOwnerless(ctx context.Context, actor string) error {
    // 只动 actor = '' 的行
    // 不碰有主行
    // 空 actor 参数直接拒
}

限制:


Seed 声明与运行时配额

Seed 播种机制

CellSpec.Cron 是出生声明(INV-SEED-01):

标记表 wes_spec_seed 与守护的行同层(state.db)。

配额判点

MaxCronJobs 判据落在唯一增长点 cron.Scheduler.Add(INV-QUOTA-05):


各产品场景

产品Cron 边界影响
wescodeactor 恒为 "local",单人无边界问题
wesclaw 桌面N=1,boot 调 ClaimOwnerless
wescraft 团队多 actor,Cron 边界生效
企业版部门多员工,定时任务按员工隔离