You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airflow.apache.org by vi...@apache.org on 2026/04/27 14:06:45 UTC

(airflow) branch main updated: Fix FAB password hashing to respect FAB_PASSWORD_HASH_METHOD config (#65735)

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

vincbeck 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 c50210e8276 Fix FAB password hashing to respect FAB_PASSWORD_HASH_METHOD config (#65735)
c50210e8276 is described below

commit c50210e8276669260059f6a087f65a0f40e7df31
Author: Sadha Chilukoori <sr...@gmail.com>
AuthorDate: Mon Apr 27 07:06:36 2026 -0700

    Fix FAB password hashing to respect FAB_PASSWORD_HASH_METHOD config (#65735)
---
 .../fab/auth_manager/security_manager/override.py  | 10 ++--
 .../tests/unit/fab/auth_manager/test_security.py   | 58 ++++++++++++++++++++++
 2 files changed, 65 insertions(+), 3 deletions(-)

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 ababfdd85bc..9bcd361f000 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
@@ -549,6 +549,10 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
         jwt_manager.init_app(current_app)
         jwt_manager.user_lookup_loader(self.load_user_jwt)
 
+    def _hash_password(self, password: str) -> str:
+        method = current_app.config.get("FAB_PASSWORD_HASH_METHOD", "scrypt")
+        return generate_password_hash(password, method=method)
+
     def reset_password(self, userid: int, password: str) -> bool:
         """
         Change/Reset a user's password for auth db.
@@ -561,7 +565,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
         user = self.get_user_by_id(userid)
         if not user:
             return False
-        user.password = generate_password_hash(password)
+        user.password = self._hash_password(password)
         self.reset_user_sessions(user)
         return self.update_user(user)
 
@@ -1404,7 +1408,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
             if hashed_password:
                 user.password = hashed_password
             else:
-                user.password = generate_password_hash(password)
+                user.password = self._hash_password(password)
             self.session.commit()
             log.info(const.LOGMSG_INF_SEC_ADD_USER, username)
 
@@ -1448,7 +1452,7 @@ class FabAirflowSecurityManagerOverride(AirflowSecurityManagerV2):
         if hashed_password:
             register_user.password = hashed_password
         else:
-            register_user.password = generate_password_hash(password)
+            register_user.password = self._hash_password(password)
         register_user.registration_hash = str(uuid.uuid1())
         try:
             self.session.add(register_user)
diff --git a/providers/fab/tests/unit/fab/auth_manager/test_security.py b/providers/fab/tests/unit/fab/auth_manager/test_security.py
index 389c054e11b..e80e5f3b077 100644
--- a/providers/fab/tests/unit/fab/auth_manager/test_security.py
+++ b/providers/fab/tests/unit/fab/auth_manager/test_security.py
@@ -1215,3 +1215,61 @@ def test_users_can_be_found(app, security_manager, session, caplog):
     assert len(users) == 1 + prior_user_count
     delete_user(app, "Test_cbf")
     assert "Error adding new user to database" in caplog.text
+
+
+OVERRIDE_STRING = "airflow.providers.fab.auth_manager.security_manager.override.{}"
+
+
+@mock.patch(OVERRIDE_STRING.format("generate_password_hash"))
+def test_hash_password_uses_fab_password_hash_method(mock_hash, app, security_manager):
+    """_hash_password must forward FAB_PASSWORD_HASH_METHOD from app config to generate_password_hash."""
+    with app.test_request_context():
+        with mock.patch.dict(app.config, {"FAB_PASSWORD_HASH_METHOD": "pbkdf2:sha256"}):
+            security_manager._hash_password("mypassword")
+            mock_hash.assert_called_once_with("mypassword", method="pbkdf2:sha256")
+
+
+@mock.patch(OVERRIDE_STRING.format("generate_password_hash"))
+def test_hash_password_defaults_to_scrypt_when_config_absent(mock_hash, app, security_manager):
+    """_hash_password falls back to scrypt when FAB_PASSWORD_HASH_METHOD is not in config."""
+    with app.test_request_context():
+        with mock.patch.dict(app.config, {}, clear=False) as cfg:
+            cfg.pop("FAB_PASSWORD_HASH_METHOD", None)
+            security_manager._hash_password("mypassword")
+            mock_hash.assert_called_once_with("mypassword", method="scrypt")
+
+
+def test_reset_password_uses_configured_hash_method(app, security_manager):
+    """reset_password must use FAB_PASSWORD_HASH_METHOD, not werkzeug's default."""
+    try:
+        user = create_user(app, "hash_method_reset_test")
+        with app.test_request_context():
+            with mock.patch.dict(app.config, {"FAB_PASSWORD_HASH_METHOD": "pbkdf2:sha256"}):
+                with mock.patch(
+                    OVERRIDE_STRING.format("generate_password_hash"), return_value="hashed_pw"
+                ) as mock_hash:
+                    security_manager.reset_password(user.id, "newpassword")
+                mock_hash.assert_called_with("newpassword", method="pbkdf2:sha256")
+    finally:
+        delete_user(app, "hash_method_reset_test")
+
+
+@mock.patch(OVERRIDE_STRING.format("generate_password_hash"))
+def test_add_user_uses_configured_hash_method(mock_hash, app, security_manager):
+    """add_user must use FAB_PASSWORD_HASH_METHOD, not werkzeug's default."""
+    mock_hash.return_value = "hashed_pw"
+    with app.test_request_context():
+        with mock.patch.dict(app.config, {"FAB_PASSWORD_HASH_METHOD": "pbkdf2:sha256"}):
+            role = security_manager.find_role("Admin")
+            try:
+                security_manager.add_user(
+                    username="hash_method_add_test",
+                    first_name="Hash",
+                    last_name="Test",
+                    email="hash_method_add_test@example.com",
+                    role=role,
+                    password="plaintext",
+                )
+                mock_hash.assert_called_with("plaintext", method="pbkdf2:sha256")
+            finally:
+                delete_user(app, "hash_method_add_test")