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

eladkal 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 b18c2689aae Enable LDAP users to generate an Airflow token with 
`FabAuthManager` (#52295)
b18c2689aae is described below

commit b18c2689aae9d5e55eaf166191113143df41fa09
Author: Vincent <[email protected]>
AuthorDate: Wed Jul 2 02:26:52 2025 -0400

    Enable LDAP users to generate an Airflow token with `FabAuthManager` 
(#52295)
---
 providers/fab/docs/auth-manager/token.rst          |   3 +
 .../fab/auth_manager/api_fastapi/services/login.py |  25 +++--
 .../fab/auth_manager/security_manager/override.py  |  12 +-
 .../api_fastapi/services/test_login.py             | 121 ++++++++++-----------
 4 files changed, 88 insertions(+), 73 deletions(-)

diff --git a/providers/fab/docs/auth-manager/token.rst 
b/providers/fab/docs/auth-manager/token.rst
index 35c9bb3a74a..af610ab6ad4 100644
--- a/providers/fab/docs/auth-manager/token.rst
+++ b/providers/fab/docs/auth-manager/token.rst
@@ -40,3 +40,6 @@ Example
         }'
 
 This process will return a token that you can use in the Airflow public API 
requests.
+
+Only users from database (`AUTH_TYPE = AUTH_DB`) or from LDAP (`AUTH_TYPE = 
AUTH_LDAP`) can be used to generate a token.
+See :doc:`Airflow public API <webserver-authentication>` for more details.
diff --git 
a/providers/fab/src/airflow/providers/fab/auth_manager/api_fastapi/services/login.py
 
b/providers/fab/src/airflow/providers/fab/auth_manager/api_fastapi/services/login.py
index 0792efd18ce..7024581137d 100644
--- 
a/providers/fab/src/airflow/providers/fab/auth_manager/api_fastapi/services/login.py
+++ 
b/providers/fab/src/airflow/providers/fab/auth_manager/api_fastapi/services/login.py
@@ -18,6 +18,7 @@ from __future__ import annotations
 
 from typing import TYPE_CHECKING, cast
 
+from flask_appbuilder.const import AUTH_LDAP
 from starlette import status
 from starlette.exceptions import HTTPException
 
@@ -44,14 +45,22 @@ class FABAuthManagerLogin:
             )
 
         auth_manager = cast("FabAuthManager", get_auth_manager())
-        user: User = 
auth_manager.security_manager.find_user(username=body.username)
+        user: User | None = None
+
+        if auth_manager.security_manager.auth_type == AUTH_LDAP:
+            user = auth_manager.security_manager.auth_user_ldap(
+                body.username, body.password, rotate_session_id=False
+            )
+        if user is None:
+            user = auth_manager.security_manager.auth_user_db(
+                body.username, body.password, rotate_session_id=False
+            )
+
         if not user:
-            raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, 
detail="Invalid username")
+            raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, 
detail="Invalid credentials")
 
-        if 
auth_manager.security_manager.check_password(username=body.username, 
password=body.password):
-            return LoginResponse(
-                access_token=auth_manager.generate_jwt(
-                    user=user, 
expiration_time_in_seconds=expiration_time_in_seconds
-                )
+        return LoginResponse(
+            access_token=auth_manager.generate_jwt(
+                user=user, 
expiration_time_in_seconds=expiration_time_in_seconds
             )
-        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, 
detail="Invalid password")
+        )
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 db260f7e9cf..1c6293bd441 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
@@ -1725,7 +1725,7 @@ class 
FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
     --------------------
     """
 
-    def auth_user_ldap(self, username, password):
+    def auth_user_ldap(self, username, password, rotate_session_id=True):
         """
         Authenticate user with LDAP.
 
@@ -1892,7 +1892,8 @@ class 
FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
 
             # LOGIN SUCCESS (only if user is now registered)
             if user:
-                self._rotate_session_id()
+                if rotate_session_id:
+                    self._rotate_session_id()
                 self.update_user_auth_stat(user)
                 return user
             return None
@@ -1921,7 +1922,7 @@ class 
FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
             return False
         return check_password_hash(user.password, password)
 
-    def auth_user_db(self, username, password):
+    def auth_user_db(self, username, password, rotate_session_id=True):
         """
         Authenticate user, auth db style.
 
@@ -1929,6 +1930,8 @@ class 
FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
             The username or registered email address
         :param password:
             The password, will be tested against hashed password on db
+        :param rotate_session_id:
+            Whether to rotate the session ID
         """
         if username is None or username == "":
             return None
@@ -1944,7 +1947,8 @@ class 
FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
             log.info(LOGMSG_WAR_SEC_LOGIN_FAILED, username)
             return None
         if check_password_hash(user.password, password):
-            self._rotate_session_id()
+            if rotate_session_id:
+                self._rotate_session_id()
             self.update_user_auth_stat(user, True)
             return user
         self.update_user_auth_stat(user, False)
diff --git 
a/providers/fab/tests/unit/fab/auth_manager/api_fastapi/services/test_login.py 
b/providers/fab/tests/unit/fab/auth_manager/api_fastapi/services/test_login.py
index 89dfd3e1ca4..dcc677d30da 100644
--- 
a/providers/fab/tests/unit/fab/auth_manager/api_fastapi/services/test_login.py
+++ 
b/providers/fab/tests/unit/fab/auth_manager/api_fastapi/services/test_login.py
@@ -17,16 +17,30 @@
 
 from __future__ import annotations
 
-from typing import TYPE_CHECKING
-from unittest.mock import MagicMock, patch
+from unittest.mock import ANY, MagicMock, patch
 
 import pytest
