Skip to content

fix(rls): handle same-named CTEs and quoted aliases in the SQL rewrite - #43005

Open
onheap wants to merge 1 commit into
apache:masterfrom
onheap:rls-upstream
Open

fix(rls): handle same-named CTEs and quoted aliases in the SQL rewrite#43005
onheap wants to merge 1 commit into
apache:masterfrom
onheap:rls-upstream

Conversation

@onheap

@onheap onheap commented Aug 10, 2026

Copy link
Copy Markdown

SUMMARY

Superset enforces row-level security on raw SQL by rewriting the query: every table it reads is wrapped in a subquery that filters by the user's row predicate. First it lists the tables a statement reads, with extract_tables_from_statement(). That list drives both the RLS predicate and the dataset access check. Then it rewrites each read. This PR tidies up three edge cases in that path.

1. A read whose name matches a CTE is treated as the CTE. To tell a CTE reference from a real table, extraction compares the reference's bare name to the CTE names in scope. A real table read that shares a name with a CTE is therefore misclassified as a CTE reference. It is omitted from the statement’s table set, so neither an RLS predicate nor an access check is applied. This happens for a schema- or catalog-qualified read, a non-recursive CTE's own name inside its body, and a forward reference to a later WITH item:

WITH orders AS (SELECT 1 AS d)
SELECT *
FROM (SELECT * FROM public.orders) AS z   -- a real read of public.orders; matches CTE name `orders`

2. The subquery rewrite picks the nodes to wrap by name. The AS_SUBQUERY rewrite walks the tree and wraps every table node whose name matches a RLS rule. A CTE reference with the same name as the rule's table matches too, so it also gets wrapped and the predicate is applied to it. If the CTE does not select the predicate's column, the database cannot resolve the query:

-- rule on orders: tenant = 'A'
WITH orders AS (SELECT id FROM orders)   -- inner orders: the real read
SELECT * FROM orders                     -- outer orders: the CTE; matching by name also wraps this one

3. Aliases pass through the rewrite as strings. Both RLS transformers read the alias from node.alias. That is a string with the quoting removed, and they pass it back as the alias. So a quoted alias loses its quotes, and a column-list alias keeps only its name:

-- input
SELECT * FROM tbl_a AS "a b"
-- rewrite output: the quotes are gone, so `a b` is no longer a single identifier
SELECT * FROM (SELECT * FROM tbl_a WHERE id = 42) AS a b
-- input
SELECT c1 FROM tbl_a AS x (c1, c2)
-- rewrite output: the (c1, c2) column list is dropped, so c1 resolves against the table
SELECT c1 FROM (SELECT * FROM tbl_a WHERE id = 42) AS x

BEFORE / AFTER

CTE reference vs. real tableis_cte() resolved by bare name (before) vs. through the scope (after):

WITH orders AS (SELECT 1 AS d) SELECT * FROM (SELECT * FROM public.orders) AS z
-- before: the read of public.orders is treated as the CTE `orders` and is not listed
-- after:  the read of public.orders is listed, filtered, and access-checked

Subquery rewrite of a same-named CTE (rule on orders):

-- input
WITH orders AS (SELECT id FROM orders) SELECT * FROM orders

-- before: both reads matched by name; the outer CTE reference is wrapped too, and
--         the database cannot resolve `tenant` against a projection of just `id`
-- after:  only the real read inside the CTE body is wrapped; the CTE reference is left as is
WITH orders AS (
  SELECT id FROM (SELECT * FROM orders WHERE tenant = 'A') AS orders
)
SELECT * FROM orders

Alias handling:

-- input
SELECT * FROM tbl_a AS "a b"
-- before: SELECT * FROM (SELECT * FROM tbl_a WHERE id = 42) AS a b     -- quotes dropped
-- after:  SELECT * FROM (SELECT * FROM tbl_a WHERE id = 42) AS "a b"

-- input
SELECT c1 FROM tbl_a AS x (c1, c2)
-- before: SELECT c1 FROM (SELECT * FROM tbl_a WHERE id = 42) AS x          -- (c1, c2) dropped
-- after:  SELECT c1 FROM (SELECT * FROM tbl_a WHERE id = 42) AS x(c1, c2)

TESTING INSTRUCTIONS

Unit tests are added in tests/unit_tests/sql/parse_tests.py covering extraction (CTE-vs-table shapes), both rewrite methods (quoted/column-list aliases, a same-named CTE, correlated LATERAL, a read inside a DML subquery), and a filtered-set invariant that checks each real read is wrapped exactly once.

