This is an automated email from the ASF dual-hosted git repository.

alexandrusoare pushed a commit to branch 
alexandrusoare/fix/pending-rollback-chart-data-impersonation
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to 
refs/heads/alexandrusoare/fix/pending-rollback-chart-data-impersonation by this 
push:
     new 90f059418e7 fixing database jamming issues
90f059418e7 is described below

commit 90f059418e7d1e65bd19ca20b1277547830d1820
Author: alexandrusoare <[email protected]>
AuthorDate: Fri Sep 25 15:33:52 2026 +0300

    fixing database jamming issues
---
 superset/db_engine_specs/gsheets.py                | 12 ++--
 superset/db_engine_specs/snowflake.py              |  8 +--
 superset/db_engine_specs/starrocks.py              | 12 +---
 superset/models/core.py                            | 49 ++++++++++++--
 tests/unit_tests/db_engine_specs/test_gsheets.py   |  7 +-
 tests/unit_tests/db_engine_specs/test_starrocks.py | 74 +++++----------------
 tests/unit_tests/models/core_test.py               | 72 ++++++++++++++++++++
 tests/unit_tests/utils/test_database.py            | 76 +++++++++++++++++++++-
 8 files changed, 220 insertions(+), 90 deletions(-)

diff --git a/superset/db_engine_specs/gsheets.py 
b/superset/db_engine_specs/gsheets.py
index a75c46eac71..6b9642b283e 100644
--- a/superset/db_engine_specs/gsheets.py
+++ b/superset/db_engine_specs/gsheets.py
@@ -38,7 +38,7 @@ from sqlalchemy.engine import create_engine
 from sqlalchemy.engine.reflection import Inspector
 from sqlalchemy.engine.url import URL
 
-from superset import db, security_manager
+from superset import db
 from superset.databases.schemas import encrypted_field_properties, 
EncryptedString
 from superset.db_engine_specs.base import DatabaseCategory
 from superset.db_engine_specs.shillelagh import ShillelaghEngineSpec
@@ -249,9 +249,13 @@ class GSheetsEngineSpec(ShillelaghEngineSpec):
         engine_kwargs: dict[str, Any],
     ) -> tuple[URL, dict[str, Any]]:
         if username is not None:
-            user = security_manager.find_user(username=username)
-            if user and user.email:
-                url = url.update_query_dict({"subject": user.email})
+            # Resolved from the database rather than from ``username``: with
+            # ``IMPERSONATE_WITH_EMAIL_PREFIX`` enabled the caller has already
+            # substituted the email prefix into ``username``, so looking it up
+            # here as if it were still the login finds nothing whenever the two
+            # differ, silently leaving the subject unset.
+            if email := database.get_impersonation_email():
+                url = url.update_query_dict({"subject": email})
 
         if user_token:
             url = url.update_query_dict({"access_token": user_token})
diff --git a/superset/db_engine_specs/snowflake.py 
b/superset/db_engine_specs/snowflake.py
index 63ae8886228..53fdec2b85a 100644
--- a/superset/db_engine_specs/snowflake.py
+++ b/superset/db_engine_specs/snowflake.py
@@ -37,7 +37,7 @@ from sqlalchemy.exc import DatabaseError as 
SqlalchemyDatabaseError
 from sqlalchemy.sql import quoted_name
 from sqlalchemy.sql.elements import ColumnElement
 
