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):