You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by pm...@apache.org on 2026/08/11 20:47:54 UTC
(superset) branch master updated: fix(dashboard): consistently apply the datasource-access check when serializing member charts and datasets (#42716)
This is an automated email from the ASF dual-hosted git repository.
sha174n pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new 0d57c5ab502 fix(dashboard): consistently apply the datasource-access check when serializing member charts and datasets (#42716)
0d57c5ab502 is described below
commit 0d57c5ab5026188c811d02ff4b5366e10fb3e7d0
Author: Shaitan <10...@users.noreply.github.com>
AuthorDate: Tue Aug 11 21:47:47 2026 +0100
fix(dashboard): consistently apply the datasource-access check when serializing member charts and datasets (#42716)
Co-authored-by: Claude Opus 4.8 <no...@anthropic.com>
---
superset/dashboards/api.py | 55 ++++++++++++++++++++-----
tests/integration_tests/dashboards/api_tests.py | 41 ++++++++++++++++--
2 files changed, 81 insertions(+), 15 deletions(-)
diff --git a/superset/dashboards/api.py b/superset/dashboards/api.py
index f28fb182568..d7fd878465f 100644
--- a/superset/dashboards/api.py
+++ b/superset/dashboards/api.py
@@ -267,6 +267,31 @@ CUSTOM_TAG_LIST_COLUMNS = BASE_LIST_COLUMNS + [
"custom_tags.type",
]
+# Fields dropped from a dashboard member dataset when the caller cannot access
+# that datasource on its own: everything describing the dataset's schema,
+# connection, and query construction. The identifying fields the dashboard
+# needs to render its charts are kept. This is a stricter version of the
+# narrowing DashboardDatasetSchema.post_dump already applies to guest users.
+DASHBOARD_DATASET_INACCESSIBLE_FIELDS = (
+ "sql",
+ "select_star",
+ "fetch_values_predicate",
+ "template_params",
+ "params",
+ "perm",
+ "edit_url",
+ "database",
+ "columns",
+ "column_names",
+ "column_types",
+ "metrics",
+ "verbose_map",
+ "order_by_choices",
+ "main_dttm_col",
+ "granularity_sqla",
+ "time_grain_sqla",
+)
+
# pylint: disable=too-many-public-methods
class DashboardRestApi(
@@ -628,6 +653,15 @@ class DashboardRestApi(
schema = self.dashboard_get_response_schema
result = schema.dump(dash)
+ if "charts" in result:
+ # Only name the member charts the caller can access, consistent with
+ # the per-object narrowing applied to the dashboard's datasets and
+ # charts sub-resources.
+ result["charts"] = [
+ slc.chart
+ for slc in dash.slices
+ if security_manager.can_access_chart(slc)
+ ]
if current_app.config.get("EXTRA_EDITORS_RESOLVER"):
result["extra_editors"] = get_extra_editor_subject_ids(dash)
add_extra_log_payload(
@@ -694,19 +728,18 @@ class DashboardRestApi(
def _serialize_dashboard_dataset(
self, datasource: Any, payload: dict[str, Any]
) -> dict[str, Any]:
+ """Dump a member dataset, narrowed when the caller cannot access it."""
serialized = self.dashboard_dataset_schema.dump(payload)
if not security_manager.can_access_datasource(datasource):
- for key in (
- "sql",
- "select_star",
- "fetch_values_predicate",
- "template_params",
- "params",
- ):
+ for key in DASHBOARD_DATASET_INACCESSIBLE_FIELDS:
serialized.pop(key, None)
- for collection_key in ("columns", "metrics"):
- for item in serialized.get(collection_key) or ():
- item.pop("expression", None)
+ return serialized
+
+ def _serialize_dashboard_chart(self, chart: Any) -> dict[str, Any]:
+ """Dump a member chart, narrowed when the caller cannot access it."""
+ serialized = self.chart_entity_response_schema.dump(chart)
+ if not security_manager.can_access_chart(chart):
+ serialized.pop("form_data", None)
return serialized
@expose("/<id_or_slug>/tabs", methods=("GET",))
@@ -812,7 +845,7 @@ class DashboardRestApi(
"""
try:
charts = DashboardDAO.get_charts_for_dashboard(id_or_slug)
- result = [self.chart_entity_response_schema.dump(chart) for chart in charts]
+ result = [self._serialize_dashboard_chart(chart) for chart in charts]
return self.response(200, result=result)
except DashboardAccessDeniedError:
return self.response_403()
diff --git a/tests/integration_tests/dashboards/api_tests.py b/tests/integration_tests/dashboards/api_tests.py
index 479c78f94e2..90740c795c2 100644
--- a/tests/integration_tests/dashboards/api_tests.py
+++ b/tests/integration_tests/dashboards/api_tests.py
@@ -337,19 +337,32 @@ class TestDashboardApi(ApiEditorsTestCaseMixin, InsertChartMixin, SupersetTestCa
response = self.get_assert_metric(uri, "get_datasets")
assert response.status_code == 200
data = json.loads(response.data.decode("utf-8"))
+ assert data["result"]
for dataset in data["result"]:
+ # Spelled out rather than read from the production constant, so
+ # that dropping a field from that constant fails this test.
for excluded_key in [
"sql",
"select_star",
"fetch_values_predicate",
"template_params",
"params",
+ "perm",
+ "edit_url",
+ "database",
+ "columns",
+ "column_names",
+ "column_types",
+ "metrics",
+ "verbose_map",
+ "order_by_choices",
+ "main_dttm_col",
+ "granularity_sqla",
+ "time_grain_sqla",
]:
assert excluded_key not in dataset
- for column in dataset.get("columns") or []:
- assert "expression" not in column
- for metric in dataset.get("metrics") or []:
- assert "expression" not in metric
+ # The identifying fields the dashboard needs to render are kept.
+ assert {"id", "uid", "table_name", "type"} <= set(dataset)
@pytest.mark.usefixtures("load_world_bank_dashboard_with_slices")
@patch("superset.utils.log.logger")
@@ -465,6 +478,26 @@ class TestDashboardApi(ApiEditorsTestCaseMixin, InsertChartMixin, SupersetTestCa
assert result["id"] == dashboard.slices[0].id
assert result["slice_name"] == dashboard.slices[0].slice_name
+ @pytest.mark.usefixtures("create_dashboards")
+ @patch("superset.dashboards.api.security_manager.can_access_chart")
+ def test_get_dashboard_charts_strips_form_data_without_chart_access(
+ self, can_access_chart_mock
+ ):
+ """A caller who cannot access a member chart does not receive its
+ form_data (datasource/query config), only its identifying fields."""
+ can_access_chart_mock.return_value = False
+ self.login(ADMIN_USERNAME)
+ dashboard = self.dashboards[0]
+ uri = f"api/v1/dashboard/{dashboard.id}/charts"
+ response = self.get_assert_metric(uri, "get_charts")
+ assert response.status_code == 200
+ data = json.loads(response.data.decode("utf-8"))
+ assert data["result"]
+ for chart in data["result"]:
+ assert "form_data" not in chart
+ assert "id" in chart
+ assert "slice_name" in chart
+
@pytest.mark.usefixtures("create_dashboards")
def test_get_dashboard_charts_by_slug(self):
"""