From aa3e01c3186cc594f46c4eb8b68c5e2ac77e5733 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mois=C3=A9s=20Gonz=C3=A1lez?= Date: Thu, 3 Feb 2022 20:05:01 -0400 Subject: [PATCH] feat: generate valid passwords when using additional validators (cherry picked from commit d21be1bb1a3d0171567b047442e136ebb8c01d3e) --- lms/djangoapps/support/views/manage_user.py | 3 +- .../cancel_user_retirement_request.py | 1 + openedx/core/djangoapps/user_authn/utils.py | 93 +++++++++++++++++++ .../djangoapps/user_authn/views/auto_auth.py | 3 +- .../djangoapps/user_authn/views/register.py | 4 +- 5 files changed, 97 insertions(+), 7 deletions(-) diff --git a/lms/djangoapps/support/views/manage_user.py b/lms/djangoapps/support/views/manage_user.py index e29652a905c2..2b20a3bc1c46 100644 --- a/lms/djangoapps/support/views/manage_user.py +++ b/lms/djangoapps/support/views/manage_user.py @@ -17,8 +17,7 @@ from lms.djangoapps.support.decorators import require_support_permission from openedx.core.djangoapps.user_api.accounts.serializers import AccountUserSerializer from openedx.core.djangolib.oauth2_retirement_utils import retire_dot_oauth2_models - -from edx_django_utils.user import generate_password # lint-amnesty, pylint: disable=wrong-import-order +from openedx.core.djangoapps.user_authn.utils import generate_password class ManageUserSupportView(View): diff --git a/openedx/core/djangoapps/user_api/management/commands/cancel_user_retirement_request.py b/openedx/core/djangoapps/user_api/management/commands/cancel_user_retirement_request.py index adb29a4fd3a8..92a8c5c78436 100644 --- a/openedx/core/djangoapps/user_api/management/commands/cancel_user_retirement_request.py +++ b/openedx/core/djangoapps/user_api/management/commands/cancel_user_retirement_request.py @@ -11,6 +11,7 @@ from openedx.core.djangoapps.user_api.accounts.utils import handle_retirement_cancellation from openedx.core.djangoapps.user_api.models import UserRetirementStatus +from openedx.core.djangoapps.user_authn.utils import generate_password LOGGER = logging.getLogger(__name__) diff --git a/openedx/core/djangoapps/user_authn/utils.py b/openedx/core/djangoapps/user_authn/utils.py index 5b70d2427859..9cdffcee6bdc 100644 --- a/openedx/core/djangoapps/user_authn/utils.py +++ b/openedx/core/djangoapps/user_authn/utils.py @@ -6,6 +6,8 @@ import math import random import re +import string + from urllib.parse import urlparse # pylint: disable=import-error from uuid import uuid4 # lint-amnesty, pylint: disable=unused-import @@ -63,6 +65,97 @@ def is_safe_login_or_logout_redirect(redirect_to, request_host, dot_client_id, r return is_safe_url +def password_rules(): + """ + Inspect the validators defined in AUTH_PASSWORD_VALIDATORS and define + a rule list with the set of available characters and their minimum + for a specific charset category (alphabetic, digits, uppercase, etc). + + This is based on the validators defined in + common.djangoapps.util.password_policy_validators and + django_password_validators.password_character_requirements.password_validation.PasswordCharacterValidator + """ + password_validators = settings.AUTH_PASSWORD_VALIDATORS + rules = { + "alpha": [string.ascii_letters, 0], + "digit": [string.digits, 0], + "upper": [string.ascii_uppercase, 0], + "lower": [string.ascii_lowercase, 0], + "punctuation": [string.punctuation, 0], + "symbol": ["£¥€©®™†§¶πμ'±", 0], + "min_length": ["", 0], + } + options_mapping = { + "min_alphabetic": "alpha", + "min_length_alpha": "alpha", + "min_length_digit": "digit", + "min_length_upper": "upper", + "min_length_lower": "lower", + "min_lower": "lower", + "min_upper": "upper", + "min_numeric": "digit", + "min_symbol": "symbol", + "min_punctuation": "punctuation", + } + + for validator in password_validators: + for option, mapping in options_mapping.items(): + if not validator.get("OPTIONS"): + continue + rules[mapping][1] = max( + rules[mapping][1], validator["OPTIONS"].get(option, 0) + ) + # We handle PasswordCharacterValidator separately because it can define + # its own set of special characters. + if ( + validator["NAME"] == + "django_password_validators.password_character_requirements.password_validation.PasswordCharacterValidator" + ): + min_special = validator["OPTIONS"].get("min_length_special", 0) + special_chars = validator["OPTIONS"].get( + "special_characters", "~!@#$%^&*()_+{}\":;'[]" + ) + rules["special"] = [special_chars, min_special] + + return rules + + +def generate_password(length=12, chars=string.ascii_letters + string.digits): + """Generate a valid random password. + + The original `generate_password` doesn't account for extra validators + This picks the minimum amount of characters for each charset category. + """ + if length < 8: + raise ValueError("password must be at least 8 characters") + + password = "" + password_length = length + choice = random.SystemRandom().choice + rules = password_rules() + min_length = rules.pop("min_length")[1] + password_length = max(min_length, length) + + for elems in rules.values(): + choices = elems[0] + needed = elems[1] + for _ in range(needed): + next_char = choice(choices) + password += next_char + + # fill the password to reach password_length + if len(password) < password_length: + password += "".join( + [choice(chars) for _ in range(password_length - len(password))] + ) + + password_list = list(password) + random.shuffle(password_list) + + password = "".join(password_list) + return password + + def is_registration_api_v1(request): """ Checks if registration api is v1 diff --git a/openedx/core/djangoapps/user_authn/views/auto_auth.py b/openedx/core/djangoapps/user_authn/views/auto_auth.py index 324a0c1959d4..c95cf7f0527e 100644 --- a/openedx/core/djangoapps/user_authn/views/auto_auth.py +++ b/openedx/core/djangoapps/user_authn/views/auto_auth.py @@ -35,8 +35,7 @@ create_comments_service_user ) from common.djangoapps.util.json_request import JsonResponse - -from edx_django_utils.user import generate_password # lint-amnesty, pylint: disable=wrong-import-order +from openedx.core.djangoapps.user_authn.utils import generate_password def auto_auth(request): # pylint: disable=too-many-statements diff --git a/openedx/core/djangoapps/user_authn/views/register.py b/openedx/core/djangoapps/user_authn/views/register.py index 939f2cca7ece..58806f74f07d 100644 --- a/openedx/core/djangoapps/user_authn/views/register.py +++ b/openedx/core/djangoapps/user_authn/views/register.py @@ -57,7 +57,7 @@ from openedx.core.djangoapps.user_api.preferences import api as preferences_api from openedx.core.djangoapps.user_authn.cookies import set_logged_in_cookies from openedx.core.djangoapps.user_authn.utils import ( - generate_username_suggestions, is_registration_api_v1 + generate_password, generate_username_suggestions, is_registration_api_v1 ) from openedx.core.djangoapps.user_authn.views.registration_form import ( AccountCreationForm, @@ -86,8 +86,6 @@ from common.djangoapps.util.db import outer_atomic from common.djangoapps.util.json_request import JsonResponse -from edx_django_utils.user import generate_password # lint-amnesty, pylint: disable=wrong-import-order - log = logging.getLogger("edx.student") AUDIT_LOG = logging.getLogger("audit")