From 18191897d3f3bec76f48f990f21dc8f12cc89c67 Mon Sep 17 00:00:00 2001 From: Alexander Schrijver Date: Wed, 14 Aug 2013 11:22:39 +0200 Subject: [PATCH 01/16] When the cooloff period has expired if the user is trusted: reset the failure counter otherwise obliterate the user. --- axes/decorators.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/axes/decorators.py b/axes/decorators.py index e50d79a..2cedaa1 100644 --- a/axes/decorators.py +++ b/axes/decorators.py @@ -176,9 +176,13 @@ def get_user_attempts(request): if COOLOFF_TIME: for attempt in attempts: - if attempt.attempt_time + COOLOFF_TIME < datetime.now() \ - and attempt.trusted is False: - attempt.delete() + if attempt.attempt_time + COOLOFF_TIME < datetime.now(): + if attempt.trusted: + attempt.failures_since_start = 0 + attempt.save() + else: + attempt.delete() + objects_deleted = True return attempts From 60273bab90dd1809c626f3a8573cc3868f62015c Mon Sep 17 00:00:00 2001 From: Alexander Schrijver Date: Wed, 14 Aug 2013 11:23:47 +0200 Subject: [PATCH 02/16] Reload the queryset after certain objects have been deleted. --- axes/decorators.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/axes/decorators.py b/axes/decorators.py index 2cedaa1..10215f3 100644 --- a/axes/decorators.py +++ b/axes/decorators.py @@ -144,13 +144,12 @@ def is_user_lockable(request): else: return True - -def get_user_attempts(request): +def _get_user_attempts(request): """Returns access attempt record if it exists. Otherwise return None. """ ip = get_ip(request) - + username = request.POST.get('username', None) if USE_USER_AGENT: @@ -174,6 +173,12 @@ def get_user_attempts(request): params['username'] = username attempts |= AccessAttempt.objects.filter(**params) + return attempts + +def get_user_attempts(request): + objects_deleted = False + attempts = _get_user_attempts(request) + if COOLOFF_TIME: for attempt in attempts: if attempt.attempt_time + COOLOFF_TIME < datetime.now(): @@ -184,6 +189,11 @@ def get_user_attempts(request): attempt.delete() objects_deleted = True + # If objects were deleted, we need to update the queryset to reflect this, + # so force a reload. + if objects_deleted: + attempts = _get_user_attempts(request) + return attempts From bf208944fdadfa71808ad75c160baadc67f4b5c4 Mon Sep 17 00:00:00 2001 From: Alexander Schrijver Date: Wed, 14 Aug 2013 11:29:15 +0200 Subject: [PATCH 03/16] Add tests for the cooling off period and for the cooling off period with trusted users. --- axes/test_settings.py | 3 +++ axes/tests.py | 61 ++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/axes/test_settings.py b/axes/test_settings.py index 31f71be..61a6f26 100644 --- a/axes/test_settings.py +++ b/axes/test_settings.py @@ -38,3 +38,6 @@ SECRET_KEY = 'too-secret-for-test' LOGIN_REDIRECT_URL = '/admin' AXES_LOGIN_FAILURE_LIMIT = 10 +from datetime import timedelta +AXES_COOLOFF_TIME=timedelta(seconds = 2) + diff --git a/axes/tests.py b/axes/tests.py index fe5aade..682cd49 100644 --- a/axes/tests.py +++ b/axes/tests.py @@ -1,14 +1,15 @@ import random import string +import time from django.test import TestCase +from django.test.client import Client from django.contrib.auth.models import User from django.core.urlresolvers import reverse from django.test.utils import override_settings -from axes.decorators import FAILURE_LIMIT -from axes.decorators import LOGIN_FORM_KEY -from axes.models import AccessLog +from axes.decorators import FAILURE_LIMIT, COOLOFF_TIME, LOGIN_FORM_KEY +from axes.models import AccessLog, AccessAttempt class AccessAttemptTest(TestCase): @@ -107,6 +108,60 @@ class AccessAttemptTest(TestCase): self.assertNotIn(LOGIN_FORM_KEY, response.content) + def _successful_login(self, username, password): + c = Client() + response = c.post('/admin/', { + 'username': username, + 'password': username, + 'this_is_the_login_form': 1, + }) + + return response + + + def _unsuccessful_login(self, username): + c = Client() + response = c.post('/admin/', { + 'username': username, + 'password': 'wrong', + 'this_is_the_login_form': 1, + }) + + return response + + def test_cooling_off_for_trusted_user(self): + valid_username = self._random_username(existing_username=True) + + # Test successful login, this makes the user trusted. + response = self._successful_login(valid_username, valid_username) + self.assertNotIn(LOGIN_FORM_KEY, response.content) + + self.test_cooling_off(username=valid_username) + + def test_cooling_off(self, username=None): + if username: + valid_username = username + else: + valid_username = self._random_username(existing_username=True) + + # Test unsuccessful login and stop just before lockout happens + for i in range(0, FAILURE_LIMIT): + response = self._unsuccessful_login(valid_username) + + # Check if we are in the same login page + self.assertIn(LOGIN_FORM_KEY, response.content) + + # Lock out the user + response = self._unsuccessful_login(valid_username) + self.assertIn(self.LOCKED_MESSAGE, response.content) + + # Wait for the cooling off period + time.sleep(COOLOFF_TIME.total_seconds()) + + # It should be possible to login again, make sure it is. + response = self._successful_login(valid_username, valid_username) + self.assertNotIn(self.LOCKED_MESSAGE, response.content) + def test_valid_logout(self): """Tests a valid logout and make sure the logout_time is updated """ From 4ca640d27f94fcddbe10a77b8b62f7f77d68bcee Mon Sep 17 00:00:00 2001 From: Martey Dodoo Date: Fri, 11 Oct 2013 16:26:37 -0400 Subject: [PATCH 04/16] Remove reference to axes.log in README. Since the custom logfile "axes.log" was removed in commit ef3f75e, the README should no longer reference it. --- README.rst | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 724c16f..a1c6f33 100644 --- a/README.rst +++ b/README.rst @@ -135,10 +135,7 @@ Usage Using ``django-axes`` is extremely simple. Once you install the application and the middleware, all you need to do is periodically check the Access -Attempts section of the admin. A log file is also created for you to keep -track of the events surrounding failed login attempts. This log file can be -found in your Django project directory, by the name of ``axes.log``. In the -future I plan on offering a way to customize options for logging a bit more. +Attempts section of the admin. By default, django-axes will lock out repeated attempts from the same IP address. You can allow this IP to attempt again by deleting the relevant From 85a398569b47050e73a2d72bcf4148ba33d370ed Mon Sep 17 00:00:00 2001 From: Camilo Nova Date: Tue, 22 Oct 2013 12:24:47 -0500 Subject: [PATCH 05/16] Update travis url --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index a1c6f33..6a71c09 100644 --- a/README.rst +++ b/README.rst @@ -1,9 +1,9 @@ Django Axes =========== -.. image:: https://secure.travis-ci.org/codekoala/django-axes.png?branch=master +.. image:: https://secure.travis-ci.org/django-security/django-axes.png?branch=master :alt: Build Status - :target: http://travis-ci.org/codekoala/django-axes + :target: http://travis-ci.org/django-security/django-axes ``django-axes`` is a very simple way for you to keep track of failed login attempts, both for the Django admin and for the rest of your site. The name is From c2feae329bde2b8968ef1459dc5c1d42e193342b Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Thu, 31 Oct 2013 10:01:54 +0000 Subject: [PATCH 06/16] update PyPI URL --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 21a293f..c7d383d 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ setup( author_email='codekoala@gmail.com', maintainer='Alex Clark', maintainer_email='aclark@aclark.net', - url='https://github.com/codekoala/django-axes', + url='https://github.com/django-security/django-axes', license='MIT', package_dir={'axes': 'axes'}, include_package_data=True, From 1c85247cc0658a916f986ade4cfc1b6f9bfb2c59 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Fri, 1 Nov 2013 10:43:31 +0000 Subject: [PATCH 07/16] Back to development: 1.3.4 --- CHANGES.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 632d492..870af4f 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,6 +1,12 @@ Changes ======= +1.3.4 (unreleased) +------------------ + +- Nothing changed yet. + + 1.3.3 (2013-07-05) ------------------ From 702358b87ce88e9698114349f3c98c9fd1b9a63a Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Fri, 1 Nov 2013 10:45:43 +0000 Subject: [PATCH 08/16] add history --- CHANGES.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index 870af4f..1a05271 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -4,7 +4,8 @@ Changes 1.3.4 (unreleased) ------------------ -- Nothing changed yet. +- Update README.rst for PyPI [marty] [camilonova] [graingert] +- Add cooloff period [visualspace] 1.3.3 (2013-07-05) From e1af717ce1223006b924179750c94a6ffc3833ce Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Fri, 1 Nov 2013 11:20:02 +0000 Subject: [PATCH 09/16] reverse version loading direction to support zest.releaser --- axes/__init__.py | 8 +++++--- setup.py | 4 +--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/axes/__init__.py b/axes/__init__.py index c52709d..618f94a 100644 --- a/axes/__init__.py +++ b/axes/__init__.py @@ -1,6 +1,8 @@ - -VERSION = (1, 3, 3) +try: + __version__ = __import__('pkg_resources').get_distribution('clamd').version +except: + __version__ = '' def get_version(): - return '%s.%s.%s' % VERSION + return __version__ diff --git a/setup.py b/setup.py index c7d383d..9eb3eaf 100644 --- a/setup.py +++ b/setup.py @@ -3,11 +3,9 @@ from setuptools import setup, find_packages -version = __import__('axes').get_version() - setup( name='django-axes', - version=version, + version='1.3.4.dev0', description="Keep track of failed login attempts in Django-powered sites.", long_description=(open('README.rst', 'r').read() + '\n' + open('CHANGES.txt', 'r').read()), From e3325d2f74fff4a1166cbd3944b750417666823a Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Fri, 1 Nov 2013 11:48:08 +0000 Subject: [PATCH 10/16] Preparing release 1.3.4 --- CHANGES.txt | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 1a05271..f6712e5 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,7 +1,7 @@ Changes ======= -1.3.4 (unreleased) +1.3.4 (2013-11-01) ------------------ - Update README.rst for PyPI [marty] [camilonova] [graingert] diff --git a/setup.py b/setup.py index 9eb3eaf..bf74613 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import setup, find_packages setup( name='django-axes', - version='1.3.4.dev0', + version='1.3.4', description="Keep track of failed login attempts in Django-powered sites.", long_description=(open('README.rst', 'r').read() + '\n' + open('CHANGES.txt', 'r').read()), From b06498d8f3bec85f4975d6032f99be5dcb36b476 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Fri, 1 Nov 2013 11:48:47 +0000 Subject: [PATCH 11/16] Back to development: 1.3.5 --- CHANGES.txt | 6 ++++++ setup.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index f6712e5..2d7fe81 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,6 +1,12 @@ Changes ======= +1.3.5 (unreleased) +------------------ + +- Nothing changed yet. + + 1.3.4 (2013-11-01) ------------------ diff --git a/setup.py b/setup.py index bf74613..9dfedad 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import setup, find_packages setup( name='django-axes', - version='1.3.4', + version='1.3.5.dev0', description="Keep track of failed login attempts in Django-powered sites.", long_description=(open('README.rst', 'r').read() + '\n' + open('CHANGES.txt', 'r').read()), From d524e3f3e9562964378abbb94a3da69e60959f52 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Fri, 1 Nov 2013 12:02:56 +0000 Subject: [PATCH 12/16] pull version from the correct package --- axes/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/axes/__init__.py b/axes/__init__.py index 618f94a..cb0809d 100644 --- a/axes/__init__.py +++ b/axes/__init__.py @@ -1,5 +1,7 @@ try: - __version__ = __import__('pkg_resources').get_distribution('clamd').version + __version__ = __import__('pkg_resources').get_distribution( + 'django-axes' + ).version except: __version__ = '' From 89548fd373959b8a57fe7f5878327e95195d2d98 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Fri, 1 Nov 2013 12:07:15 +0000 Subject: [PATCH 13/16] add history --- CHANGES.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index 2d7fe81..834f0f4 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -4,7 +4,7 @@ Changes 1.3.5 (unreleased) ------------------ -- Nothing changed yet. +- Fix an issue with __version__ loading the wrong version [graingert] 1.3.4 (2013-11-01) From 8ee1e138e43297de225ac4b366aac824777cb295 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Fri, 1 Nov 2013 12:07:30 +0000 Subject: [PATCH 14/16] Preparing release 1.3.5 --- CHANGES.txt | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 834f0f4..a70211c 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,7 +1,7 @@ Changes ======= -1.3.5 (unreleased) +1.3.5 (2013-11-01) ------------------ - Fix an issue with __version__ loading the wrong version [graingert] diff --git a/setup.py b/setup.py index 9dfedad..8309fae 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import setup, find_packages setup( name='django-axes', - version='1.3.5.dev0', + version='1.3.5', description="Keep track of failed login attempts in Django-powered sites.", long_description=(open('README.rst', 'r').read() + '\n' + open('CHANGES.txt', 'r').read()), From ecfee3459815e5abcb0240ba8435ce7de8b940d9 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Fri, 1 Nov 2013 12:07:50 +0000 Subject: [PATCH 15/16] Back to development: 1.3.6 --- CHANGES.txt | 6 ++++++ setup.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index a70211c..32836f7 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,6 +1,12 @@ Changes ======= +1.3.6 (unreleased) +------------------ + +- Nothing changed yet. + + 1.3.5 (2013-11-01) ------------------ diff --git a/setup.py b/setup.py index 8309fae..475efcf 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import setup, find_packages setup( name='django-axes', - version='1.3.5', + version='1.3.6.dev0', description="Keep track of failed login attempts in Django-powered sites.", long_description=(open('README.rst', 'r').read() + '\n' + open('CHANGES.txt', 'r').read()), From 50408ba3aa38cdd06044aa738936418c5e03f664 Mon Sep 17 00:00:00 2001 From: Camilo Nova Date: Thu, 7 Nov 2013 15:30:22 -0500 Subject: [PATCH 16/16] Added AttributeError in case get_profile doesn't exist --- axes/decorators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/axes/decorators.py b/axes/decorators.py index 10215f3..12556e8 100644 --- a/axes/decorators.py +++ b/axes/decorators.py @@ -133,7 +133,7 @@ def is_user_lockable(request): try: profile = user.get_profile() - except (SiteProfileNotAvailable, ObjectDoesNotExist): + except (SiteProfileNotAvailable, ObjectDoesNotExist, AttributeError): # no profile return True