This is an automated email from the ASF dual-hosted git repository. rusackas pushed a commit to branch tdd/issue-27900-drill-permission in repository https://gitbox.apache.org/repos/asf/superset.git
commit 42ee65841744b6b9df010e6fdebbefb92c9db018 Author: Claude Code <[email protected]> AuthorDate: Sat Jul 11 11:17:28 2026 -0700 test(security): prove Drill By access does not require can_explore (#27900) Add regression coverage for the Drill By backend access gate (superset/explore/utils.py::check_access, invoked by CreateFormDataCommand via ExploreFormDataRestApi) asserting that the granular "can read on Chart" permission is what governs access, and that the broad "can explore on Superset" permission is neither required nor sufficient. Closes #27900 Co-Authored-By: Claude Opus 4.8 <[email protected]> --- tests/unit_tests/explore/utils_test.py | 74 ++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/tests/unit_tests/explore/utils_test.py b/tests/unit_tests/explore/utils_test.py index 7f80f212b3d..54555d2814e 100644 --- a/tests/unit_tests/explore/utils_test.py +++ b/tests/unit_tests/explore/utils_test.py @@ -230,6 +230,80 @@ def test_saved_chart_no_access(mocker: MockerFixture) -> None: ) +def test_drill_by_access_without_can_explore(mocker: MockerFixture) -> None: + """ + Regression for #27900: performing Drill By (and Drill to Detail) must not + require the broad ``can explore on Superset`` permission. + + ``check_access`` is the backend access gate for the Drill By flow: it is + invoked by ``CreateFormDataCommand`` when the client stores the drill + ``form_data`` via ``ExploreFormDataRestApi`` (the endpoint commenters on the + issue identified as drill-by-specific). This test grants the granular + ``can read on Chart`` permission while *explicitly denying* + ``can explore on Superset`` and asserts that access is still granted. + """ + from superset.connectors.sqla.models import SqlaTable + from superset.explore.utils import check_access as check_chart_access + from superset.models.slice import Slice + + def can_access_side_effect(permission: str, view_menu: str) -> bool: + # The broad explore permission is denied; only the granular chart-read + # permission is granted. + if (permission, view_menu) == ("can_explore", "Superset"): + return False + return (permission, view_menu) == ("can_read", "Chart") + + mocker.patch(dataset_find_by_id, return_value=SqlaTable()) + mocker.patch(can_access_datasource, return_value=True) + mocker.patch(is_admin, return_value=False) + mocker.patch(is_editor, return_value=False) + mocker.patch(can_access, side_effect=can_access_side_effect) + mocker.patch(chart_find_by_id, return_value=Slice()) + + with override_user(User()): + assert ( + check_chart_access( # noqa: E712 + datasource_id=1, + chart_id=1, + datasource_type=DatasourceType.TABLE, + ) + is True + ) + + +def test_drill_by_access_can_explore_is_not_the_gate(mocker: MockerFixture) -> None: + """ + Regression for #27900: ``can explore on Superset`` is neither necessary nor + sufficient for Drill By access. Here the user holds *only* + ``can explore on Superset`` (the granular ``can read on Chart`` is denied and + the user is not an owner/admin) and access must be refused, proving the gate + is governed by the granular chart permission rather than ``can explore``. + """ + from superset.connectors.sqla.models import SqlaTable + from superset.explore.utils import check_access as check_chart_access + from superset.models.slice import Slice + + def can_access_side_effect(permission: str, view_menu: str) -> bool: + # Only the broad explore permission is granted; the granular chart-read + # permission is denied. + return (permission, view_menu) == ("can_explore", "Superset") + + mocker.patch(dataset_find_by_id, return_value=SqlaTable()) + mocker.patch(can_access_datasource, return_value=True) + mocker.patch(is_admin, return_value=False) + mocker.patch(is_editor, return_value=False) + mocker.patch(can_access, side_effect=can_access_side_effect) + mocker.patch(chart_find_by_id, return_value=Slice()) + + with raises(ChartAccessDeniedError): # noqa: PT012 + with override_user(User()): + check_chart_access( + datasource_id=1, + chart_id=1, + datasource_type=DatasourceType.TABLE, + ) + + def test_dataset_has_access(mocker: MockerFixture) -> None: from superset.connectors.sqla.models import SqlaTable from superset.explore.utils import check_datasource_access
