diff --git a/defender/data.py b/defender/data.py index d99da4e..ea4057a 100644 --- a/defender/data.py +++ b/defender/data.py @@ -58,4 +58,4 @@ def get_approx_account_lockouts_from_login_attempts(ip_address=None, username=No # TODO: Maybe we should throw an exception here instead of returning 0? return 0 - return min(AccessAttempt.objects.filter(q).count() // failure_limit, len(config.LOCKOUT_COOLOFF_TIMES) - 1) \ No newline at end of file + return AccessAttempt.objects.filter(q).count() // failure_limit \ No newline at end of file diff --git a/defender/tests.py b/defender/tests.py index 68674df..c6357ac 100644 --- a/defender/tests.py +++ b/defender/tests.py @@ -11,6 +11,7 @@ from django.test.client import RequestFactory from redis.client import Redis from django.urls import reverse +from . import data from . import utils from . import config from .signals import ( @@ -436,7 +437,7 @@ class AccessAttemptTest(DefenderTestCase): self.assertTemplateUsed(response, "defender/lockout.html") @patch("defender.config.COOLOFF_TIME", 0) - @patch("defender.config.LOCKOUT_COOLOFF_TIMES", 0) + @patch("defender.config.LOCKOUT_COOLOFF_TIMES", [0]) def test_failed_login_no_cooloff(self): """ failed login no cooloff """ for i in range(0, config.FAILURE_LIMIT): @@ -958,62 +959,35 @@ class AccessAttemptTest(DefenderTestCase): @patch("defender.config.LOCKOUT_COOLOFF_TIMES", [3, 6]) def test_lockout_cooloff_correctly_scales_with_ip_when_set(self): - self.test_ip_failure_limit() - time.sleep(config.LOCKOUT_COOLOFF_TIMES[0]) + self.assertEqual(data.get_approx_account_lockouts_from_login_attempts(ip_address="127.0.0.1"), 1) if config.MOCK_REDIS: # mock redis require that we expire on our own get_redis_connection().do_expire() # pragma: no cover - self.assertFalse(utils.is_source_ip_already_locked("127.0.0.1")) - self.test_ip_failure_limit() - time.sleep(config.LOCKOUT_COOLOFF_TIMES[0]) - self.assertTrue(utils.is_source_ip_already_locked("127.0.0.1")) utils.reset_failed_attempts(ip_address="127.0.0.1") + self.test_ip_failure_limit() + self.assertEqual(data.get_approx_account_lockouts_from_login_attempts(ip_address="127.0.0.1"), 2) + if config.MOCK_REDIS: + # mock redis require that we expire on our own + get_redis_connection().do_expire() # pragma: no cover + time.sleep(config.LOCKOUT_COOLOFF_TIMES[1]) + self.test_valid_login() @patch("defender.config.LOCKOUT_COOLOFF_TIMES", [3, 6]) def test_lockout_cooloff_correctly_scales_with_username_when_set(self): self.test_username_failure_limit() - time.sleep(config.LOCKOUT_COOLOFF_TIMES[0]) + self.assertEqual(data.get_approx_account_lockouts_from_login_attempts(username=VALID_USERNAME), 1) if config.MOCK_REDIS: # mock redis require that we expire on our own get_redis_connection().do_expire() # pragma: no cover - self.assertFalse(utils.is_user_already_locked(VALID_USERNAME)) - self.test_username_failure_limit() - time.sleep(config.LOCKOUT_COOLOFF_TIMES[0]) - self.assertTrue(utils.is_user_already_locked(VALID_USERNAME)) utils.reset_failed_attempts(username=VALID_USERNAME) - - @patch("defender.config.LOCKOUT_COOLOFF_TIMES", [3, 6]) - def test_lockout_correctly_releases_after_scaling_with_ip(self): - self.test_ip_failure_limit() - time.sleep(config.LOCKOUT_COOLOFF_TIMES[0]) - if config.MOCK_REDIS: - # mock redis require that we expire on our own - get_redis_connection().do_expire() # pragma: no cover - self.assertFalse(utils.is_source_ip_already_locked("127.0.0.1")) - self.test_ip_failure_limit() - time.sleep(config.LOCKOUT_COOLOFF_TIMES[1]) - if config.MOCK_REDIS: - # mock redis require that we expire on our own - get_redis_connection().do_expire() # pragma: no cover - self.assertFalse(utils.is_source_ip_already_locked("127.0.0.1")) - utils.reset_failed_attempts(ip_address="127.0.0.1") - - @patch("defender.config.LOCKOUT_COOLOFF_TIMES", [3, 6]) - def test_lockout_correctly_releases_after_scaling_with_username(self): self.test_username_failure_limit() - time.sleep(config.LOCKOUT_COOLOFF_TIMES[0]) + self.assertEqual(data.get_approx_account_lockouts_from_login_attempts(username=VALID_USERNAME), 2) if config.MOCK_REDIS: # mock redis require that we expire on our own get_redis_connection().do_expire() # pragma: no cover - self.assertFalse(utils.is_user_already_locked(VALID_USERNAME)) - self.test_username_failure_limit() time.sleep(config.LOCKOUT_COOLOFF_TIMES[1]) - if config.MOCK_REDIS: - # mock redis require that we expire on our own - get_redis_connection().do_expire() # pragma: no cover - self.assertFalse(utils.is_user_already_locked(VALID_USERNAME)) - utils.reset_failed_attempts(username=VALID_USERNAME) + self.test_valid_login() class SignalTest(DefenderTestCase): diff --git a/defender/utils.py b/defender/utils.py index 43b0584..f3fe15c 100644 --- a/defender/utils.py +++ b/defender/utils.py @@ -206,10 +206,10 @@ def get_user_attempts(request, get_username=get_username_from_request, username= return max(ip_count, username_count) def get_lockout_cooloff_time(ip_address=None, username=None): - index = min( + index = max(0, min( len(config.LOCKOUT_COOLOFF_TIMES) - 1, - get_approx_account_lockouts_from_login_attempts(ip_address, username) - ) + get_approx_account_lockouts_from_login_attempts(ip_address, username) - 1 + )) return config.LOCKOUT_COOLOFF_TIMES[index]