This is an automated email from the ASF dual-hosted git repository.
aminghadersohi 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 8a7be3b327b fix: avoid provider calls when rendering datasource access
denials (#44432)
8a7be3b327b is described below
commit 8a7be3b327b2cc6f82935e11dea076766c200c40
Author: Mike Bridge <[email protected]>
AuthorDate: Wed Sep 30 13:51:27 2026 -0600
fix: avoid provider calls when rendering datasource access denials (#44432)
---
.../charts/semantic_save_tests.py | 82 +++++++++++++++++++++-
.../security/test_permission_instructions_link.py | 4 +-
2 files changed, 83 insertions(+), 3 deletions(-)
diff --git a/tests/integration_tests/charts/semantic_save_tests.py
b/tests/integration_tests/charts/semantic_save_tests.py
index 06d8e86d5d1..af900f7ff6e 100644
--- a/tests/integration_tests/charts/semantic_save_tests.py
+++ b/tests/integration_tests/charts/semantic_save_tests.py
@@ -17,7 +17,7 @@
"""HTTP chart saves must preserve semantic identity and datasource access
checks."""
from typing import Any
-from unittest.mock import Mock, patch
+from unittest.mock import Mock, patch, PropertyMock
from flask import Response
from parameterized import parameterized
@@ -27,6 +27,7 @@ from superset.exceptions import SupersetSecurityException
from superset.extensions import db, security_manager
from superset.models.slice import Slice
from superset.semantic_layers.models import SemanticLayer, SemanticView
+from superset.subjects.models import Subject
from superset.utils import json
from tests.integration_tests.base_tests import SupersetTestCase
@@ -121,6 +122,85 @@ class TestSemanticChartSave(SupersetTestCase):
db.session.delete(layer)
db.session.commit()
+ @parameterized.expand([("create",), ("update",), ("form_data",)])
+ def test_gamma_denial_does_not_load_provider(self, operation: str) -> None:
+ """A denied Gamma request must return 403 even when its provider is
down."""
+ self.login("gamma")
+ layer: SemanticLayer = SemanticLayer(name="denied-layer", type="test")
+ view: SemanticView = SemanticView(name="denied-view",
semantic_layer=layer)
+ db.session.add(view)
+ db.session.commit()
+ chart: Slice | None = None
+ provider: Mock = Mock()
+ provider.get_dimensions.side_effect = RuntimeError("provider
unavailable")
+ provider.get_metrics.side_effect = RuntimeError("provider unavailable")
+ implementation: PropertyMock
+ try:
+ if operation == "update":
+ editor: Subject = (
+ db.session.query(Subject)
+
.filter_by(user_id=security_manager.find_user(username="gamma").id)
+ .one()
+ )
+ chart = Slice(
+ slice_name="gamma-owned-chart",
+ datasource_id=view.id,
+ datasource_type="semantic_view",
+ viz_type="table",
+ params="{}",
+ editors=[editor],
+ )
+ db.session.add(chart)
+ db.session.commit()
+ payload: dict[str, Any] = {
+ "slice_name": "denied-save",
+ "datasource_id": view.id,
+ "datasource_type": "semantic_view",
+ "viz_type": "table",
+ }
+ with patch.object(
+ SemanticView,
+ "implementation",
+ new_callable=PropertyMock,
+ return_value=provider,
+ ) as implementation:
+ response: Response
+ if operation == "create":
+ response = self.client.post("/api/v1/chart/", json=payload)
+ elif operation == "update":
+ assert chart is not None
+ response = self.client.put(
+ f"/api/v1/chart/{chart.id}", json=payload
+ )
+ else:
+ response = self.client.post(
+ "/api/v1/explore/form_data",
+ json={
+ "datasource_id": view.id,
+ "datasource_type": "semantic_view",
+ "form_data": json.dumps(
+ {"datasource": f"{view.id}__semantic_view"}
+ ),
+ },
+ )
+ assert response.status_code == 403
+ implementation.assert_not_called()
+ provider.get_dimensions.assert_not_called()
+ provider.get_metrics.assert_not_called()
+ assert (
+
db.session.query(Slice).filter_by(slice_name="denied-save").count() == 0
+ )
+ if chart is not None:
+ db.session.refresh(chart)
+ assert chart.slice_name == "gamma-owned-chart"
+ finally:
+ db.session.rollback()
+ if chart is not None:
+ db.session.delete(chart)
+ db.session.delete(view)
+ db.session.delete(layer)
+ db.session.commit()
+
@parameterized.expand([("query",), ("saved_query",)])
def test_unsavable_datasource_stays_422(self, datasource_type: str) ->
None:
"""The unsupported-type crash protection from #43500 remains intact."""
diff --git a/tests/unit_tests/security/test_permission_instructions_link.py
b/tests/unit_tests/security/test_permission_instructions_link.py
index 44f73da8afe..a7da6c3ebf8 100644
--- a/tests/unit_tests/security/test_permission_instructions_link.py
+++ b/tests/unit_tests/security/test_permission_instructions_link.py
@@ -146,7 +146,7 @@ def test_get_datasource_access_link_omits_datasource_name()
-> None:
# datasource_name is intentionally not forwarded to prevent name disclosure
# to users who lack access; the retired placeholder is left literal so a
# deployment still templating it sees the breakage.
- ds = MagicMock()
+ ds: MagicMock = MagicMock()
ds.id = 12
ds.data = {"id": 12, "name": "Quarterly Sales"}
with (
@@ -194,7 +194,7 @@ def test_get_table_access_link_joins_table_names() -> None:
def test_datasource_error_object_includes_sorted_owner_names() -> None:
- ds = MagicMock()
+ ds: MagicMock = MagicMock()
ds.id = 12
ds.data = {"id": 12, "name": "Quarterly Sales"}
owner_b, owner_a = MagicMock(), MagicMock()