Files
nezha_domains/cmd/dashboard/controller/api_token_legacy_migration_test.go
T
naibaandcloudcode ab25662ddd 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

79 lines
2.6 KiB
Go
Raw 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 controller
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/nezhahq/nezha/model"
)
// createAPIToken 是「旧 mcp:* → 新 nezha:*」唯一的归一化入口:
// - mcp:fs:read / mcp:server:read 归一化为 nezha:server:read
// - mcp:server:exec 归一化为 nezha:server:exec
// - mcp:fs:write / mcp:fs:delete / mcp:* 不再可签发——它们历史上覆盖范围
// 比 nezha:server:write/delete 窄(只跑 MCP fs 工具),静默映射会扩权。
//
// 这样老调用方传旧 scope 还能创建只读 PAT,但拿不到 write/delete 提权。
func TestCreateAPIToken_RewritesLegacyReadScopeToNezhaRead(t *testing.T) {
defer setupAPITokenTest(t)()
c := ctxAsUser(10, model.RoleMember)
bindJSON(c, model.APITokenCreateRequest{
Name: "legacy-reader",
Scopes: []string{"mcp:fs:read"},
})
res, err := createAPIToken(c)
require.NoError(t, err, "legacy mcp:fs:read must be accepted at create time and rewritten")
require.Equal(t, []string{model.ScopeServerRead}, res.Scopes,
"create response must reflect the new unified scope name, not the legacy alias")
}
func TestCreateAPIToken_RewritesLegacyExecScopeToNezhaExec(t *testing.T) {
defer setupAPITokenTest(t)()
c := ctxAsUser(10, model.RoleMember)
bindJSON(c, model.APITokenCreateRequest{
Name: "legacy-exec",
Scopes: []string{"mcp:server:exec"},
})
res, err := createAPIToken(c)
require.NoError(t, err)
require.Equal(t, []string{model.ScopeServerExec}, res.Scopes)
}
func TestCreateAPIToken_RejectsLegacyMCPWriteScope(t *testing.T) {
defer setupAPITokenTest(t)()
c := ctxAsUser(10, model.RoleMember)
bindJSON(c, model.APITokenCreateRequest{
Name: "legacy-writer",
Scopes: []string{"mcp:fs:write"},
})
_, err := createAPIToken(c)
require.Error(t, err,
"mcp:fs:write must be rejected: silently mapping to nezha:server:write would expand the original "+
"MCP-only write capability to every REST server mutation route")
}
func TestCreateAPIToken_RejectsLegacyMCPDeleteScope(t *testing.T) {
defer setupAPITokenTest(t)()
c := ctxAsUser(10, model.RoleMember)
bindJSON(c, model.APITokenCreateRequest{
Name: "legacy-deleter",
Scopes: []string{"mcp:fs:delete"},
})
_, err := createAPIToken(c)
require.Error(t, err)
}
func TestCreateAPIToken_RejectsLegacyMCPWildcardScope(t *testing.T) {
defer setupAPITokenTest(t)()
c := ctxAsUser(1, model.RoleAdmin)
bindJSON(c, model.APITokenCreateRequest{
Name: "legacy-admin",
Scopes: []string{"mcp:*"},
})
_, err := createAPIToken(c)
require.Error(t, err,
"mcp:* must be rejected even for admin: the new unified namespace is nezha:* / nezha:admin:*")
}