From 97ac2845c1fc6168749aa85682f4138c57c0575e Mon Sep 17 00:00:00 2001 From: Eric Fischer Date: Thu, 25 Feb 2016 09:27:20 -0500 Subject: [PATCH] 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 --- common/djangoapps/student/models.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py index 4baa13c3e3..8ff43b4456 100644 --- a/common/djangoapps/student/models.py +++ b/common/djangoapps/student/models.py @@ -771,6 +771,19 @@ class LoginFailures(models.Model): failure_count = models.IntegerField(default=0) lockout_until = models.DateTimeField(null=True) + @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): """ @@ -784,7 +797,7 @@ class LoginFailures(models.Model): Static method to return in a given user has his/her account locked out """ try: - record = LoginFailures.objects.get(user=user) + record = cls._get_record_for_user(user) if not record.lockout_until: return False @@ -819,7 +832,7 @@ class LoginFailures(models.Model): Removes the lockout counters (normally called after a successful login) """ try: - entry = LoginFailures.objects.get(user=user) + entry = cls._get_record_for_user(user) entry.delete() except ObjectDoesNotExist: return