/** * `oke.row_passes_policies` — synthetic-row RLS replay for CDC live queries. * * When a table row changes (CDC event), live-query fan-out must decide per * subscriber whether the OLD version of the row was visible (`beforeVisible`) * without heap access — after an UPDATE the heap holds only the after-image, * after a DELETE nothing remains. The subscriber's stamped connection * (`SET LOCAL ROLE oke_app` + `oke.*` GUCs) evaluates policies natively for * heap rows; this helper gives the SAME verdict for a JSONB payload by * replaying expressions straight from `pg_policies`. * * Parity contract (src/elements/store/rls-row-passes-policies.parity.test.ts): * replay(table, rowJson, "SELECT") === native stamped `WHERE pk = …` verdict * across every shipped policy family (owner / tenant / gate / scope) and their * real compositions, plus WITH CHECK probes via native INSERT attempts. * * Expression rewriting (inside the SQL function, per policy, per call): * 1. Bare column tokens (resolved from `pg_attribute` of the SAME table) are * first wrapped in a placeholder `<>`, then placeholders are * substituted with `(row_data->>'col')` once per column. Two-phase keeps * a single regexp_replace pass idempotent — a direct wrap re-matches on * every pass ((row_data->>'col')'s tail still sits between valid word * boundaries) and the loop never stabilizes. * 2. The replacement string is DOLLAR-QUOTED (`$re$\1<<\2>>\3$re$`), never * E''-quoted: plpgsql folds `E'\1'` at body-compile time into chr(1), * silently corrupting backreferences. * 3. `<`/`>` are excluded from both boundary classes so `<>` cannot * re-match; single-quoted literals are protected because `'` is excluded * from boundaries too — `'owner'` never trips a column named `owner`. * 4. `oke.*` helpers survive: their segments follow a `.` (excluded left * boundary). pg_policies stores them qualification-stripped * (`"user"()`); `"user"` is not a column token so it passes through and * resolves via search_path to oke.user() exactly as it does natively. * 5. Keyword-shaped tokens are skipped; placeholder substitution uses plain * replace() so `id` never corrupts an already-substituted accessor. * * Semantics mirror native RLS: * - `TO ` applicability: a policy applies only when the current user is * a member of one of its roles (`public`/`current_user` always; membership * via `pg_has_role`). The stamp runs as `oke_app`, so internal policies * granted to non-member roles are invisible to it natively — the replay * skips them identically (TO-role regression gate: F4-style cases). * - `ALL` policies apply to every command; others match their own. * - Clause selection per command mirrors the native backend: * SELECT/DELETE evaluate USING (qual); INSERT evaluates WITH CHECK only; * UPDATE must satisfy both (missing clause falls back to its sibling). * - PERMISSIVE OR together, RESTRICTIVE AND; zero applicable policies on an * RLS-enabled table denies everything. */ /** One statement per exec — drivers reject multi-command batches on `query`. */ export const ROW_PASSES_POLICIES_STATEMENTS: readonly string[] = [ String.raw`CREATE OR REPLACE FUNCTION oke.pol_applicable_to_role( p_roles text[] ) RETURNS boolean LANGUAGE sql STABLE AS $fn$ SELECT EXISTS ( SELECT 1 FROM unnest(p_roles) AS r(role) WHERE r.role = 'public' OR r.role = 'current_user' OR r.role = 'session_user' OR pg_has_role(current_user, r.role, 'MEMBER') ) $fn$`, String.raw`CREATE OR REPLACE FUNCTION oke.row_passes_policies( p_table text, p_row jsonb, p_command text ) RETURNS boolean LANGUAGE plpgsql STABLE AS $fn$ DECLARE v_oid oid; v_rls boolean := false; pol record; col record; expr text; next_text text; n int; acc text; verdict boolean; permissive_pass boolean := false; has_permissive boolean := false; restrictive_pass boolean := true; has_restrictive boolean := false; BEGIN SELECT c.oid, c.relrowsecurity INTO v_oid, v_rls FROM pg_class c JOIN pg_namespace nsp ON nsp.oid = c.relnamespace WHERE c.relname = split_part(p_table, '.', 2) OR c.relname = p_table ORDER BY (nsp.nspname = 'public') DESC LIMIT 1; IF v_oid IS NULL OR NOT COALESCE(v_rls, false) THEN RETURN true; -- unknown table or RLS off: everything visible (native parity) END IF; FOR pol IN SELECT policyname, permissive, cmd, roles, qual, with_check FROM pg_policies WHERE tablename = split_part(p_table, '.', 2) OR tablename = p_table LOOP -- Native TO-role semantics: a policy only applies when the current user is -- a member of (one of) its roles. oke_app (the stamp role) is not normally -- a member of custom policy roles, so such policies must be skipped — -- otherwise an internal TO-restricted PERMISSIVE policy would wrongly widen -- per-user visibility through the replay path. IF NOT pol_applicable_to_role(pol.roles) THEN CONTINUE; END IF; IF pol.cmd <> 'ALL' AND upper(p_command) <> pol.cmd THEN CONTINUE; END IF; -- Command-clause selection mirrors each native command's evaluation: -- SELECT/DELETE -> USING (qual) only -- UPDATE -> USING + WITH CHECK must both pass (native treats a -- missing clause as "same as the other") -- INSERT -> WITH CHECK only (qual does not gate new rows) -- Zero applicable policies on an RLS-enabled table still denies all. expr := CASE WHEN upper(p_command) = 'INSERT' THEN COALESCE(pol.with_check, pol.qual) ELSE COALESCE(pol.qual, pol.with_check) END; CONTINUE WHEN expr IS NULL; FOR col IN SELECT a.attname AS name FROM pg_attribute a WHERE a.attrelid = v_oid AND a.attnum > 0 AND NOT a.attisdropped ORDER BY a.attnum LOOP IF lower(col.name) IN ( 'and','or','not','is','in','like','ilike','between','exists', 'case','when','then','else','end','true','false','null' ) THEN CONTINUE; END IF; -- Phase 1: wrap bare tokens as <>. '<'/'>' sit outside the word -- boundary classes, so the substituted form can never re-match and the -- stabilization pass count is bounded by pathological overlaps only. n := 0; LOOP n := n + 1; IF n > 100 THEN RAISE EXCEPTION 'row_passes_policies rewrite did not stabilize (%)', pol.policyname; END IF; next_text := replace( regexp_replace( expr, '(^|[^A-Za-z0-9_''.<>])(' || col.name || ')($|[^A-Za-z0-9''<>])', $re$\1<<\2>>\3$re$, 'g' ), chr(1) || '<<' || col.name || '>>' || chr(1), '<<' || col.name || '>>' ); EXIT WHEN next_text = expr; expr := next_text; END LOOP; -- Phase 2: placeholders -> jsonb accessor (plain replace, order-safe). acc := '(row_data->>''' || col.name || ''')'; expr := replace(expr, '<<' || col.name || '>>', acc); END LOOP; BEGIN EXECUTE format('SELECT (%s) FROM (SELECT %L::jsonb AS row_data) AS t', expr, p_row::text) INTO verdict; EXCEPTION WHEN OTHERS THEN RAISE EXCEPTION 'row_passes_policies replay failed (%): %', pol.policyname, SQLERRM; END; IF pol.permissive = 'PERMISSIVE' THEN has_permissive := true; IF verdict THEN permissive_pass := true; END IF; ELSE has_restrictive := true; IF NOT verdict THEN restrictive_pass := false; END IF; END IF; END LOOP; IF NOT has_permissive AND NOT has_restrictive THEN RETURN false; -- RLS enabled, zero applicable policies: Postgres denies all END IF; RETURN (NOT has_restrictive OR restrictive_pass) AND (NOT has_permissive OR permissive_pass); END $fn$`, ]; /** * Probe for one replay evaluation. * * Prefer `sql`/`params` with drivers that bind JSON as an explicit type; * Bun.SQL binds JS strings as unknown, so `$2::jsonb` yields a jsonb * string scalar and `->>` lookups return NULL (live-PG parity failure). * {@link buildInlineRowPassesSql} inlines the image safely instead. * * @param table - Table (bare name or `schema.table`) * @param row - Synthetic row image (JSON-serializable) * @param command - Command context (`SELECT` = visibility) */ export interface RowPassesProbe { readonly sql: string; readonly params: [string, string, string]; } /** Escape a value for inline SQL literal embedding. */ function escapeSqlLiteral(value: string): string { return `'${value.replaceAll("'", "''")}'`; } /** * Inline-literal probe SQL — portable across pg drivers regardless of how * each binds JS strings. Row payload and table name are escaped; command * comes from a closed union. * * @param table - Table (bare name or `schema.table`) * @param row - Synthetic row image * @param command - Command context */ export function buildInlineRowPassesSql( table: string, row: Readonly>, command: "ALL" | "SELECT" | "INSERT" | "UPDATE" | "DELETE" = "SELECT", ): string { const tableArg = escapeSqlLiteral(table); const rowArg = `${escapeSqlLiteral(JSON.stringify(row))}::jsonb`; return `SELECT oke.row_passes_policies(${tableArg}, ${rowArg}, ${escapeSqlLiteral(command)}) AS ok`; } /** Build a single replay probe (runs under the caller's RLS stamp). */ export function buildRowPassesProbe( table: string, row: Readonly>, command: "ALL" | "SELECT" | "INSERT" | "UPDATE" | "DELETE" = "SELECT", ): RowPassesProbe { return { sql: "SELECT oke.row_passes_policies(?, ?, ?) AS ok", params: [table, JSON.stringify(row), command], }; }