package gateway import ( "context" "net/http" "time" "git.yeij.top/AskaEth/Cyrene_v2/internal/auth" ) // auditClaims 记录一次管理写操作,调用方身份从请求上下文读取 // (由 auth.Authorize 填充)。它是管理操作所需的 // 谁/什么/何时/结果审计链;「主代理代审」这一面向用户的提示是 // 前端的职责,不是网关的。 func (s *Server) auditClaims(r *http.Request, what, result string) { s.auditClaimsAccount(r, what, result, "") } // auditClaimsAccount 在既有审计口径(what/result 不变,便于按动作聚合)之上补一个 // 账号维度,使多账号平台上的「连/断/删的是哪一条」在审计里可追溯。accountID 为空 // (平台级动作)时不带该字段。附加字段只放账号 id —— 凭据永不进审计/日志。 func (s *Server) auditClaimsAccount(r *http.Request, what, result, accountID string) { who := "anonymous" if claims, ok := auth.ClaimsFromContext(r.Context()); ok { who = claims.Sub + ":" + string(claims.Role) } extra := []any{} if accountID != "" { extra = append(extra, "accountId", accountID) } s.audit(r.Context(), who, what, result, extra...) } // AuditEvent 记录一次带显式调用方身份的外部(非 HTTP)审计事件, // 使请求路径之外的程序化写入方(例如渠道 bridge 的 // 黑名单拒绝钩子,design/16 §4.6 §6)能追加到同一 // 谁/什么/何时/结果链。who 是自由形式的 actor 标签("channel" 或 // "bridge:telegram");what 是操作,result 是其结果。它对 // 并发使用安全。 func (s *Server) AuditEvent(who, what, result string) { s.audit(context.Background(), who, what, result) } // audit 在网关 logger 上发出一条结构化审计记录,并把它持久化到 // 已配置的 AuditStore(未注入时回退到内存存储)。 // 持久化失败会记日志,但绝不阻塞管理写,因此 // 坏掉的审计存储无法拖垮审计链的首要职责。 // extra 是追加的结构化字段(键值交替,如 "accountId", "abc"), // 只用于日志行;持久化记录的口径保持 who/what/result 不变。 func (s *Server) audit(ctx context.Context, who, what, result string, extra ...any) { fields := []any{ "who", who, "what", what, "when", time.Now().UTC().Format(time.RFC3339), "result", result, } fields = append(fields, extra...) s.log.Info("admin audit", fields...) if s.obs != nil && s.obs.audit != nil { if err := s.obs.audit.Append(ctx, who, what, result, time.Now()); err != nil { s.log.Error("admin audit persist", "err", err, "what", what) } } }