-
Notifications
You must be signed in to change notification settings - Fork 17.4k
fix: Drill to Detail for Embedded #39214
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -725,27 +725,36 @@ def _validate_child_in_parent_multilayer( | |||||||||||||||||
| except (json.JSONDecodeError, TypeError): | ||||||||||||||||||
| return False | ||||||||||||||||||
|
|
||||||||||||||||||
| def has_drill_by_access( | ||||||||||||||||||
| def has_drill_access( | ||||||||||||||||||
| self, | ||||||||||||||||||
| form_data: dict[str, Any], | ||||||||||||||||||
| dashboard: "Dashboard", | ||||||||||||||||||
| datasource: "BaseDatasource | Explorable", | ||||||||||||||||||
| ) -> bool: | ||||||||||||||||||
| """ | ||||||||||||||||||
| Return True if the form_data is performing a supported drill by operation, | ||||||||||||||||||
| False otherwise. | ||||||||||||||||||
| Return True if the form_data is performing a supported drill operation | ||||||||||||||||||
| (Drill to Detail or Drill By), False otherwise. | ||||||||||||||||||
|
|
||||||||||||||||||
| :param form_data: The form_data included in the request. | ||||||||||||||||||
| :param dashboard: The dashboard the user is drilling from. | ||||||||||||||||||
| :param datasource: The datasource being queried | ||||||||||||||||||
| :returns: Whether the user has drill by access. | ||||||||||||||||||
| :returns: Whether the user has drill access. | ||||||||||||||||||
| """ | ||||||||||||||||||
|
|
||||||||||||||||||
| from superset.models.slice import Slice | ||||||||||||||||||
|
|
||||||||||||||||||
| # Drill to Detail: no slice/chart context, dataset must belong to the dashboard | ||||||||||||||||||
| if ( | ||||||||||||||||||
| form_data.get("slice_id") is None | ||||||||||||||||||
| and form_data.get("chart_id") is None | ||||||||||||||||||
| and datasource in dashboard.datasources | ||||||||||||||||||
| ): | ||||||||||||||||||
| return True | ||||||||||||||||||
|
|
||||||||||||||||||
| # Drill By: slice_id is 0 (sentinel), chart_id identifies the source chart, | ||||||||||||||||||
| # and the requested groupby columns must be drillable | ||||||||||||||||||
| return bool( | ||||||||||||||||||
| form_data.get("type") != "NATIVE_FILTER" | ||||||||||||||||||
| and form_data.get("slice_id") == 0 | ||||||||||||||||||
| form_data.get("slice_id") == 0 | ||||||||||||||||||
| and (chart_id := form_data.get("chart_id")) | ||||||||||||||||||
| and ( | ||||||||||||||||||
| slc := self.session.query(Slice) | ||||||||||||||||||
|
|
@@ -2630,40 +2639,51 @@ def raise_for_access( # noqa: C901 | |||||||||||||||||
| ) | ||||||||||||||||||
| ) | ||||||||||||||||||
| or ( | ||||||||||||||||||
| # Chart. | ||||||||||||||||||
| form_data.get("type") != "NATIVE_FILTER" | ||||||||||||||||||
| and (slice_id := form_data.get("slice_id")) | ||||||||||||||||||
| and ( | ||||||||||||||||||
| # Direct chart access (no parent) | ||||||||||||||||||
| ( | ||||||||||||||||||
| form_data.get("parent_slice_id") is None | ||||||||||||||||||
| # Chart. | ||||||||||||||||||
| (slice_id := form_data.get("slice_id")) | ||||||||||||||||||
| and ( | ||||||||||||||||||
| slc := self.session.query(Slice) | ||||||||||||||||||
| .filter(Slice.id == slice_id) | ||||||||||||||||||
| .one_or_none() | ||||||||||||||||||
| # Direct chart access (no parent) | ||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. holly molly, maybe it's time to refactor this method ...
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll try tackling that in a follow up. I believe the logic on its own is correct, but we could move each validation block to their own helper method (like drill validation is now) |
||||||||||||||||||
| ( | ||||||||||||||||||
| form_data.get("parent_slice_id") is None | ||||||||||||||||||
| and ( | ||||||||||||||||||
| slc := self.session.query(Slice) | ||||||||||||||||||
| .filter(Slice.id == slice_id) | ||||||||||||||||||
| .one_or_none() | ||||||||||||||||||
| ) | ||||||||||||||||||
| and slc in dashboard_.slices | ||||||||||||||||||
| and slc.datasource == datasource | ||||||||||||||||||
| ) | ||||||||||||||||||
| or | ||||||||||||||||||
| # Multi-layer chart child access (has parent) | ||||||||||||||||||
| ( | ||||||||||||||||||
| ( | ||||||||||||||||||
| parent_id := form_data.get( | ||||||||||||||||||
| "parent_slice_id" | ||||||||||||||||||
| ) | ||||||||||||||||||
| ) | ||||||||||||||||||
| and ( | ||||||||||||||||||
| parent_slc := self.session.query(Slice) | ||||||||||||||||||
| .filter(Slice.id == parent_id) | ||||||||||||||||||
| .one_or_none() | ||||||||||||||||||
| ) | ||||||||||||||||||
| and parent_slc in dashboard_.slices | ||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggestion: The multi-layer child access path validates Severity Level: Critical 🚨- ❌ Embedded guests query unauthorized datasets via ChartDataRestApi.data endpoint.
- ❌ Dashboard RBAC users bypass datasource checks for deck_multi children.
- ⚠️ Inconsistent child datasource validation in SupersetSecurityManager.raise_for_access.
Suggested change
Steps of Reproduction ✅1. Configure a dashboard with roles or embedded access so that either DASHBOARD_RBAC or
EMBEDDED_SUPERSET is enabled and used (checked in
`SupersetSecurityManager.raise_for_access` at `superset/security/manager.py:162-179` and
`:269-271` via `is_feature_enabled("DASHBOARD_RBAC")` /
`is_feature_enabled("EMBEDDED_SUPERSET")`).
2. On that dashboard, add a `deck_multi` chart whose parent slice has a `deck_slices`
configuration including a child chart ID (validated by
`_validate_child_in_parent_multilayer` at `superset/security/manager.py:702-724`),
ensuring the child chart uses dataset A; also create another dataset B that the same
user/guest does NOT have `datasource_access` or schema access to.
3. As an embedded guest user (feature flag `EMBEDDED_SUPERSET`) or a RBAC viewer of that
dashboard, send a POST request to `ChartDataRestApi.data` at `/api/v1/chart/data`
(`superset/charts/data/api.py:280-99`) with a JSON body that `ChartDataQueryContextSchema`
accepts, where `datasource` points to dataset B (e.g. `{"id": <B_id>, "type": "table"}`)
and `form_data` contains `{"dashboardId": <dashboard_id>, "type": "DRILL_DETAIL",
"slice_id": <child_slice_id>, "parent_slice_id": <parent_slice_id>, ...}` so that
`slice_id` is the valid child from the parent's `deck_slices`.
4. The request flows through `ChartDataCommand.validate`
(`superset/commands/chart/data/get_data_command.py:39-40`) which calls
`QueryContext.raise_for_access` (`superset/common/query_context.py:18`), which delegates
to `QueryContextProcessor.raise_for_access` and then
`security_manager.raise_for_access(query_context=self._query_context)`
(`superset/common/query_context_processor.py:525-537);` inside
`SupersetSecurityManager.raise_for_access` (`superset/security/manager.py:145-180` and
`:2445-2647`), the user fails the normal schema/datasource/ownership checks, but passes
the multi-layer child branch at lines `2644-35` because `parent_slc` is in
`dashboard_.slices` and `_validate_child_in_parent_multilayer` returns True, and there is
currently no `child_slc.datasource == datasource` validation—so no
`SupersetSecurityException` is raised and the query executes against dataset B, returning
unauthorized data.Prompt for AI Agent 🤖This is a comment left during a code review.
**Path:** superset/security/manager.py
**Line:** 2672:2672
**Comment:**
*Security: The multi-layer child access path validates `parent_slice_id` and child membership in `deck_slices`, but it never verifies that the requested datasource actually belongs to that child chart. Because datasource authorization is granted when this branch is true, a forged request can pair a valid parent/child chart combination with an unrelated datasource and bypass datasource access checks. Add an explicit lookup of the child slice and ensure its datasource matches the requested datasource before granting access.
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.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is outside the scope of this PR. I'll tag @msyavuz here as he originally worked on this logic. Let me know if this is a concern, and if I can help with anything
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The child slice is validated against the parent's config via _validate_child_in_parent_multilayer this ensures the slice_id is actually a child of the parent_slice_id in the deck_multi chart's deck_slices param. This is a false positive
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. thanks for confirming it! |
||||||||||||||||||
| # Validate child is actually part of parent's config # noqa: E501 | ||||||||||||||||||
| and self._validate_child_in_parent_multilayer( # noqa: E501 | ||||||||||||||||||
| child_slice_id=slice_id, | ||||||||||||||||||
| parent_slice=parent_slc, | ||||||||||||||||||
| ) | ||||||||||||||||||
| ) | ||||||||||||||||||
| ) | ||||||||||||||||||
| and slc in dashboard_.slices | ||||||||||||||||||
| and slc.datasource == datasource | ||||||||||||||||||
| ) | ||||||||||||||||||
| or | ||||||||||||||||||
| # Multi-layer chart child access (has parent) | ||||||||||||||||||
| ( | ||||||||||||||||||
| (parent_id := form_data.get("parent_slice_id")) | ||||||||||||||||||
| and ( | ||||||||||||||||||
| parent_slc := self.session.query(Slice) | ||||||||||||||||||
| .filter(Slice.id == parent_id) | ||||||||||||||||||
| .one_or_none() | ||||||||||||||||||
| ) | ||||||||||||||||||
| and parent_slc in dashboard_.slices | ||||||||||||||||||
| # Validate child is actually part of parent's config | ||||||||||||||||||
| and self._validate_child_in_parent_multilayer( | ||||||||||||||||||
| child_slice_id=slice_id, | ||||||||||||||||||
| parent_slice=parent_slc, | ||||||||||||||||||
| ) | ||||||||||||||||||
| # D2D or Drill By | ||||||||||||||||||
| or self.has_drill_access( | ||||||||||||||||||
| form_data, dashboard_, datasource | ||||||||||||||||||
| ) | ||||||||||||||||||
| ) | ||||||||||||||||||
| ) | ||||||||||||||||||
| or self.has_drill_by_access(form_data, dashboard_, datasource) | ||||||||||||||||||
| ) | ||||||||||||||||||
| and self.can_access_dashboard(dashboard_) | ||||||||||||||||||
| ) | ||||||||||||||||||
|
|
||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The only real changes to this block are:
form_data.get("type") != "NATIVE_FILTER"more than once, so I moved it to the top for single validation;has_drill_by_accesstohas_drill_access.I can undo #1 if we think it's better