0.10.1: 安全与业务逻辑加固、新品牌与部署加固

三轮审查修复(60+ 项),相对远端 main(b536672)的关键变更:
- 安全: 数据面 SSRF 拨号防护(防 DNS rebinding)/上游凭据剥离/登录防枚举
  与锁定态统一/可信代理(X-Forwarded-For)限流加固/会话版本失效机制/
  撤销即时传播/弱密钥拒绝启动/脱敏字节级重写(保签名契约)
- 业务逻辑: 裸 body 上传 panic/bootstrap 审计管线卡死/定价通配符优先级/
  全局工具可见性/调度器停机补跑/TOTP 挑战令牌消费顺序/熔断探针语义/
  >4MB 响应 token 计量/管理员重置密码作废会话 等
- 前端: 新 logo(语枢 AI 网关主题)/Provider 凭据异常警示/删除入口/
  后端错误消息透传/localStorage 敏感数据收敛
- 部署: CREDENTIAL_MASTER_KEY 持久化与弱值拒绝/Provider DELETE 接口/
  nginx 安全头/worker 内存限制
- 新增迁移 000029(key_hash 索引)/000030(usage_daily 币种维度)
This commit is contained in:
2026-08-13 10:50:51 +08:00
parent b536672000
commit 9501751792
136 changed files with 8024 additions and 1476 deletions
+38 -7
View File
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"errors"
"log/slog"
"net/http"
"regexp"
"strings"
@@ -23,6 +24,7 @@ type AdminHTTPHandler struct {
changeHook func(context.Context) error
operations AdminOperations
mux *http.ServeMux
logger *slog.Logger
}
func (h *AdminHTTPHandler) SetChangeHook(hook func(context.Context) error) {
@@ -63,11 +65,12 @@ type modelRouteConditions struct {
func NewAdminHTTPHandler(repository *Repository, cipher *CredentialCipher, identityService *identity.Service, allowPrivate bool) *AdminHTTPHandler {
handler := &AdminHTTPHandler{
repository: repository, cipher: cipher, identity: identityService,
allowPrivate: allowPrivate, mux: http.NewServeMux(),
allowPrivate: allowPrivate, mux: http.NewServeMux(), logger: slog.Default(),
}
handler.mux.HandleFunc("GET /api/v1/admin/providers", handler.list)
handler.mux.HandleFunc("POST /api/v1/admin/providers", handler.create)
handler.mux.HandleFunc("PUT /api/v1/admin/providers/{provider_id}", handler.update)
handler.mux.HandleFunc("DELETE /api/v1/admin/providers/{provider_id}", handler.delete)
handler.mux.HandleFunc("POST /api/v1/admin/providers/{provider_id}/test", handler.testConnection)
handler.mux.HandleFunc("GET /api/v1/admin/providers/{provider_id}/models", handler.listModels)
handler.mux.HandleFunc("POST /api/v1/admin/providers/{provider_id}/models/sync", handler.syncModels)
@@ -138,6 +141,19 @@ func (h *AdminHTTPHandler) create(writer http.ResponseWriter, request *http.Requ
apiresponse.OK(writer, view)
}
func (h *AdminHTTPHandler) delete(writer http.ResponseWriter, request *http.Request) {
actor, ok := h.requirePermission(writer, request, identity.PermissionProviderManage)
if !ok {
return
}
if err := h.repository.Delete(request.Context(), request.PathValue("provider_id"), actor.ID); err != nil {
h.writeError(writer, err)
return
}
h.propagateChange(request.Context())
apiresponse.OK(writer, map[string]bool{"deleted": true})
}
func (h *AdminHTTPHandler) update(writer http.ResponseWriter, request *http.Request) {
actor, ok := h.requirePermission(writer, request, identity.PermissionProviderManage)
if !ok {
@@ -244,20 +260,30 @@ func (h *AdminHTTPHandler) setCredentials(record *Record, apiKey string) error {
}
func (h *AdminHTTPHandler) view(record Record) (map[string]any, error) {
// 凭据解密失败(KEK 轮换后旧记录、数据损坏)不得让整个列表接口报错:
// 降级为"未确认"状态并附警示,管理端仍可编辑/删除该记录恢复。
keyConfigured := false
masked := ""
credentialError := ""
plaintext, err := h.cipher.Decrypt(record.EncryptedCredentials, record.CredentialKEKVersion)
if err != nil {
return nil, err
}
var credentials Credentials
if err := json.Unmarshal(plaintext, &credentials); err != nil {
return nil, err
credentialError = "凭据无法解密(加密密钥不匹配或数据损坏),请重新保存凭据"
} else {
var credentials Credentials
if json.Unmarshal(plaintext, &credentials) != nil {
credentialError = "凭据数据格式无效,请重新保存凭据"
} else {
keyConfigured = credentials.APIKey != ""
masked = maskSecret(credentials.APIKey)
}
}
return map[string]any{
"id": record.ID, "code": record.Code, "adapter": record.Adapter,
"base_url": record.BaseURL, "capabilities": record.Capabilities,
"config": record.Config, "enabled": record.Enabled, "revision": record.Revision,
"credential_kek_version": record.CredentialKEKVersion,
"key_configured": credentials.APIKey != "", "api_key_masked": maskSecret(credentials.APIKey),
"key_configured": keyConfigured, "api_key_masked": masked,
"credential_error": credentialError,
}, nil
}
@@ -482,6 +508,11 @@ func (h *AdminHTTPHandler) writeError(writer http.ResponseWriter, err error) {
apiresponse.Error(writer, http.StatusServiceUnavailable, "供应商配置服务暂不可用")
case errors.Is(err, ErrProviderUpstream):
apiresponse.Error(writer, http.StatusBadGateway, "无法从上游供应商获取模型信息")
case errors.Is(err, ErrBlockedAddress):
// 原始错误含解析出的地址(如 "blocked address 10.0.0.1"),泄露内网
// 拓扑;细节只进服务端日志,客户端返回通用提示。
h.logger.Warn("provider URL blocked", "error", err)
apiresponse.Error(writer, http.StatusBadRequest, "供应商地址不允许访问内网或保留网段")
default:
apiresponse.Error(writer, http.StatusBadRequest, err.Error())
}