test(visibility): full suite green under RLS — sync paths, device-auth bridge, seed pool
Phase 5 (plan §7): the entire integration suite passes with the RLS policies live. The suite was the triage enumerator the plan predicted: - device_auth middleware: the THIRD identity bridge — KOReader/Kobo device-authenticated requests read media as the DEVICE OWNER (plan A9 called this path out; the flip was 404s on sync metadata). - conflicts/analytics/progress handlers: detached context.Background() reads threaded to the request context (the requesting user's identity) — 12+ sites; resolve helpers take ctx. - scanner: ScanFolders self-declares its configured admin identity (server maintenance, product-correct for any caller); generic job creation stamps the requesting admin. - harness: setup.DBPool and the sync-test pool declare rls.SeedIdentity at SESSION scope via BeforeAcquire — the pool never serves a second identity, so no A3 bleed; setup.DB falls back to the seed identity (NewSeedDB) for ungoverned-context calls; filter tests use seedExec for raw governed-table statements. - contract pin WIDENED (docs/contract_pins.md 2026-10-29): under RLS, item-scoped gates may answer 404 (row invisible — no existence leak) as well as 403 (Go-level checks on listings keep strict 403). - TestWorker_JobStatusTracking — the pre-existing red from the Phase 0 baseline — passes with the harness seed work; no product change.
This commit is contained in:
1 parent
bb2cc26e9d
commit
4d697f5fcf
7 files changed
+84
-38
No files matched your search
@@ -329,6 +329,26 @@ func seedCtx() context.Context {
|
||||
return rls.WithUser(context.Background(), rls.SeedIdentity)
|
||||
}
|
||||
|
||||
// seedExec runs a raw statement against the pool under the seed
|
||||
// identity. The bare pool bypasses ScopedDB (nothing reads the ctx for
|
||||
// the GUC), so governed tables need the identity declared manually
|
||||
// inside a transaction.
|
||||
func seedExec(t *testing.T, pool *pgxpool.Pool, sql string, args ...any) {
|
||||
t.Helper()
|
||||
ctx := rls.WithUser(context.Background(), rls.SeedIdentity)
|
||||
tx, err := pool.Begin(ctx)
|
||||
require.NoError(t, err)
|
||||
if _, err := tx.Exec(ctx, `SELECT set_config('app.user_id', $1, true)`, rls.SeedIdentity.String()); err != nil {
|
||||
_ = tx.Rollback(ctx)
|
||||
require.NoError(t, err, "declare seed identity")
|
||||
}
|
||||
if _, err := tx.Exec(ctx, sql, args...); err != nil {
|
||||
_ = tx.Rollback(ctx)
|
||||
require.NoError(t, err, "seed exec")
|
||||
}
|
||||
require.NoError(t, tx.Commit(ctx))
|
||||
}
|
||||
|
||||
// uuidToPGType converts uuid.UUID to pgtype.UUID
|
||||
func uuidToPGType(u uuid.UUID) pgtype.UUID {
|
||||
return pgtype.UUID{Bytes: [16]byte(u), Valid: true}
|
||||
@@ -503,6 +523,15 @@ func setupTestServer(t *testing.T) *TestServerSetup {
|
||||
dbConfig, err := pgxpool.ParseConfig(cfg.DatabaseURL())
|
||||
require.NoError(t, err, "Failed to parse database URL")
|
||||
dbConfig.MaxConns = 1
|
||||
// Harness-only session identity (plan §5): every connection in this
|
||||
// pool runs as rls.SeedIdentity at SESSION scope, so raw-pool
|
||||
// statements in tests (DBPool.Exec/QueryRow against governed tables)
|
||||
// pass the read policy. No bleed risk: this pool never serves a
|
||||
// second identity — A3 applies to the server's request pool.
|
||||
dbConfig.BeforeAcquire = func(ctx context.Context, conn *pgx.Conn) bool {
|
||||
_, err := conn.Exec(ctx, `SELECT set_config('app.user_id', $1, false)`, rls.SeedIdentity.String())
|
||||
return err == nil
|
||||
}
|
||||
dbPool, err := pgxpool.NewWithConfig(context.Background(), dbConfig)
|
||||
require.NoError(t, err, "Failed to connect to test database")
|
||||
|
||||
|
||||
Reference in new issue
Block a user