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
+41 -8
View File
@@ -10,6 +10,7 @@ import (
"time"
platformid "aigateway.local/core/internal/platform/id"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
"github.com/jackc/pgx/v5/pgxpool"
@@ -129,9 +130,25 @@ func (r *Recorder) Run(ctx context.Context) {
case event := <-r.queue:
pending = append(pending, event)
default:
flushCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
// 关闭前的最后一次落盘:数据库短暂不可用时重试有限次数,
// 而不是只试一次就把整批审计事件静默丢弃。
flushCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
if len(pending) > 0 {
_ = r.flush(flushCtx, pending)
lastErr := r.flush(flushCtx, pending)
for attempt := 0; lastErr != nil && attempt < 3; attempt++ {
select {
case <-time.After(2 * time.Second):
case <-flushCtx.Done():
lastErr = flushCtx.Err()
}
if flushCtx.Err() != nil {
break
}
lastErr = r.flush(flushCtx, pending)
}
if lastErr != nil && r.logger != nil {
r.logger.Error("audit drain failed; events lost", "events", len(pending), "error", lastErr)
}
}
cancel()
return
@@ -142,7 +159,7 @@ func (r *Recorder) Run(ctx context.Context) {
}
type dailyKey struct {
date, apiKeyID, provider, model string
date, apiKeyID, provider, model, currency string
}
type dailyValue struct {
@@ -192,7 +209,18 @@ func (r *Recorder) flush(ctx context.Context, events []Event) error {
nil, nil, labels, event.RecordedAt,
})
if event.APIKeyID != nil && *event.APIKeyID != "" {
key := dailyKey{date: event.RecordedAt.UTC().Format("2006-01-02"), apiKeyID: *event.APIKeyID, provider: event.ProviderCode, model: event.Model}
// 仅聚合合法 UUID 的 API Key:usage_daily.api_key_id 是
// REFERENCES api_keys 的 uuid 列,bootstrap 等非 UUID 身份写入
// 会让整批事务失败、审计管线永久卡死。审计事件本身仍落 audit_events。
if _, uuidErr := uuid.Parse(strings.TrimSpace(*event.APIKeyID)); uuidErr != nil {
continue
}
// 成本按币种独立聚合:不同货币的价格不能相加成单一数字。
currency := strings.ToUpper(strings.TrimSpace(event.Currency))
if currency == "" {
currency = "USD"
}
key := dailyKey{date: event.RecordedAt.UTC().Format("2006-01-02"), apiKeyID: *event.APIKeyID, provider: event.ProviderCode, model: event.Model, currency: currency}
value := daily[key]
value.requests++
if event.StatusCode >= 400 {
@@ -214,15 +242,15 @@ func (r *Recorder) flush(ctx context.Context, events []Event) error {
}
batch := &pgx.Batch{}
for key, value := range daily {
batch.Queue(`INSERT INTO gateway.usage_daily(usage_date,api_key_id,provider_code,model,requests,failed_requests,prompt_tokens,completion_tokens,cost_microunits)
VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9)
ON CONFLICT(usage_date,api_key_id,provider_code,model) DO UPDATE SET
batch.Queue(`INSERT INTO gateway.usage_daily(usage_date,api_key_id,provider_code,model,currency,requests,failed_requests,prompt_tokens,completion_tokens,cost_microunits)
VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)
ON CONFLICT(usage_date,api_key_id,provider_code,model,currency) DO UPDATE SET
requests=gateway.usage_daily.requests+EXCLUDED.requests,
failed_requests=gateway.usage_daily.failed_requests+EXCLUDED.failed_requests,
prompt_tokens=gateway.usage_daily.prompt_tokens+EXCLUDED.prompt_tokens,
completion_tokens=gateway.usage_daily.completion_tokens+EXCLUDED.completion_tokens,
cost_microunits=gateway.usage_daily.cost_microunits+EXCLUDED.cost_microunits,
updated_at=clock_timestamp()`, key.date, key.apiKeyID, key.provider, key.model, value.requests, value.failed, value.prompt, value.completion, value.cost)
updated_at=clock_timestamp()`, key.date, key.apiKeyID, key.provider, key.model, key.currency, value.requests, value.failed, value.prompt, value.completion, value.cost)
}
for _, alert := range alerts {
batch.Queue(`INSERT INTO gateway.outbox_events(event_id,event_type,event_version,tenant_id,aggregate_type,aggregate_id,payload)
@@ -257,6 +285,11 @@ func uuidPointer(value *string) any {
if value == nil || strings.TrimSpace(*value) == "" {
return nil
}
// 非 UUID 身份(bootstrap key 等)返回 nil:audit_events.api_key_id 允许
// NULL,写零值/非法值只会掩盖问题。
if _, err := uuid.Parse(strings.TrimSpace(*value)); err != nil {
return nil
}
return uuidValue(*value)
}