-from superset import is_feature_enabled, security_manager
+from superset import is_feature_enabled
 from superset.constants import TimeGrain
 from superset.databases.utils import make_url_safe
 from superset.db_engine_specs.base import (
@@ -353,10 +353,8 @@ class SnowflakeEngineSpec(PostgresBaseEngineSpec):
                         # leaving the default/service-account username paired
                         # with this user's OAuth token. Use it as given.
                         url = url.set(username=username)
-                    else:
-                        user = security_manager.find_user(username=username)
-                        if user and user.email:
-                            url = url.set(username=user.email)
+                    elif email := database.get_impersonation_email():
+                        url = url.set(username=email)
 
                 url = url.update_query_dict({"token": user_token})
 
diff --git a/superset/db_engine_specs/starrocks.py 
b/superset/db_engine_specs/starrocks.py
index 9fcaab308d9..4ad6eba429f 100644
--- a/superset/db_engine_specs/starrocks.py
+++ b/superset/db_engine_specs/starrocks.py
@@ -28,11 +28,9 @@ from sqlalchemy.engine.url import URL
 from sqlalchemy.sql.elements import ColumnElement
 from sqlalchemy.sql.type_api import TypeEngine
 
-from superset import is_feature_enabled
 from superset.db_engine_specs.base import DatabaseCategory
 from superset.db_engine_specs.mysql import MySQLEngineSpec
 from superset.errors import SupersetErrorType
-from superset.extensions import security_manager
 from superset.models.core import Database
 from superset.utils.core import GenericDataType
 
@@ -426,15 +424,9 @@ class StarRocksEngineSpec(MySQLEngineSpec):
         For StarRocks with user impersonation enabled, returns an EXECUTE AS 
statement.
         """
         if database.impersonate_user:
-            username = database.get_effective_user(database.url_object)
-
-            if username:
-                effective_username = username
-                if is_feature_enabled("IMPERSONATE_WITH_EMAIL_PREFIX"):
-                    user = security_manager.find_user(username=username)
-                    if user and user.email:
-                        effective_username = user.email.split("@", 1)[0]
+            effective_username = database.get_impersonation_username()
 
+            if effective_username:
                 escaped = effective_username.replace('"', '""')
                 return [f'EXECUTE AS "{escaped}" WITH NO REVERT;']
 
diff --git a/superset/models/core.py b/superset/models/core.py
index 24ebfd5c735..65811d6794b 100755
--- a/superset/models/core.py
+++ b/superset/models/core.py
@@ -537,6 +537,49 @@ class Database(CoreDatabase, AuditMixinNullable, 
ImportExportMixin):  # pylint:
             else None
         )
 
+    def get_impersonation_email(self, object_url: URL | None = None) -> str | 
None:
+        """
+        Get the email address of the user being impersonated.
+
+        Resolves the effective login against the metadata database. DB engine
+        specs that need the email (or a part of it) to build a connection must
+        call this rather than looking the login up themselves: the lookup is a
+        metadata-DB read that can inherit a failed transaction from earlier in
+        the request, and centralising it keeps that handling in one place.
+
+        :param object_url: URL to read the login from; defaults to this
+            database's own URL
+        :return: The impersonated user's email, or ``None`` if there is no
+            effective user or the login has no email on record
+        """
+        username = self.get_effective_user(object_url or self.url_object)
+        if not username:
+            return None
+
+        user = find_user_for_impersonation(username)
+        return user.email if user and user.email else None
+
+    def get_impersonation_username(self, object_url: URL | None = None) -> str 
| None:
+        """
+        Get the username to impersonate on the analytic database.
+
+        With ``IMPERSONATE_WITH_EMAIL_PREFIX`` enabled this is the local part 
of
+        the user's email address; otherwise it is the effective login. Falls
+        back to the login when the flag is on but the user has no email on
+        record, matching the behaviour of a connection made without the flag.
+
+        :param object_url: URL to read the login from; defaults to this
+            database's own URL
+        :return: The username to connect as, or ``None`` if there is no
+            effective user
+        """
+        username = self.get_effective_user(object_url or self.url_object)
+        if not username or not 
is_feature_enabled("IMPERSONATE_WITH_EMAIL_PREFIX"):
+            return username
+
+        email = self.get_impersonation_email(object_url)
+        return email.split("@")[0] if email else username
+
     @contextmanager
     def get_sqla_engine(  # pylint: disable=too-many-arguments
         self,
@@ -662,11 +705,7 @@ class Database(CoreDatabase, AuditMixinNullable, 
ImportExportMixin):  # pylint:
         )
         engine_kwargs["connect_args"] = connect_args
 
-        effective_username = self.get_effective_user(sqlalchemy_url)
-        if effective_username and 
is_feature_enabled("IMPERSONATE_WITH_EMAIL_PREFIX"):
-            user = find_user_for_impersonation(effective_username)
-            if user and user.email:
-                effective_username = user.email.split("@")[0]
+        effective_username = self.get_impersonation_username(sqlalchemy_url)
 
         oauth2_config = self.get_oauth2_config()
         access_token = (
diff --git a/tests/unit_tests/db_engine_specs/test_gsheets.py 
b/tests/unit_tests/db_engine_specs/test_gsheets.py
index 5981caa85c6..5fadc258f27 100644
--- a/tests/unit_tests/db_engine_specs/test_gsheets.py
+++ b/tests/unit_tests/db_engine_specs/test_gsheets.py
@@ -571,13 +571,8 @@ def test_impersonate_user_username(mocker: MockerFixture) 
-> None:
     """
     from superset.db_engine_specs.gsheets import GSheetsEngineSpec
 
-    user = mocker.MagicMock()
-    user.email = "[email protected]"
-    mocker.patch(
-        "superset.db_engine_specs.gsheets.security_manager.find_user",
-        return_value=user,
-    )
     database = mocker.MagicMock()
+    database.get_impersonation_email.return_value = "[email protected]"
 
     assert GSheetsEngineSpec.impersonate_user(
         database,
diff --git a/tests/unit_tests/db_engine_specs/test_starrocks.py 
b/tests/unit_tests/db_engine_specs/test_starrocks.py
index 5590c9d70d7..5aa7b9ca8aa 100644
--- a/tests/unit_tests/db_engine_specs/test_starrocks.py
+++ b/tests/unit_tests/db_engine_specs/test_starrocks.py
@@ -35,7 +35,6 @@ from superset.db_engine_specs.starrocks import (
     TINYINT,
 )
 from superset.utils.core import GenericDataType
-from tests.unit_tests.conftest import with_feature_flags
 from tests.unit_tests.db_engine_specs.utils import assert_column_spec
 
 
@@ -158,7 +157,7 @@ def test_impersonation_username(mocker: MockerFixture) -> 
None:
 
     database = mocker.MagicMock()
     database.impersonate_user = True
-    database.get_effective_user.return_value = "alice"
+    database.get_impersonation_username.return_value = "alice"
 
     assert StarRocksEngineSpec.impersonate_user(
         database,
@@ -172,7 +171,9 @@ def test_impersonation_username(mocker: MockerFixture) -> 
None:
         'EXECUTE AS "alice" WITH NO REVERT;'
     ]
 
-    database.get_effective_user.return_value = 'evil" WITH NO REVERT; DROP 
TABLE x--'
+    database.get_impersonation_username.return_value = (
+        'evil" WITH NO REVERT; DROP TABLE x--'
+    )
     assert StarRocksEngineSpec.get_prequeries(database) == [
         'EXECUTE AS "evil"" WITH NO REVERT; DROP TABLE x--" WITH NO REVERT;'
     ]
@@ -297,74 +298,29 @@ def test_adjust_engine_params_with_catalog(
     assert returned_url.database == expected_database
 
 
-@with_feature_flags(IMPERSONATE_WITH_EMAIL_PREFIX=True)
-def test_get_prequeries_with_email_prefix(mocker: MockerFixture) -> None:
-    """Test that get_prequeries uses email prefix when 
IMPERSONATE_WITH_EMAIL_PREFIX"""
-    from superset.db_engine_specs.starrocks import StarRocksEngineSpec
-
-    user = mocker.MagicMock()
-    user.email = "[email protected]"
-    mocker.patch(
-        "superset.db_engine_specs.starrocks.security_manager.find_user",
-        return_value=user,
-    )
-
-    database = mocker.MagicMock()
-    database.impersonate_user = True
-    database.url_object = make_url("starrocks://localhost:9030/")
-    database.get_effective_user.return_value = "[email protected]"
-
-    assert StarRocksEngineSpec.get_prequeries(database) == [
-        'EXECUTE AS "alice" WITH NO REVERT;'
-    ]
-
-
-@with_feature_flags(IMPERSONATE_WITH_EMAIL_PREFIX=True)
-def test_get_prequeries_with_email_prefix_dotted_local_part(
+def test_get_prequeries_defers_impersonation_resolution(
     mocker: MockerFixture,
 ) -> None:
-    """Test that get_prequeries uses email prefix when 
IMPERSONATE_WITH_EMAIL_PREFIX"""
-    from superset.db_engine_specs.starrocks import StarRocksEngineSpec
-
-    user = mocker.MagicMock()
-    user.email = "[email protected]"
-    mocker.patch(
-        "superset.db_engine_specs.starrocks.security_manager.find_user",
-        return_value=user,
-    )
-
-    database = mocker.MagicMock()
-    database.impersonate_user = True
-    database.url_object = make_url("starrocks://localhost:9030/")
-    database.get_effective_user.return_value = "[email protected]"
-
-    assert StarRocksEngineSpec.get_prequeries(database) == [
-        'EXECUTE AS "alice.doe" WITH NO REVERT;'
-    ]
-
+    """
+    Test that `get_prequeries` impersonates whoever `Database` resolves.
 
-@with_feature_flags(IMPERSONATE_WITH_EMAIL_PREFIX=True)
-def 
test_get_prequeries_with_email_prefix_from_user_email_when_effective_user_differs(
-    mocker: MockerFixture,
-) -> None:
-    """Use looked-up user.email local part when effective username is 
different."""
+    Deriving the name here instead (re-reading the effective user and looking 
up
+    its email) would duplicate work `Database._get_sqla_engine` has already 
done
+    for the same connection, and would put a second unguarded metadata-DB read
+    on the query path. The prefix substitution itself is covered by the
+    `Database.get_impersonation_username` tests.
+    """
     from superset.db_engine_specs.starrocks import StarRocksEngineSpec
 
-    user = mocker.MagicMock()
-    user.email = "[email protected]"
-    mocker.patch(
-        "superset.db_engine_specs.starrocks.security_manager.find_user",
-        return_value=user,
-    )
-
     database = mocker.MagicMock()
     database.impersonate_user = True
     database.url_object = make_url("starrocks://localhost:9030/")
-    database.get_effective_user.return_value = "alice"
+    database.get_impersonation_username.return_value = "alice.doe"
 
     assert StarRocksEngineSpec.get_prequeries(database) == [
         'EXECUTE AS "alice.doe" WITH NO REVERT;'
     ]
+    database.get_effective_user.assert_not_called()
 
 
 def test_time_grain_expressions_inherit_mysql() -> None:
diff --git a/tests/unit_tests/models/core_test.py 
b/tests/unit_tests/models/core_test.py
index c614e44f401..6a8a7e8150b 100644
--- a/tests/unit_tests/models/core_test.py
+++ b/tests/unit_tests/models/core_test.py
@@ -844,6 +844,78 @@ def test_get_sqla_engine_user_impersonation_email(mocker: 
MockerFixture) -> None
     )
 
 
+@with_feature_flags(IMPERSONATE_WITH_EMAIL_PREFIX=True)
+def test_get_impersonation_username_uses_email_prefix(mocker: MockerFixture) 
-> None:
+    """
+    Test that the impersonated username is the local part of the user's email.
+
+    The login and the email prefix commonly differ, so the lookup result is
+    used rather than the login it was resolved from.
+    """
+    user = mocker.MagicMock()
+    user.email = "[email protected]"
+    mocker.patch(
+        "superset.models.core.find_user_for_impersonation",
+        return_value=user,
+    )
+    mocker.patch("superset.models.core.get_username", return_value="alice")
+
+    database = Database(
+        database_name="my_db",
+        sqlalchemy_uri="trino://",
+        impersonate_user=True,
+    )
+
+    assert database.get_impersonation_username() == "alice.doe"
+    assert database.get_impersonation_email() == "[email protected]"
+
+
+@with_feature_flags(IMPERSONATE_WITH_EMAIL_PREFIX=True)
+def test_get_impersonation_username_without_email(mocker: MockerFixture) -> 
None:
+    """
+    Test that a user with no email on record falls back to the login.
+
+    This matches how the connection would be made with the flag off, rather
+    than impersonating nobody.
+    """
+    user = mocker.MagicMock()
+    user.email = None
+    mocker.patch(
+        "superset.models.core.find_user_for_impersonation",
+        return_value=user,
+    )
+    mocker.patch("superset.models.core.get_username", return_value="alice")
+
+    database = Database(
+        database_name="my_db",
+        sqlalchemy_uri="trino://",
+        impersonate_user=True,
+    )
+
+    assert database.get_impersonation_username() == "alice"
+    assert database.get_impersonation_email() is None
+
+
+def test_get_impersonation_username_without_flag(mocker: MockerFixture) -> 
None:
+    """
+    Test that the login is used verbatim when the flag is off.
+
+    No lookup should happen at all: it would be a metadata-DB read on the query
+    path whose result is then discarded.
+    """
+    find_user = 
mocker.patch("superset.models.core.find_user_for_impersonation")
+    mocker.patch("superset.models.core.get_username", return_value="alice")
+
+    database = Database(
+        database_name="my_db",
+        sqlalchemy_uri="trino://",
+        impersonate_user=True,
+    )
+
+    assert database.get_impersonation_username() == "alice"
+    find_user.assert_not_called()
+
+
 def test_get_sqla_engine_registers_prequery_event_listener(
     app_context: None,
     mocker: MockerFixture,
diff --git a/tests/unit_tests/utils/test_database.py 
b/tests/unit_tests/utils/test_database.py
index 22bea17765e..3743938a417 100644
--- a/tests/unit_tests/utils/test_database.py
+++ b/tests/unit_tests/utils/test_database.py
@@ -17,12 +17,15 @@
 """Tests for superset.utils.database module."""
 
 import pytest
+from pytest_mock import MockerFixture
 from sqlalchemy import Sequence
 from sqlalchemy.dialects import mysql, postgresql
+from sqlalchemy.exc import PendingRollbackError
 from sqlalchemy.schema import CreateSequence
 from sqlalchemy.sql.compiler import DDLCompiler
 
-from superset.utils.database import apply_mariadb_ddl_fix
+from superset.exceptions import SupersetErrorException
+from superset.utils.database import apply_mariadb_ddl_fix, 
find_user_for_impersonation
 
 
 @pytest.fixture(scope="module", autouse=True)
@@ -51,3 +54,74 @@ def test_nocycle_fix_not_applied_for_postgresql():
 
     result = compiler.visit_create_sequence(CreateSequence(seq))
     assert "NO CYCLE" in result
+
+
+def test_find_user_for_impersonation(mocker: MockerFixture) -> None:
+    """Test that a healthy session resolves the user without rolling back."""
+    user = mocker.MagicMock()
+    find_user = mocker.patch(
+        "superset.extensions.security_manager.find_user",
+        return_value=user,
+    )
+    session = mocker.patch("superset.db.session")
+
+    assert find_user_for_impersonation("alice") is user
+    find_user.assert_called_once_with(username="alice")
+    session.rollback.assert_not_called()
+
+
+def test_find_user_for_impersonation_retries_after_rollback(
+    mocker: MockerFixture,
+) -> None:
+    """
+    Test that a poisoned session is rolled back and the lookup retried.
+
+    An earlier failure in the same request leaves ``db.session`` in a failed
+    transaction, so this lookup reports ``PendingRollbackError`` for a fault
+    that has nothing to do with it. Rolling back makes the session usable
+    again, and the retry returns the user the caller asked for.
+    """
+    user = mocker.MagicMock()
+    find_user = mocker.patch(
+        "superset.extensions.security_manager.find_user",
+        side_effect=[PendingRollbackError("poisoned"), user],
+    )
+    session = mocker.patch("superset.db.session")
+
+    assert find_user_for_impersonation("alice") is user
+    session.rollback.assert_called_once()
+    assert find_user.call_count == 2
+
+
+def test_find_user_for_impersonation_raises_when_retry_fails(
+    mocker: MockerFixture,
+) -> None:
+    """
+    Test that a lookup failing past the retry raises rather than falling back.
+
+    The resolved name becomes the identity the analytic database connects as,
+    so degrading to the un-resolved login would silently run the query as a
+    different principal. Failing loudly is the safe outcome.
+    """
+    mocker.patch(
+        "superset.extensions.security_manager.find_user",
+        side_effect=PendingRollbackError("still poisoned"),
+    )
+    session = mocker.patch("superset.db.session")
+
+    with pytest.raises(SupersetErrorException):
+        find_user_for_impersonation("alice")
+
+    session.rollback.assert_called_once()
+
+
+def test_find_user_for_impersonation_unknown_login(mocker: MockerFixture) -> 
None:
+    """Test that an unknown login is reported as absent, not as a failure."""
+    find_user = mocker.patch(
+        "superset.extensions.security_manager.find_user",
+        return_value=None,
+    )
+    mocker.patch("superset.db.session")
+
+    assert find_user_for_impersonation("nobody") is None
+    find_user.assert_called_once_with(username="nobody")

Reply via email to