@@ -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 :
0 commit comments