tests.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. import random
  2. import string
  3. import time
  4. import json
  5. import datetime
  6. from django.test import TestCase
  7. from django.test.utils import override_settings
  8. from django.contrib.auth.models import User
  9. from django.urls import NoReverseMatch
  10. from django.urls import reverse
  11. from django.utils import six
  12. from axes.decorators import COOLOFF_TIME
  13. from axes.decorators import FAILURE_LIMIT
  14. from axes.decorators import is_valid_ip
  15. from axes.models import AccessAttempt, AccessLog
  16. from axes.signals import user_locked_out
  17. from axes.utils import reset, iso8601
  18. class AccessAttemptTest(TestCase):
  19. """Test case using custom settings for testing
  20. """
  21. VALID_USERNAME = 'valid-username'
  22. VALID_PASSWORD = 'valid-password'
  23. LOCKED_MESSAGE = 'Account locked: too many login attempts.'
  24. LOGIN_FORM_KEY = '<input type="submit" value="Log in" />'
  25. def _login(self, is_valid_username=False, is_valid_password=False,
  26. is_json=False, **kwargs):
  27. """Login a user. A valid credential is used when is_valid_username is True,
  28. otherwise it will use a random string to make a failed login.
  29. """
  30. try:
  31. admin_login = reverse('admin:login')
  32. except NoReverseMatch:
  33. admin_login = reverse('admin:index')
  34. if is_valid_username:
  35. # Use a valid username
  36. username = self.VALID_USERNAME
  37. else:
  38. # Generate a wrong random username
  39. chars = string.ascii_uppercase + string.digits
  40. username = ''.join(random.choice(chars) for x in range(10))
  41. if is_valid_password:
  42. password = self.VALID_PASSWORD
  43. else:
  44. password = 'invalid-password'
  45. headers = {
  46. 'user_agent': 'test-browser'
  47. }
  48. post_data = {
  49. 'username': username,
  50. 'password': password,
  51. 'this_is_the_login_form': 1,
  52. }
  53. post_data.update(kwargs)
  54. if is_json:
  55. headers.update({
  56. 'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest',
  57. 'content_type': 'application/json',
  58. })
  59. post_data = json.dumps(post_data)
  60. response = self.client.post(admin_login, post_data, **headers)
  61. return response
  62. def setUp(self):
  63. """Create a valid user for login
  64. """
  65. self.user = User.objects.create_superuser(
  66. username=self.VALID_USERNAME,
  67. email='test@example.com',
  68. password=self.VALID_PASSWORD,
  69. )
  70. def test_failure_limit_once(self):
  71. """Tests the login lock trying to login one more time
  72. than failure limit
  73. """
  74. # test until one try before the limit
  75. for i in range(1, FAILURE_LIMIT):
  76. response = self._login()
  77. # Check if we are in the same login page
  78. self.assertContains(response, self.LOGIN_FORM_KEY)
  79. # So, we shouldn't have gotten a lock-out yet.
  80. # But we should get one now
  81. response = self._login()
  82. self.assertContains(response, self.LOCKED_MESSAGE, status_code=403)
  83. def test_failure_limit_many(self):
  84. """Tests the login lock trying to login a lot of times more
  85. than failure limit
  86. """
  87. for i in range(1, FAILURE_LIMIT):
  88. response = self._login()
  89. # Check if we are in the same login page
  90. self.assertContains(response, self.LOGIN_FORM_KEY)
  91. # So, we shouldn't have gotten a lock-out yet.
  92. # We should get a locked message each time we try again
  93. for i in range(0, random.randrange(1, FAILURE_LIMIT)):
  94. response = self._login()
  95. self.assertContains(response, self.LOCKED_MESSAGE, status_code=403)
  96. def test_valid_login(self):
  97. """Tests a valid login for a real username
  98. """
  99. response = self._login(is_valid_username=True, is_valid_password=True)
  100. self.assertNotContains(response, self.LOGIN_FORM_KEY, status_code=302)
  101. def test_valid_logout(self):
  102. """Tests a valid logout and make sure the logout_time is updated
  103. """
  104. response = self._login(is_valid_username=True, is_valid_password=True)
  105. self.assertEquals(AccessLog.objects.latest('id').logout_time, None)
  106. response = self.client.get(reverse('admin:logout'))
  107. self.assertNotEquals(AccessLog.objects.latest('id').logout_time, None)
  108. self.assertContains(response, 'Logged out')
  109. def test_cooling_off(self):
  110. """Tests if the cooling time allows a user to login
  111. """
  112. self.test_failure_limit_once()
  113. # Wait for the cooling off period
  114. time.sleep(COOLOFF_TIME.total_seconds())
  115. # It should be possible to login again, make sure it is.
  116. self.test_valid_login()
  117. def test_cooling_off_for_trusted_user(self):
  118. """Test the cooling time for a trusted user
  119. """
  120. # Test successful login-logout, this makes the user trusted.
  121. self.test_valid_logout()
  122. # Try the cooling off time
  123. self.test_cooling_off()
  124. def test_long_user_agent_valid(self):
  125. """Tests if can handle a long user agent
  126. """
  127. long_user_agent = 'ie6' * 1024
  128. response = self._login(
  129. is_valid_username=True,
  130. is_valid_password=True,
  131. user_agent=long_user_agent,
  132. )
  133. self.assertNotContains(response, self.LOGIN_FORM_KEY, status_code=302)
  134. def test_long_user_agent_not_valid(self):
  135. """Tests if can handle a long user agent with failure
  136. """
  137. long_user_agent = 'ie6' * 1024
  138. for i in range(0, FAILURE_LIMIT + 1):
  139. response = self._login(user_agent=long_user_agent)
  140. self.assertContains(response, self.LOCKED_MESSAGE, status_code=403)
  141. def test_reset_ip(self):
  142. """Tests if can reset an ip address
  143. """
  144. # Make a lockout
  145. self.test_failure_limit_once()
  146. # Reset the ip so we can try again
  147. reset(ip='127.0.0.1')
  148. # Make a login attempt again
  149. self.test_valid_login()
  150. def test_reset_all(self):
  151. """Tests if can reset all attempts
  152. """
  153. # Make a lockout
  154. self.test_failure_limit_once()
  155. # Reset all attempts so we can try again
  156. reset()
  157. # Make a login attempt again
  158. self.test_valid_login()
  159. def test_send_lockout_signal(self):
  160. """Test if the lockout signal is emitted
  161. """
  162. class Scope(object): pass # this "hack" is needed so we don't have to use global variables or python3 features
  163. scope = Scope()
  164. scope.signal_received = 0
  165. def signal_handler(request, username, ip_address, *args, **kwargs):
  166. scope.signal_received += 1
  167. self.assertIsNotNone(request)
  168. # Connect signal handler
  169. user_locked_out.connect(signal_handler)
  170. # Make a lockout
  171. self.test_failure_limit_once()
  172. self.assertEquals(scope.signal_received, 1)
  173. reset()
  174. # Make another lockout
  175. self.test_failure_limit_once()
  176. self.assertEquals(scope.signal_received, 2)
  177. @override_settings(AXES_LOCK_OUT_BY_COMBINATION_USER_AND_IP=True)
  178. def test_lockout_by_combination_user_and_ip(self):
  179. """Tests the login lock with a valid username and invalid password
  180. when AXES_LOCK_OUT_BY_COMBINATION_USER_AND_IP is True
  181. """
  182. # test until one try before the limit
  183. for i in range(1, FAILURE_LIMIT):
  184. response = self._login(
  185. is_valid_username=True,
  186. is_valid_password=False,
  187. )
  188. # Check if we are in the same login page
  189. self.assertContains(response, self.LOGIN_FORM_KEY)
  190. # So, we shouldn't have gotten a lock-out yet.
  191. # But we should get one now
  192. response = self._login(is_valid_username=True, is_valid_password=False)
  193. self.assertContains(response, self.LOCKED_MESSAGE, status_code=403)
  194. def test_log_data_truncated(self):
  195. """Tests that query2str properly truncates data to the max_length (default 1024)
  196. """
  197. # An impossibly large post dict
  198. extra_data = {string.ascii_letters * x: x for x in range(0, 1000)}
  199. self._login(**extra_data)
  200. self.assertEquals(
  201. len(AccessAttempt.objects.latest('id').post_data), 1024
  202. )
  203. def test_json_response(self):
  204. """Tests response content type and status code for the ajax request
  205. """
  206. self.test_failure_limit_once()
  207. response = self._login(is_json=True)
  208. self.assertEquals(response.status_code, 403)
  209. self.assertEquals(response.get('Content-Type'), 'application/json')
  210. class IPClassifierTest(TestCase):
  211. def test_classify_private_ips(self):
  212. """Tests whether is_valid_public_ip correctly classifies IPs as being
  213. bot public and valid
  214. """
  215. EXPECTED = {
  216. 'foobar': False, # invalid - not octects
  217. '192.168.0': False, # invalid - only 3 octets
  218. '192.168.0.0': False, # private
  219. '192.168.165.1': False, # private
  220. '192.249.19.1': True, # public but 192 prefix
  221. '10.0.201.13': False, # private
  222. '172.15.12.1': True, # public but 172 prefix
  223. '172.16.12.1': False, # private
  224. '172.31.12.1': False, # private
  225. '172.32.0.1': True, # public but 127 prefix
  226. '200.150.23.5': True, # normal public
  227. }
  228. for ip_address, is_valid_public in six.iteritems(EXPECTED):
  229. self.assertEqual(is_valid_ip(ip_address), is_valid_public)
  230. class UtilsTest(TestCase):
  231. def test_iso8601(self):
  232. """Tests iso8601 correctly translates datetime.timdelta to ISO 8601
  233. formatted duration."""
  234. EXPECTED = {
  235. datetime.timedelta(days=1, hours=25, minutes=42, seconds=8):
  236. 'P2DT1H42M8S',
  237. datetime.timedelta(days=7, seconds=342):
  238. 'P7DT5M42S',
  239. datetime.timedelta(days=0, hours=2, minutes=42):
  240. 'PT2H42M',
  241. datetime.timedelta(hours=20, seconds=42):
  242. 'PT20H42S',
  243. datetime.timedelta(seconds=300):
  244. 'PT5M',
  245. datetime.timedelta(seconds=9005):
  246. 'PT2H30M5S',
  247. datetime.timedelta(minutes=9005):
  248. 'P6DT6H5M',
  249. datetime.timedelta(days=15):
  250. 'P15D'
  251. }
  252. for timedelta, iso_duration in six.iteritems(EXPECTED):
  253. self.assertEqual(iso8601(timedelta), iso_duration)