Files
superset2/tests/unit_tests/security/test_guest_token_revocation.py
2026-06-11 19:37:22 -07:00

158 lines
5.8 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 typing import Any
from unittest.mock import MagicMock, patch
from superset.security.manager import SupersetSecurityManager
_DASHBOARD_RESOURCE = {"type": "dashboard", "id": "abc-uuid"}
def _token(iat: int) -> dict[str, Any]:
return {"type": "guest", "iat": iat, "resources": [_DASHBOARD_RESOURCE]}
def _embedded(revoked_before) -> MagicMock:
embedded = MagicMock()
embedded.guest_token_revoked_before = revoked_before
return embedded
def test_guest_token_not_revoked_when_no_revocation_set() -> None:
with patch(
"superset.daos.dashboard.EmbeddedDashboardDAO.find_by_id",
return_value=_embedded(None),
):
assert SupersetSecurityManager._is_guest_token_revoked(_token(1000)) is False
def test_guest_token_revoked_when_issued_before_revocation() -> None:
with patch(
"superset.daos.dashboard.EmbeddedDashboardDAO.find_by_id",
return_value=_embedded(2000),
):
# Token issued at 1000, revocation at 2000 -> revoked.
assert SupersetSecurityManager._is_guest_token_revoked(_token(1000)) is True
def test_guest_token_valid_when_issued_after_revocation() -> None:
with patch(
"superset.daos.dashboard.EmbeddedDashboardDAO.find_by_id",
return_value=_embedded(2000),
):
# Token issued at 3000, after the revocation cutoff -> still valid.
assert SupersetSecurityManager._is_guest_token_revoked(_token(3000)) is False
def test_guest_token_without_iat_is_revoked_when_cutoff_set() -> None:
# A token lacking ``iat`` cannot prove it predates the cutoff, so it
# fails closed and is treated as revoked when a cutoff is configured.
token = {"type": "guest", "resources": [_DASHBOARD_RESOURCE]}
with patch(
"superset.daos.dashboard.EmbeddedDashboardDAO.find_by_id",
return_value=_embedded(2000),
):
assert SupersetSecurityManager._is_guest_token_revoked(token) is True
def test_guest_token_without_iat_is_not_revoked_when_no_cutoff() -> None:
token = {"type": "guest", "resources": [_DASHBOARD_RESOURCE]}
with patch(
"superset.daos.dashboard.EmbeddedDashboardDAO.find_by_id",
return_value=_embedded(None),
):
assert SupersetSecurityManager._is_guest_token_revoked(token) is False
def test_guest_token_revoked_via_legacy_dashboard_id_resource() -> None:
# During the UUID migration a dashboard resource id may be a legacy
# dashboard id rather than an embedded UUID. In that case the embedded
# config is resolved via Dashboard.get(...).embedded, and its revocation
# cutoff must still be honored.
dashboard = MagicMock()
dashboard.embedded = [_embedded(2000)]
with (
patch(
"superset.daos.dashboard.EmbeddedDashboardDAO.find_by_id",
return_value=None,
),
patch(
"superset.models.dashboard.Dashboard.get",
return_value=dashboard,
),
):
assert SupersetSecurityManager._is_guest_token_revoked(_token(1000)) is True
def test_guest_token_not_revoked_when_resource_unresolvable() -> None:
# If neither an embedded config nor a dashboard resolves, there is no
# cutoff to enforce and the token is treated as not revoked.
with (
patch(
"superset.daos.dashboard.EmbeddedDashboardDAO.find_by_id",
return_value=None,
),
patch(
"superset.models.dashboard.Dashboard.get",
return_value=None,
),
):
assert SupersetSecurityManager._is_guest_token_revoked(_token(1000)) is False
def _manager() -> SupersetSecurityManager:
# Build an instance without running the (heavy) FAB __init__: we only
# exercise revoke_guest_token_access, which depends on nothing but
# _get_current_epoch_time and the EmbeddedDashboardDAO lookup.
return SupersetSecurityManager.__new__(SupersetSecurityManager)
def test_revoke_guest_token_access_uses_explicit_before() -> None:
embedded = _embedded(None)
with patch(
"superset.daos.dashboard.EmbeddedDashboardDAO.find_by_id",
return_value=embedded,
):
_manager().revoke_guest_token_access("abc-uuid", before=1234)
assert embedded.guest_token_revoked_before == 1234
def test_revoke_guest_token_access_defaults_to_ceil_of_now() -> None:
embedded = _embedded(None)
manager = _manager()
with (
patch(
"superset.daos.dashboard.EmbeddedDashboardDAO.find_by_id",
return_value=embedded,
),
patch.object(manager, "_get_current_epoch_time", return_value=1000.25),
):
manager.revoke_guest_token_access("abc-uuid")
# Cutoff is rounded up so fractional-``iat`` tokens issued in the same
# second are reliably revoked (fails closed).
assert embedded.guest_token_revoked_before == 1001
def test_revoke_guest_token_access_noop_when_embedded_missing() -> None:
with patch(
"superset.daos.dashboard.EmbeddedDashboardDAO.find_by_id",
return_value=None,
):
# Should simply return without raising when the UUID does not resolve.
_manager().revoke_guest_token_access("missing-uuid")