fix(access): 统一 API Key 模型权限与列表契约
将全局启用、用户组基线、API Key 专属或排除规则及 scope 按固定顺序求值,避免 Key 越过所属用户组权限,并让运行时候选与模型列表共用同一权限链。 新增 Key 级可分配模型与失效规则诊断接口、OpenAI 兼容 /v1/models 及 rich 列表迁移路径;前端权限弹窗改为按当前 Key 实时加载并支持清理失效规则。 验证:Go 全量测试与 go vet 通过;Web 22 个测试文件共 142 项通过;pnpm lint、pnpm openapi、pnpm build、Compose 配置、gofmt、ShellCheck 和 git diff --check 通过;独立 PostgreSQL 真实配置验收通过。
This commit is contained in:
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/auth"
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/clients"
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/identity"
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/modelaccess"
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/netproxy"
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/runner"
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
|
||||
@@ -641,7 +642,7 @@ func (s *Server) listModels(w http.ResponseWriter, r *http.Request) {
|
||||
// @Failure 401 {object} ErrorEnvelope
|
||||
// @Failure 502 {object} ErrorEnvelope
|
||||
// @Failure 500 {object} ErrorEnvelope
|
||||
// @Router /api/v1/models [get]
|
||||
// @Router /api/v1/platform-models [get]
|
||||
// @Router /api/v1/playground/models [get]
|
||||
func (s *Server) listPlayableModels(w http.ResponseWriter, r *http.Request) {
|
||||
usageScene, err := parseModelUsageSceneQuery(r.URL.Query())
|
||||
@@ -1610,41 +1611,10 @@ func writeTaskAccepted(w http.ResponseWriter, task store.GatewayTask) {
|
||||
}
|
||||
|
||||
func apiKeyScopeAllowed(user *auth.User, kind string) bool {
|
||||
if user == nil || strings.TrimSpace(user.APIKeyID) == "" || len(user.APIKeyScopes) == 0 {
|
||||
if user == nil || strings.TrimSpace(user.APIKeyID) == "" {
|
||||
return true
|
||||
}
|
||||
required := scopeForTaskKind(kind)
|
||||
for _, scope := range user.APIKeyScopes {
|
||||
scope = strings.TrimSpace(strings.ToLower(scope))
|
||||
if scope == "*" || scope == "all" || scope == required {
|
||||
return true
|
||||
}
|
||||
if required == "chat" && (scope == "text" || scope == "text_generate") {
|
||||
return true
|
||||
}
|
||||
if required == "embedding" && scope == "text_embedding" {
|
||||
return true
|
||||
}
|
||||
if required == "rerank" && scope == "text_rerank" {
|
||||
return true
|
||||
}
|
||||
if required == "music" && (scope == "audio_generate" || scope == "music_generate" || scope == "song") {
|
||||
return true
|
||||
}
|
||||
if required == "audio" && (scope == "text_to_speech" || scope == "speech" || scope == "tts") {
|
||||
return true
|
||||
}
|
||||
if required == "voice_clone" && (scope == "audio" || scope == "text_to_speech" || scope == "speech" || scope == "tts") {
|
||||
return true
|
||||
}
|
||||
if required == "image_vectorize" && (scope == "image" || scope == "vectorize") {
|
||||
return true
|
||||
}
|
||||
if required == "video_enhance" && (scope == "video" || scope == "video_upscale" || scope == "upscale") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
return modelaccess.ScopeAllowsTask(user.APIKeyScopes, kind)
|
||||
}
|
||||
|
||||
func requestModelName(body map[string]any) string {
|
||||
@@ -1687,33 +1657,6 @@ func modelNameFromValue(value any) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func scopeForTaskKind(kind string) string {
|
||||
switch kind {
|
||||
case "chat.completions", "responses":
|
||||
return "chat"
|
||||
case "embeddings":
|
||||
return "embedding"
|
||||
case "reranks":
|
||||
return "rerank"
|
||||
case "images.generations", "images.edits":
|
||||
return "image"
|
||||
case "images.vectorize":
|
||||
return "image_vectorize"
|
||||
case "videos.generations":
|
||||
return "video"
|
||||
case "videos.upscales":
|
||||
return "video_enhance"
|
||||
case "song.generations", "music.generations":
|
||||
return "music"
|
||||
case "speech.generations":
|
||||
return "audio"
|
||||
case "voice.clone":
|
||||
return "voice_clone"
|
||||
default:
|
||||
return kind
|
||||
}
|
||||
}
|
||||
|
||||
func statusFromRunError(err error) int {
|
||||
switch {
|
||||
case clients.ErrorCode(err) == "binary_result_expired":
|
||||
|
||||
Reference in New Issue
Block a user