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 <[email protected]>
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.{}"
+
+
[email protected](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")
+
+
[email protected](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")
+
+
[email protected](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="[email protected]",
+ role=role,
+ password="plaintext",
+ )
+ mock_hash.assert_called_with("plaintext",
method="pbkdf2:sha256")
+ finally:
+ delete_user(app, "hash_method_add_test")