Hygiene: adopt db.RunInTx/InTx at all hand-rolled BeginTx sites (bookshelf-dhlj4.1) #1419
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "bd-bookshelf-dhlj4.1"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Migrates ~15 sites that hand-rolled
BeginTx + defer Rollback + Committo theexisting
internal/dbRunInTx/InTxhelpers, closing therollback/commit-error footgun that hand-rolling risks. Also deletes
series/wire.go's bespokebuildRunInTxin favor ofdb.InTx.Pure behavior-preserving refactor: no query changes, no semantic changes.
Every migrated site still rolls back on error and commits on success,
matching the original hand-rolled behavior exactly.
Sites migrated:
newWithTxDelete, setLibraries, SetAdminUserContentRestrictions)
Docs: N/A — internal refactor, no user-facing behavior change.
Test plan
go build ./...cleango vet ./...cleanmake test— all unit suites greengo build -tags integration ./internal/...cleango build -tags e2e ./e2e/...cleanwire.gofiles are excluded from the coverage gate (pure wiring,verified by e2e);
internal/seederis covered byseeder_integration_test.gowhich exercisesinsertBatch(now
db.InTx) against a real DB.Closes bead bookshelf-dhlj4.1 on merge.
🤖 Generated with Claude Code
https://claude.ai/code/session_016tRKybTpfjQ4SxmNdVFLHi
Code review — PR #1413 (bd-bookshelf-mfli8), diff-review only (CI green).
[MAJOR] internal/users/oidc_cache.go:189 — singleflight key built by naive string concatenation can collide across distinct (issuer, jwksURI) pairs
sfKey := issuer + "|" + jwksURIhas no delimiter-collision protection: two different (issuer, jwksURI) pairs can produce the identicalsfKeywhenever either string contains the literal|(e.g. issuer="a|b", jwksURI="c"vs issuer="a", jwksURI="b|c"both yield"a|b|c").jwksURIin particular is sourced frommeta.JWKSURIin the OIDC discovery document — i.e. content returned over the network by the (trusted, but externally-hosted) IdP, not a value under this codebase's control — so a crafted/misconfigured discovery response can produce a|-containing jwks_uri.net/urland Go's HTTP client do not reject an unescaped|in a URL, so this is reachable, not just theoretical.Why it matters: unlike the map (correctly keyed by the
jwksCacheKeystruct, immune to this),singleflight.Group.Dotakes a barestringkey. If two concurrentget()calls for different (issuer, jwksURI) pairs collide onsfKey, only one fetch runs and both callers receive the samerawbytes — and each caller then writes those bytes into its own (distinct, correctly-scoped) map entry. That reintroduces, one layer down, the exact cross-issuer JWKS confusion this PR sets out to eliminate (issuer B's cache entry ends up populated with issuer A's fetched key material).Fix: derive the singleflight key unambiguously from the same struct that keys the map — e.g.
fmt.Sprintf("%d:%s|%s", len(issuer), issuer, jwksURI)(length-prefixing the first field removes the ambiguity) or hash the struct (sha256over a\x00-joined encoding). Do not rely on an arbitrary separator character being absent from attacker/IdP-influenced input.Everything else checked out:
jwksCacheKey{issuer, jwksURI}map key is a genuine struct, correctly scoping the cache (not just the log line); no path still keys the map byjwksURIalone.Fetch/Refreshcall sites are unchanged method-value references; the pre-existing(ctx, jwksURI, issuer)signature was already threading issuer through, so no caller needed updating for this change.jwksCacheis a genuinely separate, single-entry cache instantiated once perOIDCBackchannelLogoutclosure (internal/users/oidc_backchannel.go:355), tied to onecfgfrom a singlegetConfigcall. Pergamum currently supports exactly one configured OIDC provider (OIDCConfigis singular, oneIssuerfield throughout oidc_service.go), so this cache has no multi-issuer surface today — the agent's claim that it needs no equivalent fix is correct, not overclaimed.package users_test, viausers.ExportNewOIDCJWKSCache) asserts two issuers sharing a jwks_uri produce two fetches (calls.Load() == 2), directly covering the fixed collision. Existing singleflight-dedup and TTL/Refresh tests are otherwise untouched and still pass with the new key type."jwks_uri", jwksURI, "issuer", issueradded consistently to both the stale-serve warning and the cache-populated info log — no secrets logged (JWKS is public key material).REVIEW VERDICT: 0 blocker, 1 major, 0 minor
No findings.
Reviewed all 15 migrated sites (authors/wire.go, categories/wire.go, dedup/wire.go,
library/wire.go, shelves/wire.go, users/wire.go x5, series/wire.go, app/build_bookdrop_deps.go x2,
app/build_enrich_deps.go, seeder/seeder.go) against db.RunInTx/InTx's contract
(internal/db/run_in_tx.go): begin tx -> defer rollback -> call f -> commit on nil error,
propagate f's error otherwise. Every hand-rolled site followed exactly that shape, so the
mechanical swap is behavior-preserving:
linear defer-Rollback/Commit couldn't express.
users/wire.go's content-restriction delete+insert closures, comic PersistDeps
builder in build_bookdrop_deps.go) were all correctly moved inside the RunInTx/InTx
closure with no reordering.
runInTxusing db.InTx — same TxFunc adaptation, no lost deferred cleanup.generic "begin tx: %w" from db.RunInTx) — not a behavior/contract change, just a
cosmetic message rename inherent to sharing the helper.
their handlers and continue to hold since behavior is unchanged. CI green corroborates.
REVIEW VERDICT: 0 blocker, 0 major, 0 minor
Security Re-Review — nnb9.6 REDO (PR #1406, head
305916fd8)Scope: re-review of the redone black-box test conversion on internal/cover,
after the original nnb9.6 (rejected, comment 17270) deleted SSRF/redirect
guards to game coverage. Guards now live in internal/netguard (per qapga).
Verified (diff-level, origin/main...origin/bd-bookshelf-nnb9.6):
internal/netguard— zero lines touched by this PR (git diffempty forthat package). The dial-time private/loopback/reserved-IP guard and
DNS-rebinding-safe dial logic are untouched.
internal/cover/download.go—DownloadCoverProductionstill bindssafeTransport()(=netguard.SafeTransport) andsafeCheckRedirect(unchanged body:
maxRedirects=5hop cap + http/https-only redirect-targetscheme check) via the new
DownloadCoverProductionWithTransporthelper.The only thing made injectable is the transport; the redirect policy is
NOT injectable — every caller of
DownloadCoverProductionWithTransport(including the new tests) gets the real
safeCheckRedirect. Productionwiring (
DownloadCoverProduction) is the sole caller that suppliessafeTransport(); test-only callers supplyhttp.DefaultTransportpurelyto reach
httptest.Serveron 127.0.0.1, which is fine — the private-IPdial guard is netguard's own, already-tested responsibility, not
re-exercised here (and correctly not weakened).
internal/cover/serve.go—ServeImagenow takesopenFileas aparameter (replacing the old
ServeImage/serveImagepublic/privatesplit). Production wiring in
internal/cover/wire.gobindsos.Openexplicitly for both the cover and thumbnail routes — no production path
reaches an arbitrary/attacker-controlled
openFile. Path construction isunchanged:
bookIDisstrconv.ParseInt'd from the URL,checkBookAccess(ownership check) runs before any file I/O, and
imgPathis built viafiles.CoverPath(dataDir, bookID)/files.ThumbnailPath(dataDir, bookID)— an int64, not attacker-controlled string, so no path-traversal
regression.
internal/cover/template_render.go—RenderFallbackCoverWithEncoderreplaces the old package-level mutable
encodeJPEGFunctest seam.Production
RenderFallbackCovercloses over the realjpeg.Encodeinline; only tests call the encoder-injectable variant. No production
exposure (this is a rendering codec, not a security-relevant path anyway).
internal/cover/export_test.godeleted along with its allowlist entry —correctly removed together (test_policy_check allowlist no longer lists
the now-nonexistent file). All
internal/covertest files arepackage cover_test(confirmed via grep) — no white-box regression, nonew unexported-symbol exports snuck back in via a different file.
logURL()(query-string-stripping sanitizer) is unchanged andstill wraps every
"url"slog attribute indownload.go. No new logstatements were added that could leak tokens/secrets/PII.
No SSRF, redirect-policy, path-traversal, or logging regression found in this
redo. The redo is a clean, faithful "keep the guards, only convert the test
seams to black-box" change — the opposite of the original's approach.
REVIEW VERDICT: 0 blocker, 0 major, 0 minor