Skip to content

fix(embedded): load guest charts with a BASE_AXIS x-axis - #42847

Draft
luizotavio32 wants to merge 2 commits into
apache:6.2from
luizotavio32:fix/guest-base-axis-x-axis-6.2
Draft

fix(embedded): load guest charts with a BASE_AXIS x-axis#42847
luizotavio32 wants to merge 2 commits into
apache:6.2from
luizotavio32:fix/guest-base-axis-x-axis-6.2

Conversation

@luizotavio32

@luizotavio32 luizotavio32 commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

SUMMARY

On 6.2, a guest loading a chart that has an x-axis gets a 403 — POST /api/v1/chart/data returns Guest user cannot modify chart payload and the chart renders as an error tile. The same chart works for Admin.

Before querying, normalizeTimeColumn (normalizeTimeColumn.ts) rewrites the chart's x-axis into a synthetic column, so the request carries:

{"columnType": "BASE_AXIS", "isColumnReference": true, "sqlExpression": "order_date",
 "label": "order_date", "expressionType": "SQL", "timeGrain": "P1D"}

while the chart stores the bare string "order_date". The anti-tamper guard query_context_modified() compares with freeze_value (exact json.dumps), and two independent mismatches each produce the 403:

  1. Shape — dict vs. string never compares equal.
  2. Location — the x-axis is stored under its own x_axis control, which the guard never reads. Its stored_values come only from params_dict["metrics" | "columns" | "groupby" | "orderby"], so even the un-synthesized string would not be found.

Fixing either alone changes nothing; both have to be addressed.

The fix

denormalize_base_axis_column() collapses a synthesized BASE_AXIS column back to the reference it stands for — a physical axis to its column name, an adhoc axis to the underlying adhoc column without the synthetic markers — and the stored x_axis control is read as an accepted column value.

Both apply to columns/groupby only. metrics and orderby keep exact comparison, so a BASE_AXIS marker cannot be smuggled onto a metric or a sort expression; those two comparisons are behaviorally identical to 6.2.

Scope note

This is a 6.2-targeted fix, not a backport of the #42150 chain. That chain reaches the same outcome via seven commits across three production files (~4,000 lines); of those, only the BASE_AXIS unwrap and the x_axis read are load-bearing for this 403. This PR is the minimal change: one production file, +104/−30 lines.

Deliberately excluded, none of which this 403 requires:

  • #39197 (user_view_menu_names guest branch) — on plain 6.2 this is what makes the guard run at all: guests have is_anonymous = False but no ab_user row, so ChartFilter hides the slice, query_context.slice_ is None, and the guard returns early. A deployment where the guard is dormant cannot be hitting this 403; the ones that are already bypass ChartFilter via all_datasource_access. It broadens guest permission resolution across every caller and wants its own PR.
  • #40979 native-filter target validation — independent hardening; replaces the chartless early-out and adds a Dashboard.json_metadata lookup inside the guard.
  • #37371 sort-by-visible-columns — the reason models/helpers.py and connectors/sqla/models.py appear in the chain, adding a new QueryObjectValidationError path for all users, not just guests.
  • granularity_sqla as a stored column source — normalizeTimeColumn only fires when x_axis is set (isXAxisSet), so it cannot produce this payload.
  • The except (ValueError, TypeError) widening in _validate_child_in_parent_multilayer.

Security posture

Safe by construction: a collapsed value must still be a member of the set stored on the chart, so tagging an unrelated column or free-form SQL as BASE_AXIS grants no additional access. The unwrap is narrow — columns only, sqlExpression must be a str, and the physical-reference branch requires isColumnReference.

Each of the six new tests was verified against a reverted production file: the three legitimate-load cases fail without the fix and pass with it; the three tamper vectors are rejected both before and after, as is the pre-existing random()-in-orderby test.

test asserts
..._physical_no_stored_context not modified — the reported bug
..._adhoc_no_stored_context not modified — adhoc x-axis
..._with_stored_context not modified — stored side collapses too
..._forged_column_reference modifiedBASE_AXIS tagging an unrelated column
..._forged_adhoc_expression modified — free-form SQL tagged BASE_AXIS
..._non_string_sql_expression modified — non-str sqlExpression
..._smuggled_into_metrics modified — marker on a metric

One note on affected charts

The bug is not limited to charts whose query_context is NULL. form_data is compared against params_dict before the stored query_context is consulted, so an already-normalized stored context cannot rescue the comparison — some Explore-saved charts are affected too. Re-saving in Explore can still mask the symptom, which is why it looks intermittent from one chart to the next.

BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF

TESTING INSTRUCTIONS

