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

vincbeck 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 0ade0fba989 providers-fab: close sessions after users and roles 
collection reads (#72578)
0ade0fba989 is described below

commit 0ade0fba989cc39486024024feea785ae6ad389f
Author: MarthalaJagruthiReddy 
<[email protected]>
AuthorDate: Wed Sep 9 07:43:36 2026 -0700

    providers-fab: close sessions after users and roles collection reads 
(#72578)
    
    Use explicit non-scoped sessions for FAB users and roles collection queries 
so sync FastAPI worker threads close their own PostgreSQL transactions.
---
 .../fab/auth_manager/api_fastapi/services/roles.py | 21 +++++-----
 .../fab/auth_manager/api_fastapi/services/users.py | 45 +++++++++++-----------
 .../api_fastapi/services/test_roles.py             | 22 ++++++-----
 .../api_fastapi/services/test_users.py             | 17 ++++----
 4 files changed, 54 insertions(+), 51 deletions(-)

diff --git 
a/providers/fab/src/airflow/providers/fab/auth_manager/api_fastapi/services/roles.py
 
b/providers/fab/src/airflow/providers/fab/auth_manager/api_fastapi/services/roles.py
index 8d64a650da6..0cb782a7d0f 100644
--- 
a/providers/fab/src/airflow/providers/fab/auth_manager/api_fastapi/services/roles.py
+++ 
b/providers/fab/src/airflow/providers/fab/auth_manager/api_fastapi/services/roles.py
@@ -34,6 +34,7 @@ from 
airflow.providers.fab.auth_manager.api_fastapi.datamodels.roles import (
 from airflow.providers.fab.auth_manager.api_fastapi.sorting import 
build_ordering
 from airflow.providers.fab.auth_manager.models import Permission, Role
 from airflow.providers.fab.www.utils import get_fab_auth_manager
+from airflow.utils.session import create_session
 
 if TYPE_CHECKING:
     from airflow.providers.fab.auth_manager.security_manager.override import 
FabAirflowSecurityManagerOverride
@@ -87,20 +88,18 @@ class FABAuthManagerRoles:
 
     @classmethod
     def get_roles(cls, *, order_by: str, limit: int, offset: int) -> 
RoleCollectionResponse:
-        security_manager = get_fab_auth_manager().security_manager
-        session = security_manager.session
-
-        total_entries = session.scalars(select(func.count(Role.id))).one()
+        with create_session(scoped=False) as session:
+            total_entries = session.scalars(select(func.count(Role.id))).one()
 
-        ordering = build_ordering(order_by, allowed={"name": Role.name, 
"role_id": Role.id})
+            ordering = build_ordering(order_by, allowed={"name": Role.name, 
"role_id": Role.id})
 
-        stmt = select(Role).order_by(ordering).offset(offset).limit(limit)
-        roles = session.scalars(stmt).unique().all()
+            stmt = select(Role).order_by(ordering).offset(offset).limit(limit)
+            roles = session.scalars(stmt).unique().all()
 
-        return RoleCollectionResponse(
-            roles=[RoleResponse.model_validate(r) for r in roles],
-            total_entries=total_entries,
-        )
+            return RoleCollectionResponse(
+                roles=[RoleResponse.model_validate(r) for r in roles],
+                total_entries=total_entries,
+            )
 
     @classmethod
     def delete_role(cls, name: str) -> None:
diff --git 
a/providers/fab/src/airflow/providers/fab/auth_manager/api_fastapi/services/users.py
 
b/providers/fab/src/airflow/providers/fab/auth_manager/api_fastapi/services/users.py
index e63577ba453..8958219322d 100644
--- 
a/providers/fab/src/airflow/providers/fab/auth_manager/api_fastapi/services/users.py
+++ 
b/providers/fab/src/airflow/providers/fab/auth_manager/api_fastapi/services/users.py
@@ -31,6 +31,7 @@ from airflow.providers.fab.auth_manager.api_fastapi.sorting 
import build_orderin
 from airflow.providers.fab.auth_manager.models import User
 from airflow.providers.fab.auth_manager.security_manager.override import 
FabAirflowSecurityManagerOverride
 from airflow.providers.fab.www.utils import get_fab_auth_manager
+from airflow.utils.session import create_session
 
 
 class FABAuthManagerUsers:
@@ -66,31 +67,29 @@ class FABAuthManagerUsers:
     @classmethod
     def get_users(cls, *, order_by: str, limit: int, offset: int) -> 
UserCollectionResponse:
         """Get users with pagination and ordering."""
-        security_manager = get_fab_auth_manager().security_manager
-        session = security_manager.session
-
-        total_entries = session.scalars(select(func.count(User.id))).one()
-
-        ordering = build_ordering(
-            order_by,
-            allowed={
-                "id": User.id,
-                "user_id": User.id,
-                "first_name": User.first_name,
-                "last_name": User.last_name,
-                "username": User.username,
-                "email": User.email,
-                "active": User.active,
-            },
-        )
+        with create_session(scoped=False) as session:
+            total_entries = session.scalars(select(func.count(User.id))).one()
+
+            ordering = build_ordering(
+                order_by,
+                allowed={
+                    "id": User.id,
+                    "user_id": User.id,
+                    "first_name": User.first_name,
+                    "last_name": User.last_name,
+                    "username": User.username,
+                    "email": User.email,
+                    "active": User.active,
+                },
+            )
 
-        stmt = select(User).order_by(ordering).offset(offset).limit(limit)
-        users = session.scalars(stmt).unique().all()
+            stmt = select(User).order_by(ordering).offset(offset).limit(limit)
+            users = session.scalars(stmt).unique().all()
 
-        return UserCollectionResponse(
-            users=[UserResponse.model_validate(u) for u in users],
-            total_entries=total_entries,
-        )
+            return UserCollectionResponse(
+                users=[UserResponse.model_validate(u) for u in users],
+                total_entries=total_entries,
+            )
 
     @classmethod
     def create_user(cls, body: UserBody) -> UserResponse:
diff --git 
a/providers/fab/tests/unit/fab/auth_manager/api_fastapi/services/test_roles.py 
b/providers/fab/tests/unit/fab/auth_manager/api_fastapi/services/test_roles.py
index 9bdd0235417..7942570618d 100644
--- 
a/providers/fab/tests/unit/fab/auth_manager/api_fastapi/services/test_roles.py
+++ 
b/providers/fab/tests/unit/fab/auth_manager/api_fastapi/services/test_roles.py
@@ -186,7 +186,8 @@ class TestRolesService:
     # GET /roles
 
     
@patch("airflow.providers.fab.auth_manager.api_fastapi.services.roles.build_ordering")
-    def test_get_roles_happy_path(self, build_ordering, get_fab_auth_manager):
+    
@patch("airflow.providers.fab.auth_manager.api_fastapi.services.roles.create_session")
+    def test_get_roles_happy_path(self, create_session, build_ordering, 
get_fab_auth_manager):
         role1 = _make_role_obj("viewer", [("can_read", "DAG")])
         role2 = _make_role_obj("admin", [("can_read", "DAG")])
         fake_roles_result = _FakeScalarRoles([role1, role2])
@@ -196,10 +197,7 @@ class TestRolesService:
             _FakeScalarCount(2),
             fake_roles_result,
         ]
-
-        fab_auth_manager = MagicMock()
-        fab_auth_manager.security_manager = MagicMock(session=session)
-        get_fab_auth_manager.return_value = fab_auth_manager
+        create_session.return_value.__enter__.return_value = session
 
         build_ordering.return_value = column("name").desc()
 
@@ -215,19 +213,23 @@ class TestRolesService:
         assert set(kwargs["allowed"].keys()) == {"name", "role_id"}
 
         assert session.scalars.call_count == 2
+        create_session.assert_called_once_with(scoped=False)
+        create_session.return_value.__exit__.assert_called_once_with(None, 
None, None)
 
     
@patch("airflow.providers.fab.auth_manager.api_fastapi.services.roles.build_ordering")
-    def test_get_roles_invalid_order_by_bubbles_400(self, build_ordering, 
get_fab_auth_manager):
-        session = MagicMock()
-        fab_auth_manager = MagicMock()
-        fab_auth_manager.security_manager = MagicMock(session=session)
-        get_fab_auth_manager.return_value = fab_auth_manager
+    
@patch("airflow.providers.fab.auth_manager.api_fastapi.services.roles.create_session")
+    def test_get_roles_invalid_order_by_bubbles_400(
+        self, create_session, build_ordering, get_fab_auth_manager
+    ):
+        create_session.return_value.__enter__.return_value = MagicMock()
 
         build_ordering.side_effect = HTTPException(status_code=400, 
detail="disallowed")
 
         with pytest.raises(HTTPException) as ex:
             FABAuthManagerRoles.get_roles(order_by="nope", limit=10, offset=0)
         assert ex.value.status_code == 400
+        create_session.assert_called_once_with(scoped=False)
+        create_session.return_value.__exit__.assert_called_once()
 
     # DELETE /roles/{name}
 
diff --git 
a/providers/fab/tests/unit/fab/auth_manager/api_fastapi/services/test_users.py 
b/providers/fab/tests/unit/fab/auth_manager/api_fastapi/services/test_users.py
index 15511a3c2b8..84076a730e4 100644
--- 
a/providers/fab/tests/unit/fab/auth_manager/api_fastapi/services/test_users.py
+++ 
b/providers/fab/tests/unit/fab/auth_manager/api_fastapi/services/test_users.py
@@ -241,8 +241,9 @@ class TestUsersService:
 
     
@patch("airflow.providers.fab.auth_manager.api_fastapi.services.users.build_ordering")
     
@patch("airflow.providers.fab.auth_manager.api_fastapi.services.users.select")
+    
@patch("airflow.providers.fab.auth_manager.api_fastapi.services.users.create_session")
     def test_get_users_success(
-        self, mock_select, mock_build_ordering, get_fab_auth_manager, 
fab_auth_manager, security_manager
+        self, mock_create_session, mock_select, mock_build_ordering, 
get_fab_auth_manager
     ):
         user1 = _make_user_obj(
             username="alice", email="[email protected]", first_name="Alice", 
last_name="Liddell"
@@ -252,9 +253,7 @@ class TestUsersService:
         mock_session = MagicMock()
         mock_session.scalars.return_value.one.return_value = 2
         mock_session.scalars.return_value.unique.return_value.all.return_value 
= [user1, user2]
-        security_manager.session = mock_session
-        fab_auth_manager.security_manager = security_manager
-        get_fab_auth_manager.return_value = fab_auth_manager
+        mock_create_session.return_value.__enter__.return_value = mock_session
 
         mock_build_ordering.return_value = "ordering"
 
@@ -264,11 +263,13 @@ class TestUsersService:
         assert len(out.users) == 2
         assert out.users[0].username == "alice"
         assert out.users[1].username == "bob"
+        mock_create_session.assert_called_once_with(scoped=False)
+        
mock_create_session.return_value.__exit__.assert_called_once_with(None, None, 
None)
 
     
@patch("airflow.providers.fab.auth_manager.api_fastapi.services.users.build_ordering")
-    def test_get_users_invalid_order_by(
-        self, mock_build_ordering, get_fab_auth_manager, fab_auth_manager, 
security_manager
-    ):
+    
@patch("airflow.providers.fab.auth_manager.api_fastapi.services.users.create_session")
+    def test_get_users_invalid_order_by(self, mock_create_session, 
mock_build_ordering, get_fab_auth_manager):
+        mock_create_session.return_value.__enter__.return_value = MagicMock()
         mock_build_ordering.side_effect = HTTPException(
             status_code=400,
             detail="Ordering with 'invalid' is disallowed or the attribute 
does not exist on the model",
@@ -279,6 +280,8 @@ class TestUsersService:
         with pytest.raises(HTTPException) as ex:
             FABAuthManagerUsers.get_users(order_by="invalid", limit=10, 
offset=0)
         assert ex.value.status_code == 400
+        mock_create_session.assert_called_once_with(scoped=False)
+        mock_create_session.return_value.__exit__.assert_called_once()
 
     def test_update_user_success(self, get_fab_auth_manager, fab_auth_manager, 
security_manager):
         user_obj = _make_user_obj(

Reply via email to