pytest tests/unit_tests/sql/ --cov=superset/sql/ --cov-fail-under=100

ADDITIONAL INFORMATION

  • Has associated issue:
  • Required feature flags:
  • Changes UI
  • Includes DB Migration (follow approval process in SIP-59)
    • Migration is atomic, supports rollback & is backwards-compatible
    • Confirm DB migration upgrade and downgrade tested
    • Runtime estimates and downtime expectations provided
  • Introduces new feature or API
  • Removes existing feature or API

@bito-code-review

bito-code-review Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Code Review Agent Run #562bee

Actionable Suggestions - 0
Review Details
  • Files reviewed - 2 · Commit Range: e6f93c6..e6f93c6
    • superset/sql/parse.py
    • tests/unit_tests/sql/parse_tests.py
  • Files skipped - 1
    • UPDATING.md - Reason: Filter setting
  • Tools
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ Successful
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers a full AI review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

@netlify

netlify Bot commented Aug 10, 2026

Copy link
Copy Markdown

Deploy Preview for superset-docs-preview ready!

Name Link
🔨 Latest commit 1bb5903
🔍 Latest deploy log https://app.netlify.com/projects/superset-docs-preview/deploys/6a7a1f4a7597880008253066
😎 Deploy Preview https://deploy-preview-43005--superset-docs-preview.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@dosubot dosubot Bot added the authentication:row-level-security Related to Row Level Security label Aug 10, 2026
@codecov

codecov Bot commented Aug 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 80.95238% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.59%. Comparing base (bc85f1e) to head (3e72252).
⚠️ Report is 8 commits behind head on master.

Files with missing lines Patch % Lines
superset/sql/parse.py 80.95% 3 Missing and 1 partial ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##           master   #43005   +/-   ##
=======================================
  Coverage   66.59%   66.59%           
=======================================
  Files        2863     2863           
  Lines      161701   161729   +28     
  Branches    37254    37261    +7     
=======================================
+ Hits       107686   107711   +25     
+ Misses      51973    51972    -1     
- Partials     2042     2046    +4     
Flag Coverage Δ
hive 38.20% <23.80%> (-0.01%) ⬇️
mysql 57.77% <80.95%> (+0.01%) ⬆️
postgres 57.83% <80.95%> (+0.01%) ⬆️
presto 40.16% <23.80%> (-0.01%) ⬇️
python 59.21% <80.95%> (+0.01%) ⬆️
sqlite 57.44% <80.95%> (+0.01%) ⬆️
unit 100.00% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Three fixes on the raw-SQL row-level-security path:

- Resolve CTE references through ``Scope.cte_sources`` in
  ``extract_tables_from_statement()`` rather than comparing bare names, so a
  real table read that shares a CTE's name -- a schema/catalog-qualified read,
  a non-recursive CTE's own name inside its body, or a forward reference to a
  later ``WITH`` item -- is reported and receives its RLS predicate and access
  check.
- Rewrite the ``AS_SUBQUERY`` method to wrap the real reads enumerated by that
  same walk, in place, instead of matching table nodes by name. A CTE reference
  that shares a rule table's name is left untouched; reads are de-duplicated by
  identity and wrapped deepest-first.
- Carry the parsed table alias through both RLS transformers instead of the
  quoting-stripped ``node.alias`` string, so a quoted alias keeps its quoting
  and a column-list alias is preserved.

Add unit tests for extraction, both rewrite methods, and a filtered-set
invariant; document the behavior change in UPDATING.md.

@bito-code-review bito-code-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Agent Run #c2ae3c

Actionable Suggestions - 1
  • tests/unit_tests/sql/parse_tests.py - 1
Review Details
  • Files reviewed - 2 · Commit Range: 3e72252..3e72252
    • superset/sql/parse.py
    • tests/unit_tests/sql/parse_tests.py
  • Files skipped - 1
    • UPDATING.md - Reason: Filter setting
  • Tools
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ Successful
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers a full AI review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo



@pytest.mark.parametrize(
"sql, read_counts",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type mismatch in parametrize decorator

The rules parameter in test_rls_subquery_transformer uses dict[Table, str] but the test passes dict[Table, list[exp.Node]] at line 3033. Multiple similar type mismatches likely exist in parameterized tests (rows 3000-3013).

Code Review Run #c2ae3c


Should Bito avoid suggestions like this for future reviews? (Manage Rules)

  • Yes, avoid them

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

authentication:row-level-security Related to Row Level Security size/L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant