Skip to content

fix(rls): preserve tables/roles on partial RLS rule updates#41294

Open
rusackas wants to merge 1 commit into
masterfrom
fix/rls-partial-update-preserve-bindings
Open

fix(rls): preserve tables/roles on partial RLS rule updates#41294
rusackas wants to merge 1 commit into
masterfrom
fix/rls-partial-update-preserve-bindings

Conversation

@rusackas

Copy link
Copy Markdown
Member

SUMMARY

UpdateRLSRuleCommand read tables/roles from the request body defaulting to [], and unconditionally wrote them back during validate(). As a result, a partial PUT that omitted those fields (e.g. {"name": "new name"}) replaced the rule's table and role bindings with empty lists — silently changing which rows the RLS filter constrains.

This makes partial updates behave as expected:

  • validate() now only resolves and overwrites the relationships that are actually present in the request body. Omitted tables/roles are left untouched.
  • RLSPutSchema.tables gains Length(1), mirroring RLSPostSchema, so tables cannot be set to an empty list on update either.

roles keeps no minimum-length constraint, matching the create schema (base filters may legitimately specify zero excluded roles).

BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF

N/A — backend command/schema behavior.

TESTING INSTRUCTIONS

Unit tests added in tests/unit_tests/commands/security/rls_test.py:

  • A partial update omitting tables/roles does not resolve or write those keys (existing bindings preserved).
  • An update touching only roles does not resolve or overwrite tables.

Run: pytest tests/unit_tests/commands/security/rls_test.py

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

Note: PUT /api/v1/rowlevelsecurity/<pk> with an explicit empty tables list is now rejected (must contain at least one table), consistent with create.

UpdateRLSRuleCommand defaulted missing tables/roles to [] and
unconditionally wrote them back during validate(), so a partial PUT (e.g.
{"name": "new name"}) replaced the rule's table and role bindings with empty
lists — silently changing which rows the filter constrains.

Only resolve and overwrite the relationships that are actually present in the
request body, leaving omitted bindings untouched. Also mirror the create-path
invariant by adding Length(1) to the tables field in RLSPutSchema, so tables
cannot be set to an empty list on update.

Adds unit tests covering partial updates that omit tables/roles and updates
that touch only one of the two relationships.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@bito-code-review

bito-code-review Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Code Review Agent Run #377065

Actionable Suggestions - 0
Additional Suggestions - 1
  • superset/row_level_security/schemas.py - 1
    • Inconsistent list field validation · Line 172-172
      The `tables` field in `RLSPutSchema` now requires a non-empty list (line 172), but the semantically equivalent `roles` field (line 174) lacks this validation. For consistency, apply the same `validate=Length(1)` constraint to `roles` in both `RLSPutSchema` and `RLSPostSchema`.
Review Details
  • Files reviewed - 3 · Commit Range: 2470dfb..2470dfb
    • superset/commands/security/update.py
    • superset/row_level_security/schemas.py
    • tests/unit_tests/commands/security/rls_test.py
  • Files skipped - 0
  • Tools
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ 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

@dosubot dosubot Bot added api Related to the REST API authentication:row-level-security Related to Row Level Security labels Jun 23, 2026
Comment on lines +59 to +68
if "tables" in self._properties:
tables = (
db.session.query(SqlaTable)
.filter(SqlaTable.id.in_(self._tables)) # type: ignore[attr-defined]
.all()
)
if len(tables) != len(self._tables):
raise DatasourceNotFoundValidationError()
raise_for_datasource_access(tables)
self._properties["tables"] = tables

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.

Suggestion: The datasource access check is now gated behind "tables" in self._properties, so a partial update that omits tables skips authorization against the rule's existing table bindings. This lets a caller update sensitive fields (like clause/roles) on an RLS rule tied to datasources they cannot access. Keep partial-update behavior, but enforce access on the effective table set: use submitted tables when present, otherwise validate against self._model.tables. [security]

Severity Level: Critical 🚨
- ❌ PUT /api/v1/rowlevelsecurity/<pk> skips table access check.
- ❌ Non-authorized users can modify RLS clause/roles bindings.
- ⚠️ Weakens dataset isolation enforced via row level security.
Steps of Reproduction ✅
1. The RLS REST API update endpoint `PUT /api/v1/rowlevelsecurity/<pk>` is implemented in
`superset/row_level_security/api.py:181-241` as `RLSRestApi.put`, which loads the request
body with `RLSPutSchema` (`edit_model_schema.load(request.json)` at line 235) and then
calls `UpdateRLSRuleCommand(pk, item).run()` at line 240.

