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

potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new ff601cb5b75 Compare session and user identifiers consistently when 
resetting a password (#72198)
ff601cb5b75 is described below

commit ff601cb5b75e77c1f28aaf014914f4e9d5cb0947
Author: Jarek Potiuk <[email protected]>
AuthorDate: Fri Aug 28 22:49:17 2026 +0200

    Compare session and user identifiers consistently when resetting a password 
(#72198)
    
    reset_user_sessions() decoded each stored session and compared its _user_id
    against user.id directly. Flask-Login stores whatever User.get_id() returns,
    which is a string, while user.id is the integer column, so the comparison 
was
    never true and no session was ever deleted. Both sides are now compared as
    strings, and a session row without a _user_id is skipped rather than 
matched.
    
    The existing test passed because its fixture wrote an integer _user_id into
    the session row by hand, exercising a comparison that does not occur in
    practice. It now stores what get_id() returns. Added coverage for sessions
    written before get_id() returned a string, and for a row carrying no
    _user_id at all.
---
 .../fab/auth_manager/security_manager/override.py  |  7 +++-
 .../fab/www/views/test_views_custom_user_views.py  | 48 ++++++++++++++++++----
 2 files changed, 47 insertions(+), 8 deletions(-)

diff --git 
a/providers/fab/src/airflow/providers/fab/auth_manager/security_manager/override.py
 
b/providers/fab/src/airflow/providers/fab/auth_manager/security_manager/override.py
index 7e9ced387cf..2bb4d6ea53c 100644
--- 
a/providers/fab/src/airflow/providers/fab/auth_manager/security_manager/override.py
+++ 
b/providers/fab/src/airflow/providers/fab/auth_manager/security_manager/override.py
@@ -619,7 +619,12 @@ class 
FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
             else:
                 for s in session.scalars(select(user_session_model)).all():
                     session_details = 
interface.serializer.decode(want_bytes(s.data))
-                    if session_details.get("_user_id") == user.id:
+                    session_user_id = session_details.get("_user_id")
+                    # Flask-Login stores whatever ``User.get_id()`` returns, 
which is a
+                    # string, while ``user.id`` is the integer column. Compare 
both sides
+                    # as strings so the two representations match; older 
sessions written
+                    # before ``get_id()`` returned a string are still handled.
+                    if session_user_id is not None and str(session_user_id) == 
str(user.id):
                         session.delete(s)
                 session.commit()
         else:
diff --git 
a/providers/fab/tests/unit/fab/www/views/test_views_custom_user_views.py 
b/providers/fab/tests/unit/fab/www/views/test_views_custom_user_views.py
index 7e61cdff785..2cdc69bf834 100644
--- a/providers/fab/tests/unit/fab/www/views/test_views_custom_user_views.py
+++ b/providers/fab/tests/unit/fab/www/views/test_views_custom_user_views.py
@@ -259,7 +259,7 @@ class TestResetUserSessions:
             delete_user(app, "user_to_delete_1")
             delete_user(app, "user_to_delete_2")
 
-    def create_user_db_session(self, session_id: str, time_delta: timedelta, 
user_id: int):
+    def create_user_db_session(self, session_id: str, time_delta: timedelta, 
user_id: str | int):
         self.session.add(
             self.model(
                 session_id=session_id,
@@ -277,8 +277,8 @@ class TestResetUserSessions:
         ],
     )
     def test_reset_user_sessions_delete(self, time_delta: timedelta, 
user_sessions_deleted: bool):
-        self.create_user_db_session("session_id_1", time_delta, self.user_1.id)
-        self.create_user_db_session("session_id_2", time_delta, self.user_2.id)
+        self.create_user_db_session("session_id_1", time_delta, 
self.user_1.get_id())
+        self.create_user_db_session("session_id_2", time_delta, 
self.user_2.get_id())
         self.session.commit()
         self.session.flush()
         assert 
self.session.scalar(select(func.count()).select_from(self.model)) == 2
@@ -296,6 +296,40 @@ class TestResetUserSessions:
             assert 
self.session.scalar(select(func.count()).select_from(self.model)) == 2
             assert self.get_session_by_id("session_id_1") is not None
 
+    def test_reset_user_sessions_delete_legacy_integer_user_id(self):
+        """Sessions written before ``get_id()`` returned a string stored an 
int."""
+        self.create_user_db_session("session_id_1", timedelta(days=1), 
self.user_1.id)
+        self.create_user_db_session("session_id_2", timedelta(days=1), 
self.user_2.id)
+        self.session.commit()
+        self.session.flush()
+
+        with self.app.app_context():
+            self.security_manager.reset_password(self.user_1.id, 
"new_password")
+        self.session.commit()
+        self.session.flush()
+
+        assert self.get_session_by_id("session_id_1") is None
+        assert self.get_session_by_id("session_id_2") is not None
+
+    def test_reset_user_sessions_ignores_sessions_without_user_id(self):
+        """A session row with no ``_user_id`` must not raise and must not be 
deleted."""
+        self.session.add(
+            self.model(
+                session_id="session_id_anon",
+                data=self.serializer.encode({}),
+                expiry=datetime.now() + timedelta(days=1),
+            )
+        )
+        self.session.commit()
+        self.session.flush()
+
+        with self.app.app_context():
+            self.security_manager.reset_password(self.user_1.id, 
"new_password")
+        self.session.commit()
+        self.session.flush()
+
+        assert self.get_session_by_id("session_id_anon") is not None
+
     def get_session_by_id(self, session_id: str):
         return 
self.session.scalar(select(self.model).where(self.model.session_id == 
session_id))
 
@@ -307,8 +341,8 @@ class TestResetUserSessions:
         
"airflow.providers.fab.auth_manager.security_manager.override.MAX_NUM_DATABASE_USER_SESSIONS",
 1
     )
     def test_refuse_delete(self, _mock_has_context, flash_mock):
-        self.create_user_db_session("session_id_1", timedelta(days=1), 
self.user_1.id)
-        self.create_user_db_session("session_id_2", timedelta(days=1), 
self.user_2.id)
+        self.create_user_db_session("session_id_1", timedelta(days=1), 
self.user_1.get_id())
+        self.create_user_db_session("session_id_2", timedelta(days=1), 
self.user_2.get_id())
         self.session.commit()
         self.session.flush()
         assert 
self.session.scalar(select(func.count()).select_from(self.model)) == 2
@@ -344,8 +378,8 @@ class TestResetUserSessions:
         
"airflow.providers.fab.auth_manager.security_manager.override.MAX_NUM_DATABASE_USER_SESSIONS",
 1
     )
     def test_refuse_delete_cli(self, log_mock):
-        self.create_user_db_session("session_id_1", timedelta(days=1), 
self.user_1.id)
-        self.create_user_db_session("session_id_2", timedelta(days=1), 
self.user_2.id)
+        self.create_user_db_session("session_id_1", timedelta(days=1), 
self.user_1.get_id())
+        self.create_user_db_session("session_id_2", timedelta(days=1), 
self.user_2.get_id())
         self.session.commit()
         self.session.flush()
         assert 
self.session.scalar(select(func.count()).select_from(self.model)) == 2

Reply via email to