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,