Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: generate valid passwords when using additional validators #4

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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
Loading