aminghadersohi commented on code in PR #45816: URL: https://github.com/apache/superset/pull/45816#discussion_r4405839525
########## tests/unit_tests/security/test_guest_query_datasource_scope.py: ########## @@ -0,0 +1,478 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +"""Embedded guests and SQL Lab ``query`` datasources. + +An embedded guest token only grants the data sources reachable through the +embedded dashboards it authorizes. The SQL Lab query branch of +``raise_for_access`` must therefore not let a guest read a stored query (its +own or another user's) merely because the guest role happens to hold access to +the tables that query touches. +""" + +from __future__ import annotations + +from typing import Any, Optional, TYPE_CHECKING +from unittest.mock import MagicMock + +import pytest +from pytest_mock import MockerFixture + +from superset.common.db_query_status import QueryStatus +from superset.errors import SupersetErrorType +from superset.exceptions import SupersetSecurityException +from superset.extensions import appbuilder +from superset.security.guest_token import ( + GuestToken, + GuestTokenResourceType, + GuestUser, +) +from superset.security.manager import SupersetSecurityManager +from superset.utils import json +from superset.utils.core import DatasourceType + +if TYPE_CHECKING: + from superset.app import SupersetApp + +QUERY_ID = 1 +DASHBOARD_ID = 10 +SLICE_ID = 20 +GUEST_DENIED = SupersetErrorType.QUERY_SECURITY_ACCESS_ERROR + + +def _guest_user(datasets: Optional[list[int]] = None) -> GuestUser: + token: GuestToken = { + "user": {}, + "resources": [ + {"type": GuestTokenResourceType.DASHBOARD, "id": str(DASHBOARD_ID)} + ], + "rls_rules": [], + "iat": 0, + "exp": 9999999999, + } + if datasets is not None: + token["datasets"] = datasets + return GuestUser(token=token, roles=[]) + + +def _query(user_id: Optional[int]) -> MagicMock: + """A persisted SQL Lab query over ``public.sales``.""" + database = MagicMock() + database.get_default_catalog.return_value = None + database.get_default_schema_for_query.return_value = "public" + database.db_engine_spec.supports_catalog = True + query = MagicMock() + query.id = QUERY_ID + query.type = DatasourceType.QUERY + query.catalog = None + query.schema = "public" + query.database = database + query.sql = "SELECT * FROM sales" + query.executed_sql = None + query.user_id = user_id + query.status = QueryStatus.SUCCESS + return query + + +def _security_manager( + mocker: MockerFixture, guest: Optional[GuestUser] +) -> SupersetSecurityManager: + """A security manager whose role grants access to the ``sales`` table.""" + sm = SupersetSecurityManager(appbuilder) + mocker.patch.object(sm, "can_access_database", return_value=False) + mocker.patch.object(sm, "get_schema_perm", return_value="[db].[public]") + mocker.patch.object(sm, "can_access", return_value=True) + mocker.patch.object(sm, "is_guest_user", return_value=guest is not None) + mocker.patch.object(sm, "get_current_guest_user_if_guest", return_value=guest) + mocker.patch("superset.security.manager.get_user_id", return_value=None) + return sm + + +def _bound_query_context( + mocker: MockerFixture, + sm: SupersetSecurityManager, + query: MagicMock, + *, + modified: bool = False, + guest_has_dashboard: bool = True, + stored_chart: bool = True, +) -> MagicMock: + """A query request naming a dataset-backed chart on an embedded dashboard.""" + slc = MagicMock() + slc.id = SLICE_ID + slc.datasource_id = QUERY_ID + slc.datasource_type = DatasourceType.TABLE + dashboard = MagicMock() + dashboard.id = DASHBOARD_ID + dashboard.slices = [slc] + + session = MagicMock() + session.query.return_value.filter.return_value.one_or_none.return_value = dashboard + mocker.patch.object( + SupersetSecurityManager, "session", new_callable=mocker.PropertyMock + ).return_value = session + mocker.patch.object(sm, "has_guest_access", return_value=guest_has_dashboard) + mocker.patch( + "superset.security.manager.query_context_modified", return_value=modified + ) + + query_context = MagicMock() + query_context.datasource = query + query_context.slice_ = slc if stored_chart else None + query_context.form_data = {"dashboardId": DASHBOARD_ID, "slice_id": SLICE_ID} + return query_context + + [email protected]( + "datasets", + [pytest.param([3], id="scoped-token"), pytest.param(None, id="unscoped-token")], +) [email protected]( + "owner_id", + [pytest.param(None, id="guest-own-query"), pytest.param(99, id="other-user")], +) +def test_guest_cannot_read_query_datasource_outside_token_scope( + mocker: MockerFixture, + app_context: None, + datasets: Optional[list[int]], + owner_id: Optional[int], +) -> None: + """A guest is refused a stored query even when the guest role can read + the tables it touches, whatever the token scope and query author.""" + sm = _security_manager(mocker, _guest_user(datasets)) + with pytest.raises(SupersetSecurityException) as excinfo: + sm.raise_for_access(query=_query(owner_id), allow_editorship=False) Review Comment: Added the requested regression coverage in c0eb8d8cc35f5b04948dbaf46b1ff5833ced3c6b. Regression test: `test_guest_refused_query_even_with_authorship_bypass`. It passes with the guest guard, fails with `DID NOT RAISE` when the guard exempts `allow_query_authorship_bypass=True`, and passes again after restoring the guard. Additional parameterized coverage pins the other query call shapes. Siblings checked: - tests/unit_tests/security/test_guest_query_datasource_scope.py — fixed in c0eb8d8cc35f5b04948dbaf46b1ff5833ced3c6b - superset/commands/explore/get.py:97 — not affected: the existing guard is unchanged; its combined query/datasource authorship-bypass shape is pinned by test_guest_refused_query_even_with_authorship_bypass. - superset/explore/utils.py:71 — not affected: the query-only authorship-bypass shape is pinned by test_guest_refused_query_call_shapes. - superset/models/sql_lab.py:334 — not affected: force_dataset_match=True is pinned by test_guest_refused_query_call_shapes; results, export, streaming export, fetch_datasource_metadata and tabular-query resolution use this model wrapper. - superset/sqllab/validators.py:33 — not affected: the template_params plus force_dataset_match shape is pinned by test_guest_refused_query_call_shapes. - superset/commands/tag/create.py:90 — not affected: the default query keyword shape is pinned by test_guest_refused_query_call_shapes; this command already catches SupersetSecurityException. - superset/commands/tag/delete.py:123 — not affected: the same default query shape is covered; this command already handles security denials. - superset/common/query_context_processor.py:1054 — not affected: query plus allow_editorship=False is covered by test_chart_data_checks_query_datasource_access and the guest chart-data tests. - superset/common/query_context_processor.py:1060 — not affected: a query context carrying a Query is covered by test_guest_cannot_read_query_passed_as_datasource. - superset/views/datasource/views.py:566 — not affected: datasource=Query for both legacy metadata routes is covered by test_legacy_datasource_views_refuse_guest_query. - superset/security/manager.py:2632 — not affected: the datasource boolean wrapper is covered by test_can_access_datasource_refuses_guest_query. - superset/commands/chart/utils.py:36 — not affected: chart create/update reject query datasource types before their datasource authorization call. - superset/connectors/sqla/models.py:1139 — not affected: self is a SqlaTable, not a Query; dataset command callers likewise resolve datasets, and the remaining datasource calls in views/core.py and views/datasource/views.py resolve tables or explicitly restrict to dataset types. -- 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]
