Merge pull request #12 from kencochrane/add_celery

Add Celery option for writing to database
This commit is contained in:
Ken Cochrane 2015-01-05 18:48:32 -05:00
commit 0402878f2f
14 changed files with 129 additions and 23 deletions

2
.coveragerc Normal file
View file

@ -0,0 +1,2 @@
[run]
omit = *_settings.py

10
.landscape.yaml Normal file
View file

@ -0,0 +1,10 @@
doc-warnings: no
test-warnings: no
strictness: high
max-line-length: 80
uses:
- django
- celery
autodetect: yes
ignore-patterns:
- .*_settings.py$

View file

@ -9,6 +9,7 @@ python:
env:
- DJANGO=Django==1.4.17
- DJANGO=Django==1.5.12
- DJANGO=Django==1.6.9
- DJANGO=Django==1.7.2
@ -19,6 +20,7 @@ install:
- pip install -q $DJANGO
- pip install coveralls
- pip install mockredispy
- pip install celery
- python setup.py develop
script:

View file

@ -120,7 +120,7 @@ to improve the login.
requirements
============
- django: 1.4.x, 1.6.x, 1.7.x
- django: 1.4.x, 1.5.x, 1.6.x, 1.7.x
- redis
- python: 2.6.x, 2.7.x, 3.3.x, 3.4.x, PyPy
@ -263,6 +263,9 @@ Default: ``redis://localhost:6379/0``
(Example with password: ``redis://:mypassword@localhost:6379/0``)
* ``DEFENDER_PROTECTED_LOGINS``: Tuple: Used by ``ViewDecoratorMiddleware`` to decide
which login urls need protecting. Default: ``('/accounts/login/',)``
* ``DEFENDER_USE_CELERY``: Boolean: If you want to use Celery to store the login
attempt to the database, set to True. If False, it is saved inline.
Default: ``False``
Running Tests
=============

View file

@ -1,7 +0,0 @@
doc-warnings: yes
test-warnings: no
strictness: veryhigh
max-line-length: 80
uses:
- django
autodetect: yes

View file

@ -50,3 +50,5 @@ LOCKOUT_URL = get_setting('DEFENDER_LOCKOUT_URL')
PROTECTED_LOGINS = get_setting('DEFENDER_PROTECTED_LOGINS',
('/accounts/login/',))
USE_CELERY = get_setting('DEFENDER_USE_CELERY', False)

14
defender/data.py Normal file
View file

@ -0,0 +1,14 @@
from .models import AccessAttempt
def store_login_attempt(user_agent, ip_address, username,
http_accept, path_info, login_valid):
""" Store the login attempt to the db. """
AccessAttempt.objects.create(
user_agent=user_agent,
ip_address=ip_address,
username=username,
http_accept=http_accept,
path_info=path_info,
login_valid=login_valid,
)

View file

@ -34,7 +34,7 @@ def watch_login(func):
# ideally make this background task, but to keep simple, keeping
# it inline for now.
utils.add_login_attempt(request, not login_unsuccessful)
utils.add_login_attempt_to_db(request, not login_unsuccessful)
if utils.check_request(request, login_unsuccessful):
return response

15
defender/tasks.py Normal file
View file

@ -0,0 +1,15 @@
from . import config
from .data import store_login_attempt
# not sure how to get this to look better. ideally we want to dynamically
# apply the celery decorator based on the USE_CELERY setting.
if config.USE_CELERY:
from celery import shared_task
@shared_task()
def add_login_attempt_task(user_agent, ip_address, username,
http_accept, path_info, login_valid):
""" Create a record for the login attempt """
store_login_attempt(user_agent, ip_address, username,
http_accept, path_info, login_valid)

View file

@ -39,3 +39,22 @@ DEFENDER_COOLOFF_TIME = 2
DEFENDER_REDIS_URL = None
# use mock redis in unit tests locally.
DEFENDER_MOCK_REDIS = True
# celery settings
CELERY_ALWAYS_EAGER = True
BROKER_BACKEND = 'memory'
BROKER_URL = 'memory://'
import os
from celery import Celery
# set the default Django settings module for the 'celery' program.
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'defender.test_settings')
app = Celery('defender')
# Using a string here means the worker will not have to
# pickle the object when using Windows.
app.config_from_object('django.conf:settings')
app.autodiscover_tasks(lambda: INSTALLED_APPS)

View file

