@@ -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+
128183def 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