You've already forked agentic-coding-workflow
102 lines
2.9 KiB
Go
102 lines
2.9 KiB
Go
// Package audit 提供登录、登出、关键管理动作等敏感事件的审计记录器。
|
|
// Recorder 接口与具体存储解耦,user 模块只依赖接口;NewPostgresRecorder
|
|
// 是默认实现,把每条 Entry 转成 audit_logs 行写入。
|
|
//
|
|
// IP 解析:调用方通常直接传 r.RemoteAddr(host:port 形式),Record 内部
|
|
// 用 net.SplitHostPort + netip.ParseAddr 拆解。解析失败时不阻断主流程,
|
|
// 只把 ip 列写为 NULL,避免审计写失败影响业务。
|
|
package audit
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net"
|
|
"net/netip"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
|
|
auditsqlc "github.com/yan1h/agent-coding-workflow/internal/audit/sqlc"
|
|
"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
|
|
)
|
|
|
|
// Recorder 是审计记录的对外契约。Record 失败时返回 errs.AppError,
|
|
// 调用方决定是否吞错(user 模块通常吞错并仅记日志)。
|
|
type Recorder interface {
|
|
Record(ctx context.Context, e Entry) error
|
|
}
|
|
|
|
// Entry 描述一条待写入的审计事件。UserID 为 nil 表示无登录上下文(如系统
|
|
// 维护任务);Action 为 dot.notation 字符串("user.login" / "user.logout"
|
|
// 等),TargetType/TargetID 描述事件指向的对象。
|
|
type Entry struct {
|
|
UserID *uuid.UUID
|
|
Action string
|
|
TargetType string
|
|
TargetID string
|
|
IP string
|
|
Metadata map[string]any
|
|
}
|
|
|
|
type pgRecorder struct {
|
|
q *auditsqlc.Queries
|
|
}
|
|
|
|
// NewPostgresRecorder 用现有 pgxpool 构造 Recorder 实现。
|
|
func NewPostgresRecorder(pool *pgxpool.Pool) Recorder {
|
|
return &pgRecorder{q: auditsqlc.New(pool)}
|
|
}
|
|
|
|
// Record 把 Entry 落地为 audit_logs 一行。
|
|
func (r *pgRecorder) Record(ctx context.Context, e Entry) error {
|
|
var meta []byte
|
|
if e.Metadata != nil {
|
|
b, err := json.Marshal(e.Metadata)
|
|
if err != nil {
|
|
return errs.Wrap(err, errs.CodeInternal, "marshal audit metadata")
|
|
}
|
|
meta = b
|
|
}
|
|
|
|
var ip *netip.Addr
|
|
if e.IP != "" {
|
|
host, _, err := net.SplitHostPort(e.IP)
|
|
if err != nil {
|
|
host = e.IP
|
|
}
|
|
if a, err := netip.ParseAddr(host); err == nil {
|
|
ip = &a
|
|
}
|
|
}
|
|
|
|
if err := r.q.InsertAuditLog(ctx, auditsqlc.InsertAuditLogParams{
|
|
UserID: pgUUIDFromPtr(e.UserID),
|
|
Action: e.Action,
|
|
TargetType: ptr(e.TargetType),
|
|
TargetID: ptr(e.TargetID),
|
|
Ip: ip,
|
|
Metadata: meta,
|
|
}); err != nil {
|
|
return errs.Wrap(err, errs.CodeInternal, "insert audit")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// pgUUIDFromPtr 把可空的领域 *uuid.UUID 转为 sqlc 期望的 pgtype.UUID。
|
|
// nil 映射为 Valid=false(DB NULL),非 nil 映射为 Valid=true + Bytes。
|
|
func pgUUIDFromPtr(u *uuid.UUID) pgtype.UUID {
|
|
if u == nil {
|
|
return pgtype.UUID{Valid: false}
|
|
}
|
|
return pgtype.UUID{Bytes: *u, Valid: true}
|
|
}
|
|
|
|
// ptr 把空字符串映射为 nil,使数据库列写入 NULL;其它字符串原样取地址。
|
|
func ptr(s string) *string {
|
|
if s == "" {
|
|
return nil
|
|
}
|
|
return &s
|
|
}
|