From 1678fbdd970e068b42ff18898d9cd4a8dabb6b2d Mon Sep 17 00:00:00 2001 From: bibi samina Date: Wed, 26 Aug 2026 12:26:54 +0530 Subject: [PATCH] use secrets for fab users --use-random-password generation random.choices draws from the Mersenne Twister PRNG that Python documents as unfit for security use, so the generated web-login credential is predictable, and string.printable also seeds the value with whitespace and control characters that land in the stored password. --- .../auth_manager/cli_commands/user_command.py | 5 +++-- .../cli_commands/test_user_command.py | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/providers/fab/src/airflow/providers/fab/auth_manager/cli_commands/user_command.py b/providers/fab/src/airflow/providers/fab/auth_manager/cli_commands/user_command.py index 4ae060e144ffd..adecdb458ffa3 100644 --- a/providers/fab/src/airflow/providers/fab/auth_manager/cli_commands/user_command.py +++ b/providers/fab/src/airflow/providers/fab/auth_manager/cli_commands/user_command.py @@ -22,8 +22,8 @@ import getpass import json import os -import random import re +import secrets import string from typing import Any @@ -113,7 +113,8 @@ def user_reset_password(args): def _create_password(args): if args.use_random_password: - password = "".join(random.choices(string.printable, k=16)) + characters = string.ascii_letters + string.digits + string.punctuation + password = "".join(secrets.choice(characters) for _ in range(16)) elif args.password: password = args.password else: diff --git a/providers/fab/tests/unit/fab/auth_manager/cli_commands/test_user_command.py b/providers/fab/tests/unit/fab/auth_manager/cli_commands/test_user_command.py index 0368d5b83eaa0..ef2664c8e9b13 100644 --- a/providers/fab/tests/unit/fab/auth_manager/cli_commands/test_user_command.py +++ b/providers/fab/tests/unit/fab/auth_manager/cli_commands/test_user_command.py @@ -16,13 +16,17 @@ # under the License. from __future__ import annotations +import argparse import json import os import re +import secrets +import string import tempfile from contextlib import redirect_stdout from importlib import reload from io import StringIO +from unittest import mock import pytest @@ -39,6 +43,18 @@ TEST_USER3_EMAIL = "test-user3@example.com" +@mock.patch( + "airflow.providers.fab.auth_manager.cli_commands.user_command.secrets.choice", + side_effect=secrets.choice, +) +def test_create_password_uses_csprng_without_whitespace(mock_choice): + args = argparse.Namespace(use_random_password=True, password=None) + password = user_command._create_password(args) + assert mock_choice.call_count == 16 + assert len(password) == 16 + assert not any(char in string.whitespace for char in password) + + def _does_user_belong_to_role(appbuilder, email, rolename): user = appbuilder.sm.find_user(email=email) role = appbuilder.sm.find_role(rolename)