Files
superset2/tests/integration_tests/embedded/test_view.py
T
Superset Dev 9b1f66823b fix(embedded): stop rendering dashboard title/description before guest auth
The Referer and Sec-Fetch-Dest checks on the pre-token embed page are
browser-cooperation only; a non-browser client can forge or omit both
headers, so anything the page renders is effectively reachable by
knowing an embed UUID. Stop passing the dashboard title and description
into the template at this stage. The embedded SPA already fetches
dashboard metadata through the guest-token-authenticated API once it
holds a token, so the pre-auth page falls back to a generic title.
2026-08-21 00:47:02 -07:00

164 lines
6.3 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 __future__ import annotations
import html
import re
from typing import TYPE_CHECKING
from unittest import mock
import pytest
from superset import db
from superset.daos.dashboard import EmbeddedDashboardDAO
from superset.models.dashboard import Dashboard
from superset.utils import json
from tests.integration_tests.fixtures.birth_names_dashboard import (
load_birth_names_dashboard_with_slices, # noqa: F401
load_birth_names_data, # noqa: F401
)
from tests.integration_tests.fixtures.client import client # noqa: F401
if TYPE_CHECKING:
from typing import Any
from flask.testing import FlaskClient
def _extract_bootstrap_data(response_data: bytes) -> dict[str, Any]:
"""Parse the JSON bootstrap payload embedded in the SPA template."""
html_body = response_data.decode("utf-8")
match = re.search(r'data-bootstrap="([^"]*)"', html_body)
assert match is not None, "bootstrap payload not found in response"
return json.loads(html.unescape(match.group(1)))
@pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
@mock.patch.dict(
"superset.extensions.feature_flag_manager._feature_flags",
EMBEDDED_SUPERSET=True,
)
def test_get_embedded_dashboard(client: FlaskClient[Any]): # noqa: F811
dash = db.session.query(Dashboard).filter_by(slug="births").first()
embedded = EmbeddedDashboardDAO.upsert(dash, [])
db.session.flush()
uri = f"embedded/{embedded.uuid}"
response = client.get(uri)
assert response.status_code == 200
# The bootstrap payload exposes the (empty) allowed-domains list so the
# frontend can validate postMessage origins.
bootstrap = _extract_bootstrap_data(response.data)
assert bootstrap["embedded"]["allowed_domains"] == []
@pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
@mock.patch.dict(
"superset.extensions.feature_flag_manager._feature_flags",
EMBEDDED_SUPERSET=True,
)
def test_get_embedded_dashboard_bootstrap_includes_allowed_domains(
client: FlaskClient[Any], # noqa: F811
):
dash = db.session.query(Dashboard).filter_by(slug="births").first()
embedded = EmbeddedDashboardDAO.upsert(dash, ["https://allowed.example.com"])
db.session.flush()
uri = f"embedded/{embedded.uuid}"
response = client.get(uri, headers={"Referer": "https://allowed.example.com"})
assert response.status_code == 200
bootstrap = _extract_bootstrap_data(response.data)
assert bootstrap["embedded"]["allowed_domains"] == ["https://allowed.example.com"]
@pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
@mock.patch.dict(
"superset.extensions.feature_flag_manager._feature_flags",
EMBEDDED_SUPERSET=True,
)
def test_get_embedded_dashboard_referrer_not_allowed(client: FlaskClient[Any]): # noqa: F811
dash = db.session.query(Dashboard).filter_by(slug="births").first()
embedded = EmbeddedDashboardDAO.upsert(dash, ["test.example.com"])
db.session.flush()
uri = f"embedded/{embedded.uuid}"
response = client.get(uri)
assert response.status_code == 403
@mock.patch.dict(
"superset.extensions.feature_flag_manager._feature_flags",
EMBEDDED_SUPERSET=True,
)
def test_get_embedded_dashboard_non_found(client: FlaskClient[Any]): # noqa: F811
uri = "embedded/bad-uuid" # noqa: F541
response = client.get(uri)
assert response.status_code == 404
@pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
@mock.patch.dict(
"superset.extensions.feature_flag_manager._feature_flags",
EMBEDDED_SUPERSET=True,
)
def test_get_embedded_dashboard_rejects_bad_sec_fetch_dest(
client: FlaskClient[Any], # noqa: F811
):
dash = db.session.query(Dashboard).filter_by(slug="births").first()
embedded = EmbeddedDashboardDAO.upsert(dash, [])
db.session.flush()
uri = f"embedded/{embedded.uuid}"
# A non-embeddable destination (e.g. loaded via <img>/<script>) is rejected.
response = client.get(uri, headers={"Sec-Fetch-Dest": "image"})
assert response.status_code == 403
@pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
@mock.patch.dict(
"superset.extensions.feature_flag_manager._feature_flags",
EMBEDDED_SUPERSET=True,
)
def test_get_embedded_dashboard_allows_iframe_sec_fetch_dest(
client: FlaskClient[Any], # noqa: F811
):
dash = db.session.query(Dashboard).filter_by(slug="births").first()
embedded = EmbeddedDashboardDAO.upsert(dash, [])
db.session.flush()
uri = f"embedded/{embedded.uuid}"
response = client.get(uri, headers={"Sec-Fetch-Dest": "iframe"})
assert response.status_code == 200
@pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
@mock.patch.dict(
"superset.extensions.feature_flag_manager._feature_flags",
EMBEDDED_SUPERSET=True,
)
def test_get_embedded_dashboard_is_neutral_shell(client: FlaskClient[Any]): # noqa: F811
"""The pre-token page must not disclose dashboard metadata.
The Referer / Sec-Fetch-Dest checks are browser cooperation only -- a
non-browser client can forge them -- so anything rendered here is
effectively public. Title and description belong behind the
guest-token-authenticated API.
"""
dash = db.session.query(Dashboard).filter_by(slug="births").first()
dash.description = "internal-only dashboard description"
embedded = EmbeddedDashboardDAO.upsert(dash, [])
db.session.flush()
response = client.get(f"embedded/{embedded.uuid}")
assert response.status_code == 200
assert dash.dashboard_title.encode() not in response.data
assert b"internal-only dashboard description" not in response.data