mirror of
https://github.com/apache/superset.git
synced 2026-06-09 17:49:26 +00:00
Compare commits
1 Commits
fix/embedd
...
feat/guest
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
985bfdc752 |
@@ -0,0 +1,44 @@
|
||||
# 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.
|
||||
"""add guest_token_revoked_before to embedded_dashboards
|
||||
|
||||
Revision ID: c8d2e3f4a5b6
|
||||
Revises: 33d7e0e21daa
|
||||
Create Date: 2026-06-01 00:10:00.000000
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from superset.migrations.shared.utils import add_columns, drop_columns
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "c8d2e3f4a5b6"
|
||||
down_revision = "33d7e0e21daa"
|
||||
|
||||
|
||||
def upgrade():
|
||||
# Epoch seconds; guest tokens for this embedded dashboard issued (iat)
|
||||
# before this value are rejected. NULL = no revocation.
|
||||
add_columns(
|
||||
"embedded_dashboards",
|
||||
sa.Column("guest_token_revoked_before", sa.Integer(), nullable=True),
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
drop_columns("embedded_dashboards", "guest_token_revoked_before")
|
||||
@@ -40,6 +40,10 @@ class EmbeddedDashboard(Model, AuditMixinNullable):
|
||||
|
||||
uuid = Column(UUIDType(binary=True), default=uuid.uuid4, primary_key=True)
|
||||
allow_domain_list = Column(Text) # reference the `allowed_domains` property instead
|
||||
# Epoch seconds; guest tokens whose `iat` predates this are rejected. Set to
|
||||
# "now" to revoke all currently-issued guest tokens for this embedded
|
||||
# dashboard. NULL = no revocation.
|
||||
guest_token_revoked_before = Column(Integer, nullable=True)
|
||||
dashboard_id = Column(
|
||||
Integer,
|
||||
ForeignKey("dashboards.id", ondelete="CASCADE"),
|
||||
|
||||
@@ -3475,6 +3475,8 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
|
||||
raise ValueError("Guest token does not contain an rls_rules claim")
|
||||
if token.get("type") != "guest":
|
||||
raise ValueError("This is not a guest token.")
|
||||
if self._is_guest_token_revoked(token):
|
||||
raise ValueError("Guest token has been revoked")
|
||||
except Exception: # pylint: disable=broad-except
|
||||
# The login manager will handle sending 401s.
|
||||
# We don't need to send a special error message.
|
||||
@@ -3483,6 +3485,44 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
|
||||
|
||||
return self.get_guest_user_from_token(cast(GuestToken, token))
|
||||
|
||||
@staticmethod
|
||||
def _is_guest_token_revoked(token: dict[str, Any]) -> bool:
|
||||
"""Return True if the token predates a revocation on any of its
|
||||
embedded-dashboard resources (``guest_token_revoked_before``)."""
|
||||
issued_at = token.get("iat")
|
||||
if not issued_at:
|
||||
return False
|
||||
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from superset.daos.dashboard import EmbeddedDashboardDAO
|
||||
|
||||
for resource in token.get("resources") or []:
|
||||
if resource.get("type") != GuestTokenResourceType.DASHBOARD.value:
|
||||
continue
|
||||
embedded = EmbeddedDashboardDAO.find_by_id(str(resource.get("id")))
|
||||
revoked_before = getattr(embedded, "guest_token_revoked_before", None)
|
||||
if revoked_before is not None and issued_at < revoked_before:
|
||||
return True
|
||||
return False
|
||||
|
||||
def revoke_guest_token_access(
|
||||
self, embedded_uuid: str, before: Optional[int] = None
|
||||
) -> None:
|
||||
"""Revoke all guest tokens issued for an embedded dashboard before
|
||||
``before`` (epoch seconds, default: now). Subsequent tokens are
|
||||
unaffected."""
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from superset import db
|
||||
from superset.daos.dashboard import EmbeddedDashboardDAO
|
||||
|
||||
embedded = EmbeddedDashboardDAO.find_by_id(str(embedded_uuid))
|
||||
if embedded is None:
|
||||
return
|
||||
embedded.guest_token_revoked_before = (
|
||||
before if before is not None else int(self._get_current_epoch_time())
|
||||
)
|
||||
db.session.commit()
|
||||
|
||||
def get_guest_user_from_token(self, token: GuestToken) -> GuestUser:
|
||||
return self.guest_user_cls(
|
||||
token=token,
|
||||
|
||||
64
tests/unit_tests/security/test_guest_token_revocation.py
Normal file
64
tests/unit_tests/security/test_guest_token_revocation.py
Normal file
@@ -0,0 +1,64 @@
|
||||
# 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_not_revoked() -> None:
|
||||
token = {"type": "guest", "resources": [_DASHBOARD_RESOURCE]}
|
||||
assert SupersetSecurityManager._is_guest_token_revoked(token) is False
|
||||
Reference in New Issue
Block a user