Commit 97ac2845 by Eric Fischer

Fix for LoginFailure.MultipleObjectsReturned in is_user_locked_out (replay)

The get_or_create function is vulnerable to race conditions in MySQL, which can
cause the model LoginFailure to, in some cases, have more than one row for the
same user, breaking the login for that user.

Addinf functionality to expect and clean the error by deleting extra rows (by
oldest lockout date), leaving just one entry and allowing the user to login.

Replayed and squashed by @efischer19, initially commited by @laq
parent c518543c
...@@ -772,6 +772,19 @@ class LoginFailures(models.Model): ...@@ -772,6 +772,19 @@ class LoginFailures(models.Model):
lockout_until = models.DateTimeField(null=True) lockout_until = models.DateTimeField(null=True)
@classmethod @classmethod
def _get_record_for_user(cls, user):
"""
Gets a user's record, and fixes any duplicates that may have arisen due to get_or_create
race conditions. See https://code.djangoproject.com/ticket/13906 for details.
Use this method in place of `LoginFailures.objects.get(user=user)`
"""
records = LoginFailures.objects.filter(user=user).order_by('-lockout_until')
for extra_record in records[1:]:
extra_record.delete()
return records.get()
@classmethod
def is_feature_enabled(cls): def is_feature_enabled(cls):
""" """
Returns whether the feature flag around this functionality has been set Returns whether the feature flag around this functionality has been set
...@@ -784,7 +797,7 @@ class LoginFailures(models.Model): ...@@ -784,7 +797,7 @@ class LoginFailures(models.Model):
Static method to return in a given user has his/her account locked out Static method to return in a given user has his/her account locked out
""" """
try: try:
record = LoginFailures.objects.get(user=user) record = cls._get_record_for_user(user)
if not record.lockout_until: if not record.lockout_until:
return False return False
...@@ -819,7 +832,7 @@ class LoginFailures(models.Model): ...@@ -819,7 +832,7 @@ class LoginFailures(models.Model):
Removes the lockout counters (normally called after a successful login) Removes the lockout counters (normally called after a successful login)
""" """
try: try:
entry = LoginFailures.objects.get(user=user) entry = cls._get_record_for_user(user)
entry.delete() entry.delete()
except ObjectDoesNotExist: except ObjectDoesNotExist:
return return
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment