Match Django Axes behavior on locked users

Match how Django Axes locks out users based on IP and username.
Also adds additional test to cover the functionality as the previous
tests were not verifying usernames.

Reference:
6bd5a50cb8/axes/decorators.py (L219)
This commit is contained in:
Marcus Martins 2015-01-11 19:09:45 -08:00
parent a0d94d5220
commit 270b1154b7
2 changed files with 43 additions and 12 deletions

View file

@ -28,10 +28,12 @@ except NoReverseMatch:
LOGIN_FORM_KEY = 'this_is_the_login_form'
VALID_USERNAME = VALID_PASSWORD = 'valid'
class AccessAttemptTest(TestCase):
""" Test case using custom settings for testing
"""
VALID_USERNAME = 'valid'
LOCKED_MESSAGE = 'Account locked: too many login attempts.'
PERMANENT_LOCKED_MESSAGE = (
LOCKED_MESSAGE + ' Contact an admin to unlock your account.'
@ -43,17 +45,23 @@ class AccessAttemptTest(TestCase):
return ''.join(random.choice(chars) for x in range(20))
def _login(self, is_valid=False, user_agent='test-browser'):
""" Login a user. A valid credential is used when is_valid is True,
otherwise it will use a random string to make a failed login.
def _login(self, username=None, password=None, user_agent='test-browser',
remote_addr='127.0.0.1'):
""" Login a user. If the username or password is not provided
it will use a random string instead. Use the VALID_USERNAME and
VALID_PASSWORD to make a valid login.
"""
username = self.VALID_USERNAME if is_valid else self._get_random_str()
if username is None:
username = self._get_random_str()
if password is None:
password = self._get_random_str()
response = self.client.post(ADMIN_LOGIN_URL, {
'username': username,
'password': username,
'password': password,
LOGIN_FORM_KEY: 1,
}, HTTP_USER_AGENT=user_agent)
}, HTTP_USER_AGENT=user_agent, REMOTE_ADDR=remote_addr)
return response
@ -61,9 +69,9 @@ class AccessAttemptTest(TestCase):
""" Create a valid user for login
"""
self.user = User.objects.create_superuser(
username=self.VALID_USERNAME,
username=VALID_USERNAME,
email='test@example.com',
password=self.VALID_USERNAME,
password=VALID_PASSWORD,
)
def tearDown(self):
@ -116,7 +124,29 @@ class AccessAttemptTest(TestCase):
def test_valid_login(self):
""" Tests a valid login for a real username
"""
response = self._login(is_valid=True)
response = self._login(username=VALID_USERNAME, password=VALID_PASSWORD)
self.assertNotContains(response, LOGIN_FORM_KEY, status_code=302)
def test_blocked_ip_cannot_login(self):
""" Test an user with blocked ip cannot login with another username
"""
for i in range(0, config.FAILURE_LIMIT + 1):
response = self._login(username=VALID_USERNAME)
# try to login with a different user
response = self._login(username='myuser')
self.assertContains(response, self.LOCKED_MESSAGE)
def test_trusted_user_can_login(self):
""" Test an user with a non-blocked ip can login after an attacker has
triggered a block with a different ip.
"""
for i in range(0, config.FAILURE_LIMIT + 1):
response = self._login(username=VALID_USERNAME)
# change the client ip so that can we login again
response = self._login(username=VALID_USERNAME, password=VALID_PASSWORD,
remote_addr='8.8.8.8')
self.assertNotContains(response, LOGIN_FORM_KEY, status_code=302)
def test_cooling_off(self):
@ -142,7 +172,8 @@ class AccessAttemptTest(TestCase):
""" Tests if can handle a long user agent
"""
long_user_agent = 'ie6' * 1024
response = self._login(is_valid=True, user_agent=long_user_agent)
response = self._login(username=VALID_USERNAME, password=VALID_PASSWORD,
user_agent=long_user_agent)
self.assertNotContains(response, LOGIN_FORM_KEY, status_code=302)
@patch('defender.config.BEHIND_REVERSE_PROXY', True)

View file

@ -216,7 +216,7 @@ def is_already_locked(request):
if not user_blocked:
user_blocked = False
return ip_blocked or user_blocked
return ip_blocked and user_blocked
def check_request(request, login_unsuccessful):