diff --git a/README.md b/README.md index 39d2c75..6191414 100644 --- a/README.md +++ b/README.md @@ -131,3 +131,13 @@ ELSE EXEC END ``` + +Running Tests +============= + +Tests can be run, after you clone the repository and having django installed, + like: + +``` +$ PYTHONPATH=$PYTHONPATH:$PWD django-admin.py test defender --settings=defender.test_settings +``` diff --git a/defender/decorators.py b/defender/decorators.py index 2e3619d..f2ddc0b 100644 --- a/defender/decorators.py +++ b/defender/decorators.py @@ -1,7 +1,7 @@ import logging import socket -import redis +from redis import StrictRedis from django.conf import settings from django.http import HttpResponse from django.http import HttpResponseRedirect @@ -50,7 +50,7 @@ VERBOSE = getattr(settings, 'DEFENDER_VERBOSE', True) ERROR_MESSAGE = ugettext_lazy("Please enter a correct username and password. " "Note that both fields are case-sensitive.") -redis_server = redis.StrictRedis( +redis_server = StrictRedis( host=REDIS_HOST, port=REDIS_PORT, db=REDIS_DB, password=REDIS_PASSWORD) log = logging.getLogger(__name__) @@ -198,12 +198,16 @@ def record_failed_attempt(ip, username): return True -def reset_failed_attempts(ip, username): - """ reset the failed attempts for these ip's and usernames """ - redis_server.delete(get_ip_attempt_cache_key(ip)) - redis_server.delete(get_username_attempt_cache_key(username)) - redis_server.delete(get_username_blocked_cache_key(username)) - redis_server.delete(get_ip_blocked_cache_key(ip)) +def reset_failed_attempts(ip=None, username=None): + """ reset the failed attempts for these ip's and usernames + TODO: run all commands in one redis transaction + """ + if ip: + redis_server.delete(get_ip_attempt_cache_key(ip)) + redis_server.delete(get_ip_blocked_cache_key(ip)) + if username: + redis_server.delete(get_username_attempt_cache_key(username)) + redis_server.delete(get_username_blocked_cache_key(username)) def lockout_response(request): diff --git a/defender/middleware.py b/defender/middleware.py new file mode 100644 index 0000000..97fa5b5 --- /dev/null +++ b/defender/middleware.py @@ -0,0 +1,35 @@ +from django.conf import settings +from django.contrib.auth import views as auth_views + +from defender.decorators import watch_login + + +class FailedLoginMiddleware(object): + def __init__(self, *args, **kwargs): + super(FailedLoginMiddleware, self).__init__(*args, **kwargs) + + # watch the auth login + auth_views.login = watch_login(auth_views.login) + + +class ViewDecoratorMiddleware(object): + """ + When the django_axes middleware is installed, by default it watches the + django.auth.views.login. + This middleware allows adding protection to other views without the need + to change any urls or dectorate them manually. + Add this middleware to your MIDDLEWARE settings after + `defender.middleware.FailedLoginMiddleware` and before the django + flatpages middleware. + """ + watched_logins = getattr( + settings, 'DEFENDER_PROTECTED_LOGINS', ( + '/accounts/login/', + ) + ) + + def process_view(self, request, view_func, view_args, view_kwargs): + if request.path in self.watched_logins: + return watch_login(view_func)(request, *view_args, **view_kwargs) + + return None diff --git a/defender/test_settings.py b/defender/test_settings.py new file mode 100644 index 0000000..1bf7006 --- /dev/null +++ b/defender/test_settings.py @@ -0,0 +1,45 @@ +import django + +if django.VERSION[:2] >= (1, 3): + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': ':memory:', + } + } +else: + DATABASE_ENGINE = 'sqlite3' + +SITE_ID = 1 + +REDIS_HOST = 'localhost' +REDIS_PORT = '1234' +REDIS_PASSWORD = 'mypassword' +REDIS_DB = 1 + +MIDDLEWARE_CLASSES = ( + 'django.middleware.common.CommonMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'defender.middleware.FailedLoginMiddleware' +) + +ROOT_URLCONF = 'defender.test_urls' + +INSTALLED_APPS = [ + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.sites', + 'django.contrib.messages', + 'django.contrib.admin', + 'defender', +] + +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/defender/test_urls.py b/defender/test_urls.py new file mode 100644 index 0000000..88ad7e7 --- /dev/null +++ b/defender/test_urls.py @@ -0,0 +1,7 @@ +from django.conf.urls import patterns, include +from django.contrib import admin + +urlpatterns = patterns( + '', + (r'^admin/', include(admin.site.urls)), +) diff --git a/defender/tests.py b/defender/tests.py new file mode 100644 index 0000000..5d73b5c --- /dev/null +++ b/defender/tests.py @@ -0,0 +1,160 @@ +import random +import string +import time +from mock import patch +import mockredis + +from django.test import TestCase +from django.contrib.auth.models import User +from django.core.urlresolvers import NoReverseMatch +from django.core.urlresolvers import reverse + +from defender.decorators import ( + COOLOFF_TIME, FAILURE_LIMIT, reset_failed_attempts) + + +# Django >= 1.7 compatibility +try: + ADMIN_LOGIN_URL = reverse('admin:login') + LOGIN_FORM_KEY = '