From 270b1154b7d327dedf7a37d6a6614e54e784a2a4 Mon Sep 17 00:00:00 2001 From: Marcus Martins Date: Sun, 11 Jan 2015 19:09:45 -0800 Subject: [PATCH] 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: https://github.com/django-pci/django-axes/blob/6bd5a50cb83b980c7ea60f45ee5e1c7d8543746b/axes/decorators.py#L219 --- defender/tests.py | 53 +++++++++++++++++++++++++++++++++++++---------- defender/utils.py | 2 +- 2 files changed, 43 insertions(+), 12 deletions(-) diff --git a/defender/tests.py b/defender/tests.py index ef63ba4..b842688 100644 --- a/defender/tests.py +++ b/defender/tests.py @@ -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) diff --git a/defender/utils.py b/defender/utils.py index 1ab5ca2..c843862 100644 --- a/defender/utils.py +++ b/defender/utils.py @@ -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):