base.py 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. from random import choice
  2. from string import ascii_letters, digits
  3. from time import sleep
  4. from django.contrib.auth import get_user_model
  5. from django.http import HttpRequest
  6. from django.test import TestCase
  7. from django.urls import reverse
  8. from django.utils.timezone import now
  9. from axes.conf import settings
  10. from axes.helpers import (
  11. get_cache,
  12. get_client_http_accept,
  13. get_client_ip_address,
  14. get_client_path_info,
  15. get_client_user_agent,
  16. get_cool_off,
  17. get_credentials,
  18. get_failure_limit,
  19. )
  20. from axes.models import AccessAttempt, AccessLog
  21. from axes.utils import reset
  22. def custom_failure_limit(request, credentials):
  23. return 3
  24. class AxesTestCase(TestCase):
  25. """
  26. Test case using custom settings for testing.
  27. """
  28. VALID_USERNAME = "axes-valid-username"
  29. VALID_PASSWORD = "axes-valid-password"
  30. VALID_EMAIL = "axes-valid-email@example.com"
  31. VALID_USER_AGENT = "axes-user-agent"
  32. VALID_IP_ADDRESS = "127.0.0.1"
  33. INVALID_USERNAME = "axes-invalid-username"
  34. INVALID_PASSWORD = "axes-invalid-password"
  35. INVALID_EMAIL = "axes-invalid-email@example.com"
  36. LOCKED_MESSAGE = "Account locked: too many login attempts."
  37. LOGOUT_MESSAGE = "Logged out"
  38. LOGIN_FORM_KEY = '<input type="submit" value="Log in" />'
  39. STATUS_SUCCESS = 200
  40. ALLOWED = 302
  41. BLOCKED = 403
  42. def setUp(self):
  43. """
  44. Create a valid user for login.
  45. """
  46. self.username = self.VALID_USERNAME
  47. self.password = self.VALID_PASSWORD
  48. self.email = self.VALID_EMAIL
  49. self.ip_address = self.VALID_IP_ADDRESS
  50. self.user_agent = self.VALID_USER_AGENT
  51. self.path_info = reverse("admin:login")
  52. self.user = get_user_model().objects.create_superuser(
  53. username=self.username, password=self.password, email=self.email
  54. )
  55. self.request = HttpRequest()
  56. self.request.method = "POST"
  57. self.request.META["REMOTE_ADDR"] = self.ip_address
  58. self.request.META["HTTP_USER_AGENT"] = self.user_agent
  59. self.request.META["PATH_INFO"] = self.path_info
  60. self.request.axes_attempt_time = now()
  61. self.request.axes_ip_address = get_client_ip_address(self.request)
  62. self.request.axes_user_agent = get_client_user_agent(self.request)
  63. self.request.axes_path_info = get_client_path_info(self.request)
  64. self.request.axes_http_accept = get_client_http_accept(self.request)
  65. self.credentials = get_credentials(self.username)
  66. def tearDown(self):
  67. get_cache().clear()
  68. def get_kwargs_with_defaults(self, **kwargs):
  69. defaults = {
  70. "user_agent": self.user_agent,
  71. "ip_address": self.ip_address,
  72. "username": self.username,
  73. }
  74. defaults.update(kwargs)
  75. return defaults
  76. def create_attempt(self, **kwargs):
  77. kwargs = self.get_kwargs_with_defaults(**kwargs)
  78. kwargs.setdefault("failures_since_start", 1)
  79. return AccessAttempt.objects.create(**kwargs)
  80. def create_log(self, **kwargs):
  81. return AccessLog.objects.create(**self.get_kwargs_with_defaults(**kwargs))
  82. def reset(self, ip=None, username=None):
  83. return reset(ip, username)
  84. def login(
  85. self,
  86. is_valid_username=False,
  87. is_valid_password=False,
  88. remote_addr=None,
  89. **kwargs
  90. ):
  91. """
  92. Login a user.
  93. A valid credential is used when is_valid_username is True,
  94. otherwise it will use a random string to make a failed login.
  95. """
  96. if is_valid_username:
  97. username = self.VALID_USERNAME
  98. else:
  99. username = "".join(choice(ascii_letters + digits) for _ in range(10))
  100. if is_valid_password:
  101. password = self.VALID_PASSWORD
  102. else:
  103. password = self.INVALID_PASSWORD
  104. post_data = {"username": username, "password": password, **kwargs}
  105. return self.client.post(
  106. reverse("admin:login"),
  107. post_data,
  108. REMOTE_ADDR=remote_addr or self.ip_address,
  109. HTTP_USER_AGENT=self.user_agent,
  110. )
  111. def logout(self):
  112. return self.client.post(
  113. reverse("admin:logout"),
  114. REMOTE_ADDR=self.ip_address,
  115. HTTP_USER_AGENT=self.user_agent,
  116. )
  117. def check_login(self):
  118. response = self.login(is_valid_username=True, is_valid_password=True)
  119. self.assertNotContains(
  120. response, self.LOGIN_FORM_KEY, status_code=self.ALLOWED, html=True
  121. )
  122. def almost_lockout(self):
  123. for _ in range(1, get_failure_limit(None, None)):
  124. response = self.login()
  125. self.assertContains(response, self.LOGIN_FORM_KEY, html=True)
  126. def lockout(self):
  127. self.almost_lockout()
  128. return self.login()
  129. def check_lockout(self):
  130. response = self.lockout()
  131. if settings.AXES_LOCK_OUT_AT_FAILURE == True:
  132. self.assertContains(response, self.LOCKED_MESSAGE, status_code=self.BLOCKED)
  133. else:
  134. self.assertNotContains(
  135. response, self.LOCKED_MESSAGE, status_code=self.STATUS_SUCCESS
  136. )
  137. def cool_off(self):
  138. sleep(get_cool_off().total_seconds())
  139. def check_logout(self):
  140. response = self.logout()
  141. self.assertContains(
  142. response, self.LOGOUT_MESSAGE, status_code=self.STATUS_SUCCESS
  143. )
  144. def check_handler(self):
  145. """
  146. Check a handler and its basic functionality with lockouts, cool offs, login, and logout.
  147. This is a check that is intended to successfully run for each and every new handler.
  148. """
  149. self.check_lockout()
  150. self.cool_off()
  151. self.check_login()
  152. self.check_logout()