mirror of
https://github.com/apache/superset.git
synced 2026-08-04 13:02:41 +00:00
294 lines
9.6 KiB
Python
294 lines
9.6 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 defaulting a new asset's viewers to its creator's groups.
|
|
|
|
Assigning the creator's groups as *viewers* (rather than editors) shares the
|
|
asset read-only, and is gated behind ``ASSIGN_CREATOR_GROUPS_AS_VIEWERS``
|
|
because populating ``viewers`` changes how access is enforced: once an asset
|
|
has any viewer, the datasource-permission fallback no longer applies to it.
|
|
"""
|
|
|
|
from typing import Any
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from superset.commands.utils import populate_subjects
|
|
from superset.subjects.models import Subject
|
|
from superset.subjects.types import SubjectType
|
|
from superset.subjects.utils import (
|
|
_assigns_creator_groups_as_viewers,
|
|
get_default_viewers_for_groups,
|
|
get_default_viewers_for_new_asset,
|
|
get_user_group_subject_ids_subquery,
|
|
)
|
|
|
|
|
|
def _group_subject(id_: int) -> Subject:
|
|
subject = Subject()
|
|
subject.id = id_
|
|
subject.type = SubjectType.GROUP
|
|
return subject
|
|
|
|
|
|
def test_gate_resolves_the_assign_creator_groups_feature_flag(app_context) -> None:
|
|
"""The gate is a feature flag read via ``is_feature_enabled`` and additionally
|
|
requires ``ENABLE_VIEWERS``."""
|
|
with patch("superset.is_feature_enabled", return_value=True) as mock_flag:
|
|
assert _assigns_creator_groups_as_viewers() is True
|
|
mock_flag.assert_any_call("ENABLE_VIEWERS")
|
|
mock_flag.assert_any_call("ASSIGN_CREATOR_GROUPS_AS_VIEWERS")
|
|
|
|
with patch("superset.is_feature_enabled", return_value=False):
|
|
assert _assigns_creator_groups_as_viewers() is False
|
|
|
|
# ASSIGN on but ENABLE_VIEWERS off → the gate stays closed.
|
|
with patch(
|
|
"superset.is_feature_enabled",
|
|
side_effect=lambda flag: flag != "ENABLE_VIEWERS",
|
|
):
|
|
assert _assigns_creator_groups_as_viewers() is False
|
|
|
|
|
|
def test_user_group_subject_ids_subquery_joins_group_membership(app_context) -> None:
|
|
sql = str(
|
|
get_user_group_subject_ids_subquery(7).compile(
|
|
compile_kwargs={"literal_binds": True}
|
|
)
|
|
)
|
|
|
|
assert "ab_user_group" in sql
|
|
assert "user_id = 7" in sql
|
|
|
|
|
|
@patch("superset.commands.utils.get_user_id", return_value=5)
|
|
@patch("superset.commands.utils.populate_subject_list")
|
|
@patch("superset.subjects.utils.get_user_group_subjects")
|
|
def test_new_asset_viewers_default_to_creator_groups_when_enabled(
|
|
mock_groups: MagicMock,
|
|
mock_populate: MagicMock,
|
|
mock_user_id: MagicMock,
|
|
app_context,
|
|
) -> None:
|
|
groups = [_group_subject(11), _group_subject(12)]
|
|
mock_groups.return_value = groups
|
|
mock_populate.return_value = [_group_subject(1)]
|
|
properties: dict[str, Any] = {}
|
|
|
|
with patch(
|
|
"superset.subjects.utils._assigns_creator_groups_as_viewers",
|
|
return_value=True,
|
|
):
|
|
populate_subjects(properties, [])
|
|
|
|
assert properties["viewers"] == groups
|
|
|
|
|
|
@patch("superset.commands.utils.get_user_id", return_value=5)
|
|
@patch("superset.commands.utils.populate_subject_list")
|
|
@patch("superset.subjects.utils.get_user_group_subjects")
|
|
def test_new_asset_gets_no_default_viewers_when_disabled(
|
|
mock_groups: MagicMock,
|
|
mock_populate: MagicMock,
|
|
mock_user_id: MagicMock,
|
|
app_context,
|
|
) -> None:
|
|
mock_groups.return_value = [_group_subject(11)]
|
|
mock_populate.return_value = [_group_subject(1)]
|
|
properties: dict[str, Any] = {}
|
|
|
|
with patch(
|
|
"superset.subjects.utils._assigns_creator_groups_as_viewers",
|
|
return_value=False,
|
|
):
|
|
populate_subjects(properties, [])
|
|
|
|
assert "viewers" not in properties
|
|
|
|
|
|
@patch("superset.subjects.utils.get_user_group_subjects")
|
|
def test_default_viewers_are_empty_for_an_anonymous_creator(
|
|
mock_groups: MagicMock,
|
|
app_context,
|
|
) -> None:
|
|
with patch(
|
|
"superset.subjects.utils._assigns_creator_groups_as_viewers",
|
|
return_value=True,
|
|
):
|
|
assert get_default_viewers_for_new_asset(None) == []
|
|
|
|
mock_groups.assert_not_called()
|
|
|
|
|
|
@patch("superset.subjects.utils.get_user_group_subjects")
|
|
def test_default_viewers_are_empty_when_the_setting_is_off(
|
|
mock_groups: MagicMock,
|
|
app_context,
|
|
) -> None:
|
|
with patch(
|
|
"superset.subjects.utils._assigns_creator_groups_as_viewers",
|
|
return_value=False,
|
|
):
|
|
assert get_default_viewers_for_new_asset(5) == []
|
|
|
|
mock_groups.assert_not_called()
|
|
|
|
|
|
@patch("superset.subjects.utils.get_user_group_subjects")
|
|
def test_default_viewers_are_the_creators_groups_when_enabled(
|
|
mock_groups: MagicMock,
|
|
app_context,
|
|
) -> None:
|
|
groups = [_group_subject(11)]
|
|
mock_groups.return_value = groups
|
|
|
|
with patch(
|
|
"superset.subjects.utils._assigns_creator_groups_as_viewers",
|
|
return_value=True,
|
|
):
|
|
assert get_default_viewers_for_new_asset(5) == groups
|
|
|
|
|
|
@patch("superset.commands.utils.get_user_id", return_value=5)
|
|
@patch("superset.commands.utils.populate_subject_list")
|
|
@patch("superset.subjects.utils.get_user_group_subjects")
|
|
def test_explicit_viewers_are_not_replaced_by_creator_groups(
|
|
mock_groups: MagicMock,
|
|
mock_populate: MagicMock,
|
|
mock_user_id: MagicMock,
|
|
app_context,
|
|
) -> None:
|
|
chosen = [_group_subject(99)]
|
|
mock_groups.return_value = [_group_subject(11)]
|
|
mock_populate.side_effect = [[_group_subject(1)], chosen]
|
|
properties: dict[str, Any] = {"viewers": [99]}
|
|
|
|
with patch(
|
|
"superset.subjects.utils._assigns_creator_groups_as_viewers",
|
|
return_value=True,
|
|
):
|
|
populate_subjects(properties, [])
|
|
|
|
assert properties["viewers"] == chosen
|
|
mock_groups.assert_not_called()
|
|
|
|
|
|
@patch("superset.subjects.utils.subjects_from_groups")
|
|
def test_default_viewers_for_groups_resolves_in_memory_groups(
|
|
mock_from_groups: MagicMock,
|
|
app_context,
|
|
) -> None:
|
|
"""Flush-time callers hold groups in memory before ab_user_group is written."""
|
|
group = MagicMock()
|
|
group.id = 11
|
|
subject = _group_subject(11)
|
|
subject.group_id = 11
|
|
mock_from_groups.return_value = [subject]
|
|
|
|
with patch(
|
|
"superset.subjects.utils._assigns_creator_groups_as_viewers",
|
|
return_value=True,
|
|
):
|
|
assert get_default_viewers_for_groups([group]) == [subject]
|
|
|
|
|
|
@patch("superset.subjects.utils.subjects_from_groups")
|
|
def test_default_viewers_for_groups_is_empty_when_the_setting_is_off(
|
|
mock_from_groups: MagicMock,
|
|
app_context,
|
|
) -> None:
|
|
with patch(
|
|
"superset.subjects.utils._assigns_creator_groups_as_viewers",
|
|
return_value=False,
|
|
):
|
|
assert get_default_viewers_for_groups([MagicMock()]) == []
|
|
|
|
mock_from_groups.assert_not_called()
|
|
|
|
|
|
@patch("superset.subjects.utils.get_user_group_subjects")
|
|
def test_user_id_zero_is_treated_as_a_real_principal(
|
|
mock_groups: MagicMock,
|
|
app_context,
|
|
) -> None:
|
|
"""A falsy-but-valid id must not be conflated with 'no user'."""
|
|
groups = [_group_subject(11)]
|
|
mock_groups.return_value = groups
|
|
|
|
with patch(
|
|
"superset.subjects.utils._assigns_creator_groups_as_viewers",
|
|
return_value=True,
|
|
):
|
|
assert get_default_viewers_for_new_asset(0) == groups
|
|
|
|
|
|
def test_user_group_subject_ids_subquery_restricts_to_group_subjects(
|
|
app_context,
|
|
) -> None:
|
|
"""Defense in depth: don't rely on ``group_id`` alone to imply the type."""
|
|
sql = str(
|
|
get_user_group_subject_ids_subquery(7).compile(
|
|
compile_kwargs={"literal_binds": True}
|
|
)
|
|
)
|
|
|
|
assert "subjects.type = 3" in sql
|
|
|
|
|
|
def test_subjects_from_groups_issues_a_single_query(app_context) -> None:
|
|
"""One query for the whole list, not one per group."""
|
|
from superset.subjects import utils as subjects_utils
|
|
|
|
with patch.object(subjects_utils, "db") as mock_db:
|
|
mock_db.session.query.return_value.filter.return_value.all.return_value = []
|
|
subjects_utils.subjects_from_groups([MagicMock(id=1), MagicMock(id=2)])
|
|
|
|
assert mock_db.session.query.call_count == 1
|
|
|
|
|
|
@patch("superset.commands.utils.get_user_id", return_value=5)
|
|
@patch("superset.commands.utils.populate_subject_list", return_value=[])
|
|
@patch("superset.subjects.utils.get_user_group_subjects")
|
|
def test_an_explicit_empty_viewers_list_suppresses_the_group_default(
|
|
mock_groups: MagicMock,
|
|
mock_populate: MagicMock,
|
|
mock_user_id: MagicMock,
|
|
app_context,
|
|
) -> None:
|
|
"""``viewers: []`` means "no viewers", not "fall back to my groups"."""
|
|
properties: dict[str, Any] = {"viewers": []}
|
|
|
|
with patch(
|
|
"superset.subjects.utils._assigns_creator_groups_as_viewers",
|
|
return_value=True,
|
|
):
|
|
populate_subjects(properties, [])
|
|
|
|
assert properties["viewers"] == []
|
|
mock_groups.assert_not_called()
|
|
|
|
|
|
@patch("superset.subjects.utils.get_user_group_subjects", return_value=[])
|
|
def test_a_creator_without_groups_leaves_the_dataset_fallback_intact(
|
|
mock_groups: MagicMock,
|
|
app_context,
|
|
) -> None:
|
|
"""No groups means no viewers, so the asset keeps dataset-based access."""
|
|
with patch(
|
|
"superset.subjects.utils._assigns_creator_groups_as_viewers",
|
|
return_value=True,
|
|
):
|
|
assert get_default_viewers_for_new_asset(5) == []
|