Files
superset2/tests/unit_tests/security/test_permission_instructions_link.py
T
2026-08-08 11:51:15 -07:00

182 lines
6.2 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.
"""Unit tests for templated PERMISSION_INSTRUCTIONS_LINK rendering."""
from unittest.mock import MagicMock, patch
from superset.security.manager import (
_render_permission_instructions_link,
SupersetSecurityManager,
)
from superset.sql.parse import Table
MANAGER = "superset.security.manager"
def _render(
template: str | None,
*,
username: str = "alice",
anonymous: bool = False,
**kwargs: str,
) -> str | None:
with (
patch(
f"{MANAGER}.get_conf",
return_value={"PERMISSION_INSTRUCTIONS_LINK": template},
),
patch(f"{MANAGER}.g") as g_mock,
):
g_mock.user.is_anonymous = anonymous
g_mock.user.username = username
return _render_permission_instructions_link(**kwargs)
def test_empty_or_unset_link_returns_none() -> None:
assert _render("") is None
assert _render(None) is None
def test_plain_link_without_placeholders_is_unchanged() -> None:
assert _render("https://wiki.example.com/data-access") == (
"https://wiki.example.com/data-access"
)
def test_datasource_placeholders_are_filled_and_url_encoded() -> None:
out = _render(
"https://acme.example.com/req?id={datasource_id}"
"&name={datasource_name}&u={username}",
datasource_id="12",
datasource_name="Quarterly Sales",
)
# space in the dataset name is URL-encoded; username injected from g.user
assert out == ("https://acme.example.com/req?id=12&name=Quarterly%20Sales&u=alice")
def test_table_names_filled_and_encoded() -> None:
out = _render(
"https://acme.example.com/req?tables={table_names}",
table_names="public.sales,public.users",
)
assert out == ("https://acme.example.com/req?tables=public.sales%2Cpublic.users")
def test_anonymous_user_renders_empty_username() -> None:
out = _render(
"https://acme.example.com/req?u={username}",
anonymous=True,
)
assert out == "https://acme.example.com/req?u="
def test_unsupplied_placeholders_render_empty() -> None:
# datasource link doesn't supply table_names; that token renders as
# an empty value (matching the helper's documented behavior)
out = _render(
"https://acme.example.com/req?id={datasource_id}&t={table_names}",
datasource_id="9",
)
assert out == "https://acme.example.com/req?id=9&t="
def test_get_datasource_access_link_pulls_from_datasource_data() -> None:
ds = MagicMock()
ds.data = {"id": 12, "name": "Quarterly Sales"}
with (
patch(
f"{MANAGER}.get_conf",
return_value={
"PERMISSION_INSTRUCTIONS_LINK": (
"https://acme.example.com/req?id={datasource_id}"
"&name={datasource_name}"
)
},
),
patch(f"{MANAGER}.g") as g_mock,
):
g_mock.user.is_anonymous = False
g_mock.user.username = "alice"
out = SupersetSecurityManager.get_datasource_access_link(ds)
assert out == "https://acme.example.com/req?id=12&name=Quarterly%20Sales"
def test_get_table_access_link_joins_table_names() -> None:
sm = SupersetSecurityManager.__new__(SupersetSecurityManager)
with (
patch(
f"{MANAGER}.get_conf",
return_value={
"PERMISSION_INSTRUCTIONS_LINK": (
"https://acme.example.com/req?tables={table_names}"
)
},
),
patch(f"{MANAGER}.g") as g_mock,
):
g_mock.user.is_anonymous = False
g_mock.user.username = "alice"
out = sm.get_table_access_link(
{Table("sales", "public"), Table("users", "public")}
)
assert out is not None
assert out.startswith("https://acme.example.com/req?tables=")
assert "public.sales" in out
assert "public.users" in out
def test_datasource_error_object_includes_sorted_owner_names() -> None:
ds = MagicMock()
ds.data = {"id": 12, "name": "Quarterly Sales"}
owner_b, owner_a = MagicMock(), MagicMock()
owner_b.__str__.return_value = "Zoe Chen" # type: ignore[attr-defined]
owner_a.__str__.return_value = "Amir Patel" # type: ignore[attr-defined]
ds.owners = [owner_b, owner_a]
sm = SupersetSecurityManager.__new__(SupersetSecurityManager)
with (
patch(f"{MANAGER}.get_conf", return_value={"PERMISSION_INSTRUCTIONS_LINK": ""}),
patch(f"{MANAGER}.g") as g_mock,
):
g_mock.user.is_anonymous = False
g_mock.user.username = "alice"
error = sm.get_datasource_access_error_object(ds)
assert error.extra is not None
assert error.extra["owners"] == ["Amir Patel", "Zoe Chen"]
def test_table_access_link_is_single_encoded_and_sorted() -> None:
"""Table.__str__ URL-encodes parts; the link must not encode twice, and
set iteration must not leak nondeterministic ordering into the URL."""
sm = SupersetSecurityManager.__new__(SupersetSecurityManager)
with (
patch(
f"{MANAGER}.get_conf",
return_value={
"PERMISSION_INSTRUCTIONS_LINK": (
"https://acme.example.com/req?tables={table_names}"
)
},
),
patch(f"{MANAGER}.g") as g_mock,
):
g_mock.user.is_anonymous = False
g_mock.user.username = "alice"
out = sm.get_table_access_link({Table("my table", "public")})
# single-encoded space (%20), not double-encoded (%2520)
assert out == "https://acme.example.com/req?tables=public.my%20table"