audit.go 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. // Package audit 负责写审计事件。
  2. //
  3. // 依据 docs/backend/audit.md:
  4. // - 只追加,不修改不删除;
  5. // - 写入发生在业务成功之后、响应返回之前;
  6. // - 写入失败只打错误日志,不影响业务结果(第五节)。
  7. package audit
  8. import (
  9. "context"
  10. "database/sql"
  11. "encoding/json"
  12. "fmt"
  13. "log/slog"
  14. "time"
  15. )
  16. // 事件类型,取值必须与 audit_events.event_type 的 CHECK 约束一致(audit.md 第三节)。
  17. const (
  18. EventLoginSuccess = "login_success"
  19. EventLoginFailure = "login_failure"
  20. EventLogout = "logout"
  21. EventTotpBound = "totp_bound"
  22. EventTotpDisabled = "totp_disabled"
  23. EventPasswordChanged = "password_changed"
  24. EventSessionRevoked = "session_revoked"
  25. EventTrustedDeviceAdded = "trusted_device_added"
  26. EventTrustedDeviceRevoke = "trusted_device_revoked"
  27. EventTokenCreated = "token_created"
  28. EventTokenRevoked = "token_revoked"
  29. EventNotePublished = "note_published"
  30. EventNoteUnpublished = "note_unpublished"
  31. )
  32. // target_type 取值(audit.md 第三节说明)。'null' 是字符串字面量,表示无对象。
  33. const (
  34. TargetNull = "null"
  35. TargetUser = "user"
  36. TargetSession = "session"
  37. TargetTrustedDevice = "trusted_device"
  38. TargetToken = "token"
  39. TargetNote = "note"
  40. )
  41. // login_failure 的 reason 取值(audit.md 第三节)。
  42. const (
  43. ReasonBadPassword = "bad_password"
  44. ReasonBadTotp = "bad_totp"
  45. ReasonNoSuchUser = "no_such_user"
  46. )
  47. // Event 是一条待写入的审计事件。
  48. type Event struct {
  49. // ActorUserID 可为空(登录失败且用户名不存在时)。
  50. ActorUserID string
  51. // ActorUsername 是用户名快照,用户被删除后仍保留。
  52. ActorUsername string
  53. EventType string
  54. // TargetType 为空时写入 'null'。
  55. TargetType string
  56. TargetID string
  57. Metadata map[string]any
  58. IP string
  59. UserAgent string
  60. }
  61. // Writer 写审计事件。
  62. type Writer struct {
  63. db *sql.DB
  64. }
  65. // NewWriter 构造 Writer。
  66. func NewWriter(db *sql.DB) *Writer {
  67. return &Writer{db: db}
  68. }
  69. // Record 写入一条审计事件。
  70. //
  71. // 依据 audit.md 第五节:失败只打错误日志,不返回错误、不影响业务结果。
  72. // 因此本方法故意不返回 error。
  73. func (w *Writer) Record(ctx context.Context, e Event) {
  74. if err := w.record(ctx, e); err != nil {
  75. // 完整事件进日志,便于事后人工补录(audit.md 5.1)
  76. slog.Error("写审计事件失败",
  77. "event_type", e.EventType,
  78. "actor_user_id", e.ActorUserID,
  79. "actor_username", e.ActorUsername,
  80. "target_type", e.TargetType,
  81. "target_id", e.TargetID,
  82. "metadata", e.Metadata,
  83. "err", err)
  84. }
  85. }
  86. func (w *Writer) record(ctx context.Context, e Event) error {
  87. if w == nil || w.db == nil {
  88. return fmt.Errorf("audit writer 未初始化")
  89. }
  90. targetType := e.TargetType
  91. if targetType == "" {
  92. targetType = TargetNull
  93. }
  94. meta := e.Metadata
  95. if meta == nil {
  96. meta = map[string]any{}
  97. }
  98. rawMeta, err := json.Marshal(meta)
  99. if err != nil {
  100. return fmt.Errorf("序列化审计 metadata 失败: %w", err)
  101. }
  102. var actorUserID any
  103. if e.ActorUserID != "" {
  104. actorUserID = e.ActorUserID
  105. }
  106. var targetID any
  107. if e.TargetID != "" {
  108. targetID = e.TargetID
  109. }
  110. const q = `
  111. INSERT INTO audit_events
  112. (actor_user_id, actor_username, event_type, target_type, target_id, metadata, ip, user_agent, created_at)
  113. VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`
  114. if _, err := w.db.ExecContext(ctx, q,
  115. actorUserID, e.ActorUsername, e.EventType, targetType, targetID,
  116. rawMeta, e.IP, e.UserAgent, time.Now()); err != nil {
  117. return fmt.Errorf("写入审计事件失败: %w", err)
  118. }
  119. return nil
  120. }
苏ICP备2026082406号
苏公网安备 待审核