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>
83 lines
2.8 KiB
Go
83 lines
2.8 KiB
Go
package model
|
||
|
||
import (
|
||
"slices"
|
||
"testing"
|
||
)
|
||
|
||
// 这些测试约束「scope 命名统一」契约:
|
||
// - 只有 nezha:* 一套是 first-class scope;
|
||
// - mcp:* 不再作为 HasScope 的别名(避免 mcp:fs:write 静默扩到 REST 的 nezha:server:write);
|
||
// - AllScopes / AdminOnlyScopes 不再包含 mcp:*,新建 token 不能再签发它们。
|
||
//
|
||
// 旧 mcp:* 兼容由 createAPIToken 入口做一次性归一化(mcp:fs:read 等只读/exec 映射到
|
||
// 对应的 nezha:* read/exec),但 write/delete 类不再映射;详见 controller.createAPIToken。
|
||
|
||
func TestAllScopes_DoesNotExposeLegacyMCPScopes(t *testing.T) {
|
||
legacy := []string{
|
||
"mcp:*",
|
||
"mcp:server:read",
|
||
"mcp:server:exec",
|
||
"mcp:fs:read",
|
||
"mcp:fs:write",
|
||
"mcp:fs:delete",
|
||
}
|
||
for _, s := range legacy {
|
||
if slices.Contains(AllScopes, s) {
|
||
t.Errorf("AllScopes must not advertise legacy scope %q; only nezha:* is first-class", s)
|
||
}
|
||
if slices.Contains(AdminOnlyScopes, s) {
|
||
t.Errorf("AdminOnlyScopes must not advertise legacy scope %q", s)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestHasScope_LegacyMCPNoLongerAliasesNezhaWrite(t *testing.T) {
|
||
// 旧 token 数据库里残留 mcp:fs:write,绝不允许覆盖 REST 的 nezha:server:write。
|
||
tok := &APIToken{ScopesCSV: "mcp:fs:write"}
|
||
if tok.HasScope(ScopeServerWrite) {
|
||
t.Fatalf("legacy mcp:fs:write must NOT grant nezha:server:write via HasScope; " +
|
||
"REST routes (server/config, server/:id, batch-delete/server) would become reachable")
|
||
}
|
||
if tok.HasScope(ScopeServerDelete) {
|
||
t.Fatalf("legacy mcp:fs:write must NOT grant nezha:server:delete")
|
||
}
|
||
}
|
||
|
||
func TestHasScope_LegacyMCPDeleteNoLongerAliasesNezhaDelete(t *testing.T) {
|
||
tok := &APIToken{ScopesCSV: "mcp:fs:delete"}
|
||
if tok.HasScope(ScopeServerDelete) {
|
||
t.Fatalf("legacy mcp:fs:delete must NOT grant nezha:server:delete via HasScope")
|
||
}
|
||
}
|
||
|
||
func TestHasScope_LegacyMCPAllNoLongerWildcards(t *testing.T) {
|
||
tok := &APIToken{ScopesCSV: "mcp:*"}
|
||
for _, s := range []string{ScopeServerRead, ScopeServerWrite, ScopeServerDelete, ScopeServerExec} {
|
||
if tok.HasScope(s) {
|
||
t.Errorf("legacy mcp:* must not be treated as a nezha:* wildcard; granted %s", s)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestHasScope_NezhaWildcardStillWorks(t *testing.T) {
|
||
tok := &APIToken{ScopesCSV: ScopeNezhaAll}
|
||
for _, s := range []string{ScopeServerRead, ScopeServerWrite, ScopeServerDelete, ScopeServerExec} {
|
||
if !tok.HasScope(s) {
|
||
t.Errorf("nezha:* wildcard must still cover %s", s)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestHasScope_NezhaResourceWildcardStillWorks(t *testing.T) {
|
||
tok := &APIToken{ScopesCSV: "nezha:server:*"}
|
||
for _, s := range []string{ScopeServerRead, ScopeServerWrite, ScopeServerDelete, ScopeServerExec} {
|
||
if !tok.HasScope(s) {
|
||
t.Errorf("nezha:server:* must cover %s", s)
|
||
}
|
||
}
|
||
if tok.HasScope(ScopeServiceRead) {
|
||
t.Fatalf("nezha:server:* must NOT leak into nezha:service:* family")
|
||
}
|
||
}
|