feat(scanner): background jobs declare acting identity for RLS

Phase 4 of the RLS plan: detached contexts (context.Background()) that
reach media_items now carry identities — with the policies live, a bare
context made governed reads raise (fresh connections: 42704 unrecognized
parameter; reused pool connections: 22P02 empty-string cast after the
LOCAL GUC reverted — the plan's A2 signature).

- scanner jobs (API-triggered, mtime-poll, initial scan): the job's
  Context now carries the acting admin's identity — the library owner,
  first-admin fallback (the job's UserID already encoded this trust
  decision; the Context just needed to carry it). performInitialScan
  uses the scanner's configured admin.
- websocket initial-sync read (GetUserMediaItemsForSync): runs as the
  connection's own user.
- hash backfill: runs as first admin (server self-heal over all media).

Verified on the dev stack: forced rescan of the 1199-file Audiobooks
library completed with 0 errors under RLS (previously every SHA lookup
and create raised); hash-backfill refusals gone. The sync queue
processor needs no identity — sync_queue is not an RLS-governed table.
Known remaining detached reads (conflicts/analytics handlers using
context.Background()) are HTTP-path triage, enumerated by the full
suite next.
This commit is contained in:
John O'Keefe committed 2026-10-29 09:08:54 -04:00
1 parent 922b92a8b6
commit 385a5d9dba
4 files changed
+36 -8

No files matched your search

+6 -1
View File
@@ -11,6 +11,8 @@ import (
jwt "github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
"bookhoard/internal/rls"
"github.com/gorilla/websocket"
"github.com/jackc/pgx/v5/pgtype"
"github.com/labstack/echo/v5"
@@ -201,7 +203,10 @@ func (h *WSHandler) getInitialState(userID string) map[string]interface{} {
pgUserID := pgtype.UUID{Bytes: userUUID, Valid: true}
mediaItems, err := h.db.GetUserMediaItemsForSync(context.Background(), pgUserID)
// RLS plan §6: this read is the connection's OWN user's media —
// declare that identity explicitly (context.Background() carries
// nothing through ScopedDB, and media_items is RLS-governed).
mediaItems, err := h.db.GetUserMediaItemsForSync(rls.WithUser(context.Background(), pgUserID), pgUserID)
if err != nil {
return map[string]interface{}{"error": "failed to fetch initial state"}
}