From 502e8b5db5d9872dfa623aaf65ca24c68baf2a1e Mon Sep 17 00:00:00 2001 From: Pine Date: Sat, 29 Aug 2026 00:31:34 +0800 Subject: [PATCH] =?UTF-8?q?feat(auth):=20=E5=8F=97=E4=BF=A1=E4=BB=BB?= =?UTF-8?q?=E5=86=85=E9=83=A8=E8=B0=83=E7=94=A8=E6=96=B9=E4=BB=A4=E7=89=8C?= =?UTF-8?q?=E7=9B=B4=E9=80=9A(PINEAGENTS=5FINTERNAL=5FTOKEN)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 命中内部令牌时鉴权层合成 root 身份,供 server-core 作为唯一入口无权限限制管理微服务 - 为空时关闭该能力;constant-time 比较防时序侧信道 --- common/env.go | 11 +++++++++++ middleware/auth.go | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/common/env.go b/common/env.go index 1aa340f..c774048 100644 --- a/common/env.go +++ b/common/env.go @@ -6,6 +6,17 @@ import ( "strconv" ) +// InternalAccessToken 受信任内部调用方令牌:命中后鉴权层直接合成 root 身份, +// 放开全部管理/root 端点(用于 server-core 作为唯一入口无权限限制地管理微服务)。 +// 为空时关闭该能力。 +// +// 需在请求时惰性读取(而非包 init 时),因为 main() 里 godotenv.Load(".env") +// 晚于包 init 执行,过早读取会拿到空串。 +func InternalAccessToken() string { + return os.Getenv("PINEAGENTS_INTERNAL_TOKEN") +} + + func GetEnvOrDefault(env string, defaultValue int) int { if env == "" || os.Getenv(env) == "" { return defaultValue diff --git a/middleware/auth.go b/middleware/auth.go index 9f2a9df..f82f52a 100644 --- a/middleware/auth.go +++ b/middleware/auth.go @@ -7,6 +7,8 @@ import ( "net/http" "strings" + "crypto/subtle" + "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" "github.com/QuantumNous/new-api/i18n" @@ -42,7 +44,40 @@ func validUserInfo(username string, role int) bool { return true } +// bypassTrustedInternal 受信任内部调用方(server-core):命中 PINEAGENTS_INTERNAL_TOKEN 时 +// 不查库、直接合成 root 身份,放开 AdminAuth/RootAuth/RequirePermission 的权限限制。 +// 返回 true 表示已处理(含 c.Next())。 +func bypassTrustedInternal(c *gin.Context, minRole int) bool { + internalToken := common.InternalAccessToken() + if internalToken == "" { + return false + } + raw, ok := authorizationToken(c.GetHeader("Authorization")) + if !ok || subtle.ConstantTimeCompare([]byte(raw), []byte(internalToken)) != 1 { + return false + } + user := &model.UserBase{ + Id: -1, + Group: "default", + Role: common.RoleRootUser, + Status: common.UserStatusEnabled, + Username: "internal-server", + } + identity := service.AuthIdentity{UserID: user.Id, UserAuthVersion: 1} + setDashboardAuthContext(c, user, identity, true) + var auditWriter *auditResponseWriter + if minRole >= common.RoleAdminUser { + auditWriter = beginAdminAudit(c) + } + c.Next() + finishAdminAudit(c, auditWriter) + return true +} + func authHelper(c *gin.Context, minRole int) { + if bypassTrustedInternal(c, minRole) { + return + } user, identity, useAccessToken, err := authenticateDashboardRequest(c) if err != nil { writeDashboardAuthError(c, err)