Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/django/contrib/auth/password_validation.py: 46%
119 statements
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
1import functools
2import gzip
3import re
4from difflib import SequenceMatcher
5from pathlib import Path
7from django.conf import settings
8from django.core.exceptions import (
9 FieldDoesNotExist,
10 ImproperlyConfigured,
11 ValidationError,
12)
13from django.utils.functional import cached_property, lazy
14from django.utils.html import format_html, format_html_join
15from django.utils.module_loading import import_string
16from django.utils.translation import gettext as _
17from django.utils.translation import ngettext
20@functools.lru_cache(maxsize=None)
21def get_default_password_validators():
22 return get_password_validators(settings.AUTH_PASSWORD_VALIDATORS)
25def get_password_validators(validator_config):
26 validators = []
27 for validator in validator_config:
28 try:
29 klass = import_string(validator["NAME"])
30 except ImportError:
31 msg = (
32 "The module in NAME could not be imported: %s. Check your "
33 "AUTH_PASSWORD_VALIDATORS setting."
34 )
35 raise ImproperlyConfigured(msg % validator["NAME"])
36 validators.append(klass(**validator.get("OPTIONS", {})))
38 return validators
41def validate_password(password, user=None, password_validators=None):
42 """
43 Validate whether the password meets all validator requirements.
45 If the password is valid, return ``None``.
46 If the password is invalid, raise ValidationError with all error messages.
47 """
48 errors = []
49 if password_validators is None:
50 password_validators = get_default_password_validators()
51 for validator in password_validators:
52 try:
53 validator.validate(password, user)
54 except ValidationError as error:
55 errors.append(error)
56 if errors:
57 raise ValidationError(errors)
60def password_changed(password, user=None, password_validators=None):
61 """
62 Inform all validators that have implemented a password_changed() method
63 that the password has been changed.
64 """
65 if password_validators is None: 65 ↛ 67line 65 didn't jump to line 67, because the condition on line 65 was never false
66 password_validators = get_default_password_validators()
67 for validator in password_validators:
68 password_changed = getattr(validator, "password_changed", lambda *a: None)
69 password_changed(password, user)
72def password_validators_help_texts(password_validators=None):
73 """
74 Return a list of all help texts of all configured validators.
75 """
76 help_texts = []
77 if password_validators is None:
78 password_validators = get_default_password_validators()
79 for validator in password_validators:
80 help_texts.append(validator.get_help_text())
81 return help_texts
84def _password_validators_help_text_html(password_validators=None):
85 """
86 Return an HTML string with all help texts of all configured validators
87 in an <ul>.
88 """
89 help_texts = password_validators_help_texts(password_validators)
90 help_items = format_html_join(
91 "", "<li>{}</li>", ((help_text,) for help_text in help_texts)
92 )
93 return format_html("<ul>{}</ul>", help_items) if help_items else ""
96password_validators_help_text_html = lazy(_password_validators_help_text_html, str)
99class MinimumLengthValidator:
100 """
101 Validate whether the password is of a minimum length.
102 """
104 def __init__(self, min_length=8):
105 self.min_length = min_length
107 def validate(self, password, user=None):
108 if len(password) < self.min_length:
109 raise ValidationError(
110 ngettext(
111 "This password is too short. It must contain at least "
112 "%(min_length)d character.",
113 "This password is too short. It must contain at least "
114 "%(min_length)d characters.",
115 self.min_length,
116 ),
117 code="password_too_short",
118 params={"min_length": self.min_length},
119 )
121 def get_help_text(self):
122 return ngettext(
123 "Your password must contain at least %(min_length)d character.",
124 "Your password must contain at least %(min_length)d characters.",
125 self.min_length,
126 ) % {"min_length": self.min_length}
129def exceeds_maximum_length_ratio(password, max_similarity, value):
130 """
131 Test that value is within a reasonable range of password.
133 The following ratio calculations are based on testing SequenceMatcher like
134 this:
136 for i in range(0,6):
137 print(10**i, SequenceMatcher(a='A', b='A'*(10**i)).quick_ratio())
139 which yields:
141 1 1.0
142 10 0.18181818181818182
143 100 0.019801980198019802
144 1000 0.001998001998001998
145 10000 0.00019998000199980003
146 100000 1.999980000199998e-05
148 This means a length_ratio of 10 should never yield a similarity higher than
149 0.2, for 100 this is down to 0.02 and for 1000 it is 0.002. This can be
150 calculated via 2 / length_ratio. As a result we avoid the potentially
151 expensive sequence matching.
152 """
153 pwd_len = len(password)
154 length_bound_similarity = max_similarity / 2 * pwd_len
155 value_len = len(value)
156 return pwd_len >= 10 * value_len and value_len < length_bound_similarity
159class UserAttributeSimilarityValidator:
160 """
161 Validate whether the password is sufficiently different from the user's
162 attributes.
164 If no specific attributes are provided, look at a sensible list of
165 defaults. Attributes that don't exist are ignored. Comparison is made to
166 not only the full attribute value, but also its components, so that, for
167 example, a password is validated against either part of an email address,
168 as well as the full address.
169 """
171 DEFAULT_USER_ATTRIBUTES = ("username", "first_name", "last_name", "email")
173 def __init__(self, user_attributes=DEFAULT_USER_ATTRIBUTES, max_similarity=0.7):
174 self.user_attributes = user_attributes
175 if max_similarity < 0.1: 175 ↛ 176line 175 didn't jump to line 176, because the condition on line 175 was never true
176 raise ValueError("max_similarity must be at least 0.1")
177 self.max_similarity = max_similarity
179 def validate(self, password, user=None):
180 if not user:
181 return
183 password = password.lower()
184 for attribute_name in self.user_attributes:
185 value = getattr(user, attribute_name, None)
186 if not value or not isinstance(value, str):
187 continue
188 value_lower = value.lower()
189 value_parts = re.split(r"\W+", value_lower) + [value_lower]
190 for value_part in value_parts:
191 if exceeds_maximum_length_ratio(
192 password, self.max_similarity, value_part
193 ):
194 continue
195 if (
196 SequenceMatcher(a=password, b=value_part).quick_ratio()
197 >= self.max_similarity
198 ):
199 try:
200 verbose_name = str(
201 user._meta.get_field(attribute_name).verbose_name
202 )
203 except FieldDoesNotExist:
204 verbose_name = attribute_name
205 raise ValidationError(
206 _("The password is too similar to the %(verbose_name)s."),
207 code="password_too_similar",
208 params={"verbose_name": verbose_name},
209 )
211 def get_help_text(self):
212 return _(
213 "Your password can’t be too similar to your other personal information."
214 )
217class CommonPasswordValidator:
218 """
219 Validate whether the password is a common password.
221 The password is rejected if it occurs in a provided list of passwords,
222 which may be gzipped. The list Django ships with contains 20000 common
223 passwords (lowercased and deduplicated), created by Royce Williams:
224 https://gist.github.com/roycewilliams/281ce539915a947a23db17137d91aeb7
225 The password list must be lowercased to match the comparison in validate().
226 """
228 @cached_property
229 def DEFAULT_PASSWORD_LIST_PATH(self):
230 return Path(__file__).resolve().parent / "common-passwords.txt.gz"
232 def __init__(self, password_list_path=DEFAULT_PASSWORD_LIST_PATH):
233 if password_list_path is CommonPasswordValidator.DEFAULT_PASSWORD_LIST_PATH: 233 ↛ 235line 233 didn't jump to line 235, because the condition on line 233 was never false
234 password_list_path = self.DEFAULT_PASSWORD_LIST_PATH
235 try:
236 with gzip.open(password_list_path, "rt", encoding="utf-8") as f:
237 self.passwords = {x.strip() for x in f}
238 except OSError:
239 with open(password_list_path) as f:
240 self.passwords = {x.strip() for x in f}
242 def validate(self, password, user=None):
243 if password.lower().strip() in self.passwords:
244 raise ValidationError(
245 _("This password is too common."),
246 code="password_too_common",
247 )
249 def get_help_text(self):
250 return _("Your password can’t be a commonly used password.")
253class NumericPasswordValidator:
254 """
255 Validate whether the password is alphanumeric.
256 """
258 def validate(self, password, user=None):
259 if password.isdigit():
260 raise ValidationError(
261 _("This password is entirely numeric."),
262 code="password_entirely_numeric",
263 )
265 def get_help_text(self):
266 return _("Your password can’t be entirely numeric.")