mirror of
https://github.com/apache/superset.git
synced 2026-09-01 21:11:28 +00:00
Co-authored-by: Greg Neighbors <gregneighbors@Gregs-Air-2.lan> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Greg Neighbors <gregneighbors@Gregs-MacBook-Air-2.local>
182 lines
6.2 KiB
Python
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"
|