跳转至

授权层级与访问控制矩阵(源码增强版)

1. 双层的授权体系

MonkeyCode 存在两套平行的授权体系:后端层(Go)和代理层(TypeScript)。

1.1 后端 Go 授权体系

后端是真正的授权执行者,基于 Cookie Session + 中间件链

请求进入
Auth() 中间件
    │ Cookie → Redis Lookup Key → Redis Hash Key
    │ 提取 user_id, email, role
    │ 检查 session 是否过期
    │ 401 失败则返回(不继续)
Role 检查(admin 端点额外检查 role === "admin")
    │ 403 失败则返回
TargetActive 中间件
    │ 检查用户 status === "active"
    │ "inactive" 或 "banded" 用户 403
Handler(执行业务逻辑)

1.2 代理 TypeScript 授权体系

代理层有自己的简化授权,由于所有代理请求都通过同一个后端账号转发:

请求进入代理
模型检查(models.ts: resolveModel)
    │ 所有通过代理的请求共享同一个后端 Session
    │ 无用户级鉴权(代理不区分用户)
任务创建(task-runner.ts: createTask)
    │ 使用号池中获取的 AuthManager
    │ 自动处理 Session 过期和重登录
MonkeyCode 后端(真正的授权执行者)

2. 后端 Go 授权角色(5 种)

