fix(chart): require chart access for query_context-only updates - #40648
Conversation
✅ Deploy Preview for superset-docs-preview ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #40648 +/- ##
==========================================
- Coverage 64.12% 64.11% -0.01%
==========================================
Files 2654 2654
Lines 143723 143767 +44
Branches 33153 33161 +8
==========================================
+ Hits 92157 92180 +23
- Misses 49951 49971 +20
- Partials 1615 1616 +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:
|
There was a problem hiding this comment.
Pull request overview
This PR tightens object-level authorization for “query_context-only” chart updates by requiring chart access (via security_manager.raise_for_access(chart=...)) instead of skipping authorization entirely for that update shape. This closes a path where users with broad “can_write Chart” could overwrite query_context on charts they don’t own and shouldn’t be able to access.
Changes:
- Enforce
raise_for_access(chart=...)for query_context-only updates inUpdateChartCommand.validate(). - Add an integration test asserting a non-owner without datasource access is rejected with
ChartForbiddenError.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
superset/commands/chart/update.py |
Adds chart-access authorization enforcement on the query_context-only update path. |
tests/integration_tests/charts/commands_tests.py |
Adds a regression test covering the forbidden case for query_context-only updates. |
UpdateChartCommand skips the ownership check for "query_context-only" updates
(payload == {query_context, query_context_generation:true}) so report workers
and the UI's lazy query_context backfill can run as non-owners. But it skipped
ALL authorization, so any user with can_write on Chart could rewrite the
query_context of a chart they don't own (CWE-639).
Replace the unconditional skip with security_manager.raise_for_access(chart=...)
on that path. That still permits the legitimate non-owner flows (admins, owners,
and any user with access to the chart's datasource — which includes viewers who
can render the chart and the report executor), while rejecting users who cannot
access the chart at all.
DRAFT: needs CI / manual validation that the report-execution screenshot path
(executor user) passes raise_for_access in all configurations before merge.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…equires_chart_access Three @patch decorators but only two function parameters meant the mock for superset.commands.chart.update.g was never captured and its .user was never set. Added mock_u_g as the third parameter and set mock_u_g.user = gamma so all three g objects are consistent. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Rebased on master and updated the tests for the new raise_for_access on the query_context-only update path: - the unit test now mocks raise_for_access (so it no longer hits an unmocked g.user) and asserts access is enforced while ownership is relaxed; adds a forbidden-access case. - the integration test also accepts ChartNotFoundError, since a no-access user is filtered by the DAO access filter before the explicit check is reached. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
004df4b to
f915b46
Compare
|
Lifting the hold — the test failures are resolved (rebased on master; the unit tests now cover that a non-owner with chart access, i.e. the report-worker / Explore-backfill case, still succeeds, while a non-owner without access is rejected). The remaining |
Code Review Agent Run #c13a15Actionable 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 |
richardfogaca
left a comment
There was a problem hiding this comment.
Posting on Richard's behalf — this is his PR reviewer agent. Forward any pushback to him and he'll loop me back in.
The fix itself looks right — correct layer, correct primitive, and the error translation mirrors the existing pattern in the same method. The main thing worth addressing is that the new integration test doesn't actually prove the fix; the rest are smaller suggestions. All line numbers verified against HEAD f915b46.
Functional — worth a look before merge
-
tests/integration_tests/charts/commands_tests.py:483This test passes on master too, so it doesn't regression-test the vulnerability.
validate()loads the chart viaChartDAO.find_by_id(superset/commands/chart/update.py:119), which applies theChartFilterbase filter — gamma has no energy-datasource access, so the chart is never found and the command raisesChartNotFoundErroratupdate.py:121on both branches. Thepytest.raises((ChartForbiddenError, ChartNotFoundError))tuple accepts that, which the docstring acknowledges, but it means the newraise_for_accessbranch is likely never reached here.Could we patch
ChartDAO.find_by_idto return the chart (bypassing the base filter) and assertChartForbiddenErroronly? That would make the test fail on master and deterministically exercise the new check end-to-end. (The unit tests do pin the new behavior well — this is about the integration test earning its keep.) -
superset/commands/chart/update.py:144Related observation: since
ChartFilteralready blocks no-datasource-access users atfind_by_id, the new check is mostly defense-in-depth for callers that bypass the base filter — and the residual surface is that any non-owner with datasource access can still overwrite a sibling chart's storedquery_contextwith arbitrary content (nothing validates it references the chart's own datasource, and report executors later run the stored payload with their broader permissions). That gap predates this PR, so no need to solve it here — but would it be worth a follow-up issue to validatequery_context.datasourceagainst the chart'sdatasource_idon this path? Happy to keep as-is for this PR.
Re: the "WORTH NOTING" question in the PR body
We statically traced the report/alert path: the worker never calls UpdateChartCommand directly — when query_context is missing it takes a headless screenshot (superset/commands/report/execute.py:519-521) rendering Explore as the executor, and the frontend PUT fires with the executor's session. For the screenshot to produce a query_context at all, the executor must already pass raise_for_access(query_context=...) on the same datasource — so the new check should never be the binding constraint for a report that could succeed before. End-to-end validation is still prudent, but the static trace looks safe.
Other suggestions
-
tests/unit_tests/commands/chart/update_test.py:76Small suggestion:
raise_for_access.assert_called_once_with(chart=find_by_id.return_value)instead ofassert_called_once()— pins what gets authorized so a future refactor can't silently switch the resource. Totally optional. -
Few small nits, take or leave:
tests/integration_tests/charts/commands_tests.py:460— missing-> Noneon the new test method (the new unit tests have it).tests/integration_tests/charts/commands_tests.py:456-461— mock param names are swapped relative to the decorators (mock_gbindssuperset.utils.core.g,mock_u_gbindssuperset.commands.chart.update.g); harmless since all three get the same user, but confusing for the next reader.tests/integration_tests/charts/commands_tests.py:470—db.session.query(Slice).all()[0]is order-dependent; filtering byslice_namelike neighboring tests would avoid flakiness if another fixture's chart sorts first.tests/unit_tests/commands/chart/update_test.py:86-89— reusing_ownership_exc()as theraise_for_accessside effect labels an access failure as an ownership error; a tiny_access_exc()helper would keep the fixture honest.
Praise
The unit tests at tests/unit_tests/commands/chart/update_test.py:54-94 pin both halves of the contract — ownership still relaxed for the report-worker path (raise_for_ownership.assert_not_called()) and access newly enforced with the right error translation. That's exactly the pair of invariants a future refactor needs to trip over.
…ith access Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Patch ChartDAO.find_by_id in the integration test to bypass the ChartFilter base filter so the new raise_for_access gate is what denies; assert ChartForbiddenError only (fails on master). - Add -> None, fix mock param names, filter by slice_name. - Pin raise_for_access(chart=...) in unit tests; add _access_exc() helper so the access-denied case uses an access error, not an ownership error. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@richardfogaca Thanks for the thorough pass — addressed the actionable items in d796fc3:
On the |
Code Review Agent Run #7a9188Actionable 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 |
|
Opened the follow-up I promised for the |
richardfogaca
left a comment
There was a problem hiding this comment.
LGTM, great work 💯
Two CI integration failures (surfaced once the alembic head-fork was resolved and the test jobs actually ran): - VersioningFlaskPlugin.transaction_args recorded whatever get_user_id() returned, guarding only `is None`. A mocked `g` (master's test_query_context_update_requires_chart_access, apache#40648) yields a Mock g.user.id, which then hit the integer version_transaction.user_id column and blew up the flush with a SQL bind error. Guard on isinstance(int) — production-safe (user_id is always int/None there), fixes the test. - test_list_versions_denies_non_owner (chart/dashboard/dataset) asserted Alpha -> 403 on a "row-level ownership" theory the endpoint never implemented: it gates via raise_for_access, and Alpha has all_datasource_access + can_write (FR-013), so 200 is correct. Renamed to _allows_can_write_non_owner and assert 200; model-level denial stays covered by the Gamma tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two CI integration failures (surfaced once the alembic head-fork was resolved and the test jobs actually ran): - VersioningFlaskPlugin.transaction_args recorded whatever get_user_id() returned, guarding only `is None`. A mocked `g` (master's test_query_context_update_requires_chart_access, apache#40648) yields a Mock g.user.id, which then hit the integer version_transaction.user_id column and blew up the flush with a SQL bind error. Guard on isinstance(int) — production-safe (user_id is always int/None there), fixes the test. - test_list_versions_denies_non_owner (chart/dashboard/dataset) asserted Alpha -> 403 on a "row-level ownership" theory the endpoint never implemented: it gates via raise_for_access, and Alpha has all_datasource_access + can_write (FR-013), so 200 is correct. Renamed to _allows_can_write_non_owner and assert 200; model-level denial stays covered by the Gamma tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SUMMARY
UpdateChartCommand.validate()skips the ownership check for "query_context-only" updates — when the payload is exactly{query_context, query_context_generation: true}(is_query_context_update()). This bypass exists so report workers and the Explore UI's lazyquery_contextbackfill can run as non-owners.The problem: it skipped all authorization, gated only by the coarse
can_write on ChartFAB permission. So any authenticated user withcan_write on Chart(Gamma has it in many deployments) could PUT{query_context, query_context_generation: true}to any chart id and rewrite itsquery_context— a chart they don't own and may have no access to (CWE-639 / broken object-level authorization).APPROACH
Replace the unconditional skip with
security_manager.raise_for_access(chart=self._model)on the query_context path.raise_for_access(chart=...)permits admins, owners, and users with access to the chart's datasource, and rejects everyone else. That preserves the legitimate non-owner flows:…while closing the "any
can_write Chartholder can tamper with any chart" gap.WORTH NOTING:
The report-execution path renders the chart via a headless browser as the configured executor user, then that session hits the chart PUT. This change assumes the executor always passes
raise_for_access(chart=...). That's expected (the executor must be able to render the chart), but it depends on the executor configuration (THUMBNAIL_EXECUTE_AS/ report executor settings) and should be validated against the report/alert flow before merge.TESTING INSTRUCTIONS
test_query_context_update_requires_chart_access: a non-owner without datasource access (gamma) is rejected withChartForbiddenError.ADDITIONAL INFORMATION
🤖 Generated with Claude Code