Skip to content

Commit

Permalink
feat: generate valid passwords when using additional validators
Browse files Browse the repository at this point in the history
(cherry picked from commit d21be1b)
  • Loading branch information
MoisesGSalas authored and johanseto committed Jan 22, 2024
1 parent cf6e266 commit aa3e01c
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 7 deletions.
3 changes: 1 addition & 2 deletions lms/djangoapps/support/views/manage_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down
93 changes: 93 additions & 0 deletions openedx/core/djangoapps/user_authn/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
3 changes: 1 addition & 2 deletions openedx/core/djangoapps/user_authn/views/auto_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 1 addition & 3 deletions openedx/core/djangoapps/user_authn/views/register.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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")

Expand Down

0 comments on commit aa3e01c

Please sign in to comment.