From 08690d9db1e51b0fc0e7b896eb408c3230f4cdc5 Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Fri, 4 Nov 2016 11:54:17 +0100 Subject: [PATCH 01/19] Fix Travis creds and release conditions --- .travis.yml | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/.travis.yml b/.travis.yml index d80005f..bfb375e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,28 +1,26 @@ language: python - python: - - 2.7 - - 3.5 - +- 2.7 +- 3.5 env: - - DJANGO="Django>=1.8,<1.9" - - DJANGO="Django>=1.9,<1.10" - - DJANGO="Django --pre" - +- DJANGO="Django>=1.8,<1.9" +- DJANGO="Django>=1.9,<1.10" +- DJANGO="Django --pre" install: - - pip install -q $DJANGO coveralls +- pip install -q $DJANGO coveralls script: - - coverage run --source=axes runtests.py - - coverage report - +- coverage run --source=axes runtests.py +- coverage report after_success: - - coveralls - +- coveralls deploy: provider: pypi user: jazzband - distributions: "sdist bdist_wheel" + distributions: sdist bdist_wheel password: - secure: VD+63Tnv0VYNfFQv9f1KZ0k79HSX8veNk4dTy42Hriteci50z5uSQdZMnqqD83xQJa4VF6N7DHkxHnBVOWLCqGQZeYqR/5BuDFNUewcr6O14dk31HvxMsWDaN1KW0Qwtus8ZrztwGhZtZ/92ODA6luHI4mCTzqX0gcG0/aKd75s= + secure: KR7AiS0+1WHkDLKV6eru4l1VOuO1DvVuYiRr4UWqFa9tjgN0ZH4itJkw/LSMRHof9fvg8ZWvPxsb3ifM2lXv7Nrk81fv97MO8XbLdVyF5xyvVEYOii0cu4ffxttj7BjL6aRDrpdG0OqayCsXYgkXngC7zdBrdLxKOq7CIbJ6oUo= on: tags: true + repo: jazzband/django-axes + condition: $DJANGO = "Django>=1.8,<1.9" + python: "2.7" From 10208e7d70271578450cdd0f678cf7986e4efb87 Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Fri, 4 Nov 2016 12:02:26 +0100 Subject: [PATCH 02/19] Update changelog and pump version to 2.3.0 --- CHANGES.txt | 13 ++++++++++++- axes/__init__.py | 2 +- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index b2f82a9..f6e5ba8 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,9 +1,20 @@ Changes ======= -2.2.1 (2016-09-26) +2.3.0 (2016-11-04) ------------------ +- Fixed ``axes_reset`` management command to skip "ip" prefix to command + arguments. + [EvaMarques] + +- Added ``axes_reset_user`` management command to reset lockouts and failed + login records for given users. + [vladimirnani] + +- Fixed Travis-PyPI release configuration. + [jezdez] + - Make IP position argument optional. [aredalen] diff --git a/axes/__init__.py b/axes/__init__.py index 833bae8..18f5fed 100644 --- a/axes/__init__.py +++ b/axes/__init__.py @@ -1,4 +1,4 @@ -__version__ = '2.2.1' +__version__ = '2.3.0' default_app_config = 'axes.apps.AppConfig' From 5869ce037a4d6359eeb9baf34ab06883e47ab439 Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Fri, 4 Nov 2016 12:31:59 +0100 Subject: [PATCH 03/19] Add Django 1.10 to test matrix --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index bfb375e..dbbb53b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,7 @@ python: env: - DJANGO="Django>=1.8,<1.9" - DJANGO="Django>=1.9,<1.10" -- DJANGO="Django --pre" +- DJANGO="Django>=1.10,<1.11" install: - pip install -q $DJANGO coveralls script: From 2689e46f9119c2e8def73fab715aa3bd9d2fd676 Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Fri, 4 Nov 2016 12:32:28 +0100 Subject: [PATCH 04/19] Fix PyPI password again This referes to https://github.com/travis-ci/dpl/issues/377 basically --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index dbbb53b..660c02b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,9 +18,9 @@ deploy: user: jazzband distributions: sdist bdist_wheel password: - secure: KR7AiS0+1WHkDLKV6eru4l1VOuO1DvVuYiRr4UWqFa9tjgN0ZH4itJkw/LSMRHof9fvg8ZWvPxsb3ifM2lXv7Nrk81fv97MO8XbLdVyF5xyvVEYOii0cu4ffxttj7BjL6aRDrpdG0OqayCsXYgkXngC7zdBrdLxKOq7CIbJ6oUo= + secure: jFsQCmQ2ih3FNatGSxQ5cRwLgXFLPReJw2+GeBLK4nkL2zr6z6PM5j74pVk8cS5uKrxNhunrwZBeamG+N4J0TnYCKKJxP8hQIRQ96N8n/YL5ycm6XnPALpfhHXXcfoBeqZQCoeueKOMswBY+0eOJv+7Bj/O0GgSOeQC2/1CNhZk= on: tags: true repo: jazzband/django-axes condition: $DJANGO = "Django>=1.8,<1.9" - python: "2.7" + python: '2.7' From 94202905420a3daaa5a964398150dec97af65212 Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Fri, 4 Nov 2016 12:42:11 +0100 Subject: [PATCH 05/19] Yet another try. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 660c02b..a279e7d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,7 +18,7 @@ deploy: user: jazzband distributions: sdist bdist_wheel password: - secure: jFsQCmQ2ih3FNatGSxQ5cRwLgXFLPReJw2+GeBLK4nkL2zr6z6PM5j74pVk8cS5uKrxNhunrwZBeamG+N4J0TnYCKKJxP8hQIRQ96N8n/YL5ycm6XnPALpfhHXXcfoBeqZQCoeueKOMswBY+0eOJv+7Bj/O0GgSOeQC2/1CNhZk= + secure: WIkx80DYd/6+aOgWHmGE+Ni/KIiuRvvivsNPD9ZqX6ssLoxFUzfRkSUDADzTI6gvqqoEKxUEr5w3CZJ9RuyI/bZSfLJjuD8bUL6ana4Wv/hKx4UpsjYD9MvA7D+M/VC0GIh+YrqPOHwOUa4fTF8K92VHovm+fkIU12rKHRoj+0A= on: tags: true repo: jazzband/django-axes From d07a5e09fbf71601f4fa044a63d5c6a1bef85e67 Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Fri, 4 Nov 2016 12:56:01 +0100 Subject: [PATCH 06/19] Removed Travis PyPI deploy due to brokeness of password handling --- .travis.yml | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/.travis.yml b/.travis.yml index a279e7d..2e6cb43 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,14 +13,3 @@ script: - coverage report after_success: - coveralls -deploy: - provider: pypi - user: jazzband - distributions: sdist bdist_wheel - password: - secure: WIkx80DYd/6+aOgWHmGE+Ni/KIiuRvvivsNPD9ZqX6ssLoxFUzfRkSUDADzTI6gvqqoEKxUEr5w3CZJ9RuyI/bZSfLJjuD8bUL6ana4Wv/hKx4UpsjYD9MvA7D+M/VC0GIh+YrqPOHwOUa4fTF8K92VHovm+fkIU12rKHRoj+0A= - on: - tags: true - repo: jazzband/django-axes - condition: $DJANGO = "Django>=1.8,<1.9" - python: '2.7' From b49e685603064b75b01d32c435938ebf4185c3dc Mon Sep 17 00:00:00 2001 From: Yi Ming Yung Date: Fri, 4 Nov 2016 14:09:48 +0100 Subject: [PATCH 07/19] Added settings for disabling success accesslogs and added complete disabling of accesslogs --- axes/decorators.py | 19 ++++++++--------- axes/settings.py | 2 ++ axes/tests.py | 46 +++++++++++++++++++++++++++++++++++++++--- docs/configuration.rst | 3 ++- 4 files changed, 57 insertions(+), 13 deletions(-) diff --git a/axes/decorators.py b/axes/decorators.py index 3e69716..5f8a4a0 100644 --- a/axes/decorators.py +++ b/axes/decorators.py @@ -221,15 +221,16 @@ def watch_login(func): user_agent = request.META.get('HTTP_USER_AGENT', '')[:255] http_accept = request.META.get('HTTP_ACCEPT', '')[:1025] path_info = request.META.get('PATH_INFO', '')[:255] - if login_unsuccessful or not DISABLE_ACCESS_LOG: - AccessLog.objects.create( - user_agent=user_agent, - ip_address=get_ip(request), - username=request.POST.get(USERNAME_FORM_FIELD, None), - http_accept=http_accept, - path_info=path_info, - trusted=not login_unsuccessful, - ) + if not DISABLE_ACCESS_LOG: + if login_unsuccessful or not DISABLE_SUCCESS_ACCESS_LOG: + AccessLog.objects.create( + user_agent=user_agent, + ip_address=get_ip(request), + username=request.POST.get(USERNAME_FORM_FIELD, None), + http_accept=http_accept, + path_info=path_info, + trusted=not login_unsuccessful, + ) if check_request(request, login_unsuccessful): return response diff --git a/axes/settings.py b/axes/settings.py index 9e0beb4..9b9ab92 100644 --- a/axes/settings.py +++ b/axes/settings.py @@ -36,6 +36,8 @@ if (isinstance(COOLOFF_TIME, int) or isinstance(COOLOFF_TIME, float)): DISABLE_ACCESS_LOG = getattr(settings, 'AXES_DISABLE_ACCESS_LOG', False) +DISABLE_SUCCESS_ACCESS_LOG = getattr(settings, 'AXES_DISABLE_SUCCESS_ACCESS_LOG', False) + LOGGER = getattr(settings, 'AXES_LOGGER', 'axes.watch_login') LOCKOUT_TEMPLATE = getattr(settings, 'AXES_LOCKOUT_TEMPLATE', None) diff --git a/axes/tests.py b/axes/tests.py index 6fabb34..7bd60c7 100644 --- a/axes/tests.py +++ b/axes/tests.py @@ -289,6 +289,42 @@ class AccessAttemptTest(TestCase): self.assertEquals(response.status_code, 403) self.assertEquals(response.get('Content-Type'), 'application/json') + @patch('axes.decorators.DISABLE_SUCCESS_ACCESS_LOG', True) + def test_valid_logout_without_success_log(self): + AccessLog.objects.all().delete() + + response = self._login(is_valid_username=True, is_valid_password=True) + response = self.client.get(reverse('admin:logout')) + + self.assertEquals(AccessLog.objects.all().count(), 0) + self.assertContains(response, 'Logged out') + + @patch('axes.decorators.DISABLE_SUCCESS_ACCESS_LOG', True) + def test_non_valid_login_without_success_log(self): + """ + A non-valid login does generate an AccessLog when + `DISABLE_SUCCESS_ACCESS_LOG=True`. + """ + AccessLog.objects.all().delete() + + response = self._login(is_valid_username=True, is_valid_password=False) + self.assertEquals(response.status_code, 200) + + self.assertEquals(AccessLog.objects.all().count(), 1) + + @patch('axes.decorators.DISABLE_SUCCESS_ACCESS_LOG', True) + def test_valid_login_without_success_log(self): + """ + A valid login doesn't generate an AccessLog when + `DISABLE_SUCCESS_ACCESS_LOG=True`. + """ + AccessLog.objects.all().delete() + + response = self._login(is_valid_username=True, is_valid_password=True) + + self.assertEqual(response.status_code, 302) + self.assertEqual(AccessLog.objects.all().count(), 0) + @patch('axes.decorators.DISABLE_ACCESS_LOG', True) def test_valid_logout_without_log(self): AccessLog.objects.all().delete() @@ -301,18 +337,22 @@ class AccessAttemptTest(TestCase): @patch('axes.decorators.DISABLE_ACCESS_LOG', True) def test_non_valid_login_without_log(self): + """ + A non-valid login does generate an AccessLog when + `DISABLE_ACCESS_LOG=True`. + """ AccessLog.objects.all().delete() response = self._login(is_valid_username=True, is_valid_password=False) self.assertEquals(response.status_code, 200) - self.assertEquals(AccessLog.objects.all().count(), 1) + self.assertEquals(AccessLog.objects.all().count(), 0) @patch('axes.decorators.DISABLE_ACCESS_LOG', True) def test_valid_login_without_log(self): """ - A valid login doesn't generate an access attempt when - `AXES_DISABLE_ACCESS_LOG=True`. + A valid login doesn't generate an AccessLog when + `DISABLE_ACCESS_LOG=True`. """ AccessLog.objects.all().delete() diff --git a/docs/configuration.rst b/docs/configuration.rst index 9d17b0f..7b77eac 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -63,4 +63,5 @@ These should be defined in your ``settings.py`` file. Default: ``False`` * ``AXES_REVERSE_PROXY_HEADER``: If ``AXES_BEHIND_REVERSE_PROXY`` is ``True``, it will look for the IP address from this header. Default: ``HTTP_X_FORWARDED_FOR`` -* ``AXES_DISABLE_ACCESS_LOG``: If ``True``, successful logins will not be logged, so the access log shown in the admin interface will only list unsuccessful login attempts. +* ``AXES_DISABLE_ACCESS_LOG``: If ``True``, disable all access logging, so the admin interface will be empty. +* ``AXES_DISABLE_SUCCESS_ACCESS_LOG``: If ``True``, successful logins will not be logged, so the access log shown in the admin interface will only list unsuccessful login attempts. From c86f234a3aee8b4451691c54ba229990df0072cf Mon Sep 17 00:00:00 2001 From: Sam Kuehn Date: Fri, 4 Nov 2016 14:54:03 -0600 Subject: [PATCH 08/19] add test for is_ipv6 --- axes/tests.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/axes/tests.py b/axes/tests.py index 7bd60c7..d83b9bd 100644 --- a/axes/tests.py +++ b/axes/tests.py @@ -386,3 +386,9 @@ class UtilsTest(TestCase): } for timedelta, iso_duration in six.iteritems(EXPECTED): self.assertEqual(iso8601(timedelta), iso_duration) + + def test_is_ipv6(self): + from decorators import is_ipv6 + self.assertTrue(is_ipv6('ff80::220:16ff:fec9:1')) + self.assertFalse(is_ipv6('67.255.125.204')) + self.assertFalse(is_ipv6('foo')) From 7e6ac85d4ec7d97732cda75d6da67c95ae281ac7 Mon Sep 17 00:00:00 2001 From: Sam Kuehn Date: Fri, 4 Nov 2016 14:59:42 -0600 Subject: [PATCH 09/19] fix #201 error: illegal IP address string passed to inet_pton --- axes/decorators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/axes/decorators.py b/axes/decorators.py index 5f8a4a0..eb788fd 100644 --- a/axes/decorators.py +++ b/axes/decorators.py @@ -31,7 +31,7 @@ if BEHIND_REVERSE_PROXY: def is_ipv6(ip): try: inet_pton(AF_INET6, ip) - except OSError: + except: return False return True From a32f030c6aa10482af6fe67af075f32c161aa232 Mon Sep 17 00:00:00 2001 From: Sam Kuehn Date: Fri, 4 Nov 2016 15:27:19 -0600 Subject: [PATCH 10/19] fix exception too broad --- axes/decorators.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/axes/decorators.py b/axes/decorators.py index eb788fd..1b6005f 100644 --- a/axes/decorators.py +++ b/axes/decorators.py @@ -1,6 +1,6 @@ import json import logging -from socket import inet_pton, AF_INET6 +from socket import inet_pton, AF_INET6, error from django.contrib.auth import get_user_model from django.contrib.auth import logout @@ -31,7 +31,7 @@ if BEHIND_REVERSE_PROXY: def is_ipv6(ip): try: inet_pton(AF_INET6, ip) - except: + except (OSError, error): return False return True From 610f04120fe7f42e3a64815c73d54c9b92e11d5c Mon Sep 17 00:00:00 2001 From: Sam Kuehn Date: Mon, 7 Nov 2016 09:02:13 -0700 Subject: [PATCH 11/19] fix python3 import --- axes/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/axes/tests.py b/axes/tests.py index d83b9bd..eb894e3 100644 --- a/axes/tests.py +++ b/axes/tests.py @@ -388,7 +388,7 @@ class UtilsTest(TestCase): self.assertEqual(iso8601(timedelta), iso_duration) def test_is_ipv6(self): - from decorators import is_ipv6 + from axes.decorators import is_ipv6 self.assertTrue(is_ipv6('ff80::220:16ff:fec9:1')) self.assertFalse(is_ipv6('67.255.125.204')) self.assertFalse(is_ipv6('foo')) From acbccda6f5e76dc999c0d3fb3f539c9893a97121 Mon Sep 17 00:00:00 2001 From: Ben Konrath Date: Thu, 10 Nov 2016 13:05:00 +0100 Subject: [PATCH 12/19] Update configuration.rst --- docs/configuration.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/configuration.rst b/docs/configuration.rst index 7b77eac..cebb438 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -49,9 +49,9 @@ These should be defined in your ``settings.py`` file. Default: ``True`` * ``AXES_USERNAME_FORM_FIELD``: the name of the form field that contains your users usernames. Default: ``username`` -* ``AXES_LOCK_OUT_BY_COMBINATION_USER_AND_IP``: If ``True`` prevents to login - from IP under particular user if attempts limit exceed, otherwise lock out - based on IP. +* ``AXES_LOCK_OUT_BY_COMBINATION_USER_AND_IP``: If ``True`` prevents the login + from IP under a particular user if the attempt limit has been exceeded, + otherwise lock out based on IP. Default: ``False`` * ``AXES_ONLY_USER_FAILURES`` : If ``True`` only locks based on user id and never locks by IP if attempts limit exceed, otherwise utilize the existing IP and user locking logic From ef3d527bee81e10455f73e57aaea13a222c9b792 Mon Sep 17 00:00:00 2001 From: Camilo Nova Date: Sat, 12 Nov 2016 16:06:49 -0500 Subject: [PATCH 13/19] Bump version --- CHANGES.txt | 10 ++++++++++ axes/__init__.py | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index f6e5ba8..032ef08 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,6 +1,16 @@ Changes ======= +2.3.1 (2016-11-12) +------------------ + +- Added settings for disabling success accesslogs + [Minkey27] + +- Fixed illegal IP address string passed to inet_pton + [samkuehn] + + 2.3.0 (2016-11-04) ------------------ diff --git a/axes/__init__.py b/axes/__init__.py index 18f5fed..eb10616 100644 --- a/axes/__init__.py +++ b/axes/__init__.py @@ -1,4 +1,4 @@ -__version__ = '2.3.0' +__version__ = '2.3.1' default_app_config = 'axes.apps.AppConfig' From c94e381bb7a7a0c7fee94522e2e0c33e62d5b913 Mon Sep 17 00:00:00 2001 From: Matthew Schinckel Date: Thu, 17 Nov 2016 16:23:42 +1030 Subject: [PATCH 14/19] Only look for lockable users on a POST. Resolves #205. --- axes/decorators.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/axes/decorators.py b/axes/decorators.py index 1b6005f..d835267 100644 --- a/axes/decorators.py +++ b/axes/decorators.py @@ -94,6 +94,9 @@ def is_user_lockable(request): If so, then return the value to see if this user is special and doesn't get their account locked out """ + if request.method != 'POST': + return False + try: field = getattr(get_user_model(), 'USERNAME_FIELD', 'username') kwargs = { From 68c71288857e78b3f722779ff3a8c8460688b2a0 Mon Sep 17 00:00:00 2001 From: Matthew Schinckel Date: Thu, 17 Nov 2016 16:46:30 +1030 Subject: [PATCH 15/19] Playing around with different is_user_lockable ideas. --- axes/decorators.py | 5 ++++- axes/tests.py | 11 +++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/axes/decorators.py b/axes/decorators.py index d835267..6534948 100644 --- a/axes/decorators.py +++ b/axes/decorators.py @@ -94,8 +94,11 @@ def is_user_lockable(request): If so, then return the value to see if this user is special and doesn't get their account locked out """ + if hasattr(request.user, 'nolockout'): + return not request.user.nolockout + if request.method != 'POST': - return False + return True try: field = getattr(get_user_model(), 'USERNAME_FIELD', 'username') diff --git a/axes/tests.py b/axes/tests.py index eb894e3..614e275 100644 --- a/axes/tests.py +++ b/axes/tests.py @@ -361,6 +361,17 @@ class AccessAttemptTest(TestCase): self.assertEqual(response.status_code, 302) self.assertEqual(AccessLog.objects.all().count(), 0) + @patch('axes.decorators.DISABLE_ACCESS_LOG', True) + def test_check_is_not_made_on_GET(self): + AccessLog.objects.all().delete() + + try: + admin_login = reverse('admin:login') + except NoReverseMatch: + admin_login = reverse('admin:index') + + response = self.client.get(admin_login) + class UtilsTest(TestCase): def test_iso8601(self): From ddfd53d67828df08f96e43a971b78ff960acc8e0 Mon Sep 17 00:00:00 2001 From: Matthew Schinckel Date: Thu, 17 Nov 2016 16:53:15 +1030 Subject: [PATCH 16/19] More tests. Still not entirely sure where I'm going with this yet. --- axes/tests.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/axes/tests.py b/axes/tests.py index 614e275..a0816ed 100644 --- a/axes/tests.py +++ b/axes/tests.py @@ -371,6 +371,13 @@ class AccessAttemptTest(TestCase): admin_login = reverse('admin:index') response = self.client.get(admin_login) + self.assertEqual(response.status_code, 200) + + response = self._login(is_valid_username=True, is_valid_password=True) + self.assertEqual(response.status_code, 302) + + response = self.client.get(admin_login) + self.assertEqual(response.status_code, 200) class UtilsTest(TestCase): From 90bf691e17fa3064cf573d28e08fd0e85db3ff36 Mon Sep 17 00:00:00 2001 From: Matthew Schinckel Date: Thu, 17 Nov 2016 16:57:12 +1030 Subject: [PATCH 17/19] Fix failing test. I think I'm just ensuring test coverage is not reduced now. --- axes/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/axes/tests.py b/axes/tests.py index a0816ed..28df463 100644 --- a/axes/tests.py +++ b/axes/tests.py @@ -376,7 +376,7 @@ class AccessAttemptTest(TestCase): response = self._login(is_valid_username=True, is_valid_password=True) self.assertEqual(response.status_code, 302) - response = self.client.get(admin_login) + response = self.client.get(reverse('admin:index')) self.assertEqual(response.status_code, 200) From 41877cdecd400231c7642e511ace194ecea8a76f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleksi=20H=C3=A4kli?= Date: Mon, 21 Nov 2016 21:33:53 +0200 Subject: [PATCH 18/19] Fix and add tests for IPv4 and IPv6 parsing This patch does not fix IPv6 parsing with ports --- .travis.yml | 4 +- axes/decorators.py | 33 +++++++--- axes/test_settings_proxy.py | 3 + axes/test_settings_proxy_custom_header.py | 3 + axes/tests.py | 74 ++++++++++++++++++++++- runtests.py | 15 ++++- runtests_proxy.py | 8 +++ runtests_proxy_custom_header.py | 8 +++ 8 files changed, 133 insertions(+), 15 deletions(-) create mode 100644 axes/test_settings_proxy.py create mode 100644 axes/test_settings_proxy_custom_header.py create mode 100644 runtests_proxy.py create mode 100644 runtests_proxy_custom_header.py diff --git a/.travis.yml b/.travis.yml index 2e6cb43..5df10d4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,9 @@ env: install: - pip install -q $DJANGO coveralls script: -- coverage run --source=axes runtests.py +- coverage run -a --source=axes runtests.py +- coverage run -a --source=axes runtests_proxy.py +- coverage run -a --source=axes runtests_proxy_custom_header.py - coverage report after_success: - coveralls diff --git a/axes/decorators.py b/axes/decorators.py index 1b6005f..31e9904 100644 --- a/axes/decorators.py +++ b/axes/decorators.py @@ -37,15 +37,18 @@ def is_ipv6(ip): def get_ip(request): - ip = request.META.get('REMOTE_ADDR', '') + """Parse IP address from REMOTE_ADDR or + AXES_REVERSE_PROXY_HEADER if AXES_BEHIND_REVERSE_PROXY is set.""" if BEHIND_REVERSE_PROXY: - ip = request.META.get(REVERSE_PROXY_HEADER, '').split(',', 1)[0] - ip = ip.strip() + # For requests originating from behind a reverse proxy, + # resolve the IP address from the given AXES_REVERSE_PROXY_HEADER. + # AXES_REVERSE_PROXY_HEADER defaults to HTTP_X_FORWARDED_FOR if not given, + # which is the Django calling format for the HTTP X-Forwarder-For header. + # Please see RFC7239 for additional information: + # https://tools.ietf.org/html/rfc7239#section-5 - # IIS seems to add the port number to HTTP_X_FORWARDED_FOR - if ':' in ip: - ip = ip.split(':')[0] + ip = request.META.get(REVERSE_PROXY_HEADER, '') if not ip: raise Warning( @@ -54,11 +57,21 @@ def get_ip(request): 'server settings to make sure this header value is being ' 'passed. Header value {0}'.format(REVERSE_PROXY_HEADER) ) - if not is_ipv6(ip): - # Fix for IIS adding client port number to 'HTTP_X_FORWARDED_FOR' header (removes port number). - ip = ''.join(ip.split(':')[:-1]) - return ip + # X-Forwarded-For IPs can have multiple IPs of which the first one is the + # originating reverse and the rest are proxies that are between the client + ip = ip.split(',', 1)[0] + + # As spaces are permitted between given X-Forwarded-For IP addresses, strip them as well + ip = ip.strip() + + # Fix IIS adding client port number to 'X-Forwarded-For' header (strip port) + if not is_ipv6(ip): + ip = ip.split(':', 1)[0] + + return ip + + return request.META.get('REMOTE_ADDR', '') def query2str(items, max_length=1024): diff --git a/axes/test_settings_proxy.py b/axes/test_settings_proxy.py new file mode 100644 index 0000000..2adf793 --- /dev/null +++ b/axes/test_settings_proxy.py @@ -0,0 +1,3 @@ +from .test_settings import * + +AXES_BEHIND_REVERSE_PROXY = True diff --git a/axes/test_settings_proxy_custom_header.py b/axes/test_settings_proxy_custom_header.py new file mode 100644 index 0000000..8e092ae --- /dev/null +++ b/axes/test_settings_proxy_custom_header.py @@ -0,0 +1,3 @@ +from .test_settings_proxy import * + +AXES_REVERSE_PROXY_HEADER = 'HTTP_X_AXES_CUSTOM_HEADER' diff --git a/axes/tests.py b/axes/tests.py index eb894e3..87795ca 100644 --- a/axes/tests.py +++ b/axes/tests.py @@ -4,14 +4,17 @@ import time import json import datetime +from mock import patch + +from django.conf import settings from django.test import TestCase from django.test.utils import override_settings from django.contrib.auth.models import User from django.core.urlresolvers import NoReverseMatch from django.core.urlresolvers import reverse from django.utils import six -from mock import patch +from axes.decorators import get_ip from axes.settings import COOLOFF_TIME from axes.settings import FAILURE_LIMIT from axes.models import AccessAttempt, AccessLog @@ -19,6 +22,11 @@ from axes.signals import user_locked_out from axes.utils import reset, iso8601 +class MockRequest: + def __init__(self): + self.META = dict() + + class AccessAttemptTest(TestCase): """Test case using custom settings for testing """ @@ -392,3 +400,67 @@ class UtilsTest(TestCase): self.assertTrue(is_ipv6('ff80::220:16ff:fec9:1')) self.assertFalse(is_ipv6('67.255.125.204')) self.assertFalse(is_ipv6('foo')) + + +class GetIPProxyTest(TestCase): + """Test get_ip returns correct addresses with proxy + """ + def setUp(self): + self.request = MockRequest() + + def test_iis_ipv4_port_stripping(self): + self.ip = '192.168.1.1' + + valid_headers = [ + '192.168.1.1:6112', + '192.168.1.1:6033, 192.168.1.2:9001', + ] + + for header in valid_headers: + self.request.META['HTTP_X_FORWARDED_FOR'] = header + self.assertEqual(self.ip, get_ip(self.request)) + + def test_valid_ipv4_parsing(self): + self.ip = '192.168.1.1' + + valid_headers = [ + '192.168.1.1', + '192.168.1.1, 192.168.1.2', + ' 192.168.1.1 , 192.168.1.2 ', + ' 192.168.1.1 , 2001:db8:cafe::17 ', + ] + + for header in valid_headers: + self.request.META['HTTP_X_FORWARDED_FOR'] = header + self.assertEqual(self.ip, get_ip(self.request)) + + def test_valid_ipv6_parsing(self): + self.ip = '2001:db8:cafe::17' + + valid_headers = [ + '2001:db8:cafe::17', + '2001:db8:cafe::17 , 2001:db8:cafe::18', + '2001:db8:cafe::17, 2001:db8:cafe::18, 192.168.1.1', + ] + + for header in valid_headers: + self.request.META['HTTP_X_FORWARDED_FOR'] = header + self.assertEqual(self.ip, get_ip(self.request)) + + +class GetIPProxyCustomHeaderTest(TestCase): + """Test that get_ip returns correct addresses with a custom proxy header + """ + def setUp(self): + self.request = MockRequest() + + def test_custom_header_parsing(self): + self.ip = '2001:db8:cafe::17' + + valid_headers = [ + ' 2001:db8:cafe::17 , 2001:db8:cafe::18', + ] + + for header in valid_headers: + self.request.META[settings.AXES_REVERSE_PROXY_HEADER] = header + self.assertEqual(self.ip, get_ip(self.request)) diff --git a/runtests.py b/runtests.py index 3003d3b..f76e943 100755 --- a/runtests.py +++ b/runtests.py @@ -1,4 +1,5 @@ #!/usr/bin/env python + import os import sys @@ -6,10 +7,18 @@ import django from django.conf import settings from django.test.utils import get_runner -if __name__ == "__main__": - os.environ['DJANGO_SETTINGS_MODULE'] = 'axes.test_settings' + +def run_tests(settings_module, *modules): + os.environ['DJANGO_SETTINGS_MODULE'] = settings_module django.setup() TestRunner = get_runner(settings) test_runner = TestRunner() - failures = test_runner.run_tests(["axes"]) + failures = test_runner.run_tests(*modules) sys.exit(bool(failures)) + + +if __name__ == '__main__': + run_tests('axes.test_settings', [ + 'axes.tests.AccessAttemptTest', + 'axes.tests.UtilsTest', + ]) diff --git a/runtests_proxy.py b/runtests_proxy.py new file mode 100644 index 0000000..6ffa46d --- /dev/null +++ b/runtests_proxy.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python + +from runtests import run_tests + +if __name__ == '__main__': + run_tests('axes.test_settings_proxy', [ + 'axes.tests.GetIPProxyTest', + ]) diff --git a/runtests_proxy_custom_header.py b/runtests_proxy_custom_header.py new file mode 100644 index 0000000..19d5c50 --- /dev/null +++ b/runtests_proxy_custom_header.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python + +from runtests import run_tests + +if __name__ == '__main__': + run_tests('axes.test_settings_proxy_custom_header', [ + 'axes.tests.GetIPProxyCustomHeaderTest', + ]) From 634c542dad93faf888008330409302c0b8005da0 Mon Sep 17 00:00:00 2001 From: Camilo Nova Date: Thu, 24 Nov 2016 08:55:38 -0500 Subject: [PATCH 19/19] Bump version --- CHANGES.txt | 10 ++++++++++ axes/__init__.py | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index 032ef08..57ab858 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,6 +1,16 @@ Changes ======= +2.3.2 (2016-11-24) +------------------ + +- Only look for lockable users on a POST + [schinckel] + +- Fix and add tests for IPv4 and IPv6 parsing + [aleksihakli] + + 2.3.1 (2016-11-12) ------------------ diff --git a/axes/__init__.py b/axes/__init__.py index eb10616..b068f8a 100644 --- a/axes/__init__.py +++ b/axes/__init__.py @@ -1,4 +1,4 @@ -__version__ = '2.3.1' +__version__ = '2.3.2' default_app_config = 'axes.apps.AppConfig'