|
@@ -115,6 +115,36 @@ class MinimumLengthValidator:
|
|
|
) % {'min_length': self.min_length}
|
|
|
|
|
|
|
|
|
+def exceeds_maximum_length_ratio(password, max_similarity, value):
|
|
|
+ """
|
|
|
+ Test that value is within a reasonable range of password.
|
|
|
+
|
|
|
+ The following ratio calculations are based on testing SequenceMatcher like
|
|
|
+ this:
|
|
|
+
|
|
|
+ for i in range(0,6):
|
|
|
+ print(10**i, SequenceMatcher(a='A', b='A'*(10**i)).quick_ratio())
|
|
|
+
|
|
|
+ which yields:
|
|
|
+
|
|
|
+ 1 1.0
|
|
|
+ 10 0.18181818181818182
|
|
|
+ 100 0.019801980198019802
|
|
|
+ 1000 0.001998001998001998
|
|
|
+ 10000 0.00019998000199980003
|
|
|
+ 100000 1.999980000199998e-05
|
|
|
+
|
|
|
+ This means a length_ratio of 10 should never yield a similarity higher than
|
|
|
+ 0.2, for 100 this is down to 0.02 and for 1000 it is 0.002. This can be
|
|
|
+ calculated via 2 / length_ratio. As a result we avoid the potentially
|
|
|
+ expensive sequence matching.
|
|
|
+ """
|
|
|
+ pwd_len = len(password)
|
|
|
+ length_bound_similarity = max_similarity / 2 * pwd_len
|
|
|
+ value_len = len(value)
|
|
|
+ return pwd_len >= 10 * value_len and value_len < length_bound_similarity
|
|
|
+
|
|
|
+
|
|
|
class UserAttributeSimilarityValidator:
|
|
|
"""
|
|
|
Validate that the password is sufficiently different from the user's
|
|
@@ -130,19 +160,25 @@ class UserAttributeSimilarityValidator:
|
|
|
|
|
|
def __init__(self, user_attributes=DEFAULT_USER_ATTRIBUTES, max_similarity=0.7):
|
|
|
self.user_attributes = user_attributes
|
|
|
+ if max_similarity < 0.1:
|
|
|
+ raise ValueError('max_similarity must be at least 0.1')
|
|
|
self.max_similarity = max_similarity
|
|
|
|
|
|
def validate(self, password, user=None):
|
|
|
if not user:
|
|
|
return
|
|
|
|
|
|
+ password = password.lower()
|
|
|
for attribute_name in self.user_attributes:
|
|
|
value = getattr(user, attribute_name, None)
|
|
|
if not value or not isinstance(value, str):
|
|
|
continue
|
|
|
- value_parts = re.split(r'\W+', value) + [value]
|
|
|
+ value_lower = value.lower()
|
|
|
+ value_parts = re.split(r'\W+', value_lower) + [value_lower]
|
|
|
for value_part in value_parts:
|
|
|
- if SequenceMatcher(a=password.lower(), b=value_part.lower()).quick_ratio() >= self.max_similarity:
|
|
|
+ if exceeds_maximum_length_ratio(password, self.max_similarity, value_part):
|
|
|
+ continue
|
|
|
+ if SequenceMatcher(a=password, b=value_part).quick_ratio() >= self.max_similarity:
|
|
|
try:
|
|
|
verbose_name = str(user._meta.get_field(attribute_name).verbose_name)
|
|
|
except FieldDoesNotExist:
|