Files
easyai-ai-gateway/apps/api/internal/modelaccess/scopes.go
T
wangbo cc97e6649c 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 真实配置验收通过。
2026-08-03 09:17:15 +08:00

167 lines
4.2 KiB
Go

package modelaccess
import "strings"
// ScopeAllowsTask reports whether scopes authorize one public task kind.
// Empty scopes retain the legacy unrestricted behavior for old API keys.
func ScopeAllowsTask(scopes []string, kind string) bool {
if len(scopes) == 0 {
return true
}
return scopeAllowsCapability(scopes, capabilityForTaskKind(kind))
}
// ScopeAllowsModelType reports whether scopes authorize one runtime model type.
// Unknown model types are deny-by-default unless the key has all or an exact
// custom scope matching that model type.
func ScopeAllowsModelType(scopes []string, modelType string) bool {
if len(scopes) == 0 {
return true
}
modelType = normalize(modelType)
if modelType == "" {
return false
}
capability := capabilityForModelType(modelType)
if capability == "" {
return hasScope(scopes, modelType)
}
return scopeAllowsCapability(scopes, capability)
}
// FilterModelTypes keeps the declared order while removing model types that
// the key cannot invoke.
func FilterModelTypes(scopes []string, modelTypes []string) []string {
if len(modelTypes) == 0 {
return nil
}
filtered := make([]string, 0, len(modelTypes))
seen := map[string]bool{}
for _, modelType := range modelTypes {
normalized := normalize(modelType)
if normalized == "" || seen[normalized] || !ScopeAllowsModelType(scopes, normalized) {
continue
}
seen[normalized] = true
filtered = append(filtered, normalized)
}
return filtered
}
func capabilityForTaskKind(kind string) string {
switch normalize(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 normalize(kind)
}
}
func capabilityForModelType(modelType string) string {
switch normalize(modelType) {
case "text_generate", "tools_call":
return "chat"
case "text_embedding":
return "embedding"
case "text_rerank":
return "rerank"
case "image_generate", "image_edit", "image_analysis":
return "image"
case "image_vectorize":
return "image_vectorize"
case "video_generate", "image_to_video", "text_to_video", "video_edit", "video_reference", "video_first_last_frame", "video_understanding", "omni_video", "omni":
return "video"
case "video_enhance":
return "video_enhance"
case "audio_generate", "music_generate":
return "music"
case "text_to_speech", "audio_understanding":
return "audio"
case "voice_clone":
return "voice_clone"
default:
return ""
}
}
func scopeAllowsCapability(scopes []string, capability string) bool {
capability = normalize(capability)
if capability == "" {
return false
}
for _, scope := range scopes {
scope = normalize(scope)
if scope == "*" || scope == "all" || scope == capability {
return true
}
switch capability {
case "chat":
if scope == "text" || scope == "text_generate" {
return true
}
case "embedding":
if scope == "text_embedding" {
return true
}
case "rerank":
if scope == "text_rerank" {
return true
}
case "music":
if scope == "audio_generate" || scope == "music_generate" || scope == "song" {
return true
}
case "audio":
if scope == "text_to_speech" || scope == "speech" || scope == "tts" {
return true
}
case "voice_clone":
if scope == "audio" || scope == "text_to_speech" || scope == "speech" || scope == "tts" {
return true
}
case "image_vectorize":
if scope == "image" || scope == "vectorize" {
return true
}
case "video_enhance":
if scope == "video" || scope == "video_upscale" || scope == "upscale" {
return true
}
}
}
return false
}
func hasScope(scopes []string, want string) bool {
want = normalize(want)
for _, scope := range scopes {
scope = normalize(scope)
if scope == "*" || scope == "all" || scope == want {
return true
}
}
return false
}
func normalize(value string) string {
return strings.ReplaceAll(strings.ToLower(strings.TrimSpace(value)), "-", "_")
}