角色(role) 中间件检查 API 访问范围 典型用户
individual Auth /api/v1/users/* 个人用户
enterprise Auth /api/v1/users/* 企业用户
subaccount Auth + 主账号校验 /api/v1/users/*(受限) 子账号
admin Auth + admin 角色 /api/v1/admin/* 管理员
gittask Auth + 特殊校验 /api/v1/users/tasks/* Git 任务机器人

2.1 用户状态(3 种)

状态(status) 说明 API 访问
active 正常 ✅ 允许
inactive 未激活/被禁用 ❌ 拒绝(TargetActive 中间件)
banded 被封禁 ❌ 拒绝(TargetActive 中间件)

2.2 团队成员角色(TeamMemberRole)

角色 说明 Session Cookie
团队管理员(team_admin 管理团队资源、成员 monkeycode_ai_team_session
团队成员(team_member 使用团队资源 monkeycode_ai_team_session

3. 代理层的实际权限模式

代理层通过不同的 AuthManager 实现两种访问模式:

3.1 单账号模式(开发/个人使用)

// proxy/src/auth.ts
export class AuthManager {
  private sessionCookie: string = ""
  private sessionCookieName: string = "monkeycode_ai_session"
  private email: string = ""
  private passwordHash: string = ""
  private captchaToken: string = ""
  private lastAuthTime: number = 0
  private sessionTTL: number = 24 * 60 * 60 * 1000  // 代理侧的 24h Session TTL
  private loginMode: LoginMode = "user"

  constructor() {
    this.email = process.env.MONKEYCODE_EMAIL || process.env.MONKEYCODE_USERNAME || ""
    this.passwordHash = process.env.MONKEYCODE_PASSWORD_HASH || ""
    this.captchaToken = process.env.MONKEYCODE_CAPTCHA_TOKEN || ""

    // 明文密码支持
    const plainPassword = process.env.MONKEYCODE_PASSWORD || ""
    if (plainPassword && !this.passwordHash) {
      this.passwordHash = plainPassword.trim()
    }

    // Cookie 直接注入
    const existingCookie = process.env.MONKEYCODE_SESSION_COOKIE || ""
    if (existingCookie) {
      this.sessionCookie = existingCookie
      this.lastAuthTime = Date.now()
    }
  }

  // 获取当前 Cookie(过期自动重登录)
  async getSessionCookie(): Promise<string> {
    if (this.sessionCookie && Date.now() - this.lastAuthTime < this.sessionTTL) {
      return this.sessionCookie
    }
    await this.login()
    return this.sessionCookie
  }
}

关键配置差异: - 后端 Session TTL:30 天(Redis 端硬限制,不可刷新) - 代理 Session TTL:24 小时(仅代理侧缓存,实际 TTL 仍由后端控制)

3.2 号池模式(多用户/生产使用)

// proxy/src/account-pool.ts
// HTTP 共享模式:取最久未用的活跃账号
acquireHttp(): AuthManager | null {
  const candidates = this.accounts
    .filter((a) => a.status === "ACTIVE" && !a.lockedByWs)
    .sort((a, b) => a.lastUsedAt - b.lastUsedAt)

  const idx = this.roundRobinIndex % candidates.length
  this.roundRobinIndex++
  const chosen = candidates[idx]
  chosen.lastUsedAt = Date.now()
  return chosen.auth
}

// WS 独占模式:锁定一个账号直到流结束
acquireWs(): AuthManager | null {
  const candidates = this.accounts
    .filter((a) => a.status === "ACTIVE" && !a.lockedByWs)
    .sort((a, b) => a.lastUsedAt - b.lastUsedAt)

  const chosen = candidates[0]
  chosen.lockedByWs = true   // 标记为 WS 独占
  chosen.lockedAt = Date.now()
  chosen.lastUsedAt = Date.now()
  return chosen.auth
}

// 释放 WS 独占锁
releaseWs(auth: AuthManager): void {
  const entry = this.findByAuth(auth)
  if (entry) {
    entry.lockedByWs = false
    entry.lockedAt = null
  }
}

4. 代理管理端点的权限现状

代理包含了无内置认证的管理端点,这对安全性有重要影响:

// proxy/src/server.ts — 管理端点定义

// 设置 Session Cookie(可覆盖当前认证状态)
app.post("/admin/session", express.text(), (req, res) => {
  const cookie = req.body
  if (!cookie) {
    res.status(400).json({ error: "Cookie value required" })
    return
  }
  singleAuth?.setSessionCookie(cookie)
  res.json({ status: "ok", message: "Session cookie set" })
})

// OAuth 登录管理
app.post("/admin/login/send-code", async (req, res) => { ... })  // 发送短信验证码
app.post("/admin/login/verify", async (req, res) => { ... })     // 验证码登录
app.post("/admin/login/callback", async (req, res) => { ... })    // 回调登录

// 号池管理
app.get("/admin/pool/status", (_req, res) => { ... })             // 号池状态
app.post("/admin/pool/refresh", async (_req, res) => { ... })     // 刷新号池

// 模型管理
app.post("/admin/refresh-models", async (_req, res) => { ... })   // 刷新模型缓存
app.get("/admin/discover", async (_req, res) => { ... })          // 自动发现 image_id

注意: 以上端点均无认证检查。任何能访问代理端口的人都可以: - 替换 Session Cookie(劫持会话) - 读取号池状态(了解账号健康状况) - 重新登录号池账号

5. AI SDK 的模型 ID 解析授权

代理层中 modelManager.resolveModel() 实现了 6 层回退解析:

async resolveModel(openaiModelId: string): Promise<MonkeyCodeModel | null> {
  const models = await this.fetchModels()

  // 1. 精确匹配 monkeycode/provider/model
  const exact = models.find((m) => this.toOpenAIModelId(m) === openaiModelId)
  if (exact) return exact

  // 2. provider/model 格式
  const byProviderModel = models.find(
    (m) => `${m.provider}/${m.model}` === openaiModelId
  )
  if (byProviderModel) return byProviderModel

  // 3. 模糊匹配 model 名称
  const byModelName = models.find((m) => m.model === openaiModelId)
  if (byModelName) return byModelName

  // 4. 匹配 display_name
  const byDisplayName = models.find((m) => m.display_name === openaiModelId)
  if (byDisplayName) return byDisplayName

  // 5. 回退到默认模型
  const defaultModel = models.find((m) => m.is_default)
  if (defaultModel) return defaultModel

  // 6. 最后的回退
  return models[0] || null
}

这意味着如果传递的 model_id 完全不存在,代理仍会返回模型列表中的第一个模型,而不是拒绝请求。

6. 实体级权限(Owner 机制)

6.1 模型所有权

类型 源码常量 说明 可见性
私有 private 用户个人创建 仅创建者
团队 team 团队共享 团队内成员
公开 public 管理员创建 所有认证用户
// proxy/src/types.ts
export type OwnerType = "private" | "team" | "public"

// 模型的 API Key 可见性
// - public: API Key 被隐藏(只返回空字符串)
// - private: API Key 对创建者可见
// - team: API Key 对管理员可见

6.2 模型访问级别

export type AccessLevel = "basic" | "pro" | "ultra"
级别 说明 可用模型
basic 基础订阅 免费模型 + basic 模型
pro 专业订阅 basic 模型 + pro 模型
ultra 高级订阅 所有模型

7. 中间件链代码级分析

// middleware/auth.go(基于源码反推)
func Auth() gin.HandlerFunc {
    return func(c *gin.Context) {
        // 1. 从 Cookie 提取 session uuid
        cookie, err := c.Cookie("monkeycode_ai_session")
        if err != nil {
            c.AbortWithStatusJSON(401, Response{Code: 40100, Msg: "unauthorized"})
            return
        }

        // 2. Redis Lookup Key → user_id
        //    Key: session_lookup:{uuid}
        //    Value: user_id
        userID, err := redis.Get(fmt.Sprintf("session_lookup:%s", cookie))
        if err != nil {
            c.AbortWithStatusJSON(401, Response{Code: 40100, Msg: "session expired"})
            return
        }

        // 3. Redis Hash Key → 用户完整信息
        //    Key: session:{uuid}
        //    Fields: user_id, email, role, created_at, expires_at
        sessionData, err := redis.HGetAll(fmt.Sprintf("session:%s", cookie))
        if err != nil {
            c.AbortWithStatusJSON(401, Response{Code: 40100, Msg: "session not found"})
            return
        }

        // 4. 注入上下文
        c.Set("user_id", userID)
        c.Set("email", sessionData["email"])
        c.Set("role", sessionData["role"])
        c.Set("session_uuid", cookie)

        c.Next()
    }
}

7.2 admin 角色检查

func AdminRequired() gin.HandlerFunc {
    return func(c *gin.Context) {
        role := c.GetString("role")
        if role != "admin" {
            c.AbortWithStatusJSON(403, Response{Code: 40300, Msg: "forbidden"})
            return
        }
        c.Next()
    }
}

7.3 代理层的自动回复(代替用户授权)

在多轮对话和任务执行中,代理层自动回复 Agent 的用户确认请求,实现完全自动化

// proxy/src/task-runner.ts — 自动回复用户问题
ws.on("open", () => {
  // 启用自动审批模式
  ws.send(JSON.stringify({ type: "auto-approve" }))
  // 发送用户输入
  ws.send(JSON.stringify({ type: "user-input", data: prompt }))
})

// 处理 Agent 的用户确认请求
if (msg.kind === "acp_ask_user_question") {
  const questionData = JSON.parse(msg.data)
  ws.send(JSON.stringify({
    type: "reply-question",
    data: JSON.stringify({
      request_id: questionData.request_id,
      answers_json: "",
      cancelled: false,  // 不取消,自动继续
    }),
  }))
}

8. 授权推荐与安全建议

8.1 反向代理场景的账号选择

对于 LLM 反向代理场景,推荐使用个人用户账号: - 需要的核心 API:模型列表 + 任务创建/流/控制 - 这些 API 全部在用户级端点下,不需要团队或管理员权限 - 团队级 API 不需要(如团队管理、批量邀请) - 管理员 API 不需要(除非要在代理中创建公开模型)

8.2 安全加固建议

风险 建议措施
代理管理端点无认证 设置 Express trust proxy,仅允许内部 IP
或添加自定义认证中间件
共享 Session 泄露 代理绑定到 127.0.0.1
使用 iptables 限制访问来源
号池文件明文密码 chmod 600 accounts.json
或使用环境变量注入配置
WebSocket 未加密 使用 WSS(通过 nginx 反向代理)

相关章节