wip(rls): engine-side fail-loud + harness seed identity [pin: work interrupted]

Design correction discovered by the auth chicken-and-egg: the
decorator-level refusal was re-created a convention gate (plan B
thinking). ScopedDB now NEVER refuses — it declares the GUC when the
context carries an identity and declares nothing otherwise; the ENGINE
is the fail-loud layer (policies raise on governed tables, ungoverned
tables like auth lookups simply work). rls.WithInternal removed as a
dead concept.

- search service's raw DBTX path now receives the ScopedDB itself
  (main.go + harness) — a bare pool bypassed the GUC.
- database/schema.go: migration tx declares a synthetic identity
  (schema.sql's dedup DO block SELECTs media_items; with policies
  live, boot 2 raised 'unrecognized configuration parameter' — A11).
- harness: TestServerSetup.DB is a raw-pool SEED handle (write
  policies permissive, other seeded tables ungoverned); the server
  under test keeps the scoped wiring. 33 CreateMediaItem sites use
  seedCtx() — a seed identity with no visibility rows — because
  RETURNING requires SELECT visibility of the new row.
- invariant test: regex UUID-scraping replaced with a structured JSON
  decode of the sections payload.

INTERRUPTION NOTE: an edit to this same effect (seedCtx) was applied,
confirmed by the tool, and subsequently vanished from disk while an
'opencode serve' process (PID 84551) is alive on the machine. This
commit pins the tree against further loss; implementation is PAUSED
pending the user killing that process.
This commit is contained in:
John O'Keefe committed 2026-10-29 08:44:12 -04:00
1 parent 004d4197a4
commit 8c3d919aa0
19 files changed
+124 -111

No files matched your search

+23 -7
View File
@@ -318,6 +318,17 @@ func createRegularUserOnce(t *testing.T, db *database.Queries) UserTestData {
}
}
// seedCtx is the context harness-level media seeding runs under. The
// seed identity is a valid UUID with NO library_visibility rows, so
// RLS grants it full visibility — needed because sqlc's :one inserts
// use RETURNING, which requires SELECT visibility of the new row.
// Product code never uses this; requests carry real personas.
var seedIdentity = pgtype.UUID{Bytes: [16]byte(uuid.MustParse("00000000-0000-0000-0000-000000000002")), Valid: true}
func seedCtx() context.Context {
return rls.WithUser(context.Background(), seedIdentity)
}
// uuidToPGType converts uuid.UUID to pgtype.UUID
func uuidToPGType(u uuid.UUID) pgtype.UUID {
return pgtype.UUID{Bytes: [16]byte(u), Valid: true}
@@ -497,10 +508,15 @@ func setupTestServer(t *testing.T) *TestServerSetup {
// ScopedDB: the test server must behave EXACTLY like the production
// wiring (main.go) — every statement through Queries declares the
// acting user for RLS. Helper seeding that must bypass user scoping
// uses direct pool writes (permissive write policies) or
// rls.WithInternal for non-policy'd tables.
queries := database.New(database.NewScopedDB(dbPool))
// acting user for RLS, including the search service's raw DBTX path.
// TestServerSetup.DB below is a SEPARATE raw-pool handle used ONLY
// for harness seeding/cleanup: write policies are permissive (no GUC
// read) and every seeded table except media_items is RLS-ungoverned,
// so seeding needs no identity; direct media SELECTs through it
// raise loudly, which is correct — tests read media through HTTP.
scopedDB := database.NewScopedDB(dbPool)
queries := database.New(scopedDB)
seedDB := database.New(dbPool)
// Wire the tunable settings registry exactly like main.go: Load
// populates the cache (missing rows fall back to compiled defaults)
@@ -509,7 +525,7 @@ func setupTestServer(t *testing.T) *TestServerSetup {
// initialized") — tests that write settings through the API (the
// only sanctioned path) need this wired.
settingsRegistry := database.NewSettingsRegistry(queries)
if err := settingsRegistry.Load(rls.WithInternal(context.Background())); err != nil {
if err := settingsRegistry.Load(context.Background()); err != nil {
t.Logf("settings registry Load failed (defaults apply): %v", err)
}
@@ -558,7 +574,7 @@ func setupTestServer(t *testing.T) *TestServerSetup {
statisticsService := services.NewStatisticsService(queries)
statsHandler := handlers.NewStatsHandler(statisticsService)
seriesHandler := handlers.NewSeriesHandler(queries)
mediaHandler := handlers.NewMediaHandler(queries, dbPool, libraryService, progressService, annotationService, worker)
mediaHandler := handlers.NewMediaHandler(queries, scopedDB, libraryService, progressService, annotationService, worker)
matchingHandler := handlers.NewMatchingHandler(queries, connManager)
// Create conversion service for OPDS
@@ -762,7 +778,7 @@ func setupTestServer(t *testing.T) *TestServerSetup {
// Create TestServerSetup struct with all resources
setup := &TestServerSetup{
Server: ts,
DB: queries,
DB: seedDB, // harness seeding/cleanup handle (see comment above)
DBPool: dbPool,
Config: cfg,
ConnManager: connManager,