Skip to content

feat(audit): sync upstream localized security audit logs + auth method tracking - #159

Closed
jjcc123312 wants to merge 4 commits into
mainfrom
feat/audit-logs
Closed

feat(audit): sync upstream localized security audit logs + auth method tracking#159
jjcc123312 wants to merge 4 commits into
mainfrom
feat/audit-logs

Conversation

@jjcc123312

Copy link
Copy Markdown

Summary

Syncs the upstream security audit logs feature into the fork. Audit logs record manage/operation actions (channel CRUD, user CRUD, redemption codes, system settings, 2FA/passkey changes, upstream model sync, option changes) plus successful logins, with localized human-readable descriptions surfaced in the usage-logs UI.

Commits synced (in order)

  1. d0c4305a1 — feat(audit): add localized security audit logs (feat(audit): add localized security audit logs QuantumNous/new-api#5462)
    • New files: controller/audit.go, middleware/audit.go
    • model/log.go: adds LogTypeLogin = 7 (no DB schema change)
    • Audit calls added to controller/{channel,user,twofa,passkey,redemption,option,channel_upstream_update}.go and middleware/auth.go
    • Frontend usage-logs columns/dialog/format/types + 6 locales
  2. 1ac0f5807 — feat(audit): add authentication method tracking in audit logs (applied cleanly)
  3. i18n(audit) — adds es/pt translations (see below)

Conflict resolutions

  • controller/user.go (substantive): in setupLogin, kept our fork's variadic signature (isNewUser ...bool), our data map, and the is_new_user onboarding block, AND added upstream's recordLoginAudit(user, c) call. They coexist — the audit call only reads user+context and writes a log; it does not touch the data map. All other user.go hunks (UpdateUser/DeleteUser/CreateUser/ManageUser/AdminClearUserBinding audit calls) took upstream's additions.
  • constant/context_key.go: take-both — kept our ContextKeyBlockRunSettlement and added upstream's ContextKeyAuditLogged in the same const block.
  • web/default/src/i18n/locales/{en,fr,ja,ru,vi,zh}.json: positional take-both — kept our existing keys and upstream's new audit keys; valid JSON.
  • AGENTS.md, CLAUDE.md: kept ours (--ours); skipped upstream's cosmetic doc note. Verified 0 diff vs main.
  • Everything else auto-merged.

es/pt translations (our fork has 8 locales; upstream edited only 6)

Upstream's two commits added 72 new frontend keys but only to 6 locales. Added real Spanish and Portuguese translations for all 72 keys to es.json and pt.json so they do not fall back to English. i18next template vars ({{method}}, {{route}}, {{from}}, {{to}}, {{count}}, {{id}}, etc.) preserved; the format-literal key {{method}} {{route}} kept as-is.

Verified via bun run i18n:sync: all 8 locales report missingCount: 0, untranslatedCount: 0. No per-locale *.untranslated.json files were produced (the tool only writes them when untranslated keys exist), confirming none of the new audit keys are untranslated.

No DB schema change

LogTypeLogin = 7 is a new log-type constant only; no migration.

Verification

  • go build ./controller/... ./middleware/... ./model/... ./constant/... → exit 0
  • go vet ./controller/... ./middleware/... ./model/... → exit 0
  • Conflict-marker grep across controller middleware model constant web/default/src → no matches
  • (Note: go build ./... fails on a pre-existing web/classic/dist embed unrelated to this change; built specific packages instead.)

🤖 Generated with Claude Code

Calcium-Ion and others added 4 commits June 17, 2026 18:11
Upstream commits d0c4305 and 1ac0f58 only updated 6 of our 8 frontend
locales. Add real Spanish and Portuguese translations for the 72 new audit
log keys so they do not fall back to English. Template vars ({{method}},
{{route}}, {{from}}, {{to}}, etc.) preserved. Verified via bun run i18n:sync:
es/pt untranslatedCount=0, missingCount=0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Existing locale entries keep Passkey untranslated; align the 3 new audit
keys (Deleted/Registered/Reset passkey) to the same house style.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@KingCesc

Copy link
Copy Markdown

🤖 OpenCodeReview · 评审 commit 87a409cb · 共 10 条

controller/audit.go

  • L97-98: [阻塞] 这里在写审计日志后无条件设置 audit_logged,但 RecordOperationAuditLog 当前只记录 SysLog、不向调用方返回写库失败。若日志表写入失败,请求会被标记为已审计,鉴权链路的兜底审计也会跳过,导致关键管理操作完全没有审计记录。建议让日志写入函数返回 error/成功状态,并且仅在写入成功后再标记;写入失败时不要阻止兜底记录。
if err := model.RecordOperationAuditLog(logUserId, auditContentEN(action, params), c.ClientIP(), action, params, auditOperatorInfo(c), nil); err == nil {
		markAuditLogged(c)
	}

controller/channel.go

  • L1362-1370: [严重] 这里在具体 action 参数校验和 channel.Update() 成功之前就写入审计并标记已记录;例如 key_index 缺失/越界、删除最后一个 key、没有可禁用/删除的 key 或 DB 更新失败时,仍会留下 channel.multi_key_manage 的成功操作痕迹,并且兜底审计被跳过,导致审计日志与真实状态不一致。建议仅在各修改分支成功更新并准备返回成功响应后记录审计,失败路径不要调用 markAuditLogged
// get_key_status 为只读查询,不记录审计;修改操作请在各分支实际更新成功后再记录,避免失败请求被记为已执行。
	if request.Action == "get_key_status" {
		markAuditLogged(c)
	}
  • L1015-1033: [严重] 更新审计只覆盖了少数字段,但该接口实际可修改 namepriorityweightauto_banmodel_mappingsettingparam_overrideheader_overridetagother/settings 等会影响路由、可用性、鉴权或供应商行为的关键配置。现在这些变更会产生 channel.update 日志但 changed_fields 为空或不完整,安全审计无法还原高风险配置修改。建议基于所有可变字段统一生成变更字段列表,敏感字段仅记录字段名/已变更,不记录明文值。
changedFields := make([]string, 0)
	if channel.Name != originChannel.Name {
		changedFields = append(changedFields, "name")
	}
	if channel.Status != originChannel.Status {
		changedFields = append(changedFields, "status")
	}
	if channel.Models != originChannel.Models {
		changedFields = append(changedFields, "models")
	}
	if channel.Group != originChannel.Group {
		changedFields = append(changedFields, "group")
	}
	if channel.Type != originChannel.Type {
		changedFields = append(changedFields, "type")
	}
	if !equalStringPtr(channel.BaseURL, originChannel.BaseURL) {
		changedFields = append(changedFields, "base_url")
	}
	if channel.Key != "" && channel.Key != originChannel.Key {
		changedFields = append(changedFields, "key")
	}
	if channel.OpenAIOrganization != originChannel.OpenAIOrganization {
		changedFields = append(changedFields, "openai_organization")
	}
	if channel.ModelMapping != originChannel.ModelMapping {
		changedFields = append(changedFields, "model_mapping")
	}
	if channel.StatusCodeMapping != originChannel.StatusCodeMapping {
		changedFields = append(changedFields, "status_code_mapping")
	}
	if channel.Priority != originChannel.Priority {
		changedFields = append(changedFields, "priority")
	}
	if channel.Weight != originChannel.Weight {
		changedFields = append(changedFields, "weight")
	}
	if channel.AutoBan != originChannel.AutoBan {
		changedFields = append(changedFields, "auto_ban")
	}
	if channel.Tag != originChannel.Tag {
		changedFields = append(changedFields, "tag")
	}
	if channel.Setting != originChannel.Setting {
		changedFields = append(changedFields, "setting")
	}
	if channel.ParamOverride != originChannel.ParamOverride {
		changedFields = append(changedFields, "param_override")
	}
	if channel.HeaderOverride != originChannel.HeaderOverride {
		changedFields = append(changedFields, "header_override")
	}
	if channel.Other != originChannel.Other {
		changedFields = append(changedFields, "other")
	}
	if channel.OtherInfo != originChannel.OtherInfo {
		changedFields = append(changedFields, "other_info")
	}
	if channel.OtherSettings != originChannel.OtherSettings {
		changedFields = append(changedFields, "settings")
	}

model/log.go

  • L180-183: [严重] 这里固定按 logUserId 回查用户名,会导致删除用户等“操作已完成后再记审计”的场景无法再查到用户,最终审计日志的 username 为空,影响后续按用户名检索/导出和审计追踪完整性。建议允许调用方传入已持有的用户名,或至少在回查为空时从 params["username"] 等结构化参数中兜底。
username, _ := GetUsernameById(logUserId, false)
	if username == "" {
		if name, ok := params["username"].(string); ok {
			username = name
		}
	}
	other := map[string]interface{}{
		"op": buildOpField(action, params),
	}

web/default/src/features/usage-logs/lib/format.ts

  • L0: [严重] 这里用最多 64KB 的响应体反推业务成功,截断后 JSON 解析失败会直接回退为 status < 400。项目里不少管理接口以 HTTP 200 + success:false 表示业务失败;如果响应体较大且 success 字段落在截断范围外(例如 gin.Hdata 先输出),审计会把失败操作记录为成功,影响安全追责和告警判断。建议在 writer 中记录是否截断,截断且无法解析 success 时不要按 2xx 认定成功,或由业务层显式设置审计成功/失败状态。
success := auditResponseSuccess(status, writer.body.Bytes()) // TODO: 传入/判断响应体是否被截断,截断且无法解析 success 时不要默认按 2xx 记为成功
  • L0: [严重] 兜底审计记录后没有设置 ContextKeyAuditLogged,在嵌套鉴权链路中会产生重复日志。例如 /api/channel/fetch_models 位于已挂 AdminAuth() 的分组内,同时路由自身又挂 RootAuth();内层 finishAdminAudit 记录后外层仍会再次记录同一请求,导致安全审计数据重复、统计与追责失真。建议在兜底记录前同步标记已审计,或在 beginAdminAudit 检测已有审计 writer 时避免二次包装。
common.SetContextKey(c, constant.ContextKeyAuditLogged, true)
	gopool.Go(func() {
		model.RecordOperationAuditLog(operatorId, content, ip, action, opParams, adminInfo, auditInfo)
	})

middleware/audit.go

  • L138: [严重] 这里依赖最多 64KB 的响应体反解析 success 来判定业务结果;Gin 的 gin.H 编码时 success 不一定在前面,若 2xx 业务失败响应携带较大的 data 或响应被截断,JSON 解析失败后会回退成 true,导致审计日志把失败操作记为成功,影响安全追责与告警。建议由 handler/统一响应层显式写入审计结果到 context,或至少在缓存被截断/JSON 解析失败时不要按 HTTP 2xx 直接判定成功。
success, successKnown := auditResponseSuccess(status, writer.body.Bytes(), writer.truncated)
	if !successKnown {
		// 建议改为读取 handler/统一响应层显式设置的业务结果,避免依赖截断响应体反推。
		success = false
	}
  • L91-93: [严重] 当前映射遗漏了同一路由组内已经注册的管理写接口,例如 PATCH /api/subscription/admin/plans/:id、用户订阅创建/失效/删除以及部署改名/续期等;这些操作会全部落到 generic,导致本地化审计动作和关键操作分类缺失。建议将所有 Admin/Root 写路由集中注册 action,或在路由注册时绑定审计 action,并补齐覆盖测试,避免新增/变更路由时静默降级。
"POST /api/subscription/admin/plans":                         "subscription.plan_create",
	"PUT /api/subscription/admin/plans/:id":                      "subscription.plan_update",
	"PATCH /api/subscription/admin/plans/:id":                    "subscription.plan_status_update",
	"POST /api/subscription/admin/bind":                          "subscription.bind",
	"POST /api/subscription/admin/users/:id/subscriptions":       "subscription.user_create",
	"POST /api/subscription/admin/user_subscriptions/:id/invalidate": "subscription.user_invalidate",
	"DELETE /api/subscription/admin/user_subscriptions/:id":      "subscription.user_delete",

controller/user.go

  • L1144-1148: [严重] 当同一路由同时经过 AdminAuth 和 RootAuth(例如管理员分组内再挂 RootAuth 的写接口)时,两个 authHelper 会各自调用 begin/finish;兜底审计写入后没有设置 ContextKeyAuditLogged,内层 RootAuth 记录完成后外层 AdminAuth 仍会再次记录,导致同一次高危操作产生重复审计日志,影响审计追踪与告警统计。建议在兜底记录前设置该标记,或在 begin 阶段检测已有 audit writer,确保同一请求只记录一次。
common.SetContextKey(c, constant.ContextKeyAuditLogged, true)
	gopool.Go(func() {
		model.RecordOperationAuditLog(operatorId, content, ip, action, opParams, adminInfo, auditInfo)
	})

web/default/src/i18n/locales/zh.json

  • L4845: [严重] 兜底审计写入后没有设置 ContextKeyAuditLogged。当前部分路由在 AdminAuth 分组内又叠加 RootAuth(例如 /api/channel/:id/key、/api/channel/fetch_models),两个 authHelper 都会各自 begin/finish;如果 handler 没有手动埋点,内层 finish 写完后外层 finish 仍会再次写入,导致同一次高危操作产生重复审计日志,影响审计准确性。建议在兜底记录前先标记已审计,让外层鉴权链路跳过。
common.SetContextKey(c, constant.ContextKeyAuditLogged, true)
	gopool.Go(func() {
		model.RecordOperationAuditLog(operatorId, content, ip, action, opParams, adminInfo, auditInfo)
	})

@jjcc123312

Copy link
Copy Markdown
Author

暂不同步:此 PR 同步的是上游代码,且 OpenCodeReview 指出上游实现存在质量问题(非本次合并引入)。当前无强需求,改为按需单独同步。先关闭,需要时再开。

@jjcc123312 jjcc123312 closed this Jun 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants