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")