The full suite enumerated the remaining triage class: tests drive the scanner directly (ScanFolders(context.Background())) and read seeded media via Queries with bare contexts — every governed read raised. - MediaScanner.ScanFolders self-declares its configured admin (SetAdminID — set by main AND the test helpers): a scan is server maintenance, product-correct regardless of caller. - ScopedDB gains a Fallback identity, exposed as NewSeedDB for the TEST HARNESS ONLY: setup.DB falls back to rls.SeedIdentity when the context carries none, so direct seeding/verification helpers work uniformly instead of per-site context threading. rls.SeedIdentity is the shared constant (no library_visibility rows => full visibility). - generic job creation (jobs.go) stamps the requesting admin into the job Context, same as the scan endpoint. The sync queue processor stays identity-free: sync_queue is not an RLS-governed table.
64 lines
2.6 KiB
Go
64 lines
2.6 KiB
Go
// Package rls carries the authenticated end user's identity through
|
|
// the stdlib context so the database layer can declare it to Postgres
|
|
// per transaction (the `app.user_id` GUC the RLS policies read — see
|
|
// database/schema/schema.sql and
|
|
// ~/.opencode/plan/server-rls-library-visibility.md).
|
|
//
|
|
// WHY A SEPARATE PACKAGE: Echo's c.Set/c.Get is echo-local storage and
|
|
// never reaches the context.Context handed to database calls; the auth
|
|
// middleware therefore bridges the user id into the request context
|
|
// here, and ScopedDB (internal/database) reads it back. A query whose
|
|
// context carries no identity is REFUSED (fail-loud) — a code path
|
|
// that forgets the middleware breaks visibly, it never leaks.
|
|
package rls
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
type ctxKey int
|
|
|
|
// CtxKeyUserID is the context key the auth middleware bridge sets and
|
|
// ScopedDB reads. Typed key — no string-key collisions.
|
|
const CtxKeyUserID ctxKey = iota
|
|
|
|
// FromContext returns the authenticated end user's UUID.
|
|
// ok=false means the caller forgot the middleware bridge — the
|
|
// decorator will refuse the query loudly.
|
|
func FromContext(ctx context.Context) (pgtype.UUID, bool) {
|
|
uid, ok := ctx.Value(CtxKeyUserID).(pgtype.UUID)
|
|
if !ok || !uid.Valid {
|
|
return pgtype.UUID{}, false
|
|
}
|
|
return uid, true
|
|
}
|
|
|
|
// WithUser re-attaches an identity to a (possibly detached) context.
|
|
// Background jobs MUST capture the acting user's UUID from the request
|
|
// context BEFORE `go func` detaches — context.Background() carries
|
|
// nothing — and rebuild their job context with this.
|
|
func WithUser(ctx context.Context, uid pgtype.UUID) context.Context {
|
|
return context.WithValue(ctx, CtxKeyUserID, uid)
|
|
}
|
|
|
|
// SeedIdentity is the harness seeding identity (plan §5): a valid UUID
|
|
// with NO library_visibility rows, so RLS grants it full visibility.
|
|
// It exists so test seeding and direct DB verification can read and
|
|
// write governed tables without impersonating a real user. Product
|
|
// code never runs as it.
|
|
var SeedIdentity = pgtype.UUID{Bytes: [16]byte(uuid.MustParse("00000000-0000-0000-0000-000000000002")), Valid: true}
|
|
|
|
// DeclareUser sets the RLS identity GUC inside an ALREADY-OPEN
|
|
// transaction. The `true` argument is is_local: transaction-scoped,
|
|
// evaporating at COMMIT/ROLLBACK — NEVER session-scoped on a pooled
|
|
// connection (cross-request identity bleed). For the manual-tx paths
|
|
// the ScopedDB decorator cannot wrap.
|
|
func DeclareUser(ctx context.Context, tx pgx.Tx, uid pgtype.UUID) error {
|
|
_, err := tx.Exec(ctx, `SELECT set_config('app.user_id', $1, true)`, uid.String())
|
|
return err
|
|
}
|