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

jscheffl 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 e38f8099c7e fix(providers/fab): prevent AppBuilder re-initialization 
race in FastAPI login (#64151) (#64418)
e38f8099c7e is described below

commit e38f8099c7e027479208130c51d17cd3cf8aa60d
Author: Pradeep Kalluri <[email protected]>
AuthorDate: Sat Apr 4 12:45:49 2026 -0700

    fix(providers/fab): prevent AppBuilder re-initialization race in FastAPI 
login (#64151) (#64418)
---
 .../openapi/v2-fab-auth-manager-generated.yaml     |  2 +-
 .../fab/auth_manager/api_fastapi/routes/login.py   | 31 +++++++++++++++++-----
 .../providers/fab/auth_manager/fab_auth_manager.py |  4 +++
 .../auth_manager/api_fastapi/routes/test_login.py  | 16 ++++++++---
 4 files changed, 42 insertions(+), 11 deletions(-)

diff --git 
a/providers/fab/src/airflow/providers/fab/auth_manager/api_fastapi/openapi/v2-fab-auth-manager-generated.yaml
 
b/providers/fab/src/airflow/providers/fab/auth_manager/api_fastapi/openapi/v2-fab-auth-manager-generated.yaml
index 5d81b0b416f..f56de9cccb6 100644
--- 
a/providers/fab/src/airflow/providers/fab/auth_manager/api_fastapi/openapi/v2-fab-auth-manager-generated.yaml
+++ 
b/providers/fab/src/airflow/providers/fab/auth_manager/api_fastapi/openapi/v2-fab-auth-manager-generated.yaml
@@ -91,7 +91,7 @@ paths:
       tags:
       - FabAuthManager
       summary: Logout
-      description: Generate a new API token.
+      description: Clear session cookies and redirect to the login page.
       operationId: logout
       responses:
         '307':
diff --git 
a/providers/fab/src/airflow/providers/fab/auth_manager/api_fastapi/routes/login.py
 
b/providers/fab/src/airflow/providers/fab/auth_manager/api_fastapi/routes/login.py
index 168b9476d80..a59dfa0372f 100644
--- 
a/providers/fab/src/airflow/providers/fab/auth_manager/api_fastapi/routes/login.py
+++ 
b/providers/fab/src/airflow/providers/fab/auth_manager/api_fastapi/routes/login.py
@@ -18,7 +18,7 @@ from __future__ import annotations
 
 from typing import Any
 
-from fastapi import Body, Request, status
+from fastapi import Body, HTTPException, Request, status
 from fastapi.responses import RedirectResponse
 
 from airflow.api_fastapi.app import get_auth_manager
@@ -28,7 +28,6 @@ from airflow.providers.common.compat.sdk import conf
 from airflow.providers.fab.auth_manager.api_fastapi.datamodels.login import 
LoginResponse
 from airflow.providers.fab.auth_manager.api_fastapi.routes.router import 
auth_router
 from airflow.providers.fab.auth_manager.api_fastapi.services.login import 
FABAuthManagerLogin
-from airflow.providers.fab.auth_manager.cli_commands.utils import 
get_application_builder
 from airflow.providers.fab.version_compat import AIRFLOW_V_3_1_8_PLUS
 
 if AIRFLOW_V_3_1_8_PLUS:
@@ -37,6 +36,26 @@ else:
     get_cookie_path = lambda: "/"
 
 
+def _get_flask_app():
+    from airflow.providers.fab.auth_manager.fab_auth_manager import 
FabAuthManager
+
+    auth_manager = get_auth_manager()
+    if not isinstance(auth_manager, FabAuthManager):
+        raise HTTPException(
+            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+            detail=(
+                "FabAuthManager is not configured as the auth manager. "
+                "Ensure AUTH_MANAGER is set to FabAuthManager in your Airflow 
configuration."
+            ),
+        )
+    if not auth_manager.flask_app:
+        raise HTTPException(
+            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+            detail="Flask app is not initialized. Check that FabAuthManager 
started up correctly.",
+        )
+    return auth_manager.flask_app
+
+
 @auth_router.post(
     "/token",
     response_model=LoginResponse,
@@ -45,7 +64,7 @@ else:
 )
 def create_token(request: Request, body: dict[str, Any] = Body(...)) -> 
LoginResponse:
     """Generate a new API token."""
-    with get_application_builder():
+    with _get_flask_app().app_context():
         return FABAuthManagerLogin.create_token(headers=dict(request.headers), 
body=body)
 
 
@@ -57,7 +76,7 @@ def create_token(request: Request, body: dict[str, Any] = 
Body(...)) -> LoginRes
 )
 def create_token_cli(request: Request, body: dict[str, Any] = Body(...)) -> 
LoginResponse:
     """Generate a new CLI API token."""
-    with get_application_builder():
+    with _get_flask_app().app_context():
         return FABAuthManagerLogin.create_token(
             headers=dict(request.headers),
             body=body,
@@ -70,8 +89,8 @@ def create_token_cli(request: Request, body: dict[str, Any] = 
Body(...)) -> Logi
     status_code=status.HTTP_307_TEMPORARY_REDIRECT,
 )
 def logout(request: Request) -> RedirectResponse:
-    """Generate a new API token."""
-    with get_application_builder():
+    """Clear session cookies and redirect to the login page."""
+    with _get_flask_app().app_context():
         login_url = get_auth_manager().get_url_login()
         secure = request.base_url.scheme == "https" or bool(conf.get("api", 
"ssl_cert", fallback=""))
         cookie_path = get_cookie_path()
diff --git 
a/providers/fab/src/airflow/providers/fab/auth_manager/fab_auth_manager.py 
b/providers/fab/src/airflow/providers/fab/auth_manager/fab_auth_manager.py
index 1e12b6658a7..fa8fe2a9edf 100644
--- a/providers/fab/src/airflow/providers/fab/auth_manager/fab_auth_manager.py
+++ b/providers/fab/src/airflow/providers/fab/auth_manager/fab_auth_manager.py
@@ -91,6 +91,8 @@ from airflow.providers.fab.www.utils import 
get_fab_action_from_method_map
 from airflow.utils.session import NEW_SESSION, provide_session
 
 if TYPE_CHECKING:
+    from flask import Flask
+
     from airflow.api_fastapi.auth.managers.base_auth_manager import 
ResourceMethod
     from airflow.cli.cli_config import (
         CLICommand,
@@ -174,6 +176,7 @@ class FabAuthManager(BaseAuthManager[User]):
 
     cache: TTLCache = TTLCache(maxsize=1024, ttl=CACHE_TTL)
     appbuilder: AirflowAppBuilder | None = None
+    flask_app: Flask | None = None
 
     def init_flask_resources(self) -> None:
         self._sync_appbuilder_roles()
@@ -198,6 +201,7 @@ class FabAuthManager(BaseAuthManager[User]):
         )
 
         flask_app = create_app(enable_plugins=False)
+        self.flask_app = flask_app
 
         app = FastAPI(
             title="FAB auth manager API",
diff --git 
a/providers/fab/tests/unit/fab/auth_manager/api_fastapi/routes/test_login.py 
b/providers/fab/tests/unit/fab/auth_manager/api_fastapi/routes/test_login.py
index 984221b41cd..6ed9691300a 100644
--- a/providers/fab/tests/unit/fab/auth_manager/api_fastapi/routes/test_login.py
+++ b/providers/fab/tests/unit/fab/auth_manager/api_fastapi/routes/test_login.py
@@ -32,9 +32,11 @@ class TestLogin:
     dummy_login_body = {"username": "dummy", "password": "dummy"}
     dummy_token = LoginResponse(access_token="DUMMY_TOKEN")
 
+    
@patch("airflow.providers.fab.auth_manager.api_fastapi.routes.login._get_flask_app")
     
@patch("airflow.providers.fab.auth_manager.api_fastapi.routes.login.FABAuthManagerLogin")
-    def test_create_token(self, mock_fab_auth_manager_login, test_client):
+    def test_create_token(self, mock_fab_auth_manager_login, 
mock_get_flask_app, test_client):
         mock_fab_auth_manager_login.create_token.return_value = 
self.dummy_token
+        
mock_get_flask_app.return_value.app_context.return_value.__enter__.return_value 
= None
 
         response = test_client.post(
             "/token",
@@ -43,9 +45,11 @@ class TestLogin:
         assert response.status_code == 201
         assert response.json()["access_token"] == self.dummy_token.access_token
 
+    
@patch("airflow.providers.fab.auth_manager.api_fastapi.routes.login._get_flask_app")
     
@patch("airflow.providers.fab.auth_manager.api_fastapi.routes.login.FABAuthManagerLogin")
-    def test_create_token_cli(self, mock_fab_auth_manager_login, test_client):
+    def test_create_token_cli(self, mock_fab_auth_manager_login, 
mock_get_flask_app, test_client):
         mock_fab_auth_manager_login.create_token.return_value = 
LoginResponse(access_token="DUMMY_TOKEN")
+        
mock_get_flask_app.return_value.app_context.return_value.__enter__.return_value 
= None
 
         response = test_client.post(
             "/token/cli",
@@ -54,7 +58,9 @@ class TestLogin:
         assert response.status_code == 201
         assert response.json()["access_token"] == self.dummy_token.access_token
 
-    def test_logout(self, test_client):
+    
@patch("airflow.providers.fab.auth_manager.api_fastapi.routes.login._get_flask_app")
+    def test_logout(self, mock_get_flask_app, test_client):
+        
mock_get_flask_app.return_value.app_context.return_value.__enter__.return_value 
= None
         response = test_client.get("/logout", follow_redirects=False)
         assert response.status_code == 307
         assert response.headers["location"] == "/auth/login"
@@ -62,11 +68,13 @@ class TestLogin:
         assert any("session=" in c for c in cookies)
         assert any(f"{COOKIE_NAME_JWT_TOKEN}=" in c for c in cookies)
 
+    
@patch("airflow.providers.fab.auth_manager.api_fastapi.routes.login._get_flask_app")
     @patch(
         
"airflow.providers.fab.auth_manager.api_fastapi.routes.login.get_cookie_path", 
return_value=SUBPATH
     )
-    def test_logout_cookie_uses_subpath(self, mock_cookie_path, test_client):
+    def test_logout_cookie_uses_subpath(self, mock_cookie_path, 
mock_get_flask_app, test_client):
         """Cookies on logout must be scoped to the configured subpath."""
+        
mock_get_flask_app.return_value.app_context.return_value.__enter__.return_value 
= None
         response = test_client.get("/logout", follow_redirects=False)
         assert response.status_code == 307
         cookies = response.headers.get_list("set-cookie")

Reply via email to