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