diff --git a/axes/__init__.py b/axes/__init__.py index 4a2dd26..9d066a3 100644 --- a/axes/__init__.py +++ b/axes/__init__.py @@ -1,15 +1,18 @@ VERSION = (1, 2, 4, 'rc1') + def get_version(): return '%s.%s.%s-%s' % VERSION try: from django.conf import settings - import logging, os + import logging + import os LOGFILE = os.path.join(settings.DIRNAME, 'axes.log') + log_format = '%(asctime)s %(name)-12s %(levelname)-8s %(message)s' logging.basicConfig(level=logging.DEBUG, - format='%(asctime)s %(name)-12s %(levelname)-8s %(message)s', + format=log_format, datefmt='%a, %d %b %Y %H:%M:%S', filename=LOGFILE, filemode='w') @@ -18,7 +21,8 @@ try: fileLog.setLevel(logging.DEBUG) # set a format which is simpler for console use - formatter = logging.Formatter('%(asctime)s %(name)-12s: %(levelname)-8s %(message)s') + console_format = '%(asctime)s %(name)-12s: %(levelname)-8s %(message)s' + formatter = logging.Formatter(console_format) # tell the handler to use this format fileLog.setFormatter(formatter) @@ -26,5 +30,6 @@ try: # add the handler to the root logger logging.getLogger('').addHandler(fileLog) except: - # if we have any problems, we most likely don't have a settings module loaded + # if we have any problems, we most likely don't have a settings module + # loaded pass diff --git a/axes/admin.py b/axes/admin.py index dc623b7..c832258 100644 --- a/axes/admin.py +++ b/axes/admin.py @@ -1,8 +1,10 @@ from django.contrib import admin from axes.models import AccessAttempt + class AccessAttemptAdmin(admin.ModelAdmin): - list_display = ('attempt_time', 'ip_address', 'user_agent', 'path_info', 'failures_since_start') + list_display = ('attempt_time', 'ip_address', 'user_agent', 'path_info', + 'failures_since_start') list_filter = ['attempt_time', 'ip_address', 'path_info'] search_fields = ['ip_address', 'user_agent', 'path_info'] date_hierarchy = 'attempt_time' @@ -18,4 +20,4 @@ class AccessAttemptAdmin(admin.ModelAdmin): }) ) -admin.site.register(AccessAttempt, AccessAttemptAdmin) \ No newline at end of file +admin.site.register(AccessAttempt, AccessAttemptAdmin) diff --git a/axes/decorators.py b/axes/decorators.py index adc9709..f2beebd 100644 --- a/axes/decorators.py +++ b/axes/decorators.py @@ -37,6 +37,7 @@ LOCKOUT_TEMPLATE = getattr(settings, 'AXES_LOCKOUT_TEMPLATE', None) LOCKOUT_URL = getattr(settings, 'AXES_LOCKOUT_URL', None) VERBOSE = getattr(settings, 'AXES_VERBOSE', True) + def query2str(items): """Turns a dictionary into an easy-to-read list of key-value pairs. @@ -55,6 +56,7 @@ if VERBOSE: log.info('AXES: BEGIN LOG') log.info('Using django-axes ' + axes.get_version()) + def get_user_attempt(request): """ Returns access attempt record if it exists. @@ -82,6 +84,7 @@ def get_user_attempt(request): return attempt + def watch_login(func): """ Used to decorate the django.contrib.admin.site.login method. @@ -91,8 +94,10 @@ def watch_login(func): # share some useful information if func.__name__ != 'decorated_login' and VERBOSE: log.info('AXES: Calling decorated function: %s' % func.__name__) - if args: log.info('args: %s' % args) - if kwargs: log.info('kwargs: %s' % kwargs) + if args: + log.info('args: %s' % args) + if kwargs: + log.info('kwargs: %s' % kwargs) # call the login function response = func(request, *args, **kwargs) @@ -120,6 +125,7 @@ def watch_login(func): return decorated_login + def lockout_response(request): if LOCKOUT_TEMPLATE: context = { @@ -127,7 +133,7 @@ def lockout_response(request): 'failure_limit': FAILURE_LIMIT, } return render_to_response(LOCKOUT_TEMPLATE, context, - context_instance = RequestContext(request)) + context_instance=RequestContext(request)) if LOCKOUT_URL: return HttpResponseRedirect(LOCKOUT_URL) @@ -139,6 +145,7 @@ def lockout_response(request): return HttpResponse("Account locked: too many login attempts. " "Contact an admin to unlock your account.") + def check_request(request, login_unsuccessful): failures = 0 attempt = get_user_attempt(request) @@ -200,9 +207,11 @@ def check_request(request, login_unsuccessful): return True -ERROR_MESSAGE = ugettext_lazy("Please enter a correct username and password. Note that both fields are case-sensitive.") +ERROR_MESSAGE = ugettext_lazy("Please enter a correct username and password. " + "Note that both fields are case-sensitive.") LOGIN_FORM_KEY = 'this_is_the_login_form' + def _display_login_form(request, error_message=''): request.session.set_test_cookie() return render_to_response('admin/login.html', { @@ -211,6 +220,7 @@ def _display_login_form(request, error_message=''): 'error_message': error_message }, context_instance=template.RequestContext(request)) + def staff_member_required(view_func): """ Decorator for views that checks that the user is logged in and is a staff @@ -232,8 +242,8 @@ def staff_member_required(view_func): documentation and/or other materials provided with the distribution. 3. Neither the name of Django nor the names of its contributors may be - used to endorse or promote products derived from this software without - specific prior written permission. + used to endorse or promote products derived from this software + without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE diff --git a/axes/management/commands/axes_reset.py b/axes/management/commands/axes_reset.py index 95df888..27fe9ca 100644 --- a/axes/management/commands/axes_reset.py +++ b/axes/management/commands/axes_reset.py @@ -1,9 +1,10 @@ from django.core.management.base import BaseCommand, CommandError from axes.utils import reset + class Command(BaseCommand): args = '' - help = ("resets any lockouts or failed login records. If called with an " + + help = ("resets any lockouts or failed login records. If called with an " "IP, resets only for that IP") def handle(self, *args, **kwargs): diff --git a/axes/middleware.py b/axes/middleware.py index c3cf389..cc85c52 100644 --- a/axes/middleware.py +++ b/axes/middleware.py @@ -2,6 +2,7 @@ from django.contrib import admin from django.contrib.auth import views as auth_views from axes.decorators import watch_login + class FailedLoginMiddleware(object): def __init__(self, *args, **kwargs): @@ -13,6 +14,7 @@ class FailedLoginMiddleware(object): # and the regular auth login page auth_views.login = watch_login(auth_views.login) + class FailedAdminLoginMiddleware(object): def __init__(self, *args, **kwargs): super(FailedAdminLoginMiddleware, self).__init__(*args, **kwargs) @@ -20,6 +22,7 @@ class FailedAdminLoginMiddleware(object): # watch the admin login page admin.site.login = watch_login(admin.site.login) + class FailedAuthLoginMiddleware(object): def __init__(self, *args, **kwargs): super(FailedAuthLoginMiddleware, self).__init__(*args, **kwargs) diff --git a/axes/models.py b/axes/models.py index f2f06e1..eae53ca 100644 --- a/axes/models.py +++ b/axes/models.py @@ -6,6 +6,7 @@ FAILURES_DESC = 'Failed Logins' # set unique by user_agent, ip # make user agent, ip indexed fields + class AccessAttempt(models.Model): user_agent = models.CharField(max_length=255) ip_address = models.IPAddressField('IP Address') diff --git a/axes/tests.py b/axes/tests.py index 0eb8923..15f464d 100644 --- a/axes/tests.py +++ b/axes/tests.py @@ -10,6 +10,8 @@ from decorators import FAILURE_LIMIT # Only run tests if they have axes in middleware # Basically a functional test + + class AccessAttemptTest(TestCase): NOT_GONNA_BE_PASSWORD = "sfdlermmvnLsefrlg0c9gjjPxmvLlkdf2#" NOT_GONNA_BE_USERNAME = "whywouldyouohwhy" diff --git a/axes/utils.py b/axes/utils.py index c9d95f4..3a5de6f 100644 --- a/axes/utils.py +++ b/axes/utils.py @@ -1,5 +1,6 @@ from axes.models import AccessAttempt + def reset(ip=None, silent=False): if not ip: attempts = AccessAttempt.objects.all() @@ -16,4 +17,3 @@ def reset(ip=None, silent=False): print 'No matching attempt found.' else: attempt.delete() -