Files
superset2/tests/unit_tests/subjects/test_raise_for_access.py

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)