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

potiuk 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 45edb1750f1 Use only the configured auth type when creating tokens in 
the FAB auth manager (#76231)
45edb1750f1 is described below

commit 45edb1750f1a9f3e8050bf593e012966505046ef
Author: Jarek Potiuk <[email protected]>
AuthorDate: Wed Nov 4 02:15:54 2026 +0100

    Use only the configured auth type when creating tokens in the FAB auth 
manager (#76231)
    
    * Use only the configured auth type when creating tokens in the FAB auth 
manager
    
    Generated-by: Claude Opus 5
    
    * Mock security manager with specific class for tests
    
    Co-authored-by: Copilot Autofix powered by AI 
<[email protected]>
    
    ---------
    
    Co-authored-by: Copilot Autofix powered by AI 
<[email protected]>
---
 providers/fab/docs/changelog.rst                   |  5 ++++
 .../providers/fab/auth_manager/fab_auth_manager.py | 35 ++++++++++++----------
 .../unit/fab/auth_manager/test_fab_auth_manager.py | 34 ++++++++++++++++++++-
 3 files changed, 58 insertions(+), 16 deletions(-)

diff --git a/providers/fab/docs/changelog.rst b/providers/fab/docs/changelog.rst
index 5edc0481a53..13c12992f93 100644
--- a/providers/fab/docs/changelog.rst
+++ b/providers/fab/docs/changelog.rst
@@ -26,6 +26,11 @@ Changelog
     bundle pages and API to every role, including ``Admin``, while still 
showing the Browse menu
     entry. Custom roles need ``can read`` and ``menu access`` on ``Dag 
Bundles`` added by hand.
 
+    ``POST /auth/token`` now checks a username and password only against the 
configured
+    ``AUTH_TYPE``: with ``AUTH_LDAP`` it no longer falls back to database 
passwords, and with other
+    authentication types (OAuth, remote user) it no longer issues tokens for a 
username and
+    password; use that type's own login flow instead. Passwords made only of 
NUL bytes are rejected.
+
 3.10.2
 ......
 
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 2806cdf5e96..a81b480008c 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
@@ -29,7 +29,7 @@ from cachetools import TTLCache, cachedmethod
 from fastapi import FastAPI
 from fastapi.middleware.wsgi import WSGIMiddleware
 from flask import current_app, g
-from flask_appbuilder.const import AUTH_LDAP
+from flask_appbuilder.const import AUTH_DB, AUTH_LDAP
 from sqlalchemy import select
 from sqlalchemy.exc import NoResultFound, SQLAlchemyError
 from sqlalchemy.orm import Session, joinedload
@@ -415,27 +415,32 @@ class FabAuthManager(BaseAuthManager[User]):
         """
         Create a new token from a payload.
 
-        By default, it uses basic authentication (username and password).
+        By default, it uses basic authentication (username and password), 
checked against the
+        configured ``AUTH_TYPE``. Tokens are only issued for ``AUTH_DB`` and 
``AUTH_LDAP``.
         Override this method to use a different authentication method (e.g. 
oauth).
 
         :param headers: request headers
         :param body: request body
         """
-        if not body.get("username") or not body.get("password"):
+        username = body.get("username")
+        password = body.get("password")
+        # Treat a password made only of NUL bytes as empty, as 
Flask-AppBuilder's login API does.
+        if (
+            not isinstance(username, str)
+            or not isinstance(password, str)
+            or not username
+            or not password.strip("\x00")
+        ):
             raise ValueError("Username and password must be provided")
 
-        user: User | None = None
-
-        if self.security_manager.auth_type == AUTH_LDAP:
-            user = self.security_manager.auth_user_ldap(
-                body["username"], body["password"], rotate_session_id=False
-            )
-        if user is None:
-            user = self.security_manager.auth_user_db(
-                body["username"], body["password"], rotate_session_id=False
-            )
-
-        return user
+        # Only check the password against the configured authentication type. 
Other types (OAuth,
+        # remote user, ...) do not authenticate with a username and password, 
so no token is issued.
+        auth_type = self.security_manager.auth_type
+        if auth_type == AUTH_LDAP:
+            return self.security_manager.auth_user_ldap(username, password, 
rotate_session_id=False)
+        if auth_type == AUTH_DB:
+            return self.security_manager.auth_user_db(username, password, 
rotate_session_id=False)
+        return None
 
     def is_authorized_configuration(
         self,
diff --git a/providers/fab/tests/unit/fab/auth_manager/test_fab_auth_manager.py 
b/providers/fab/tests/unit/fab/auth_manager/test_fab_auth_manager.py
index f494ff2eda1..8e20396b425 100644
--- a/providers/fab/tests/unit/fab/auth_manager/test_fab_auth_manager.py
+++ b/providers/fab/tests/unit/fab/auth_manager/test_fab_auth_manager.py
@@ -26,7 +26,7 @@ from urllib.parse import urlencode
 
 import pytest
 from flask import g
-from flask_appbuilder.const import AUTH_DB, AUTH_LDAP
+from flask_appbuilder.const import AUTH_DB, AUTH_LDAP, AUTH_OAUTH, 
AUTH_REMOTE_USER
 from sqlalchemy.exc import OperationalError, PendingRollbackError
 from sqlalchemy.orm import Session
 
@@ -439,6 +439,11 @@ class TestFabAuthManager:
             ["", ""],
             ["test", ""],
             ["", "test"],
+            ["test", "\x00"],
+            ["test", "\x00" * 64],
+            ["test", 123],
+            ["test", ["password"]],
+            [123, "test"],
         ],
     )
     def test_create_token_wrong_values(self, username, password, 
auth_manager_with_appbuilder):
@@ -447,6 +452,33 @@ class TestFabAuthManager:
                 headers={}, body={"username": username, "password": password}
             )
 
+    @pytest.mark.parametrize("auth_type", [AUTH_OAUTH, AUTH_REMOTE_USER])
+    def test_create_token_other_auth_types(self, auth_type, 
auth_manager_with_appbuilder):
+        security_manager = Mock(spec=FabAirflowSecurityManagerOverride)
+        security_manager.auth_type = auth_type
+        auth_manager_with_appbuilder.security_manager = security_manager
+
+        result = auth_manager_with_appbuilder.create_token(
+            headers={}, body={"username": "username", "password": "password"}
+        )
+
+        assert result is None
+        security_manager.auth_user_db.assert_not_called()
+        security_manager.auth_user_ldap.assert_not_called()
+
+    def test_create_token_ldap_does_not_fall_back_to_db(self, 
auth_manager_with_appbuilder):
+        security_manager = Mock()
+        security_manager.auth_type = AUTH_LDAP
+        security_manager.auth_user_ldap.return_value = None
+        auth_manager_with_appbuilder.security_manager = security_manager
+
+        result = auth_manager_with_appbuilder.create_token(
+            headers={}, body={"username": "username", "password": "password"}
+        )
+
+        assert result is None
+        security_manager.auth_user_db.assert_not_called()
+
     @pytest.mark.parametrize(
         ("api_name", "method", "user_permissions", "expected_result"),
         chain(

Reply via email to