Repository navigation
fix(security): apply global guest RLS rules to sub-queries in virtual datasets - #44650
Conversation
apply_rls skipped global (unscoped) guest-token RLS rules for every table it rewrote. That is right for a virtual dataset's inner SQL, because the outer query already applies those rules, but an adhoc sub-query is not constrained by the outer WHERE clause. With ALLOW_ADHOC_SUBQUERY enabled, a sub-query in an adhoc metric or column therefore ran without the guest's clause-only rules. apply_rls and get_predicates_for_table take an include_global_guest_rls flag, defaulting to the old behaviour, and validate_adhoc_subquery turns it on. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Only a virtual dataset's inner SQL needs global guest RLS rules left out, since its outer query applies them. Make inclusion the default in apply_rls and get_predicates_for_table and have the virtual dataset paths (inner SQL rewrite, fail-closed check and cache key) opt out, so SQL Lab and any new caller stay scoped to the guest token. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… datasets A virtual dataset's inner SQL left global (unscoped) guest RLS rules to the outer query, which only filters the rows the inner SQL returns. A table read inside a sub-query there, such as a scalar COUNT(*) over another table, was therefore not scoped to the guest token. SQLStatement.apply_rls takes a separate predicate set for reads inside a SUBQUERY scope (scalar, IN, EXISTS), scopes nested in one and CTEs they read. utils.apply_rls passes the global guest rules in that set when it leaves them to the outer query, so FROM, join, derived table and LATERAL reads still avoid double application. apply_rls reports whether a rule was actually injected, so an untouched statement isn't re-rendered, and the fail-closed check in get_from_clause counts the sub-query rules it would have applied. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Code Review Agent Run #bc57cfActionable Suggestions - 0Additional Suggestions - 8
Filtered by Review RulesBito filtered these suggestions based on rules created automatically for your feedback. Manage rules.
Review Details
Bito Usage GuideCommands Type the following command in the pull request comment and save the comment.
Refer to the documentation for additional commands. Configuration This repository uses Documentation & Help |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #44650 +/- ##
=======================================
Coverage 81.22% 81.22%
=======================================
Files 2968 2968
Lines 179363 179399 +36
Branches 41597 41601 +4
=======================================
+ Hits 145681 145713 +32
- Misses 30971 30976 +5
+ Partials 2711 2710 -1
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
# Conflicts: # UPDATING.md # superset/models/helpers.py # superset/utils/rls.py # tests/unit_tests/models/test_virtual_dataset_format.py # tests/unit_tests/security/guest_rls_test.py
- Keep calling the RLS transformer for every read and derive the applied flag from get_predicate(), so the transformers' own no-predicate paths stay reachable and covered. - Only look up the second, global-guest predicate set for a guest user; other users have no global guest rules, and has_subquery() is also true for plain derived tables and UNIONs. - Cover a top-level UNION and a nested sub-query in the parse tests, and a non-guest user doing a single lookup per table. - Reword the fail-closed comment in get_from_clause. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Yes, the behavior described is expected and is explicitly noted in the updated documentation within the PR. The PR changes how global guest RLS rules are injected: they are now applied to tables read inside sub-queries (such as scalar sub-queries, As noted in the updated UPDATING.md |
…e outer filter A correlated sub-query, such as a lookup keyed by lookup.id = a.lid, is tied to the virtual dataset's own rows like a join, so global guest RLS rules are left to the outer query there, the same as for joins. A lookup table without the rule's column keeps working. Uncorrelated sub-queries, which no outer filter constrains, still get the rules. Correlation is detected from columns qualified with an enclosing table's name or alias. sqlglot's Scope.is_correlated_subquery isn't used because it treats every unqualified column as external; here an unqualified column counts as local, erring toward applying the rules. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
✅ Deploy Preview for superset-docs-preview ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for superset-docs-preview ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for superset-docs-preview ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
Code Review Agent Run #1454b2Actionable Suggestions - 0Review Details
Bito Usage GuideCommands Type the following command in the pull request comment and save the comment.
Refer to the documentation for additional commands. Configuration This repository uses Documentation & Help |
Address review on the virtual-dataset sub-query RLS change: - _is_correlated only looks at a sub-query's own columns. Scope.columns also carries the outer references of nested sub-queries, so an uncorrelated sub-query wrapping a correlated one lost the rules. - Sub-query reads no longer exclude the virtual dataset's own RLS, since the outer query doesn't constrain them either. The second lookup only runs for non-guests when the dataset has RLS for the current user, and the fail-closed check matches. - Reword the correlated sub-query docstring: it is skipped to keep lookups without the rule's column working, not because the outer filter scopes it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ing case A sub-query's Scope.sources holds every CTE in lexical scope, not only the ones it reads. That made a sub-query correlated to an outer CTE by name look uncorrelated, and pulled the stricter rules into CTEs only joined in the main FROM. Use selected_sources for both the correlation check and the CTEs a sub-query reads. Compare the names ignoring letter-case, so an alias such as `B` shadowing an outer `b` on a folding engine counts as the sub-query's own table and keeps the rules, instead of reading as correlated and skipping them. Also pin exclude_dataset_id in the fail-closed tests, with a real dataset id so a regression back to self_id fails. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Code Review Agent Run #a4efbcActionable Suggestions - 0Additional Suggestions - 1
Review Details
Bito Usage GuideCommands Type the following command in the pull request comment and save the comment.
Refer to the documentation for additional commands. Configuration This repository uses Documentation & Help |
alexandrusoare
left a comment
There was a problem hiding this comment.
Only one comment and we are good
|
Bito Automatic Review Skipped – PR Already Merged |
SUMMARY
Follow-up to #44645, from @msyavuz's review there. Stacked on #44645: until that merges, this diff also shows its three commits, so only d164260 is new here.
When a guest queries a virtual dataset, global guest RLS rules (no
datasetkey) are left out of the inner SQL and applied once, on the outer query (#37395, for #37359). The outer filter only narrows the rows the inner SQL returns, though. A table read inside a sub-query of the virtual SQL isn't constrained by it, so inwith a global
org_id = 1,ncounted every org's rows inb.The fix separates the two kinds of table read:
SQLStatement.apply_rlstakes an optionalsubquery_predicatesset. It's used for reads inside a sqlglotSUBQUERYscope (scalar,IN,EXISTS), for any scope nested inside one, and for CTEs such a scope reads from (plus their nested scopes). Every other read keeps usingpredicates. ALATERAL/CROSS APPLYbody is also typed as aSUBQUERYscope, but its rows reach the output like a join's, so it's left out.utils.apply_rlsleaves global guest rules out ofpredicatesas before, but includes them insubquery_predicateswhen the statement has a sub-query.FROM, join, derived table, CTE andLATERALreads still don't get the rule twice.SQLStatement.apply_rlsreturns whether it actually injected a rule, andutils.apply_rlsreturns that. This way an untouched statement isn't re-rendered through sqlglot, which is the round trip therls_appliedguard exists to avoid.get_from_clause, which runs when injecting RLS raised, counts global guest rules for statements with a sub-query, matching whatapply_rlswould have injected.Scope decision: joins are left to the outer filter as before. Applying global rules to joined tables would also close the join case, but it would break every virtual dataset that joins a registered lookup table without the rule's column. This PR only covers sub-queries, whose rows never reach the outer filter at all.
BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF
N/A, backend only.
TESTING INSTRUCTIONS
test_rls_subquery_predicates(parse tests) covers which reads get which predicates. Cases: plain read, join, derived table, CTE inFROM, scalar /IN/EXISTSsub-queries, the same table both inFROMand in a sub-query, a derived table inside a sub-query, and a CTE read only from a sub-query. There's also anAS_SUBQUERYvariant, aLATERAL/CROSS APPLYcase, and a test for the new return value.test_global_guest_rule_applied_to_virtual_dataset_subqueryruns the example above throughapply_rlsthe wayget_from_clausedoes. It fails on fix(security): apply global guest RLS rules inside adhoc sub-queries #44645's head and passes here.test_global_guest_rule_left_to_outer_query_without_subquerychecks that the join case keeps its single application.test_get_from_clause_fail_closed_counts_global_guest_rls_in_subqueriescovers the fail-closed check.Manual check: with
EMBEDDED_SUPERSETon, create a virtual dataset like the one above, embed a chart on it, and request its data with a guest token carrying a clause-only rule. The generated SQL (result_type: "query") has the clause inside the scalar sub-query, and still only once fora, on the outer query.ADDITIONAL INFORMATION
EMBEDDED_SUPERSETThe UPDATING.md entry added in #44645 is updated to describe the virtual dataset behaviour.
🤖 Generated with Claude Code