tenseleyflow/shithub / 61261e7

Browse files

repos/sigverify: sqlc-backed Lookups + DispatchForKey/DispatchAll helpers

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
61261e71632de205d74185990b7873b582e281a3
Parents
ee55fd7
Tree
2742586

2 changed files

StatusFile+-
A internal/repos/sigverify/dispatch.go 93 0
A internal/repos/sigverify/lookups_sqlc.go 104 0
internal/repos/sigverify/dispatch.goadded
@@ -0,0 +1,93 @@
1
+// SPDX-License-Identifier: AGPL-3.0-or-later
2
+
3
+package sigverify
4
+
5
+import (
6
+	"context"
7
+	"fmt"
8
+
9
+	"github.com/jackc/pgx/v5/pgtype"
10
+
11
+	reposdb "github.com/tenseleyFlow/shithub/internal/repos/sqlc"
12
+	"github.com/tenseleyFlow/shithub/internal/worker"
13
+)
14
+
15
+// BackfillPayload is the on-the-wire schema for the gpg:backfill
16
+// worker job. One job per repo; the handler walks every signed
17
+// commit and tag on the repo's default branch and writes
18
+// commit_verification_cache rows.
19
+//
20
+// The handler is intentionally NOT key-scoped — it verifies every
21
+// signed object regardless of which user key signed it. The
22
+// orchestrator returns ReasonUnknownKey for signatures it can't
23
+// resolve; the cache row carries that state forward and the
24
+// rendering path treats it as "Unverified". When new keys arrive
25
+// later, those previously-unknown-key cache rows will get
26
+// re-stamped by the next DispatchForKey or DispatchAll run.
27
+type BackfillPayload struct {
28
+	RepoID int64 `json:"repo_id"`
29
+}
30
+
31
+// DispatchForKey enqueues backfill jobs for every repo owned by the
32
+// user who just added a GPG key. This is the eager-backfill path:
33
+// "user uploads key → existing signed commits in their own repos
34
+// retroactively get Verified badges within minutes" — the gh
35
+// behavior we're matching.
36
+//
37
+// We deliberately scope to OWNED repos rather than every repo the
38
+// user has ever committed to. Owned repos are the common case
39
+// (~90%); cross-repo authorship (PR contributions to others'
40
+// repos) gets picked up by the next DispatchAll run. Without a
41
+// commits-author index, walking every repo on every key upload
42
+// would be O(repos × commits) per upload and is the wrong cost
43
+// shape for the eager path.
44
+//
45
+// The caller passes (db) so this can run inside an existing
46
+// transaction (typical pattern in the add-key handler: insert key
47
+// row → insert subkey rows → dispatch backfill → commit). Notify
48
+// is called inside the same tx so workers wake up on commit, not
49
+// before.
50
+func DispatchForKey(ctx context.Context, db reposdb.DBTX, userID int64) error {
51
+	rq := reposdb.New()
52
+	rows, err := rq.ListReposForOwnerUser(ctx, db, pgtype.Int8{Int64: userID, Valid: true})
53
+	if err != nil {
54
+		return fmt.Errorf("sigverify: list user repos: %w", err)
55
+	}
56
+	for _, row := range rows {
57
+		if _, err := worker.Enqueue(ctx, db, worker.KindGPGBackfill, BackfillPayload{
58
+			RepoID: row.ID,
59
+		}, worker.EnqueueOptions{}); err != nil {
60
+			return fmt.Errorf("sigverify: enqueue backfill for repo %d: %w", row.ID, err)
61
+		}
62
+	}
63
+	if err := worker.Notify(ctx, db); err != nil {
64
+		return fmt.Errorf("sigverify: notify workers: %w", err)
65
+	}
66
+	return nil
67
+}
68
+
69
+// DispatchAll enqueues backfill jobs for every active repo in the
70
+// system. Called by `shithubd gpg-backfill-all`. Returns the number
71
+// of jobs enqueued so the admin command can log progress.
72
+//
73
+// Unlike DispatchForKey this is NOT bounded to a single user; it
74
+// walks every active repo. Use sparingly — typically once on
75
+// initial S51 deploy, or after a known mass-key-upload event.
76
+func DispatchAll(ctx context.Context, db reposdb.DBTX) (int, error) {
77
+	rq := reposdb.New()
78
+	rows, err := rq.ListAllActiveReposWithOwner(ctx, db)
79
+	if err != nil {
80
+		return 0, fmt.Errorf("sigverify: list all repos: %w", err)
81
+	}
82
+	for _, row := range rows {
83
+		if _, err := worker.Enqueue(ctx, db, worker.KindGPGBackfill, BackfillPayload{
84
+			RepoID: row.ID,
85
+		}, worker.EnqueueOptions{}); err != nil {
86
+			return 0, fmt.Errorf("sigverify: enqueue backfill for repo %d: %w", row.ID, err)
87
+		}
88
+	}
89
+	if err := worker.Notify(ctx, db); err != nil {
90
+		return 0, fmt.Errorf("sigverify: notify workers: %w", err)
91
+	}
92
+	return len(rows), nil
93
+}
internal/repos/sigverify/lookups_sqlc.goadded
@@ -0,0 +1,104 @@
1
+// SPDX-License-Identifier: AGPL-3.0-or-later
2
+
3
+package sigverify
4
+
5
+import (
6
+	"context"
7
+	"errors"
8
+
9
+	"github.com/jackc/pgx/v5"
10
+
11
+	usersdb "github.com/tenseleyFlow/shithub/internal/users/sqlc"
12
+)
13
+
14
+// DBTX is the pgx interface the sqlc-backed Lookups need. Matches the
15
+// sqlc-generated DBTX exactly so callers can pass either a *pgxpool.Pool
16
+// or a pgx.Tx without conversion.
17
+type DBTX interface {
18
+	usersdb.DBTX
19
+}
20
+
21
+// SQLCLookups is the production Lookups implementation, backed by the
22
+// usersdb-generated queries. Used by the orchestrator when invoked
23
+// from the backfill worker, the verification render path, and the
24
+// commits REST handler.
25
+type SQLCLookups struct {
26
+	DB DBTX
27
+	Q  *usersdb.Queries
28
+}
29
+
30
+// NewSQLCLookups constructs a Lookups bound to the given DB handle.
31
+// The handle can be a connection pool (for concurrent reads) or a tx
32
+// (for invalidation-followed-by-re-verify flows that need to see
33
+// uncommitted writes within the same transaction).
34
+func NewSQLCLookups(db DBTX) *SQLCLookups {
35
+	return &SQLCLookups{DB: db, Q: usersdb.New()}
36
+}
37
+
38
+// SubkeyByFingerprint resolves a 40-hex fingerprint to a Subkey row.
39
+// Returns (_, false, nil) on miss; only DB errors propagate.
40
+func (l *SQLCLookups) SubkeyByFingerprint(ctx context.Context, fingerprint string) (Subkey, bool, error) {
41
+	row, err := l.Q.GetUserGPGSubkeyByFingerprint(ctx, l.DB, fingerprint)
42
+	if errors.Is(err, pgx.ErrNoRows) {
43
+		return Subkey{}, false, nil
44
+	}
45
+	if err != nil {
46
+		return Subkey{}, false, err
47
+	}
48
+	sk := Subkey{
49
+		ID:                row.ID,
50
+		GPGKeyID:          row.GpgKeyID,
51
+		Fingerprint:       row.Fingerprint,
52
+		KeyID:             row.KeyID,
53
+		CanSign:           row.CanSign,
54
+		CanEncryptComms:   row.CanEncryptComms,
55
+		CanEncryptStorage: row.CanEncryptStorage,
56
+		CanCertify:        row.CanCertify,
57
+	}
58
+	if row.ExpiresAt.Valid {
59
+		sk.ExpiresAt = row.ExpiresAt.Time
60
+	}
61
+	if row.RevokedAt.Valid {
62
+		sk.RevokedAt = row.RevokedAt.Time
63
+	}
64
+	return sk, true, nil
65
+}
66
+
67
+// GPGKeyByID resolves a primary user_gpg_keys row by id (NOT scoped
68
+// to a user_id — the verification path discovers user_id from the
69
+// row itself). Returns (_, false, nil) on miss; only DB errors
70
+// propagate.
71
+func (l *SQLCLookups) GPGKeyByID(ctx context.Context, id int64) (GPGKey, bool, error) {
72
+	row, err := l.Q.GetUserGPGKeyForVerification(ctx, l.DB, id)
73
+	if errors.Is(err, pgx.ErrNoRows) {
74
+		return GPGKey{}, false, nil
75
+	}
76
+	if err != nil {
77
+		return GPGKey{}, false, err
78
+	}
79
+	return GPGKey{
80
+		ID:          row.ID,
81
+		UserID:      row.UserID,
82
+		Fingerprint: row.Fingerprint,
83
+		KeyID:       row.KeyID,
84
+		Armored:     row.Armored,
85
+	}, true, nil
86
+}
87
+
88
+// UserEmailsByUserID returns every email associated with the user
89
+// (verified or not) so the orchestrator can run the bad_email vs
90
+// unverified_email vs valid discrimination.
91
+func (l *SQLCLookups) UserEmailsByUserID(ctx context.Context, userID int64) ([]UserEmail, error) {
92
+	rows, err := l.Q.ListUserEmailsForUser(ctx, l.DB, userID)
93
+	if err != nil {
94
+		return nil, err
95
+	}
96
+	emails := make([]UserEmail, 0, len(rows))
97
+	for _, row := range rows {
98
+		emails = append(emails, UserEmail{
99
+			Email:    row.Email,
100
+			Verified: row.Verified,
101
+		})
102
+	}
103
+	return emails, nil
104
+}