Expand description
Host-side parse-and-rewrite confinement of a guest’s raw-SQL target read (R4/D8): the
AST-level analog of the orm path’s PerTableTarget per-table confinement, injecting
tenant = B AND <public subset> onto EVERY table reference so a target read of another tenant
B can reach only B’s declared public rows — the guest cannot reposition or OR-escape it.
Host-side parse-and-rewrite confinement of a guest’s raw-SQL target read (R4/D8).
The orm binding confines a target read (reading ANOTHER tenant B’s PUBLIC subset) per table
via TableKeys::PerTableTarget: every accessed table is
rewritten to tenant = B AND <that table's public predicate>, and a table with no declared
public subset is refused (deny-by-default). Raw SQL had only the guest-cooperative {scope}
marker — a single, single-table, guest-placed injection point that a guest could reposition
(leaving joined tables unconfined) or OR-escape (WHERE {scope} OR 1=1). That is
structurally unfixable with a text marker.
This module closes it by doing to raw SQL what the ORM does to typed queries: it parses the
guest statement into an AST and injects the same per-table confinement onto EVERY table
reference — the root FROM, every JOIN, every subquery, CTE, and set-operation arm — at the
AST level, where the guest cannot move or escape it. The guest’s own WHERE is parenthesised
before the confinement is AND-ed on, so a top-level OR in the guest predicate can never widen
past the tenant/public gate.
§Why this is safe (the completeness argument)
A single missed table reference is a cross-tenant leak, so completeness cannot rest on a hand-rolled belief that every AST position has been enumerated. Instead:
- The traversal is sqlparser’s derived
VisitMutwalk, which is maintained by sqlparser to cover the WHOLE grammar. EveryQuerynode in the tree — including those buried inIN (SELECT …),EXISTS (…), scalar subqueries, derived tables, andUNION/INTERSECT/EXCEPTarms — receives apre_visit_query, where its ownSELECTs are confined. - Every table reference lives in a
SELECT’sFROM(directly or under aNESTED JOIN), and everySELECTis confined by exactly one enclosing query’s visit — so every base table is reached exactly once. WithWITH/CTEs refused up front, a bareFROM foois ALWAYS a base table (derived tables are a distinct AST node, subqueries are their ownQuery), so there is no name-shadowing case in which a reference could be mistaken for a non-table and skipped. - Anything the confinement cannot reason about — a table-valued function,
UNNEST,PIVOT, a schema-qualified name, a CTE, a write smuggled into a read position, an exotic table source — is refused (fail-closed), never silently passed. A secondpre_visit_table_factorguard rejects any un-confinable table source anywhere in the tree as belt-and-suspenders.
The injected B and public-subset literals are host-held (from the routing context + the
operator’s schema), never guest input, and are rendered through sqlparser’s own escaping
(Value::SingleQuotedString doubles quotes) — so they are safe as
literals and, unlike bound parameters, do not disturb the guest’s own positional placeholders
(which matters for the positional-parameter dialects).
Enums§
- Target
Rewrite Error - Why a raw-SQL target read is refused before it reaches the backend (always fail-closed — a target read that cannot be provably confined does not run).
Functions§
- extract_
raw_ write_ scope_ value - Best-effort extraction of the single tenant value a raw-SQL
allwrite declares, so the host can set the RLS tenant GUC to it (v0.4.20 — the raw-path analog ofInsert::uniform_scope_value/Update::pinned_scope_value). Parsesstatementand: - rewrite_
target_ select - Rewrite a guest’s raw-SQL target read so every table reference is confined to
tenant = <tenant_value> AND <that table's public subset>(R4/D8).keysandpublicare the project schema’s per-table tenant-key map and per-table lowered public-subset terms (exactly the two maps aTableKeys::PerTableTargetcarries);tenant_valueis the host-resolved target tenantB(NEVER guest input);dialectselects the parser. Returns the rewritten SQL text (the guest’s own positional params are untouched —Band the public literals are injected as escaped literals), or aTargetRewriteError(fail-closed — the read does not run).