mirror of
https://github.com/apache/superset.git
synced 2026-07-12 01:35:36 +00:00
281 lines
9.0 KiB
Python
281 lines
9.0 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.
|
|
|
|
from types import SimpleNamespace
|
|
from unittest.mock import MagicMock, patch, PropertyMock
|
|
|
|
import pytest
|
|
|
|
from superset.subjects.models import Subject
|
|
from superset.subjects.types import SubjectType
|
|
|
|
|
|
def _make_subject(id_: int, type_: SubjectType = SubjectType.USER) -> Subject:
|
|
s = Subject()
|
|
s.id = id_
|
|
s.type = type_
|
|
return s
|
|
|
|
|
|
def _make_resource(
|
|
editor_ids: list[int] | None = None,
|
|
viewer_ids: list[int] | None = None,
|
|
) -> MagicMock:
|
|
"""Mock resource; when *_ids is None the corresponding attr is removed."""
|
|
resource = MagicMock()
|
|
if editor_ids is not None:
|
|
resource.editors = [_make_subject(eid) for eid in editor_ids]
|
|
else:
|
|
del resource.editors
|
|
if viewer_ids is not None:
|
|
resource.viewers = [_make_subject(vid) for vid in viewer_ids]
|
|
else:
|
|
del resource.viewers
|
|
return resource
|
|
|
|
|
|
def _make_sm():
|
|
from superset.security.manager import SupersetSecurityManager
|
|
|
|
return SupersetSecurityManager.__new__(SupersetSecurityManager)
|
|
|
|
|
|
def test_is_editor_admin_bypass():
|
|
"""Admins are always editors, even with an empty editors list."""
|
|
sm = _make_sm()
|
|
with patch.object(sm, "is_admin", return_value=True):
|
|
assert sm.is_editor(_make_resource(editor_ids=[])) is True
|
|
assert sm.is_editor(_make_resource(editor_ids=[99])) is True
|
|
|
|
|
|
def test_is_editor_no_editors_relationship_returns_false():
|
|
"""Returns False when the resource lacks an editors relationship."""
|
|
sm = _make_sm()
|
|
with patch.object(sm, "is_admin", return_value=False):
|
|
assert sm.is_editor(_make_resource(editor_ids=None)) is False
|
|
|
|
|
|
def test_get_extra_editor_subject_ids_normalizes_resolver_output():
|
|
from superset.security.manager import get_extra_editor_subject_ids
|
|
|
|
resource = _make_resource()
|
|
|
|
def resolver(_: object) -> list[object]:
|
|
return [
|
|
_make_subject(10),
|
|
20,
|
|
{"id": "30"},
|
|
{"id": None},
|
|
{"id": "invalid"},
|
|
MagicMock(),
|
|
20,
|
|
]
|
|
|
|
with (
|
|
patch("superset.security.manager.has_app_context", return_value=True),
|
|
patch(
|
|
"superset.security.manager.current_app",
|
|
SimpleNamespace(config={"EXTRA_EDITORS_RESOLVER": resolver}),
|
|
),
|
|
):
|
|
assert get_extra_editor_subject_ids(resource) == [10, 20, 30]
|
|
|
|
|
|
def test_is_editor_extra_editors_resolver_match():
|
|
"""Dynamic editor subjects grant edit access."""
|
|
sm = _make_sm()
|
|
resource = _make_resource(editor_ids=[])
|
|
|
|
with (
|
|
patch.object(sm, "is_admin", return_value=False),
|
|
patch("superset.security.manager.get_user_id", return_value=1),
|
|
patch(
|
|
"superset.subjects.utils.get_user_subject_ids",
|
|
return_value=[77],
|
|
),
|
|
patch(
|
|
"superset.security.manager.get_extra_editor_subject_ids",
|
|
return_value=[77],
|
|
),
|
|
):
|
|
assert sm.is_editor(resource) is True
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"user_id,user_subject_ids,editor_ids,expected",
|
|
[
|
|
(None, [], [10], False), # anonymous
|
|
(1, [10], [10, 20], True), # direct user match
|
|
(1, [10], [20, 30], False), # no match
|
|
(1, [10, 50, 100], [50], True), # role match
|
|
(1, [10, 50, 100], [100], True), # group match
|
|
(1, [10], [], False), # empty editors
|
|
],
|
|
ids=["anonymous", "direct", "no-match", "role", "group", "empty-editors"],
|
|
)
|
|
def test_is_editor(user_id, user_subject_ids, editor_ids, expected):
|
|
"""Covers direct user, role, group, anonymous, and empty scenarios."""
|
|
sm = _make_sm()
|
|
resource = _make_resource(editor_ids=editor_ids)
|
|
|
|
with (
|
|
patch.object(sm, "is_admin", return_value=False),
|
|
patch(
|
|
"superset.security.manager.get_user_id",
|
|
return_value=user_id,
|
|
),
|
|
patch(
|
|
"superset.subjects.utils.get_user_subject_ids",
|
|
return_value=user_subject_ids,
|
|
),
|
|
):
|
|
assert sm.is_editor(resource) is expected
|
|
|
|
|
|
def test_raise_for_editorship_requeries_soft_deleted_resource(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""Soft-deleted resources are loaded with a scoped visibility bypass."""
|
|
from superset.models.helpers import SKIP_VISIBILITY_FILTER_CLASSES
|
|
|
|
class _SoftDeleteBase:
|
|
pass
|
|
|
|
class _Resource(_SoftDeleteBase):
|
|
id: int
|
|
|
|
requested = _Resource()
|
|
requested.id = 123
|
|
persisted = _make_resource(editor_ids=[10])
|
|
sm = _make_sm()
|
|
query = MagicMock()
|
|
query.execution_options.return_value.get.return_value = persisted
|
|
session = MagicMock()
|
|
session.query.return_value = query
|
|
|
|
monkeypatch.setattr("superset.models.helpers.SoftDeleteMixin", _SoftDeleteBase)
|
|
|
|
with (
|
|
patch.object(type(sm), "session", new_callable=PropertyMock) as mock_session,
|
|
patch.object(sm, "is_admin", return_value=False),
|
|
patch("superset.security.manager.get_user_id", return_value=1),
|
|
patch(
|
|
"superset.subjects.utils.get_user_subject_ids",
|
|
return_value=[10],
|
|
),
|
|
):
|
|
mock_session.return_value = session
|
|
sm.raise_for_editorship(requested)
|
|
|
|
session.query.assert_called_once_with(_Resource)
|
|
query.execution_options.assert_called_once_with(
|
|
**{SKIP_VISIBILITY_FILTER_CLASSES: {_Resource}}
|
|
)
|
|
query.execution_options.return_value.get.assert_called_once_with(123)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# is_viewer tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_is_viewer_admin_bypass():
|
|
"""Admins are always viewers."""
|
|
sm = _make_sm()
|
|
with patch.object(sm, "is_admin", return_value=True):
|
|
assert sm.is_viewer(_make_resource(editor_ids=[], viewer_ids=[])) is True
|
|
|
|
|
|
def test_is_viewer_anonymous():
|
|
"""Anonymous users are never viewers."""
|
|
sm = _make_sm()
|
|
with (
|
|
patch.object(sm, "is_admin", return_value=False),
|
|
patch("superset.security.manager.get_user_id", return_value=None),
|
|
):
|
|
assert sm.is_viewer(_make_resource(editor_ids=[10], viewer_ids=[10])) is False
|
|
|
|
|
|
def test_is_viewer_extra_editors_resolver_match():
|
|
"""Dynamic editor subjects also imply view access."""
|
|
sm = _make_sm()
|
|
resource = _make_resource(editor_ids=[], viewer_ids=[])
|
|
|
|
with (
|
|
patch.object(sm, "is_admin", return_value=False),
|
|
patch("superset.security.manager.get_user_id", return_value=1),
|
|
patch(
|
|
"superset.subjects.utils.get_user_subject_ids",
|
|
return_value=[77],
|
|
),
|
|
patch(
|
|
"superset.security.manager.get_extra_editor_subject_ids",
|
|
return_value=[77],
|
|
),
|
|
):
|
|
assert sm.is_viewer(resource) is True
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"user_subject_ids,editor_ids,viewer_ids,expected",
|
|
[
|
|
([10], [10], [], True), # editor implies viewer
|
|
([10], [], [10], True), # direct viewer match
|
|
([10], [20], [10], True), # viewer match (not editor)
|
|
([10, 50], [50], [20], True), # editor via role subject
|
|
([10], [20], [30], False), # no match at all
|
|
([10], [], [], False), # empty editors + viewers
|
|
],
|
|
ids=[
|
|
"editor-implies-viewer",
|
|
"viewer-only",
|
|
"viewer-not-editor",
|
|
"editor-role-match",
|
|
"no-match",
|
|
"empty-both",
|
|
],
|
|
)
|
|
def test_is_viewer(user_subject_ids, editor_ids, viewer_ids, expected):
|
|
sm = _make_sm()
|
|
resource = _make_resource(editor_ids=editor_ids, viewer_ids=viewer_ids)
|
|
|
|
with (
|
|
patch.object(sm, "is_admin", return_value=False),
|
|
patch("superset.security.manager.get_user_id", return_value=1),
|
|
patch(
|
|
"superset.subjects.utils.get_user_subject_ids",
|
|
return_value=user_subject_ids,
|
|
),
|
|
):
|
|
assert sm.is_viewer(resource) is expected
|
|
|
|
|
|
def test_is_viewer_missing_relationships():
|
|
"""is_viewer returns False (no raise) when editors/viewers attrs missing."""
|
|
sm = _make_sm()
|
|
resource = _make_resource(editor_ids=None, viewer_ids=None)
|
|
|
|
with (
|
|
patch.object(sm, "is_admin", return_value=False),
|
|
patch("superset.security.manager.get_user_id", return_value=1),
|
|
patch(
|
|
"superset.subjects.utils.get_user_subject_ids",
|
|
return_value=[10],
|
|
),
|
|
):
|
|
assert sm.is_viewer(resource) is False
|