mirror of
https://github.com/jazzband/django-defender.git
synced 2026-03-16 22:10:32 +00:00
first pass at some unit tests, still not working
This commit is contained in:
parent
03488469f5
commit
6786499ba1
7 changed files with 271 additions and 10 deletions
10
README.md
10
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
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
35
defender/middleware.py
Normal file
35
defender/middleware.py
Normal file
|
|
@ -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
|
||||
45
defender/test_settings.py
Normal file
45
defender/test_settings.py
Normal file
|
|
@ -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)
|
||||
7
defender/test_urls.py
Normal file
7
defender/test_urls.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
from django.conf.urls import patterns, include
|
||||
from django.contrib import admin
|
||||
|
||||
urlpatterns = patterns(
|
||||
'',
|
||||
(r'^admin/', include(admin.site.urls)),
|
||||
)
|
||||
160
defender/tests.py
Normal file
160
defender/tests.py
Normal file
|
|
@ -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 = '<form action="/admin/login/" method="post" \
|
||||
id="login-form">'
|
||||
except NoReverseMatch:
|
||||
ADMIN_LOGIN_URL = reverse('admin:index')
|
||||
LOGIN_FORM_KEY = 'this_is_the_login_form'
|
||||
|
||||
|
||||
class AccessAttemptTest(TestCase):
|
||||
"""Test case using custom settings for testing
|
||||
"""
|
||||
VALID_USERNAME = 'valid'
|
||||
LOCKED_MESSAGE = 'Account locked: too many login attempts.'
|
||||
|
||||
@patch('redis.StrictRedis', mockredis.mock_strict_redis_client)
|
||||
def the_test(self):
|
||||
from redis import StrictRedis
|
||||
print(StrictRedis)
|
||||
|
||||
def the_test2(self):
|
||||
from redis import StrictRedis
|
||||
dir(StrictRedis)
|
||||
print(StrictRedis)
|
||||
|
||||
def _get_random_str(self):
|
||||
""" Returns a random str """
|
||||
chars = string.ascii_uppercase + string.digits
|
||||
|
||||
return ''.join(random.choice(chars) for x in range(20))
|
||||
|
||||
@patch('redis.StrictRedis', mockredis.mock_strict_redis_client)
|
||||
def _login(self, is_valid=False, user_agent='test-browser'):
|
||||
"""Login a user. A valid credential is used when is_valid is True,
|
||||
otherwise it will use a random string to make a failed login.
|
||||
"""
|
||||
username = self.VALID_USERNAME if is_valid else self._get_random_str()
|
||||
|
||||
response = self.client.post(ADMIN_LOGIN_URL, {
|
||||
'username': username,
|
||||
'password': username,
|
||||
'this_is_the_login_form': 1,
|
||||
}, HTTP_USER_AGENT=user_agent)
|
||||
|
||||
return response
|
||||
|
||||
def setUp(self):
|
||||
"""Create a valid user for login
|
||||
"""
|
||||
self.user = User.objects.create_superuser(
|
||||
username=self.VALID_USERNAME,
|
||||
email='test@example.com',
|
||||
password=self.VALID_USERNAME,
|
||||
)
|
||||
|
||||
@patch('redis.StrictRedis', mockredis.mock_strict_redis_client)
|
||||
def test_failure_limit_once(self):
|
||||
"""Tests the login lock trying to login one more time
|
||||
than failure limit
|
||||
"""
|
||||
for i in range(0, FAILURE_LIMIT):
|
||||
response = self._login()
|
||||
# Check if we are in the same login page
|
||||
self.assertContains(response, LOGIN_FORM_KEY)
|
||||
|
||||
# So, we shouldn't have gotten a lock-out yet.
|
||||
# But we should get one now
|
||||
response = self._login()
|
||||
self.assertContains(response, self.LOCKED_MESSAGE)
|
||||
|
||||
@patch('redis.StrictRedis', mockredis.mock_strict_redis_client)
|
||||
def test_failure_limit_many(self):
|
||||
"""Tests the login lock trying to login a lot of times more
|
||||
than failure limit
|
||||
"""
|
||||
for i in range(0, FAILURE_LIMIT):
|
||||
response = self._login()
|
||||
# Check if we are in the same login page
|
||||
self.assertContains(response, LOGIN_FORM_KEY)
|
||||
|
||||
# So, we shouldn't have gotten a lock-out yet.
|
||||
# But we should get one now
|
||||
for i in range(0, random.randrange(1, 10)):
|
||||
# try to log in a bunch of times
|
||||
response = self._login()
|
||||
self.assertContains(response, self.LOCKED_MESSAGE)
|
||||
|
||||
@patch('redis.StrictRedis', mockredis.mock_strict_redis_client)
|
||||
def test_valid_login(self):
|
||||
"""Tests a valid login for a real username
|
||||
"""
|
||||
response = self._login(is_valid=True)
|
||||
self.assertNotContains(response, LOGIN_FORM_KEY, status_code=302)
|
||||
|
||||
@patch('redis.StrictRedis', mockredis.mock_strict_redis_client)
|
||||
def test_cooling_off(self):
|
||||
"""Tests if the cooling time allows a user to login
|
||||
"""
|
||||
self.test_failure_limit_once()
|
||||
|
||||
# Wait for the cooling off period
|
||||
time.sleep(COOLOFF_TIME.total_seconds())
|
||||
|
||||
# It should be possible to login again, make sure it is.
|
||||
self.test_valid_login()
|
||||
|
||||
@patch('redis.StrictRedis', mockredis.mock_strict_redis_client)
|
||||
def test_cooling_off_for_trusted_user(self):
|
||||
"""Test the cooling time for a trusted user
|
||||
"""
|
||||
|
||||
# Try the cooling off time
|
||||
self.test_cooling_off()
|
||||
|
||||
@patch('redis.StrictRedis', mockredis.mock_strict_redis_client)
|
||||
def test_long_user_agent_valid(self):
|
||||
"""Tests if can handle a long user agent
|
||||
"""
|
||||
long_user_agent = 'ie6' * 1024
|
||||
response = self._login(is_valid=True, user_agent=long_user_agent)
|
||||
self.assertNotContains(response, LOGIN_FORM_KEY, status_code=302)
|
||||
|
||||
@patch('redis.StrictRedis', mockredis.mock_strict_redis_client)
|
||||
def test_long_user_agent_not_valid(self):
|
||||
"""Tests if can handle a long user agent with failure
|
||||
"""
|
||||
long_user_agent = 'ie6' * 1024
|
||||
for i in range(0, FAILURE_LIMIT + 1):
|
||||
response = self._login(user_agent=long_user_agent)
|
||||
|
||||
self.assertContains(response, self.LOCKED_MESSAGE)
|
||||
|
||||
@patch('redis.StrictRedis', mockredis.mock_strict_redis_client)
|
||||
def test_reset_ip(self):
|
||||
"""Tests if can reset an ip address
|
||||
"""
|
||||
# Make a lockout
|
||||
self.test_failure_limit_once()
|
||||
|
||||
# Reset the ip so we can try again
|
||||
reset_failed_attempts(ip='127.0.0.1')
|
||||
|
||||
# Make a login attempt again
|
||||
self.test_valid_login()
|
||||
4
setup.py
4
setup.py
|
|
@ -30,6 +30,6 @@ setup(name='django-defender',
|
|||
author_email='kencochrane@gmail.com',
|
||||
license='Apache 2',
|
||||
packages=['defender'],
|
||||
install_requires=['django==1.6.7', 'redis==2.10.3', 'hiredis==0.1.4', ],
|
||||
|
||||
install_requires=['django==1.6.8', 'redis==2.10.3', 'hiredis==0.1.4', ],
|
||||
tests_require=['mock', 'mockredispy'],
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in a new issue