mirror of
https://github.com/Buriburizaem0n/nezha_domains.git
synced 2026-09-19 09:40:12 +00:00
Introduce Personal Access Tokens (nzp_*) as a stateless auth path alongside
JWT, gated per-endpoint by a scope middleware (nezha:{resource}:{verb}) with
fail-closed empty-scope defaults and a server-id whitelist. Self-management
endpoints (profile, api-tokens, oauth2 bind, refresh-token) explicitly reject
PATs to block privilege-escalation chains. A revoke registry tears down active
long-lived connections (terminal, fm, ws, transfer, mcp) the moment a PAT is
deleted, with a tombstone closing the revoke->register race.
Add an MCP endpoint that proxies tool calls (exec, fs read/write/delete,
transfer) to agents over gRPC, guarded by origin/DNS-rebinding checks, a
per-token rate limiter, audit logging, and a kill switch. Serialize all
sends through the IOStream wrapper to honour grpc-go's concurrency contract.
Add CSRF double-submit protection on unsafe cookie-authenticated methods,
exempting authenticated PAT requests by context identity (not a forgeable
Authorization header). Apply visibility/whitelist filtering consistently
across list, get-by-id, and mutate paths to enforce tenant isolation.
Migrate legacy mcp:* scopes: rewrite read/exec to nezha:* equivalents and
drop dangerous write/delete/wildcard grants.
Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
75 lines
1.9 KiB
Go
75 lines
1.9 KiB
Go
package controller
|
||
|
||
import (
|
||
"strings"
|
||
|
||
"github.com/nezhahq/nezha/model"
|
||
)
|
||
|
||
// MCPMinAgentVersion 是支持 MCP 的最低 agent 版本。
|
||
//
|
||
// release 流程:在 agent ship 了 MCP handlers 后,把此值更新为该 release 的版本号。
|
||
// 不变量:必须为非空。否则旧 agent 收到 TaskTypeExec/TaskTypeFs* 等新任务类型时
|
||
// 走 default 分支不回 TaskResult,dashboard 要等 CallAgent 超时(30s)甚至更久
|
||
// (fs.transfer 的 IOStream attach 30s)才能感知,这是 server-transfer 已经
|
||
// 通过 MinServerTransferAgentVersion 修复过的同类问题。
|
||
const MCPMinAgentVersion = "v2.1.0"
|
||
|
||
// requireAgentSupportsMCP 在 tool handler 调 CallAgent 之前快速失败不支持的 agent。
|
||
// 仅作为 UX 优化:真正的安全/正确性由 agent 端 task switch 的 default 分支保障。
|
||
func requireAgentSupportsMCP(server *model.Server) error {
|
||
if MCPMinAgentVersion == "" || server == nil || server.Host == nil {
|
||
return nil
|
||
}
|
||
if compareSemver(server.Host.Version, MCPMinAgentVersion) < 0 {
|
||
return errMCPUnsupported
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// compareSemver 比较两个 "MAJOR.MINOR.PATCH[-suffix]" 字符串。
|
||
// 返回 -1/0/1。无法解析时按字符串字典序比较,保证全序但可能不精确——
|
||
// 对于 "agent 太老" 的快速失败用途已经足够。
|
||
func compareSemver(a, b string) int {
|
||
if a == b {
|
||
return 0
|
||
}
|
||
aparts := semverParts(a)
|
||
bparts := semverParts(b)
|
||
for i := 0; i < 3; i++ {
|
||
if aparts[i] < bparts[i] {
|
||
return -1
|
||
}
|
||
if aparts[i] > bparts[i] {
|
||
return 1
|
||
}
|
||
}
|
||
if a < b {
|
||
return -1
|
||
}
|
||
if a > b {
|
||
return 1
|
||
}
|
||
return 0
|
||
}
|
||
|
||
func semverParts(v string) [3]int {
|
||
v = strings.TrimPrefix(v, "v")
|
||
if i := strings.IndexAny(v, "-+"); i >= 0 {
|
||
v = v[:i]
|
||
}
|
||
var out [3]int
|
||
parts := strings.Split(v, ".")
|
||
for i := 0; i < 3 && i < len(parts); i++ {
|
||
n := 0
|
||
for _, c := range parts[i] {
|
||
if c < '0' || c > '9' {
|
||
break
|
||
}
|
||
n = n*10 + int(c-'0')
|
||
}
|
||
out[i] = n
|
||
}
|
||
return out
|
||
}
|