sha174n commented on code in PR #44494:
URL: https://github.com/apache/superset/pull/44494#discussion_r4085511065


##########
tests/integration_tests/charts/commands_tests.py:
##########
@@ -573,6 +591,71 @@ def test_query_context_update_requires_chart_access(
         with pytest.raises(ChartForbiddenError):
             UpdateChartCommand(pk, json_obj).run()
 
+    @patch.dict(
+        "superset.extensions.feature_flag_manager._feature_flags",
+        EMBEDDED_SUPERSET=True,
+    )
+    @patch("superset.commands.chart.update.ChartDAO.find_by_id")
+    @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
+    def test_query_context_update_denies_guest(self, mock_find_by_id) -> None:
+        """
+        The relaxed path gates on chart access, which a guest token does pass
+        for the member charts of the dashboard it embeds. A guest nonetheless
+        holds no write capability, so a query-context-only update is denied.
+        """
+        dashboard = self.get_dash_by_slug("births")
+        chart = dashboard.slices[0]
+        original_query_context = chart.query_context
+        dashboard_was_embedded = bool(dashboard.embedded)
+        embedded = EmbeddedDashboardDAO.upsert(dashboard, [])
+        db.session.flush()  # the uuid is only populated on flush
+
+        # A real guest principal for a dashboard that actually contains the
+        # chart, so ``is_guest_user`` and ``raise_for_access`` both run for
+        # real rather than a mock standing in for either.
+        guest = security_manager.get_guest_user_from_token(
+            {
+                "user": {},
+                "resources": [
+                    {
+                        "type": GuestTokenResourceType.DASHBOARD,
+                        "id": str(embedded.uuid),
+                    }
+                ],
+                "rls_rules": [],
+                "iat": 10,
+                "exp": 20,
+            }
+        )
+
+        # Bypass ChartFilter so the command's own gates decide the outcome.
+        mock_find_by_id.return_value = chart
+
+        json_obj = {
+            "query_context_generation": True,
+            "query_context": json.dumps({"foo": "bar"}),
+        }
+        try:
+            with override_user(guest):
+                # Precondition: this guest clears the access gate, so the deny
+                # below can only come from the guest check itself.
+                security_manager.raise_for_access(chart=chart)
+
+                with pytest.raises(ChartForbiddenError):
+                    UpdateChartCommand(chart.id, json_obj).run()
+        finally:
+            # Should the guest gate regress, ``run()`` commits before
+            # ``pytest.raises`` fails, persisting both the embedded row and the
+            # new query context. A rollback cannot undo a commit, so clear them
+            # explicitly rather than leaking them into every later test.
+            db.session.rollback()
+            if not dashboard_was_embedded:
+                db.session.query(EmbeddedDashboard).filter_by(
+                    dashboard_id=dashboard.id
+                ).delete()

Review Comment:
   `dashboard_was_embedded` is bound on the line *before* `upsert`, not 
re-evaluated in the `finally`, so it is already the pre-`upsert` snapshot you 
are asking for. Confirmed by simulating the gate regressing: the delete does 
run and the row count ends at 0.
   
   Tightened it anyway in 590bffbdcf, the delete now targets the uuid captured 
at flush rather than the dashboard id, and a comment records why the snapshot 
has to precede `upsert` (it returns the existing row when the dashboard already 
has one).



##########
tests/integration_tests/charts/commands_tests.py:
##########
@@ -573,6 +591,71 @@ def test_query_context_update_requires_chart_access(
         with pytest.raises(ChartForbiddenError):
             UpdateChartCommand(pk, json_obj).run()
 
+    @patch.dict(
+        "superset.extensions.feature_flag_manager._feature_flags",
+        EMBEDDED_SUPERSET=True,
+    )
+    @patch("superset.commands.chart.update.ChartDAO.find_by_id")
+    @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
+    def test_query_context_update_denies_guest(self, mock_find_by_id) -> None:
+        """
+        The relaxed path gates on chart access, which a guest token does pass
+        for the member charts of the dashboard it embeds. A guest nonetheless
+        holds no write capability, so a query-context-only update is denied.
+        """
+        dashboard = self.get_dash_by_slug("births")
+        chart = dashboard.slices[0]
+        original_query_context = chart.query_context
+        dashboard_was_embedded = bool(dashboard.embedded)
+        embedded = EmbeddedDashboardDAO.upsert(dashboard, [])
+        db.session.flush()  # the uuid is only populated on flush
+
+        # A real guest principal for a dashboard that actually contains the
+        # chart, so ``is_guest_user`` and ``raise_for_access`` both run for
+        # real rather than a mock standing in for either.
+        guest = security_manager.get_guest_user_from_token(
+            {
+                "user": {},
+                "resources": [
+                    {
+                        "type": GuestTokenResourceType.DASHBOARD,
+                        "id": str(embedded.uuid),
+                    }
+                ],
+                "rls_rules": [],
+                "iat": 10,
+                "exp": 20,
+            }
+        )
+
+        # Bypass ChartFilter so the command's own gates decide the outcome.
+        mock_find_by_id.return_value = chart
+
+        json_obj = {
+            "query_context_generation": True,
+            "query_context": json.dumps({"foo": "bar"}),
+        }
+        try:
+            with override_user(guest):
+                # Precondition: this guest clears the access gate, so the deny
+                # below can only come from the guest check itself.
+                security_manager.raise_for_access(chart=chart)
+
+                with pytest.raises(ChartForbiddenError):
+                    UpdateChartCommand(chart.id, json_obj).run()
+        finally:
+            # Should the guest gate regress, ``run()`` commits before
+            # ``pytest.raises`` fails, persisting both the embedded row and the
+            # new query context. A rollback cannot undo a commit, so clear them
+            # explicitly rather than leaking them into every later test.
+            db.session.rollback()
+            if not dashboard_was_embedded:
+                db.session.query(EmbeddedDashboard).filter_by(
+                    dashboard_id=dashboard.id
+                ).delete()
+            chart.query_context = original_query_context
+            db.session.commit()

Review Comment:
   The `finally` rolls back before it commits, so an early failure has nothing 
uncommitted left to persist. I simulated a raise at the precondition on line 
642: no embedded row and no `query_context` change survives, the commit only 
persists the restore.
   
   The commit is needed for the other direction, where a regressed gate makes 
`run()` commit and a rollback can no longer undo it. Comment reworded in 
590bffbdcf to state both halves.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to