This is an automated email from the ASF dual-hosted git repository. michaelsmolina pushed a commit to branch pulse in repository https://gitbox.apache.org/repos/asf/superset.git
commit 8c9489d72f69a44b43b0d835990e51f78fb928e2 Author: Daniel Vaz Gaspar <[email protected]> AuthorDate: Tue Oct 7 07:17:49 2025 +0100 fix: update chart with dashboards validation (#35523) (cherry picked from commit 9d50f1b8a244471659449672ac252d642bf27fe0) --- superset/commands/chart/update.py | 27 ++++ tests/integration_tests/charts/commands_tests.py | 151 +++++++++++++++++++++++ 2 files changed, 178 insertions(+) diff --git a/superset/commands/chart/update.py b/superset/commands/chart/update.py index b7d0a4dab9..25ca2539a1 100644 --- a/superset/commands/chart/update.py +++ b/superset/commands/chart/update.py @@ -37,6 +37,7 @@ from superset.commands.utils import get_datasource_by_id, update_tags, validate_ from superset.daos.chart import ChartDAO from superset.daos.dashboard import DashboardDAO from superset.exceptions import SupersetSecurityException +from superset.models.dashboard import Dashboard from superset.models.slice import Slice from superset.tags.models import ObjectType from superset.utils.decorators import on_error, transaction @@ -71,6 +72,28 @@ class UpdateChartCommand(UpdateMixin, BaseCommand): return ChartDAO.update(self._model, self._properties) + def _validate_new_dashboard_access( + self, requested_dashboards: list[Dashboard], exceptions: list[Exception] + ) -> None: + """ + Validate user has access to any NEW dashboard relationships. + Existing relationships are preserved to maintain chart ownership rights. + """ + if not self._model: + return + + existing_dashboard_ids = {d.id for d in self._model.dashboards} + requested_dashboard_ids = {d.id for d in requested_dashboards} + + if new_dashboard_ids := requested_dashboard_ids - existing_dashboard_ids: + # For NEW dashboard relationships, verify user has access + accessible_dashboards = DashboardDAO.find_by_ids(list(new_dashboard_ids)) + accessible_dashboard_ids = {d.id for d in accessible_dashboards} + unauthorized_dashboard_ids = new_dashboard_ids - accessible_dashboard_ids + + if unauthorized_dashboard_ids: + exceptions.append(DashboardsNotFoundValidationError()) + def validate(self) -> None: # noqa: C901 exceptions: list[ValidationError] = [] dashboard_ids = self._properties.get("dashboards") @@ -120,12 +143,16 @@ class UpdateChartCommand(UpdateMixin, BaseCommand): # Validate/Populate dashboards only if it's a list if dashboard_ids is not None: + # First, verify all requested dashboards exist dashboards = DashboardDAO.find_by_ids( dashboard_ids, skip_base_filter=True, ) if len(dashboards) != len(dashboard_ids): exceptions.append(DashboardsNotFoundValidationError()) + else: + # Then, validate user has access to any NEW dashboard relationships + self._validate_new_dashboard_access(dashboards, exceptions) self._properties["dashboards"] = dashboards if exceptions: diff --git a/tests/integration_tests/charts/commands_tests.py b/tests/integration_tests/charts/commands_tests.py index 3a193b40e2..c39caf489c 100644 --- a/tests/integration_tests/charts/commands_tests.py +++ b/tests/integration_tests/charts/commands_tests.py @@ -415,6 +415,157 @@ class TestChartsUpdateCommand(SupersetTestCase): assert len(chart.owners) == 1 assert chart.owners[0] == admin + @patch("superset.commands.chart.update.g") + @patch("superset.utils.core.g") + @patch("superset.security.manager.g") + @pytest.mark.usefixtures("load_energy_table_with_slice") + def test_update_chart_dashboard_security_existing_relationship( + self, mock_sm_g, mock_u_g, mock_c_g + ): + """Test that chart owners can update charts linked to inaccessible + dashboards (existing relationships)""" + from superset.models.dashboard import Dashboard + + # Create a chart owned by alpha + admin = security_manager.find_user(username="admin") + alpha = security_manager.find_user(username="alpha") + + # Set user context for dashboard creation + mock_u_g.user = mock_c_g.user = mock_sm_g.user = admin + + chart = db.session.query(Slice).first() + chart.owners = [alpha] + + # Create a dashboard owned by admin (not accessible to alpha) + admin_dashboard = Dashboard( + dashboard_title="Admin Dashboard", + slug="admin-dashboard", + owners=[admin], + published=False, + ) + db.session.add(admin_dashboard) + + # Link chart to admin's dashboard (alpha owns chart, admin owns dashboard) + chart.dashboards.append(admin_dashboard) + db.session.commit() + + # Alpha should still be able to update their chart + # even though it's linked to admin's dashboard + mock_u_g.user = mock_c_g.user = mock_sm_g.user = alpha + + json_obj = { + "description": "Updated description", + "dashboards": [ + d.id for d in chart.dashboards + ], # Keep existing relationships + } + command = UpdateChartCommand(chart.id, json_obj) + command.run() + + # Should succeed - alpha can update their chart + updated_chart = db.session.query(Slice).get(chart.id) + assert updated_chart.description == "Updated description" + + # Clean up + db.session.delete(admin_dashboard) + db.session.commit() + + @patch("superset.commands.chart.update.g") + @patch("superset.utils.core.g") + @patch("superset.security.manager.g") + @pytest.mark.usefixtures("load_energy_table_with_slice") + def test_update_chart_dashboard_security_new_unauthorized_relationship( + self, mock_sm_g, mock_u_g, mock_c_g + ): + """Test that users cannot add charts to dashboards they don't have access to""" + from superset.commands.chart.exceptions import ChartInvalidError + from superset.models.dashboard import Dashboard + + admin = security_manager.find_user(username="admin") + alpha = security_manager.find_user(username="alpha") + + # Set user context for dashboard creation + mock_u_g.user = mock_c_g.user = mock_sm_g.user = admin + + # Create chart owned by alpha + chart = db.session.query(Slice).first() + chart.owners = [alpha] + + # Create private dashboard owned by admin (not accessible to alpha) + admin_dashboard = Dashboard( + dashboard_title="Admin Private Dashboard", + slug="admin-private-dashboard", + owners=[admin], + published=False, # Private dashboard + ) + db.session.add(admin_dashboard) + db.session.commit() + + # Alpha tries to add their chart to admin's private dashboard + mock_u_g.user = mock_c_g.user = mock_sm_g.user = alpha + + json_obj = { + "description": "Trying to add to unauthorized dashboard", + "dashboards": [admin_dashboard.id], # NEW unauthorized relationship + } + command = UpdateChartCommand(chart.id, json_obj) + + # Should fail - alpha cannot access admin's private dashboard + with self.assertRaises(ChartInvalidError): # noqa: PT027 + command.run() + + # Clean up + db.session.delete(admin_dashboard) + db.session.commit() + + @patch("superset.commands.chart.update.g") + @patch("superset.utils.core.g") + @patch("superset.security.manager.g") + @pytest.mark.usefixtures("load_energy_table_with_slice") + def test_update_chart_dashboard_security_admin_bypass( + self, mock_sm_g, mock_u_g, mock_c_g + ): + """Test that admins can add charts to any dashboard""" + from superset.models.dashboard import Dashboard + + admin = security_manager.find_user(username="admin") + alpha = security_manager.find_user(username="alpha") + + # Set user context for dashboard creation + mock_u_g.user = mock_c_g.user = mock_sm_g.user = alpha + + # Create chart owned by admin + chart = db.session.query(Slice).first() + chart.owners = [admin] + + # Create private dashboard owned by alpha + alpha_dashboard = Dashboard( + dashboard_title="Alpha Private Dashboard", + slug="alpha-private-dashboard", + owners=[alpha], + published=False, + ) + db.session.add(alpha_dashboard) + db.session.commit() + + # Admin should be able to add chart to any dashboard + mock_u_g.user = mock_c_g.user = mock_sm_g.user = admin + + json_obj = { + "description": "Admin adding to any dashboard", + "dashboards": [alpha_dashboard.id], + } + command = UpdateChartCommand(chart.id, json_obj) + command.run() + + # Should succeed - admin has access to all dashboards + updated_chart = db.session.query(Slice).get(chart.id) + assert alpha_dashboard in updated_chart.dashboards + + # Clean up + db.session.delete(alpha_dashboard) + db.session.commit() + class TestChartWarmUpCacheCommand(SupersetTestCase): def test_warm_up_cache_command_chart_not_found(self):
