diff --git a/axes/tests.py b/axes/tests.py index b45cdf7..0840858 100644 --- a/axes/tests.py +++ b/axes/tests.py @@ -443,6 +443,287 @@ class AccessAttemptTest(TestCase): self.assertEqual(response.status_code, 200) +class AccessAttemptConfigTest(TestCase): + """ This set of tests checks for lockouts under different configurations + and circumstances to prevent false positives and false negatives. + Always block attempted logins for the same user from the same IP. + Always allow attempted logins for a different user from a different IP. + """ + + IP_1 = '10.1.1.1' + IP_2 = '10.2.2.2' + USER_1 = 'valid-user-1' + USER_2 = 'valid-user-2' + VALID_PASSWORD = 'valid-password' + WRONG_PASSWORD = 'wrong-password' + LOCKED_MESSAGE = 'Account locked: too many login attempts.' + LOGIN_FORM_KEY = '' + ALLOWED = 302 + BLOCKED = 403 + + def _login(self, username, password, ip_addr='127.0.0.1', + is_json=False, **kwargs): + """Login a user and get the response. + IP address can be configured to test IP blocking functionality. + """ + try: + admin_login = reverse('admin:login') + except NoReverseMatch: + admin_login = reverse('admin:index') + + headers = { + 'user_agent': 'test-browser' + } + post_data = { + 'username': username, + 'password': password, + 'this_is_the_login_form': 1, + } + post_data.update(kwargs) + + if is_json: + headers.update({ + 'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest', + 'content_type': 'application/json', + }) + post_data = json.dumps(post_data) + + response = self.client.post( + admin_login, post_data, REMOTE_ADDR=ip_addr, **headers + ) + return response + + def _lockout_user1_from_ip1(self): + for i in range(1, FAILURE_LIMIT+1): + response = self._login( + username=self.USER_1, + password=self.WRONG_PASSWORD, + ip_addr=self.IP_1 + ) + return response + + def setUp(self): + """Create two valid users for authentication. + """ + + self.user = User.objects.create_superuser( + username=self.USER_1, + email='test_1@example.com', + password=self.VALID_PASSWORD, + ) + self.user = User.objects.create_superuser( + username=self.USER_2, + email='test_2@example.com', + password=self.VALID_PASSWORD, + ) + + # Test for true and false positives when blocking by IP *OR* user (default). + # Cache disabled. Default settings. + @patch('axes.decorators.cache.set', return_value=None) + @patch('axes.decorators.cache.get', return_value=None) + def test_lockout_by_ip_blocks_when_same_user_same_ip_without_cache( + self, cache_get_mock=None, cache_set_mock=None + ): + # User 1 is locked out from IP 1. + self._lockout_user1_from_ip1() + + # User 1 is still blocked from IP 1. + response = self._login( + self.USER_1, + self.VALID_PASSWORD, + ip_addr=self.IP_1 + ) + self.assertEqual(response.status_code, self.BLOCKED) + + @patch('axes.decorators.cache.set', return_value=None) + @patch('axes.decorators.cache.get', return_value=None) + def test_lockout_by_ip_allows_when_same_user_diff_ip_without_cache( + self, cache_get_mock=None, cache_set_mock=None + ): + # User 1 is locked out from IP 1. + self._lockout_user1_from_ip1() + + # User 1 can still login from IP 2. + response = self._login( + self.USER_1, + self.VALID_PASSWORD, + ip_addr=self.IP_2 + ) + self.assertEqual(response.status_code, self.ALLOWED) + + @patch('axes.decorators.cache.set', return_value=None) + @patch('axes.decorators.cache.get', return_value=None) + def test_lockout_by_ip_blocks_when_diff_user_same_ip_without_cache( + self, cache_get_mock=None, cache_set_mock=None + ): + # User 1 is locked out from IP 1. + self._lockout_user1_from_ip1() + + # User 2 is also locked out from IP 1. + response = self._login( + self.USER_2, + self.VALID_PASSWORD, + ip_addr=self.IP_1 + ) + self.assertEqual(response.status_code, self.BLOCKED) + + @patch('axes.decorators.cache.set', return_value=None) + @patch('axes.decorators.cache.get', return_value=None) + def test_lockout_by_ip_allows_when_diff_user_diff_ip_without_cache( + self, cache_get_mock=None, cache_set_mock=None + ): + # User 1 is locked out from IP 1. + self._lockout_user1_from_ip1() + + # User 2 can still login from IP 2. + response = self._login( + self.USER_2, + self.VALID_PASSWORD, + ip_addr=self.IP_2 + ) + self.assertEqual(response.status_code, self.ALLOWED) + + # Test for true and false positives when blocking by user only. + # Cache disabled. When AXES_ONLY_USER_FAILURES = True + @patch('axes.decorators.AXES_ONLY_USER_FAILURES', True) + @patch('axes.decorators.cache.set', return_value=None) + @patch('axes.decorators.cache.get', return_value=None) + def test_lockout_by_user_blocks_when_same_user_same_ip_without_cache( + self, cache_get_mock=None, cache_set_mock=None + ): + # User 1 is locked out from IP 1. + self._lockout_user1_from_ip1() + + # User 1 is still blocked from IP 1. + response = self._login( + self.USER_1, + self.VALID_PASSWORD, + ip_addr=self.IP_1 + ) + self.assertEqual(response.status_code, self.BLOCKED) + + @patch('axes.decorators.AXES_ONLY_USER_FAILURES', True) + @patch('axes.decorators.cache.set', return_value=None) + @patch('axes.decorators.cache.get', return_value=None) + def test_lockout_by_user_blocks_when_same_user_diff_ip_without_cache( + self, cache_get_mock=None, cache_set_mock=None + ): + # User 1 is locked out from IP 1. + self._lockout_user1_from_ip1() + + # User 1 is also locked out from IP 2. + response = self._login( + self.USER_1, + self.VALID_PASSWORD, + ip_addr=self.IP_2 + ) + self.assertEqual(response.status_code, self.BLOCKED) + + @patch('axes.decorators.AXES_ONLY_USER_FAILURES', True) + @patch('axes.decorators.cache.set', return_value=None) + @patch('axes.decorators.cache.get', return_value=None) + def test_lockout_by_user_allows_when_diff_user_same_ip_without_cache( + self, cache_get_mock=None, cache_set_mock=None + ): + # User 1 is locked out from IP 1. + self._lockout_user1_from_ip1() + + # User 2 can still login from IP 1. + response = self._login( + self.USER_2, + self.VALID_PASSWORD, + ip_addr=self.IP_1 + ) + self.assertEqual(response.status_code, self.ALLOWED) + + @patch('axes.decorators.AXES_ONLY_USER_FAILURES', True) + @patch('axes.decorators.cache.set', return_value=None) + @patch('axes.decorators.cache.get', return_value=None) + def test_lockout_by_user_allows_when_diff_user_diff_ip_without_cache( + self, cache_get_mock=None, cache_set_mock=None + ): + # User 1 is locked out from IP 1. + self._lockout_user1_from_ip1() + + # User 2 can still login from IP 2. + response = self._login( + self.USER_2, + self.VALID_PASSWORD, + ip_addr=self.IP_2 + ) + self.assertEqual(response.status_code, self.ALLOWED) + + # Test for true and false positives when blocking by user and IP together. + # Cache disabled. When LOCK_OUT_BY_COMBINATION_USER_AND_IP = True + @patch('axes.decorators.LOCK_OUT_BY_COMBINATION_USER_AND_IP', True) + @patch('axes.decorators.cache.set', return_value=None) + @patch('axes.decorators.cache.get', return_value=None) + def test_lockout_by_user_and_ip_blocks_when_same_user_same_ip_without_cache( + self, cache_get_mock=None, cache_set_mock=None + ): + # User 1 is locked out from IP 1. + self._lockout_user1_from_ip1() + + # User 1 is still blocked from IP 1. + response = self._login( + self.USER_1, + self.VALID_PASSWORD, + ip_addr=self.IP_1 + ) + self.assertEqual(response.status_code, self.BLOCKED) + + @patch('axes.decorators.LOCK_OUT_BY_COMBINATION_USER_AND_IP', True) + @patch('axes.decorators.cache.set', return_value=None) + @patch('axes.decorators.cache.get', return_value=None) + def test_lockout_by_user_and_ip_allows_when_same_user_diff_ip_without_cache( + self, cache_get_mock=None, cache_set_mock=None + ): + # User 1 is locked out from IP 1. + self._lockout_user1_from_ip1() + + # User 1 can still login from IP 2. + response = self._login( + self.USER_1, + self.VALID_PASSWORD, + ip_addr=self.IP_2 + ) + self.assertEqual(response.status_code, self.ALLOWED) + + @patch('axes.decorators.LOCK_OUT_BY_COMBINATION_USER_AND_IP', True) + @patch('axes.decorators.cache.set', return_value=None) + @patch('axes.decorators.cache.get', return_value=None) + def test_lockout_by_user_and_ip_allows_when_diff_user_same_ip_without_cache( + self, cache_get_mock=None, cache_set_mock=None + ): + # User 1 is locked out from IP 1. + self._lockout_user1_from_ip1() + + # User 2 can still login from IP 1. + response = self._login( + self.USER_2, + self.VALID_PASSWORD, + ip_addr=self.IP_1 + ) + self.assertEqual(response.status_code, self.ALLOWED) + + @patch('axes.decorators.LOCK_OUT_BY_COMBINATION_USER_AND_IP', True) + @patch('axes.decorators.cache.set', return_value=None) + @patch('axes.decorators.cache.get', return_value=None) + def test_lockout_by_user_and_ip_allows_when_diff_user_diff_ip_without_cache( + self, cache_get_mock=None, cache_set_mock=None + ): + # User 1 is locked out from IP 1. + self._lockout_user1_from_ip1() + + # User 2 can still login from IP 2. + response = self._login( + self.USER_2, + self.VALID_PASSWORD, + ip_addr=self.IP_2 + ) + self.assertEqual(response.status_code, self.ALLOWED) + + class UtilsTest(TestCase): def test_iso8601(self): """Tests iso8601 correctly translates datetime.timdelta to ISO 8601 diff --git a/runtests.py b/runtests.py index f76e943..085472a 100755 --- a/runtests.py +++ b/runtests.py @@ -20,5 +20,6 @@ def run_tests(settings_module, *modules): if __name__ == '__main__': run_tests('axes.test_settings', [ 'axes.tests.AccessAttemptTest', + 'axes.tests.AccessAttemptConfigTest', 'axes.tests.UtilsTest', ])