Skip to main content

Module target_sql

Module target_sql 

Source
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:

  1. The traversal is sqlparser’s derived VisitMut walk, which is maintained by sqlparser to cover the WHOLE grammar. Every Query node in the tree — including those buried in IN (SELECT …), EXISTS (…), scalar subqueries, derived tables, and UNION/INTERSECT/EXCEPT arms — receives a pre_visit_query, where its own SELECTs are confined.
  2. Every table reference lives in a SELECT’s FROM (directly or under a NESTED JOIN), and every SELECT is confined by exactly one enclosing query’s visit — so every base table is reached exactly once. With WITH/CTEs refused up front, a bare FROM foo is ALWAYS a base table (derived tables are a distinct AST node, subqueries are their own Query), so there is no name-shadowing case in which a reference could be mistaken for a non-table and skipped.
  3. 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 second pre_visit_table_factor guard 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§

TargetRewriteError
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 all write declares, so the host can set the RLS tenant GUC to it (v0.4.20 — the raw-path analog of Insert::uniform_scope_value / Update::pinned_scope_value). Parses statement and:
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). keys and public are the project schema’s per-table tenant-key map and per-table lowered public-subset terms (exactly the two maps a TableKeys::PerTableTarget carries); tenant_value is the host-resolved target tenant B (NEVER guest input); dialect selects the parser. Returns the rewritten SQL text (the guest’s own positional params are untouched — B and the public literals are injected as escaped literals), or a TargetRewriteError (fail-closed — the read does not run).