Go · 3346 bytes Raw Blame History
1 // SPDX-License-Identifier: AGPL-3.0-or-later
2
3 // Package audit writes structured rows into the auth_audit_log table for
4 // security-relevant events. The schema is generic — actor, action, target,
5 // meta JSON — so future sprints reuse this for permission changes,
6 // org-membership changes, admin actions, etc.
7 //
8 // Callers MUST NOT put secret material in the meta JSON. The values land
9 // unredacted in the DB; treat the table contents as confidential but
10 // never sensitive (no plaintext passwords, no TOTP secrets, no PATs).
11 package audit
12
13 import (
14 "context"
15 "encoding/json"
16 "errors"
17 "fmt"
18
19 "github.com/jackc/pgx/v5/pgtype"
20
21 usersdb "github.com/tenseleyFlow/shithub/internal/users/sqlc"
22 )
23
24 // DBTX matches sqlc's DBTX so callers can pass *pgxpool.Pool or a tx.
25 type DBTX = usersdb.DBTX
26
27 // Action is a typed action constant. Reuse these across packages so the
28 // set stays tight and grep-able.
29 type Action string
30
31 const (
32 Action2FAEnabled Action = "2fa_enabled"
33 Action2FADisabled Action = "2fa_disabled"
34 ActionRecoveryCodesIssued Action = "recovery_codes_issued"
35 ActionRecoveryCodeUsed Action = "recovery_code_used"
36 ActionRecoveryRegenerated Action = "recovery_codes_regenerated"
37 ActionAdminCleared2FA Action = "admin_cleared_2fa"
38 ActionPasswordChanged Action = "password_changed"
39 ActionPasswordReset Action = "password_reset_consumed"
40 ActionLoginSucceeded Action = "login_succeeded"
41 ActionLoginFailedThrottled Action = "login_failed_throttled"
42 ActionAccountSuspended Action = "account_suspended"
43 ActionSSHKeyAdded Action = "ssh_key_added"
44 ActionSSHKeyDeleted Action = "ssh_key_deleted"
45 ActionPATCreated Action = "pat_created"
46 ActionPATRevoked Action = "pat_revoked"
47 ActionUsernameChanged Action = "username_changed"
48 )
49
50 // Target is a typed target-type constant.
51 type Target string
52
53 const (
54 TargetUser Target = "user"
55 )
56
57 // Recorder writes audit rows. Bound to the sqlc queries handle.
58 type Recorder struct {
59 q *usersdb.Queries
60 }
61
62 // NewRecorder constructs a recorder.
63 func NewRecorder() *Recorder { return &Recorder{q: usersdb.New()} }
64
65 // Record inserts a single audit-log row. actorID is the user_id of the
66 // actor performing the action (use 0 for system / admin-CLI actions).
67 // targetID is the affected entity (use 0 for actions without a single
68 // concrete target).
69 //
70 // meta MUST be a JSON-serializable map; secrets and PII (tokens, raw
71 // passwords, etc.) MUST NOT appear here.
72 func (r *Recorder) Record(
73 ctx context.Context, db DBTX,
74 actorID int64, action Action, target Target, targetID int64, meta map[string]any,
75 ) error {
76 if action == "" || target == "" {
77 return errors.New("audit: action and target_type required")
78 }
79 if meta == nil {
80 meta = map[string]any{}
81 }
82 metaJSON, err := json.Marshal(meta)
83 if err != nil {
84 return fmt.Errorf("audit: marshal meta: %w", err)
85 }
86 var actorParam pgtype.Int8
87 if actorID != 0 {
88 actorParam = pgtype.Int8{Int64: actorID, Valid: true}
89 }
90 var targetParam pgtype.Int8
91 if targetID != 0 {
92 targetParam = pgtype.Int8{Int64: targetID, Valid: true}
93 }
94 return r.q.InsertAuditLog(ctx, db, usersdb.InsertAuditLogParams{
95 ActorID: actorParam,
96 Action: string(action),
97 TargetType: string(target),
98 TargetID: targetParam,
99 Meta: metaJSON,
100 })
101 }
102