diff --git a/CHANGES.txt b/CHANGES.txt index 632d492..32836f7 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,6 +1,25 @@ Changes ======= +1.3.6 (unreleased) +------------------ + +- Nothing changed yet. + + +1.3.5 (2013-11-01) +------------------ + +- Fix an issue with __version__ loading the wrong version [graingert] + + +1.3.4 (2013-11-01) +------------------ + +- Update README.rst for PyPI [marty] [camilonova] [graingert] +- Add cooloff period [visualspace] + + 1.3.3 (2013-07-05) ------------------ diff --git a/README.rst b/README.rst index 8c37f06..3267056 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 @@ -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 diff --git a/axes/__init__.py b/axes/__init__.py index c52709d..cb0809d 100644 --- a/axes/__init__.py +++ b/axes/__init__.py @@ -1,6 +1,10 @@ - -VERSION = (1, 3, 3) +try: + __version__ = __import__('pkg_resources').get_distribution( + 'django-axes' + ).version +except: + __version__ = '' def get_version(): - return '%s.%s.%s' % VERSION + return __version__ diff --git a/axes/decorators.py b/axes/decorators.py index e50d79a..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 @@ -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,11 +173,26 @@ 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() \ - 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 + + # 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 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 fcd7f90..fd13d64 100644 --- a/axes/tests.py +++ b/axes/tests.py @@ -1,10 +1,11 @@ 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 @@ -108,6 +109,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 """ diff --git a/setup.py b/setup.py index 21a293f..475efcf 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.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()), @@ -16,7 +14,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,