+from flask_appbuilder.const import AUTH_DB, AUTH_LDAP, AUTH_OID
 from starlette.exceptions import HTTPException
 
 from airflow.providers.fab.auth_manager.api_fastapi.services.login import 
FABAuthManagerLogin
 
-if TYPE_CHECKING:
-    from airflow.providers.fab.auth_manager.api_fastapi.datamodels.login 
import LoginResponse
+
[email protected]
+def auth_manager():
+    return MagicMock()
+
+
[email protected]
+def security_manager():
+    return MagicMock()
+
+
[email protected]
+def user():
+    user = MagicMock()
+    user.password = "dummy"
+    return user
 
 
 
@patch("airflow.providers.fab.auth_manager.api_fastapi.services.login.get_auth_manager")
@@ -34,71 +48,56 @@ class TestLogin:
     def setup_method(
         self,
     ):
-        self.dummy_auth_manager = MagicMock()
-        self.dummy_app_builder = MagicMock()
-        self.dummy_app = MagicMock()
-        self.dummy_login_body = MagicMock()
-        self.dummy_user = MagicMock()
-        self.dummy_user.password = "dummy"
-        self.dummy_security_manager = MagicMock()
-        self.dummy_login_body.username = "dummy"
-        self.dummy_login_body.password = "dummy"
+        self.login_body = MagicMock()
+        self.login_body.username = "username"
+        self.login_body.password = "password"
         self.dummy_token = "DUMMY_TOKEN"
 
-    def test_create_token(self, get_auth_manager):
-        get_auth_manager.return_value = self.dummy_auth_manager
-        self.dummy_auth_manager.security_manager = self.dummy_security_manager
-        self.dummy_security_manager.find_user.return_value = self.dummy_user
-        self.dummy_auth_manager.generate_jwt.return_value = self.dummy_token
-        self.dummy_security_manager.check_password.return_value = True
+    @pytest.mark.parametrize(
+        "auth_type, method",
+        [
+            [AUTH_DB, "auth_user_db"],
+            [AUTH_LDAP, "auth_user_ldap"],
+        ],
+    )
+    def test_create_token(self, get_auth_manager, auth_type, method, 
auth_manager, security_manager, user):
+        security_manager.auth_type = auth_type
+        getattr(security_manager, method).return_value = user
 
-        login_response: LoginResponse = FABAuthManagerLogin.create_token(
-            body=self.dummy_login_body,
-            expiration_time_in_seconds=1,
-        )
-        assert login_response.access_token == self.dummy_token
+        auth_manager.security_manager = security_manager
+        auth_manager.generate_jwt.return_value = self.dummy_token
 
-    def test_create_token_invalid_username(self, get_auth_manager):
-        get_auth_manager.return_value = self.dummy_auth_manager
-        self.dummy_auth_manager.security_manager = self.dummy_security_manager
-        self.dummy_security_manager.find_user.return_value = None
-        self.dummy_security_manager.check_password.return_value = False
+        get_auth_manager.return_value = auth_manager
 
-        with pytest.raises(HTTPException) as ex:
-            FABAuthManagerLogin.create_token(
-                body=self.dummy_login_body,
-                expiration_time_in_seconds=1,
-            )
-        assert ex.value.status_code == 401
-        assert ex.value.detail == "Invalid username"
-
-    def test_create_token_invalid_password(self, get_auth_manager):
-        get_auth_manager.return_value = self.dummy_auth_manager
-        self.dummy_auth_manager.security_manager = self.dummy_security_manager
-        self.dummy_security_manager.find_user.return_value = self.dummy_user
-        self.dummy_user.password = "invalid_password"
-        self.dummy_security_manager.check_password.return_value = False
+        result = FABAuthManagerLogin.create_token(
+            body=self.login_body,
+        )
+        assert result.access_token == self.dummy_token
+        getattr(security_manager, method).assert_called_once_with(
+            self.login_body.username, self.login_body.password, 
rotate_session_id=False
+        )
+        auth_manager.generate_jwt.assert_called_once_with(user=user, 
expiration_time_in_seconds=ANY)
 
-        with pytest.raises(HTTPException) as ex:
-            FABAuthManagerLogin.create_token(
-                body=self.dummy_login_body,
-                expiration_time_in_seconds=1,
-            )
-        assert ex.value.status_code == 401
-        assert ex.value.detail == "Invalid password"
+    @pytest.mark.parametrize(
+        "auth_type, methods",
+        [
+            [AUTH_DB, ["auth_user_db"]],
+            [AUTH_LDAP, ["auth_user_ldap", "auth_user_db"]],
+            [AUTH_OID, ["auth_user_db"]],
+        ],
+    )
+    def test_create_token_no_user(
+        self, get_auth_manager, auth_type, methods, auth_manager, 
security_manager, user
+    ):
+        security_manager.auth_type = auth_type
+        for method in methods:
+            getattr(security_manager, method).return_value = None
 
-    def test_create_token_empty_user_password(self, get_auth_manager):
-        get_auth_manager.return_value = self.dummy_auth_manager
-        self.dummy_auth_manager.security_manager = self.dummy_security_manager
-        self.dummy_security_manager.find_user.return_value = self.dummy_user
-        self.dummy_login_body.username = ""
-        self.dummy_login_body.password = ""
-        self.dummy_security_manager.check_password.return_value = False
+        auth_manager.security_manager = security_manager
+        get_auth_manager.return_value = auth_manager
 
         with pytest.raises(HTTPException) as ex:
             FABAuthManagerLogin.create_token(
-                body=self.dummy_login_body,
-                expiration_time_in_seconds=1,
+                body=self.login_body,
             )
-        assert ex.value.status_code == 400
-        assert ex.value.detail == "Username and password must be provided"
+        assert ex.value.status_code == 401

Reply via email to