You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ru...@apache.org on 2026/08/10 21:18:35 UTC
(superset) branch master updated: fix: hydrate JWT user before list API filters (#39946)
This is an automated email from the ASF dual-hosted git repository.
rusackas pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new 2eb1e042836 fix: hydrate JWT user before list API filters (#39946)
2eb1e042836 is described below
commit 2eb1e042836c73f6b6661e6c65577442061d9d72
Author: Mike Ma <mi...@lmctl.com>
AuthorDate: Mon Aug 10 16:18:28 2026 -0500
fix: hydrate JWT user before list API filters (#39946)
Co-authored-by: Evan Rusackas <ev...@preset.io>
---
superset/security/manager.py | 21 +++++++++++-
tests/integration_tests/base_tests.py | 11 ++++++
tests/integration_tests/databases/api_tests.py | 47 +++++++++++++++++++++++++-
tests/integration_tests/datasets/api_tests.py | 40 +++++++++++++++++++++-
tests/unit_tests/security/manager_test.py | 27 +++++++++++++++
5 files changed, 143 insertions(+), 3 deletions(-)
diff --git a/superset/security/manager.py b/superset/security/manager.py
index 5dc044d4a89..233ee32ca38 100644
--- a/superset/security/manager.py
+++ b/superset/security/manager.py
@@ -60,6 +60,7 @@ from flask_appbuilder.security.views import (
ViewMenuModelView,
)
from flask_babel import lazy_gettext as _
+from flask_jwt_extended import get_jwt_identity, verify_jwt_in_request
from flask_login import AnonymousUserMixin, LoginManager
from jwt.api_jwt import _jwt_global_obj
from sqlalchemy import and_, func as sa_func, inspect, or_
@@ -1615,7 +1616,25 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
from superset.extensions import feature_flag_manager
if feature_flag_manager.is_feature_enabled("EMBEDDED_SUPERSET"):
- return self.get_guest_user_from_request(request)
+ raw_guest_token = request.headers.get(
+ get_conf()["GUEST_TOKEN_HEADER_NAME"]
+ ) or request.form.get("guest_token")
+ if guest_user := self.get_guest_user_from_request(request):
+ return guest_user
+ if raw_guest_token is not None:
+ # Keep invalid guest tokens from falling through to Bearer auth here.
+ return None
+
+ if request.headers.get("Authorization", "").lower().startswith("bearer "):
+ try:
+ verify_jwt_in_request()
+ user = self.load_user(get_jwt_identity())
+ except Exception: # pylint: disable=broad-except
+ return None
+ if user is None:
+ return None
+ g.user = user
+ return user
return None
def get_catalog_perm(
diff --git a/tests/integration_tests/base_tests.py b/tests/integration_tests/base_tests.py
index d8e4ca1afaa..aaf82ff1449 100644
--- a/tests/integration_tests/base_tests.py
+++ b/tests/integration_tests/base_tests.py
@@ -334,6 +334,17 @@ class SupersetTestCase(TestCase):
def login(self, username, password=DEFAULT_PASSWORD):
return login(self.client, username, password)
+ def get_bearer_auth_header(
+ self, username: str = ADMIN_USERNAME, password: str = DEFAULT_PASSWORD
+ ) -> dict[str, str]:
+ """Return an Authorization header with a login access token."""
+ response = self.client.post(
+ "/api/v1/security/login",
+ json={"username": username, "password": password, "provider": "db"},
+ )
+ assert response.status_code == 200
+ return {"Authorization": f"Bearer {response.json['access_token']}"}
+
def get_slice(self, slice_name: str) -> Slice:
return db.session.query(Slice).filter_by(slice_name=slice_name).one()
diff --git a/tests/integration_tests/databases/api_tests.py b/tests/integration_tests/databases/api_tests.py
index 76a2517e814..73f6f69a417 100644
--- a/tests/integration_tests/databases/api_tests.py
+++ b/tests/integration_tests/databases/api_tests.py
@@ -47,8 +47,9 @@ from superset.db_engine_specs.hana import HanaEngineSpec
from superset.errors import SupersetError
from superset.models.core import Database, ConfigurationMethod
from superset.reports.models import ReportSchedule, ReportScheduleType
-from superset.utils.database import get_example_database, get_main_database
from superset.utils import json
+from superset.utils.core import shortid
+from superset.utils.database import get_example_database, get_main_database
from tests.conftest import with_config
from tests.integration_tests.base_tests import SupersetTestCase
from tests.integration_tests.constants import ADMIN_USERNAME, GAMMA_USERNAME
@@ -207,6 +208,50 @@ class TestDatabaseApi(SupersetTestCase):
assert response["count"] > 0
assert list(response["result"][0].keys()) == expected_columns
+ def test_get_items_with_jwt_auth(self):
+ """
+ Database API: Test get items with JWT authentication
+ """
+ example_db = get_example_database()
+ test_database = self.insert_database(
+ f"jwt-test-database-{shortid()}",
+ example_db.sqlalchemy_uri_decrypted,
+ expose_in_sqllab=True,
+ )
+ headers = self.get_bearer_auth_header()
+
+ try:
+ client = self.create_app().test_client()
+ arguments = {
+ "filters": [
+ {
+ "col": "database_name",
+ "opr": "eq",
+ "value": test_database.database_name,
+ }
+ ]
+ }
+ uri = f"api/v1/database/?q={rison.dumps(arguments)}"
+ rv = client.get(uri, headers=headers)
+ assert rv.status_code == 200
+ response = json.loads(rv.data.decode("utf-8"))
+ assert response["count"] == 1
+ assert response["result"][0]["id"] == test_database.id
+ finally:
+ db.session.delete(test_database)
+ db.session.commit()
+
+ def test_get_items_with_invalid_jwt_auth(self):
+ """
+ Database API: Test get items with invalid JWT authentication
+ """
+ client = self.create_app().test_client()
+ rv = client.get(
+ "api/v1/database/",
+ headers={"Authorization": "Bearer not-a-token"},
+ )
+ assert rv.status_code == 422
+
def test_get_items_filter(self):
"""
Database API: Test get items with filter
diff --git a/tests/integration_tests/datasets/api_tests.py b/tests/integration_tests/datasets/api_tests.py
index 75406921be7..56f3e38efd7 100644
--- a/tests/integration_tests/datasets/api_tests.py
+++ b/tests/integration_tests/datasets/api_tests.py
@@ -41,7 +41,7 @@ from superset.models.slice import Slice
from superset.subjects.models import Subject
from superset.subjects.types import SubjectType
from superset.utils import json
-from superset.utils.core import backend, get_example_default_schema
+from superset.utils.core import backend, get_example_default_schema, shortid
from superset.utils.database import get_example_database, get_main_database
from superset.utils.dict_import_export import export_to_dict
from tests.integration_tests.base_tests import (
@@ -330,6 +330,44 @@ class TestDatasetApi(SupersetTestCase):
]
assert sorted(response["result"][0]) == expected_columns
+ def test_get_dataset_list_with_jwt_auth(self):
+ """
+ Dataset API: Test get dataset list with JWT authentication
+ """
+ database = self.insert_database(f"jwt_dataset_db_{shortid()}")
+ dataset = self.insert_dataset(
+ f"jwt_dataset_{shortid()}",
+ [self.get_user("admin").id],
+ database,
+ fetch_metadata=False,
+ )
+ headers = self.get_bearer_auth_header()
+
+ try:
+ client = self.create_app().test_client()
+ arguments = {"filters": [{"col": "id", "opr": "eq", "value": dataset.id}]}
+ uri = f"api/v1/dataset/?q={rison.dumps(arguments)}"
+ rv = client.get(uri, headers=headers)
+ assert rv.status_code == 200
+ response = json.loads(rv.data.decode("utf-8"))
+ assert response["count"] == 1
+ assert response["result"][0]["id"] == dataset.id
+ finally:
+ db.session.delete(dataset)
+ db.session.delete(database)
+ db.session.commit()
+
+ def test_get_dataset_list_with_invalid_jwt_auth(self):
+ """
+ Dataset API: Test get dataset list with invalid JWT authentication
+ """
+ client = self.create_app().test_client()
+ rv = client.get(
+ "api/v1/dataset/",
+ headers={"Authorization": "Bearer not-a-token"},
+ )
+ assert rv.status_code == 422
+
def test_get_dataset_list_gamma(self):
"""
Dataset API: Test get dataset list gamma
diff --git a/tests/unit_tests/security/manager_test.py b/tests/unit_tests/security/manager_test.py
index b940225c6cb..4003cec1872 100644
--- a/tests/unit_tests/security/manager_test.py
+++ b/tests/unit_tests/security/manager_test.py
@@ -24,6 +24,7 @@ from typing import Any, Optional
from unittest.mock import MagicMock
import pytest
+from flask import current_app
from flask_appbuilder.security.sqla.models import Role, User
from pytest_mock import MockerFixture
@@ -3283,6 +3284,32 @@ def test_user_view_menu_names_for_guest_user_no_roles(
mock_get_user_id.assert_not_called()
+def test_request_loader_rejects_invalid_guest_token_before_bearer(
+ mocker: MockerFixture,
+ app_context: None,
+) -> None:
+ """
+ Invalid guest tokens must not fall through to Bearer JWT auth.
+ """
+ sm = SupersetSecurityManager(appbuilder)
+ header_name = current_app.config["GUEST_TOKEN_HEADER_NAME"]
+ request = SimpleNamespace(
+ headers={header_name: "invalid-guest-token", "Authorization": "Bearer valid"},
+ form={},
+ )
+
+ mocker.patch(
+ "superset.extensions.feature_flag_manager.is_feature_enabled",
+ return_value=True,
+ )
+ mocker.patch.object(sm, "get_guest_user_from_request", return_value=None)
+ verify_jwt = mocker.patch("superset.security.manager.verify_jwt_in_request")
+
+ assert sm.request_loader(request) is None
+
+ verify_jwt.assert_not_called()
+
+
def test_reset_password_self_service_clears_flag(
mocker: MockerFixture,
app_context: None,