@ -294,7 +294,7 @@ class AccessAttemptTest(TestCase):
response = self._login()
self.assertContains(response, LOGIN_FORM_KEY)
self.assertEquals(AccessAttempt.objects.count(), 1)
self.assertIsNotNone(AccessAttempt.objects.all()[0])
self.assertIsNotNone(str(AccessAttempt.objects.all()[0]))
def test_is_valid_ip(self):
""" Test the is_valid_ip() method
@ -413,6 +413,7 @@ class AccessAttemptTest(TestCase):
req = HttpRequest()
req.META['HTTP_X_PROXIED'] = '1.2.3.4'
self.assertEqual(utils.get_ip(req), '1.2.3.4')
req = HttpRequest()
req.META['HTTP_X_PROXIED'] = '1.2.3.4, 5.6.7.8, 127.0.0.1'
self.assertEqual(utils.get_ip(req), '1.2.3.4')
@ -450,6 +451,7 @@ class AccessAttemptTest(TestCase):
)
def test_admin(self):
""" test the admin pages for this app """
from .admin import AccessAttemptAdmin
AccessAttemptAdmin
@ -484,3 +486,23 @@ class AccessAttemptTest(TestCase):
self.assertContains(response, LOGIN_FORM_KEY)
response = self.client.get(ADMIN_LOGIN_URL)
self.assertNotContains(response, self.LOCKED_MESSAGE)
@patch('defender.config.USE_CELERY', True)
def test_use_celery(self):
""" Check that use celery works"""
self.assertEquals(AccessAttempt.objects.count(), 0)
for i in range(0, int(config.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)
self.assertEquals(AccessAttempt.objects.count(),
config.FAILURE_LIMIT+1)
self.assertIsNotNone(str(AccessAttempt.objects.all()[0]))

View file

@ -39,3 +39,22 @@ DEFENDER_COOLOFF_TIME = 2
DEFENDER_REDIS_URL = "redis://localhost:6379/1"
# don't use mock redis in unit tests, we will use real redis on travis.
DEFENDER_MOCK_REDIS = False
# Celery settings:
CELERY_ALWAYS_EAGER = True
BROKER_BACKEND = 'memory'
BROKER_URL = 'memory://'
import os
from celery import Celery
# set the default Django settings module for the 'celery' program.
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'defender.travis_settings')
app = Celery('defender')
# Using a string here means the worker will not have to
# pickle the object when using Windows.
app.config_from_object('django.conf:settings')
app.autodiscover_tasks(lambda: INSTALLED_APPS)

View file

@ -6,9 +6,9 @@ from django.http import HttpResponseRedirect
from django.shortcuts import render_to_response
from django.template import RequestContext
from .models import AccessAttempt
from .connection import get_redis_connection
from . import config
from .data import store_login_attempt
redis_server = get_redis_connection()
@ -233,14 +233,19 @@ def check_request(request, login_unsuccessful):
return record_failed_attempt(ip_address, username)
def add_login_attempt(request, login_valid):
""" Create a record for the login attempt """
AccessAttempt.objects.create(
user_agent=request.META.get('HTTP_USER_AGENT',
'<unknown>')[:255],
ip_address=get_ip(request),
username=request.POST.get(config.USERNAME_FORM_FIELD, None),
http_accept=request.META.get('HTTP_ACCEPT', '<unknown>'),
path_info=request.META.get('PATH_INFO', '<unknown>'),
login_valid=login_valid,
)
def add_login_attempt_to_db(request, login_valid):
""" Create a record for the login attempt If using celery call celery
task, if not, call the method normally """
user_agent = request.META.get('HTTP_USER_AGENT', '<unknown>')[:255]
ip_address = get_ip(request)
username = request.POST.get(config.USERNAME_FORM_FIELD, None)
http_accept = request.META.get('HTTP_ACCEPT', '<unknown>')
path_info = request.META.get('PATH_INFO', '<unknown>')
if config.USE_CELERY:
from .tasks import add_login_attempt_task
add_login_attempt_task.delay(user_agent, ip_address, username,
http_accept, path_info, login_valid)
else:
store_login_attempt(user_agent, ip_address, username,
http_accept, path_info, login_valid)

View file

@ -38,5 +38,5 @@ setup(name='django-defender',
license='Apache 2',
packages=['defender'],
install_requires=['django==1.6.8', 'redis==2.10.3', 'hiredis==0.1.4', ],
tests_require=['mock', 'mockredispy', 'coverage'],
tests_require=['mock', 'mockredispy', 'coverage', 'celery'],
)