// 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 }