Skip to content

Commit 1cf37cf

Browse files
authored
Merge pull request #255 from eduNEXT/mgs/generate-valid-passwords
Generate valid passwords when using additional validators
2 parents 5d4a262 + 33958fb commit 1cf37cf

1 file changed

Lines changed: 83 additions & 6 deletions

File tree

  • openedx/core/djangoapps/user_authn

openedx/core/djangoapps/user_authn/utils.py

Lines changed: 83 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -125,17 +125,94 @@ def is_safe_login_or_logout_redirect(redirect_to, request_host, dot_client_id, r
125125
return is_safe_url
126126

127127

128+
def password_rules():
129+
"""
130+
Inspect the validators defined in AUTH_PASSWORD_VALIDATORS and define
131+
a rule list with the set of available characters and their minimum
132+
for a specific charset category (alphabetic, digits, uppercase, etc).
133+
134+
This is based on the validators defined in
135+
common.djangoapps.util.password_policy_validators and
136+
django_password_validators.password_character_requirements.password_validation.PasswordCharacterValidator
137+
"""
138+
password_validators = settings.AUTH_PASSWORD_VALIDATORS
139+
rules = {
140+
"alpha": [string.ascii_letters, 0],
141+
"digit": [string.digits, 0],
142+
"upper": [string.ascii_uppercase, 0],
143+
"lower": [string.ascii_lowercase, 0],
144+
"punctuation": [string.punctuation, 0],
145+
"symbol": ["£¥€©®™†§¶πμ'±", 0],
146+
"min_length": ["", 0],
147+
}
148+
options_mapping = {
149+
"min_alphabetic": "alpha",
150+
"min_length_alpha": "alpha",
151+
"min_length_digit": "digit",
152+
"min_length_upper": "upper",
153+
"min_length_lower": "lower",
154+
"min_lower": "lower",
155+
"min_upper": "upper",
156+
"min_numeric": "digit",
157+
"min_symbol": "symbol",
158+
"min_punctuation": "punctuation",
159+
}
160+
161+
for validator in password_validators:
162+
for option, mapping in options_mapping.items():
163+
if not validator.get("OPTIONS"):
164+
continue
165+
rules[mapping][1] = max(
166+
rules[mapping][1], validator["OPTIONS"].get(option, 0)
167+
)
168+
# We handle PasswordCharacterValidator separately because it can define
169+
# its own set of special characters.
170+
if (
171+
validator["NAME"] ==
172+
"django_password_validators.password_character_requirements.password_validation.PasswordCharacterValidator"
173+
):
174+
min_special = validator["OPTIONS"].get("min_length_special", 0)
175+
special_chars = validator["OPTIONS"].get(
176+
"special_characters", "~!@#$%^&*()_+{}\":;'[]"
177+
)
178+
rules["special"] = [special_chars, min_special]
179+
180+
return rules
181+
182+
128183
def generate_password(length=12, chars=string.ascii_letters + string.digits):
129-
"""Generate a valid random password"""
184+
"""Generate a valid random password.
185+
186+
The original `generate_password` doesn't account for extra validators
187+
This picks the minimum amount of characters for each charset category.
188+
"""
130189
if length < 8:
131190
raise ValueError("password must be at least 8 characters")
132191

192+
password = ""
193+
password_length = length
133194
choice = random.SystemRandom().choice
134-
135-
password = ''
136-
password += choice(string.digits)
137-
password += choice(string.ascii_letters)
138-
password += ''.join([choice(chars) for _i in range(length - 2)])
195+
rules = password_rules()
196+
min_length = rules.pop("min_length")[1]
197+
password_length = max(min_length, length)
198+
199+
for rule, elems in rules.items():
200+
choices = elems[0]
201+
needed = elems[1]
202+
for _ in range(needed):
203+
next_char = choice(choices)
204+
password += next_char
205+
206+
# fill the password to reach password_length
207+
if len(password) < password_length:
208+
password += "".join(
209+
[choice(chars) for _ in range(password_length - len(password))]
210+
)
211+
212+
password_list = list(password)
213+
random.shuffle(password_list)
214+
215+
password = "".join(password_list)
139216
return password
140217

141218

0 commit comments

Comments
 (0)