wip(rls): ScopedDB + rls package + schema policies + echo bridge [pin compiling state]

Phase 3 of the RLS plan, pinned mid-flight because a parallel writer
was actively editing cmd/server/tests/rls_visibility_test.go (A14):
the file flip-flopped between two implementations and gained a compile
error after the previous commit. This snapshot makes the whole tree
compile; functional verification (two-boot gate, invariant green,
full suite) lands in the next commit(s).

- internal/rls: typed ctx keys — user identity (bridged from echo by
  the router) and an INTERNAL marker whose semantics grant nothing:
  no GUC is declared, so policy'd tables still raise at the engine;
  only RLS-ungoverned tables (system_settings, users counts) work for
  user-less startup paths (settings registry Load, CountAdmins).
- internal/database/scoped.go: DBTX decorator — per-statement tx,
  commit-on-Close (:many) / commit-on-Scan (:one), error paths always
  terminate the tx, refusal before SQL when no identity and not
  internal.
- database/schema/schema.sql: idempotent policy block (DROP IF EXISTS
  + CREATE — schema.sql re-runs every boot): ENABLE + FORCE, read
  policy exactly mirroring COALESCE(is_visible,true), permissive
  per-command write stand-ins (FOR ALL would OR into SELECT).
- router.go: RLS bridge at BOTH c.Set("user") sites (JWT + device
  auth) — echo-local storage never reaches the stdlib ctx.
- main.go + test harness: database.New over ScopedDB at both wiring
  sites; startup reads use rls.WithInternal.
This commit is contained in:
John O'Keefe committed 2026-10-29 08:11:20 -04:00
1 parent a19d7a4cbf
commit f4b581872f
7 files changed
+337 -9

No files matched your search

+8 -3
View File
@@ -7,6 +7,7 @@ import (
"bookhoard/internal/handlers"
"bookhoard/internal/middleware"
ratelimit "bookhoard/internal/middleware"
"bookhoard/internal/rls"
"bookhoard/internal/router"
"bookhoard/internal/services"
"bookhoard/internal/sync"
@@ -41,7 +42,7 @@ func main() {
}
defer dbPool.Close()
queries := database.New(dbPool)
queries := database.New(database.NewScopedDB(dbPool))
// Initialize database schema
log.Println("🔧 Ensuring database schema is initialized...")
@@ -53,8 +54,10 @@ func main() {
// Load tunable settings from the DB into the registry. All values fall back
// to compiled defaults if a row is missing, so this never blocks startup.
// INTERNAL context: startup has no end-user identity; system_settings
// is not RLS-governed, so the statement runs without the GUC (plan §5).
registry := database.NewSettingsRegistry(queries)
if err := registry.Load(ctx); err != nil {
if err := registry.Load(rls.WithInternal(ctx)); err != nil {
log.Printf("⚠️ Could not load system settings (using defaults): %v", err)
}
@@ -65,7 +68,9 @@ func main() {
if registry.RegistrationEnabled() {
return true
}
count, err := queries.CountAdmins(context.Background())
// INTERNAL: a "how many admins exist" read is not user-scoped;
// users is not RLS-governed (plan §5).
count, err := queries.CountAdmins(rls.WithInternal(context.Background()))
if err != nil {
return true // fail open, matching setupstatus
}