mirror of
https://github.com/apache/superset.git
synced 2026-07-11 17:25:31 +00:00
404 lines
14 KiB
Python
404 lines
14 KiB
Python
# 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.
|
|
|
|
"""Tests for raise_for_access viewer access paths (Phase 4, Step 7)."""
|
|
|
|
from unittest.mock import MagicMock, patch, PropertyMock
|
|
|
|
import pytest
|
|
from flask import current_app
|
|
|
|
from superset.exceptions import SupersetSecurityException
|
|
|
|
|
|
def _make_sm():
|
|
from superset.security.manager import SupersetSecurityManager
|
|
|
|
return SupersetSecurityManager.__new__(SupersetSecurityManager)
|
|
|
|
|
|
def _make_dashboard(
|
|
*,
|
|
published: bool = True,
|
|
has_viewers: bool = False,
|
|
has_datasources: bool = True,
|
|
):
|
|
dashboard = MagicMock()
|
|
dashboard.published = published
|
|
if has_viewers:
|
|
dashboard.viewers = [MagicMock()]
|
|
else:
|
|
dashboard.viewers = []
|
|
if has_datasources:
|
|
dashboard.datasources = [MagicMock()]
|
|
else:
|
|
dashboard.datasources = []
|
|
return dashboard
|
|
|
|
|
|
def test_raise_for_access_editor_allows(app_context):
|
|
"""Editor always gets access, regardless of ENABLE_VIEWERS."""
|
|
sm = _make_sm()
|
|
dashboard = _make_dashboard()
|
|
|
|
with (
|
|
patch.object(sm, "is_admin", return_value=False),
|
|
patch.object(sm, "is_editor", return_value=True),
|
|
patch.object(sm, "is_guest_user", return_value=False),
|
|
patch("superset.is_feature_enabled", return_value=True),
|
|
):
|
|
sm.raise_for_access(dashboard=dashboard)
|
|
|
|
|
|
def test_raise_for_access_viewer_published_allows(app_context):
|
|
"""Viewer can access a published dashboard with explicit viewers."""
|
|
sm = _make_sm()
|
|
dashboard = _make_dashboard(published=True, has_viewers=True)
|
|
|
|
with (
|
|
patch.object(sm, "is_admin", return_value=False),
|
|
patch.object(sm, "is_editor", return_value=False),
|
|
patch.object(sm, "is_viewer", return_value=True),
|
|
patch.object(sm, "is_guest_user", return_value=False),
|
|
patch(
|
|
"superset.is_feature_enabled",
|
|
side_effect=lambda flag: flag == "ENABLE_VIEWERS",
|
|
),
|
|
):
|
|
sm.raise_for_access(dashboard=dashboard)
|
|
|
|
|
|
def test_raise_for_access_viewer_unpublished_denies(app_context):
|
|
"""Viewer cannot access an unpublished dashboard."""
|
|
sm = _make_sm()
|
|
dashboard = _make_dashboard(published=False, has_viewers=True)
|
|
|
|
with (
|
|
patch.object(sm, "is_admin", return_value=False),
|
|
patch.object(sm, "is_editor", return_value=False),
|
|
patch.object(sm, "is_viewer", return_value=True),
|
|
patch.object(sm, "is_guest_user", return_value=False),
|
|
patch(
|
|
"superset.is_feature_enabled",
|
|
side_effect=lambda flag: flag == "ENABLE_VIEWERS",
|
|
),
|
|
patch.object(sm, "get_dashboard_access_error_object", return_value=MagicMock()),
|
|
):
|
|
with pytest.raises(SupersetSecurityException):
|
|
sm.raise_for_access(dashboard=dashboard)
|
|
|
|
|
|
def test_raise_for_access_no_viewers_dataset_fallback(app_context):
|
|
"""When no viewers exist, dashboard access falls back to dataset access."""
|
|
sm = _make_sm()
|
|
dashboard = _make_dashboard(published=True, has_viewers=False, has_datasources=True)
|
|
|
|
with (
|
|
patch.object(sm, "is_admin", return_value=False),
|
|
patch.object(sm, "is_editor", return_value=False),
|
|
patch.object(sm, "is_guest_user", return_value=False),
|
|
patch.object(sm, "can_access_datasource", return_value=True),
|
|
patch(
|
|
"superset.is_feature_enabled",
|
|
side_effect=lambda flag: flag == "ENABLE_VIEWERS",
|
|
),
|
|
):
|
|
sm.raise_for_access(dashboard=dashboard)
|
|
|
|
|
|
def test_raise_for_access_no_viewers_no_datasources_allows(app_context):
|
|
"""Dashboard with no viewers and no datasources is allowed."""
|
|
sm = _make_sm()
|
|
dashboard = _make_dashboard(
|
|
published=True, has_viewers=False, has_datasources=False
|
|
)
|
|
|
|
with (
|
|
patch.object(sm, "is_admin", return_value=False),
|
|
patch.object(sm, "is_editor", return_value=False),
|
|
patch.object(sm, "is_guest_user", return_value=False),
|
|
patch(
|
|
"superset.is_feature_enabled",
|
|
side_effect=lambda flag: flag == "ENABLE_VIEWERS",
|
|
),
|
|
):
|
|
sm.raise_for_access(dashboard=dashboard)
|
|
|
|
|
|
def test_raise_for_access_legacy_when_viewers_off(app_context):
|
|
"""When no viewers exist, implicit dataset-based access still applies."""
|
|
sm = _make_sm()
|
|
dashboard = _make_dashboard(published=True)
|
|
|
|
with (
|
|
patch.object(sm, "is_admin", return_value=False),
|
|
patch.object(sm, "is_editor", return_value=False),
|
|
patch.object(sm, "is_guest_user", return_value=False),
|
|
patch.object(sm, "can_access_datasource", return_value=True),
|
|
patch(
|
|
"superset.is_feature_enabled",
|
|
return_value=False,
|
|
),
|
|
):
|
|
sm.raise_for_access(dashboard=dashboard)
|
|
|
|
|
|
def test_raise_for_access_dashboard_viewers_override_implicit_access(app_context):
|
|
"""Explicit dashboard viewers disable implicit dataset-based view access."""
|
|
sm = _make_sm()
|
|
dashboard = _make_dashboard(published=True, has_viewers=True)
|
|
|
|
with (
|
|
patch.object(sm, "is_admin", return_value=False),
|
|
patch.object(sm, "is_editor", return_value=False),
|
|
patch.object(sm, "is_viewer", return_value=False),
|
|
patch.object(sm, "is_guest_user", return_value=False),
|
|
patch.object(sm, "can_access_datasource", return_value=True),
|
|
patch("superset.is_feature_enabled", return_value=False),
|
|
patch.object(sm, "get_dashboard_access_error_object", return_value=MagicMock()),
|
|
):
|
|
with pytest.raises(SupersetSecurityException):
|
|
sm.raise_for_access(dashboard=dashboard)
|
|
|
|
|
|
# -- Chart access path tests --
|
|
|
|
|
|
def _make_chart(*, has_datasource: bool = True):
|
|
chart = MagicMock()
|
|
if has_datasource:
|
|
chart.datasource = MagicMock()
|
|
else:
|
|
chart.datasource = None
|
|
chart.editors = [MagicMock()]
|
|
chart.viewers = [MagicMock()]
|
|
return chart
|
|
|
|
|
|
def test_raise_for_access_chart_viewer_allows(app_context):
|
|
"""Chart viewer gets access when explicit viewers are present."""
|
|
sm = _make_sm()
|
|
chart = _make_chart()
|
|
|
|
with (
|
|
patch.object(sm, "is_admin", return_value=False),
|
|
patch.object(sm, "is_editor", return_value=False),
|
|
patch.object(sm, "is_viewer", return_value=True),
|
|
patch(
|
|
"superset.is_feature_enabled",
|
|
side_effect=lambda flag: flag == "ENABLE_VIEWERS",
|
|
),
|
|
):
|
|
sm.raise_for_access(chart=chart)
|
|
|
|
|
|
def test_raise_for_access_chart_viewer_denied_without_match(app_context):
|
|
"""Explicit chart viewers deny non-viewers without datasource fallback."""
|
|
sm = _make_sm()
|
|
chart = _make_chart()
|
|
|
|
with (
|
|
patch.object(sm, "is_admin", return_value=False),
|
|
patch.object(sm, "is_editor", return_value=False),
|
|
patch.object(sm, "can_access_datasource", return_value=False),
|
|
patch.object(sm, "get_chart_access_error_object", return_value=MagicMock()),
|
|
patch("superset.is_feature_enabled", return_value=False),
|
|
):
|
|
with pytest.raises(SupersetSecurityException):
|
|
sm.raise_for_access(chart=chart)
|
|
|
|
|
|
def test_raise_for_access_chart_viewers_override_datasource_access(app_context):
|
|
"""Explicit chart viewers disable implicit datasource-based view access."""
|
|
sm = _make_sm()
|
|
chart = _make_chart()
|
|
|
|
with (
|
|
patch.object(sm, "is_admin", return_value=False),
|
|
patch.object(sm, "is_editor", return_value=False),
|
|
patch.object(sm, "is_viewer", return_value=False),
|
|
patch.object(sm, "can_access_datasource", return_value=True),
|
|
patch.object(sm, "get_chart_access_error_object", return_value=MagicMock()),
|
|
patch("superset.is_feature_enabled", return_value=False),
|
|
):
|
|
with pytest.raises(SupersetSecurityException):
|
|
sm.raise_for_access(chart=chart)
|
|
|
|
|
|
def test_raise_for_access_chart_editor_allows(app_context):
|
|
"""Chart editor always gets access."""
|
|
sm = _make_sm()
|
|
chart = _make_chart()
|
|
|
|
with (
|
|
patch.object(sm, "is_admin", return_value=False),
|
|
patch.object(sm, "is_editor", return_value=True),
|
|
):
|
|
sm.raise_for_access(chart=chart)
|
|
|
|
|
|
# -- Datasource chart-viewer promiscuous mode tests --
|
|
|
|
|
|
def test_raise_for_access_datasource_chart_viewer_promiscuous(app_context, monkeypatch):
|
|
"""Chart viewer bypasses datasource access when VIEWER_PROMISCUOUS_MODE on."""
|
|
sm = _make_sm()
|
|
datasource = MagicMock()
|
|
datasource.id = 42
|
|
chart = MagicMock()
|
|
chart.datasource_id = 42
|
|
chart.datasource = datasource
|
|
|
|
mock_session = MagicMock()
|
|
mock_session.query.return_value.filter.return_value.one_or_none.return_value = chart
|
|
monkeypatch.setitem(current_app.config, "VIEWER_PROMISCUOUS_MODE", True)
|
|
|
|
with (
|
|
patch.object(sm, "can_access_schema", return_value=False),
|
|
patch.object(sm, "can_access", return_value=False),
|
|
patch.object(sm, "is_editor", return_value=False),
|
|
patch.object(sm, "is_viewer", return_value=True),
|
|
patch(
|
|
"superset.is_feature_enabled",
|
|
side_effect=lambda flag: flag == "ENABLE_VIEWERS",
|
|
),
|
|
patch.object(
|
|
type(sm), "session", new_callable=PropertyMock, return_value=mock_session
|
|
),
|
|
):
|
|
sm.raise_for_access(
|
|
datasource=datasource,
|
|
query_context=MagicMock(
|
|
datasource=datasource,
|
|
form_data={"slice_id": 1},
|
|
),
|
|
)
|
|
|
|
|
|
def test_raise_for_access_datasource_chart_editor_promiscuous(app_context, monkeypatch):
|
|
"""Chart editor also bypasses datasource access via promiscuous mode."""
|
|
sm = _make_sm()
|
|
datasource = MagicMock()
|
|
datasource.id = 42
|
|
chart = MagicMock()
|
|
chart.datasource_id = 42
|
|
chart.datasource = datasource
|
|
|
|
mock_session = MagicMock()
|
|
mock_session.query.return_value.filter.return_value.one_or_none.return_value = chart
|
|
monkeypatch.setitem(current_app.config, "VIEWER_PROMISCUOUS_MODE", True)
|
|
|
|
with (
|
|
patch.object(sm, "can_access_schema", return_value=False),
|
|
patch.object(sm, "can_access", return_value=False),
|
|
patch.object(sm, "is_editor", side_effect=lambda resource: resource is chart),
|
|
patch.object(sm, "is_viewer", return_value=False),
|
|
patch(
|
|
"superset.is_feature_enabled",
|
|
side_effect=lambda flag: flag == "ENABLE_VIEWERS",
|
|
),
|
|
patch.object(
|
|
type(sm), "session", new_callable=PropertyMock, return_value=mock_session
|
|
),
|
|
):
|
|
sm.raise_for_access(
|
|
datasource=datasource,
|
|
query_context=MagicMock(
|
|
datasource=datasource,
|
|
form_data={"slice_id": 1},
|
|
),
|
|
)
|
|
|
|
|
|
def test_raise_for_access_datasource_chart_viewer_no_promiscuous_denies(
|
|
app_context, monkeypatch
|
|
):
|
|
"""Chart viewer denied datasource access when VIEWER_PROMISCUOUS_MODE off."""
|
|
sm = _make_sm()
|
|
datasource = MagicMock()
|
|
monkeypatch.setitem(current_app.config, "VIEWER_PROMISCUOUS_MODE", False)
|
|
|
|
with (
|
|
patch.object(sm, "can_access_schema", return_value=False),
|
|
patch.object(sm, "can_access", return_value=False),
|
|
patch.object(sm, "is_editor", return_value=False),
|
|
patch.object(
|
|
sm, "get_datasource_access_error_object", return_value=MagicMock()
|
|
),
|
|
):
|
|
with pytest.raises(SupersetSecurityException):
|
|
sm.raise_for_access(
|
|
datasource=datasource,
|
|
query_context=MagicMock(
|
|
datasource=datasource,
|
|
form_data={"slice_id": 1},
|
|
),
|
|
)
|
|
|
|
|
|
# -- GetExploreCommand access check tests --
|
|
|
|
|
|
def test_explore_command_uses_chart_access_when_slice_exists(app_context):
|
|
"""GetExploreCommand checks chart access when a chart exists."""
|
|
from superset.commands.explore.get import GetExploreCommand
|
|
from superset.commands.explore.parameters import CommandParameters
|
|
|
|
params = CommandParameters(
|
|
permalink_key=None,
|
|
form_data_key=None,
|
|
datasource_id=1,
|
|
datasource_type="table",
|
|
slice_id=1,
|
|
)
|
|
cmd = GetExploreCommand(params)
|
|
|
|
mock_slc = MagicMock()
|
|
mock_slc.data = {"slice_id": 1}
|
|
mock_slc.created_on_humanized = "1 day ago"
|
|
mock_slc.changed_on_humanized = "1 day ago"
|
|
mock_slc.dashboards = []
|
|
mock_slc.created_by = None
|
|
mock_slc.changed_by = None
|
|
|
|
mock_datasource = MagicMock()
|
|
mock_datasource.name = "test"
|
|
mock_datasource.default_endpoint = None
|
|
|
|
mock_request = MagicMock()
|
|
mock_request.args = {}
|
|
|
|
with (
|
|
patch(
|
|
"superset.commands.explore.get.get_form_data",
|
|
return_value=({"datasource": "1__table", "viz_type": "table"}, mock_slc),
|
|
),
|
|
patch(
|
|
"superset.commands.explore.get.get_datasource_info",
|
|
return_value=(1, "table"),
|
|
),
|
|
patch(
|
|
"superset.commands.explore.get.DatasourceDAO.get_datasource",
|
|
return_value=mock_datasource,
|
|
),
|
|
patch("superset.commands.explore.get.security_manager") as mock_sm,
|
|
patch("superset.commands.explore.get.sanitize_datasource_data"),
|
|
patch("superset.commands.explore.get.request", mock_request),
|
|
):
|
|
cmd.run()
|
|
mock_sm.raise_for_access.assert_called_once_with(chart=mock_slc)
|