pytest tests/unit_tests/security/manager_test.py

End to end, on an embedded dashboard:

  1. Prove the guard is reachable first. Grant the guest role all_datasource_access, then replay POST /api/v1/chart/data with the guest token plus an extra column, or random() in orderby, and confirm 403. Without this the guard silently no-ops on 6.2 (see #39197 above) and every payload returns 200 — a green result that proves nothing.
  2. Find a chart with an x-axis: SELECT id FROM slices WHERE query_context IS NULL AND params LIKE '%x_axis%'. Do not re-save it in Explore first — that repopulates query_context and can hide the bug.
  3. Put it on a dashboard, enable embedding, load as a guest. Before: 403 with Guest user cannot modify chart payload. After: the chart renders.
  4. Re-run step 1's tamper vector — still 403.

ADDITIONAL INFORMATION

  • Has associated issue:
  • Required feature flags: EMBEDDED_SUPERSET
  • 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

A guest loading a chart that has an x-axis and no saved query_context gets a
403 (`Guest user cannot modify chart payload`) and the chart renders as an
error tile. The same chart works for Admin.

Before querying, `normalizeTimeColumn` rewrites the chart's x-axis into a
synthetic column, so the request carries
`{"columnType": "BASE_AXIS", "sqlExpression": "order_date", ...}` while the
chart stores `"order_date"` under its own `x_axis` control. Two independent
mismatches each produce the 403: the shapes differ (dict vs. string, never
equal), and the guard never reads the `x_axis` control at all.

Collapse a synthesized BASE_AXIS column back to the reference it stands for
before comparing, and read the stored `x_axis` as an accepted column value.
Both apply to `columns`/`groupby` only -- `metrics` and `orderby` keep exact
comparison, so a BASE_AXIS marker cannot be smuggled onto a metric or a sort
expression. The collapsed value must still match something stored on the
chart, so tagging an unrelated column or free-form SQL as BASE_AXIS grants no
additional access.

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

bito-code-review Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Bito Automatic Review Skipped - Branch Excluded

Bito didn't auto-review because the source or target branch is excluded from automatic reviews.
No action is needed if you didn't intend for the agent to review it. Otherwise, to manually trigger a review, type /review in a comment and save.
You can change the branch exclusion settings here, or contact your Bito workspace admin at evan@preset.io.

@netlify

netlify Bot commented Aug 6, 2026

Copy link
Copy Markdown

Deploy Preview for superset-docs-preview ready!

Name Link
🔨 Latest commit e113491
🔍 Latest deploy log https://app.netlify.com/projects/superset-docs-preview/deploys/6a7498f11404d100085bc60a
😎 Deploy Preview https://deploy-preview-42847--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.

@luizotavio32

Copy link
Copy Markdown
Contributor Author

/review

@bito-code-review

bito-code-review Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Code Review Agent Run #6a6ae9

Actionable Suggestions - 0
Review Details
  • Files reviewed - 2 · Commit Range: e113491..e113491
    • superset/security/manager.py
    • tests/unit_tests/security/manager_test.py
  • Files skipped - 0
  • 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

@codecov

codecov Bot commented Aug 6, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 50.00000% with 12 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (6.2@a812670). Learn more about missing BASE report.

Files with missing lines Patch % Lines
superset/security/manager.py 50.00% 8 Missing and 4 partials ⚠️
Additional details and impacted files
@@          Coverage Diff           @@
##             6.2   #42847   +/-   ##
======================================
  Coverage       ?   64.33%           
======================================
  Files          ?     2554           
  Lines          ?   132908           
  Branches       ?    30792           
======================================
  Hits           ?    85506           
  Misses         ?    45932           
  Partials       ?     1470           
Flag Coverage Δ
hive 39.88% <12.50%> (?)
mysql 60.40% <50.00%> (?)
postgres 60.47% <50.00%> (?)
presto 39.90% <12.50%> (?)
python 62.04% <50.00%> (?)
sqlite 60.11% <50.00%> (?)
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.

Verifying the previous commit end to end against a real embedded dashboard
surfaced a second rejection the unit tests missed. A timeseries chart with an
x-axis stores its remaining dimensions under `groupby` and leaves `columns`
unset, while `normalizeTimeColumn` sends them in `columns` together with the
synthesized axis. Comparing `columns` against only `params_dict["columns"]`
therefore still rejected the legitimate load, on the groupby dimension rather
than on the axis.

`columns` and `groupby` are already treated as equivalent when reading the
stored query context, for exactly this reason. Apply the same equivalence to
the stored params. The union only adds dimensions the chart already renders,
and an extra column is still rejected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants