@@ -115,6 +115,36 @@ def get_help_text(self):
115
115
) % {'min_length': self.min_length}
116
116
117
117
118
+
def exceeds_maximum_length_ratio(password, max_similarity, value):
119
+
"""
120
+
Test that value is within a reasonable range of password.
121
+
122
+
The following ratio calculations are based on testing SequenceMatcher like
123
+
this:
124
+
125
+
for i in range(0,6):
126
+
print(10**i, SequenceMatcher(a='A', b='A'*(10**i)).quick_ratio())
127
+
128
+
which yields:
129
+
130
+
1 1.0
131
+
10 0.18181818181818182
132
+
100 0.019801980198019802
133
+
1000 0.001998001998001998
134
+
10000 0.00019998000199980003
135
+
100000 1.999980000199998e-05
136
+
137
+
This means a length_ratio of 10 should never yield a similarity higher than
138
+
0.2, for 100 this is down to 0.02 and for 1000 it is 0.002. This can be
139
+
calculated via 2 / length_ratio. As a result we avoid the potentially
140
+
expensive sequence matching.
141
+
"""
142
+
pwd_len = len(password)
143
+
length_bound_similarity = max_similarity / 2 * pwd_len
144
+
value_len = len(value)
145
+
return pwd_len >= 10 * value_len and value_len < length_bound_similarity
146
+
147
+
118
148
class UserAttributeSimilarityValidator:
119
149
"""
120
150
Validate whether the password is sufficiently different from the user's
@@ -130,19 +160,25 @@ class UserAttributeSimilarityValidator:
130
160
131
161
def __init__(self, user_attributes=DEFAULT_USER_ATTRIBUTES, max_similarity=0.7):
132
162
self.user_attributes = user_attributes
163
+
if max_similarity < 0.1:
164
+
raise ValueError('max_similarity must be at least 0.1')
133
165
self.max_similarity = max_similarity
134
166
135
167
def validate(self, password, user=None):
136
168
if not user:
137
169
return
138
170
171
+
password = password.lower()
139
172
for attribute_name in self.user_attributes:
140
173
value = getattr(user, attribute_name, None)
141
174
if not value or not isinstance(value, str):
142
175
continue
143
-
value_parts = re.split(r'\W+', value) + [value]
176
+
value_lower = value.lower()
177
+
value_parts = re.split(r'\W+', value_lower) + [value_lower]
144
178
for value_part in value_parts:
145
-
if SequenceMatcher(a=password.lower(), b=value_part.lower()).quick_ratio() >= self.max_similarity:
179
+
if exceeds_maximum_length_ratio(password, self.max_similarity, value_part):
180
+
continue
181
+
if SequenceMatcher(a=password, b=value_part).quick_ratio() >= self.max_similarity:
146
182
try:
147
183
verbose_name = str(user._meta.get_field(attribute_name).verbose_name)
148
184
except FieldDoesNotExist:
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4