Files
superset2/tests/unit_tests/subjects/test_creator_group_defaults.py
2026-07-29 14:36:45 +02:00

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) == []