2. `RLSPutSchema` in `superset/row_level_security/schemas.py:147-187` defines `tables` as
an optional field (`required=False` at lines 167-172), so a payload like `{"clause":
"1=1"}` is valid and will be passed to `UpdateRLSRuleCommand` without a `"tables"` key.

3. `UpdateRLSRuleCommand.validate` in `superset/commands/security/update.py:49-68` loads
the existing rule via `RLSDAO.find_by_id` (lines 50-52) and then only performs the
datasource lookup and access check when `"tables" in self._properties` (line 59). If
`"tables"` is absent from the payload, the block at lines 59-68 (including
`raise_for_datasource_access(tables)` at line 67) is completely skipped, and
`self._model.tables` is never checked.

4. `raise_for_datasource_access` in `superset/commands/security/utils.py:29-42` enforces
that `security_manager.can_access_datasource(datasource=table)` is True for every
referenced table, otherwise raising `RLSDatasourceForbiddenError`, which `RLSRestApi.put`
would convert into HTTP 403 (handling at `superset/row_level_security/api.py:258-264`).
Because `UpdateRLSRuleCommand.validate` never calls `raise_for_datasource_access` when
`tables` is omitted, a caller who can hit `PUT /api/v1/rowlevelsecurity/<pk>` but for whom
`can_access_datasource` would return False for one or more of `self._model.tables` can
still successfully send a partial update payload without `"tables"` (e.g. `{"clause":
"new_clause", "roles": [...]}`), and the request will return 200 without any datasource
authorization being enforced against the rule's existing table bindings.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset/commands/security/update.py
**Line:** 59:68
**Comment:**
	*Security: The datasource access check is now gated behind `"tables" in self._properties`, so a partial update that omits `tables` skips authorization against the rule's existing table bindings. This lets a caller update sensitive fields (like `clause`/`roles`) on an RLS rule tied to datasources they cannot access. Keep partial-update behavior, but enforce access on the effective table set: use submitted tables when present, otherwise validate against `self._model.tables`.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

@bito-code-review

Copy link
Copy Markdown
Contributor

The flagged security issue is correct. When performing a partial update that omits the tables field, the current implementation skips the datasource access check entirely, potentially allowing unauthorized modifications to RLS rules tied to restricted datasources.

To resolve this, you should ensure that even if tables is omitted from the request, the command validates the existing tables associated with the rule (self._model.tables) against the user's permissions.

Here is a concise fix for superset/commands/security/update.py:

        if "tables" in self._properties:
            tables = (
                db.session.query(SqlaTable)
                .filter(SqlaTable.id.in_(self._tables))  # type: ignore[attr-defined]
                .all()
            )
            if len(tables) != len(self._tables):
                raise DatasourceNotFoundValidationError()
            raise_for_datasource_access(tables)
            self._properties["tables"] = tables
        else:
            # Always validate access to existing tables if not updating them
            raise_for_datasource_access(self._model.tables)

There are no other comments in the provided PR review data to address.

superset/commands/security/update.py

if "tables" in self._properties:
            tables = (
                db.session.query(SqlaTable)
                .filter(SqlaTable.id.in_(self._tables))  # type: ignore[attr-defined]
                .all()
            )
            if len(tables) != len(self._tables):
                raise DatasourceNotFoundValidationError()
            raise_for_datasource_access(tables)
            self._properties["tables"] = tables
        else:
            # Always validate access to existing tables if not updating them
            raise_for_datasource_access(self._model.tables)

@codecov

codecov Bot commented Jun 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 75.00000% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 64.34%. Comparing base (3b46a5f) to head (2470dfb).

Files with missing lines Patch % Lines
superset/commands/security/update.py 75.00% 0 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #41294      +/-   ##
==========================================
- Coverage   64.34%   64.34%   -0.01%     
==========================================
  Files        2653     2653              
  Lines      144952   144953       +1     
  Branches    33433    33435       +2     
==========================================
- Hits        93272    93266       -6     
- Misses      49996    49999       +3     
- Partials     1684     1688       +4     
Flag Coverage Δ
hive 39.27% <0.00%> (-0.01%) ⬇️
mysql 58.00% <75.00%> (-0.01%) ⬇️
postgres 58.06% <75.00%> (-0.01%) ⬇️
presto 40.86% <0.00%> (-0.01%) ⬇️
python 59.50% <75.00%> (-0.01%) ⬇️
sqlite 57.71% <75.00%> (-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.

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

Labels

api Related to the REST API asvs authentication:row-level-security Related to Row Level Security size/M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants