Joye Dev

Back

feat(api): mint workspace-scoped worker tokens for backend callbacks

**PR:**feat(api): mint workspace-scoped worker tokens for backend callbacks (#4156)


正文来源:飞书学习文档。以下为通过个人 Feishu API 获取并转换后的完整 Markdown 正文。

今日选择#

**PR:**feat(api): mint workspace-scoped worker tokens for backend callbacks (#4156)

**作者:**Horcrux / magicismight

**Merge 时间:**2026-07-02 12:59:44 Australia/Melbourne(2026-07-02T02:59:44Z)

链接:https://github.com/adastralab-ai/voyager/pull/4156

**模块:**backend/go internal/core/workertoken, gin middleware, public asset access, API contract tests, deploy secret injection

为什么值得学#

  • 它把“后台 worker 代表某个 workspace 回调 API”建模成短期 capability,而不是伪造用户 session 或把长期共享 secret 分发给 worker。
  • 鉴权被拆成两段:middleware 只把已验证 token 转成 request scope,真正的资源授权仍由 handler 按 workspace 匹配决定。
  • 失败路径选择了 additive/fail-through:无 token 或无效 token 不在全局 middleware 里 blanket abort,而是交给下游现有 authz 返回 401/403/404。
  • 测试没有只测 token round trip,而是用 contract test 覆盖真实 API 行为:同 workspace 可读、跨 workspace 不可读、匿名无 token 不可读。

关键代码#

1. Token 只携带 workspace capability#

type claims struct {
    jwt.RegisteredClaims
    WorkspaceID string `json:"workspace_id"`
}

func (s *Signer) Mint(workspaceID uuid.UUID, ttl time.Duration) (string, error) {
    if len(s.secret) == 0 {
        return "", errors.New("worker token secret not configured")
    }
    now := time.Now()
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims{
        RegisteredClaims: jwt.RegisteredClaims{
            IssuedAt:  jwt.NewNumericDate(now),
            ExpiresAt: jwt.NewNumericDate(now.Add(ttl)),
        },
        WorkspaceID: workspaceID.String(),
    })
    return token.SignedString(s.secret)
}
plaintext

设计点:claim 里没有 user id、role 或 arbitrary permission,只表达“这个 job 可访问哪个 workspace”。这降低了 token 被滥用时的语义范围,也避免 worker 侧理解用户权限模型。

2. Middleware 只注入 scope,不直接授权#

workspaceID, err := signer.Verify(strings.TrimPrefix(header, bearerPrefix))
if err != nil {
    c.Next()
    return
}
ctx := reqctx.WithWorkerScope(c.Request.Context(), &reqctx.WorkerScope{
    WorkspaceID: workspaceID,
})
c.Request = c.Request.WithContext(ctx)
c.Next()
plaintext

设计点:全局 middleware 不知道具体 endpoint 的资源语义,所以它只解析 capability。无效 token 继续走下游鉴权,避免一个全局中间件改变所有 route 的错误语义。

3. Handler 做 workspace 边界检查#

if scope := reqctx.GetWorkerScope(c.Request.Context()); scope != nil {
    if scope.WorkspaceID != workspaceID {
        return uuid.UUID{}, bizerrors.NotFound("workspace not found", nil)
    }
    return workspaceID, nil
}
plaintext

设计点:worker token bypass 的不是资源边界,而是 membership/file-link 这类用户路径。跨 workspace 被伪装成 NotFound,和现有防枚举策略一致。

4. Contract test 覆盖租户隔离#

t.Run("grants the scoped workspace", func(t *testing.T) { ... http.StatusOK ... })

t.Run("denies a token scoped to another workspace", func(t *testing.T) { ... http.StatusNotFound ... })

t.Run("denies an anonymous caller with no token", func(t *testing.T) { ... require.NotEqualf(t, http.StatusOK, ...) })
plaintext

设计点:这里测的是公开 asset API 的最终行为,不只是 workertoken 包的纯函数。对这种跨鉴权层改动,行为测试比 mock middleware 更有价值。

和最近学习记录的关系#

本地学习日志此前为空,所以没有看到可关联的昨日或近 7-14 条记录。今天这条后续可以和 cover render、asset/font 获取、worker callback auth、workspace isolation 相关 PR 串起来看。

我会怎么吸收#

  • 后台 job 需要访问用户/租户资源时,优先建模成短期、窄作用域 capability,而不是复用用户 session。
  • 全局 middleware 适合做“解析和注入上下文”,资源授权尽量留在懂业务语义的 handler/service 层。
  • 鉴权类 PR 的测试要覆盖真实入口和负例,尤其是跨租户 token、匿名请求、过期或错误 secret 这类边界。

边界 / 风险#

  • PR 描述里明确 token 是 stateless 且不可提前撤销,所以 TTL 必须足够短;如果未来复用到长任务或更高权限写操作,需要重新评估撤销和审计。
  • middleware 对无效 token fail-through,优点是兼容现有 authz,代价是排查 worker token 失败时需要从下游 401/403/404 和日志中定位。
  • deploy 侧 secret 注入是 fail-open,适合渐进上线;但真正接入 worker 之前要确认各环境 WORKER\_TOKEN\_SECRET 已存在,否则 mint 会报 “worker token secret not configured”。

候选说明#

2026-07-02 查到 22 个 merged PR,其中 20 个 base 是 main;2026-07-01 查到约 28 个 merged PR。最终选择 #4156,因为它涉及 API 鉴权边界、tenant isolation、middleware 与 handler 分层、secret 配置和 contract test,学习价值高于纯文案、样式、路由或小修复类 PR。由于学习日志为空,没有因为去重跳过候选。

🗂️ 这是知识库中的🔬 研究。

内容可能仍在补充或修订中。

← Back