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:
@@ -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())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user