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(