Files
nezha_domains/model/api_token_unified_scope_test.go
naibaandcloudcode e8dabf5bc6 feat(auth): add PAT auth, scoped REST/MCP access, CSRF, and tenant isolation
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>
2026-05-30 15:56:44 +00:00

83 lines
2.8 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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")
}
}