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