| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134 |
- // Package audit 负责写审计事件。
- //
- // 依据 docs/backend/audit.md:
- // - 只追加,不修改不删除;
- // - 写入发生在业务成功之后、响应返回之前;
- // - 写入失败只打错误日志,不影响业务结果(第五节)。
- package audit
- import (
- "context"
- "database/sql"
- "encoding/json"
- "fmt"
- "log/slog"
- "time"
- )
- // 事件类型,取值必须与 audit_events.event_type 的 CHECK 约束一致(audit.md 第三节)。
- const (
- EventLoginSuccess = "login_success"
- EventLoginFailure = "login_failure"
- EventLogout = "logout"
- EventTotpBound = "totp_bound"
- EventTotpDisabled = "totp_disabled"
- EventPasswordChanged = "password_changed"
- EventSessionRevoked = "session_revoked"
- EventTrustedDeviceAdded = "trusted_device_added"
- EventTrustedDeviceRevoke = "trusted_device_revoked"
- EventTokenCreated = "token_created"
- EventTokenRevoked = "token_revoked"
- EventNotePublished = "note_published"
- EventNoteUnpublished = "note_unpublished"
- )
- // target_type 取值(audit.md 第三节说明)。'null' 是字符串字面量,表示无对象。
- const (
- TargetNull = "null"
- TargetUser = "user"
- TargetSession = "session"
- TargetTrustedDevice = "trusted_device"
- TargetToken = "token"
- TargetNote = "note"
- )
- // login_failure 的 reason 取值(audit.md 第三节)。
- const (
- ReasonBadPassword = "bad_password"
- ReasonBadTotp = "bad_totp"
- ReasonNoSuchUser = "no_such_user"
- )
- // Event 是一条待写入的审计事件。
- type Event struct {
- // ActorUserID 可为空(登录失败且用户名不存在时)。
- ActorUserID string
- // ActorUsername 是用户名快照,用户被删除后仍保留。
- ActorUsername string
- EventType string
- // TargetType 为空时写入 'null'。
- TargetType string
- TargetID string
- Metadata map[string]any
- IP string
- UserAgent string
- }
- // Writer 写审计事件。
- type Writer struct {
- db *sql.DB
- }
- // NewWriter 构造 Writer。
- func NewWriter(db *sql.DB) *Writer {
- return &Writer{db: db}
- }
- // Record 写入一条审计事件。
- //
- // 依据 audit.md 第五节:失败只打错误日志,不返回错误、不影响业务结果。
- // 因此本方法故意不返回 error。
- func (w *Writer) Record(ctx context.Context, e Event) {
- if err := w.record(ctx, e); err != nil {
- // 完整事件进日志,便于事后人工补录(audit.md 5.1)
- slog.Error("写审计事件失败",
- "event_type", e.EventType,
- "actor_user_id", e.ActorUserID,
- "actor_username", e.ActorUsername,
- "target_type", e.TargetType,
- "target_id", e.TargetID,
- "metadata", e.Metadata,
- "err", err)
- }
- }
- func (w *Writer) record(ctx context.Context, e Event) error {
- if w == nil || w.db == nil {
- return fmt.Errorf("audit writer 未初始化")
- }
- targetType := e.TargetType
- if targetType == "" {
- targetType = TargetNull
- }
- meta := e.Metadata
- if meta == nil {
- meta = map[string]any{}
- }
- rawMeta, err := json.Marshal(meta)
- if err != nil {
- return fmt.Errorf("序列化审计 metadata 失败: %w", err)
- }
- var actorUserID any
- if e.ActorUserID != "" {
- actorUserID = e.ActorUserID
- }
- var targetID any
- if e.TargetID != "" {
- targetID = e.TargetID
- }
- const q = `
- INSERT INTO audit_events
- (actor_user_id, actor_username, event_type, target_type, target_id, metadata, ip, user_agent, created_at)
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`
- if _, err := w.db.ExecContext(ctx, q,
- actorUserID, e.ActorUsername, e.EventType, targetType, targetID,
- rawMeta, e.IP, e.UserAgent, time.Now()); err != nil {
- return fmt.Errorf("写入审计事件失败: %w", err)
- }
- return nil
- }
|