Add possibility to use custom utils.get_username_from_request function (#122)

* Add `DEFENDER_GET_USERNAME_FROM_REQUEST_PATH` setting

This setting allow to override default `get_username_from_request`
function.

* Add `get_username` argument to `watch_login`

To be able to propagate this argument to other utils functions calls

* Minor code-style fixes

* Add example of use of `DEFENDER_GET_USERNAME_FROM_REQUEST_PATH` setting

* Update docs
This commit is contained in:
Andrei Baryshnikov 2018-05-29 21:32:08 +07:00 committed by Ken Cochrane
parent 825afb3209
commit 3031deb761
8 changed files with 63 additions and 16 deletions

View file

@ -1,3 +1,8 @@
0.5.5
=====
- Added new setting ``DEFENDER_GET_USERNAME_FROM_REQUEST_PATH` for control how username is accessed from request [@andrewshkovskii]
- Added new argument ``get_username` for ``decorators.watch_login`` to propagate ``get_username`` argument to other utils functions calls done in ``watch_login`` [@andrewshkovskii]
0.5.4
=====
- Added 2 new setting variables for more granular failure limit control [@williamboman]

View file

@ -24,6 +24,10 @@ If you are using defender on your site, submit a PR to add to the list.
Versions
========
- 0.5.5
- Added new setting ``DEFENDER_GET_USERNAME_FROM_REQUEST_PATH`` for control how username is accessed from request [@andrewshkovskii]
- Added new argument ``get_username`` for ``decorators.watch_login`` to propagate ``get_username`` argument to other utils functions calls done in ``watch_login`` [@andrewshkovskii]
- 0.5.4
- Added 2 new setting variables for more granular failure limit control [@williamboman]
- Added ssl option when instantiating StrictRedis [@mjrimrie]
@ -368,6 +372,9 @@ attempt to the database, set to True. If False, it is saved inline.
long to keep the access attempt records in the database before the management
command cleans them up.
[Default: ``24``]
* ``DEFENDER_GET_USERNAME_FROM_REQUEST_PATH``: String: The import path of the function that access username from request.
If you want to use custom function to access and process username from request - you can specify it here.
[Default: ``defender.utils.username_from_request``]
Adapting to other authentication method
--------------------

View file

@ -76,3 +76,9 @@ except ValueError: # pragma: no cover
raise Exception(
'DEFENDER_ACCESS_ATTEMPT_EXPIRATION'
' needs to be an integer') # pragma: no cover
GET_USERNAME_FROM_REQUEST_PATH = get_setting(
'DEFENDER_GET_USERNAME_FROM_REQUEST_PATH',
'defender.utils.username_from_request'
)

View file

@ -3,7 +3,8 @@ from . import utils
import functools
def watch_login(status_code=302, msg=''):
def watch_login(status_code=302, msg='',
get_username=utils.get_username_from_request):
"""
Used to decorate the django.contrib.admin.site.login method or
any other function you want to protect by brute forcing.
@ -15,8 +16,8 @@ def watch_login(status_code=302, msg=''):
@functools.wraps(func)
def wrapper(request, *args, **kwargs):
# if the request is currently under lockout, do not proceed to the
# login function, go directly to lockout url, do not pass go, do not
# collect messages about this login attempt
# login function, go directly to lockout url, do not pass go,
# do not collect messages about this login attempt
if utils.is_already_locked(request):
return utils.lockout_response(request)
@ -39,11 +40,13 @@ def watch_login(status_code=302, msg=''):
and msg in response.content.decode('utf-8')
)
# ideally make this background task, but to keep simple, keeping
# it inline for now.
utils.add_login_attempt_to_db(request, not login_unsuccessful)
# ideally make this background task, but to keep simple,
# keeping it inline for now.
utils.add_login_attempt_to_db(request, not login_unsuccessful,
get_username)
if utils.check_request(request, login_unsuccessful):
if utils.check_request(request, login_unsuccessful,
get_username):
return response
return utils.lockout_response(request)

View file

@ -60,6 +60,10 @@ DEFENDER_COOLOFF_TIME = 60
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
# Let's use custom function and strip username string from request.
DEFENDER_GET_USERNAME_FROM_REQUEST_PATH = (
'defender.exampleapp.utils.strip_username_from_request'
)
# Celery settings:
CELERY_ALWAYS_EAGER = True

View file

@ -0,0 +1,6 @@
from defender.utils import username_from_request
def strip_username_from_request(request):
username = username_from_request(request)
return username.strip() if username else username

View file

@ -23,7 +23,10 @@ except ImportError:
from . import utils
from . import config
from .signals import ip_block as ip_block_signal, username_block as username_block_signal
from .signals import (
ip_block as ip_block_signal,
username_block as username_block_signal
)
from .connection import parse_redis_url, get_redis_connection
from .decorators import watch_login
from .models import AccessAttempt
@ -213,7 +216,6 @@ class AccessAttemptTest(DefenderTestCase):
response = self.client.get(ADMIN_LOGIN_URL)
self.assertContains(response, self.LOCKED_MESSAGE)
def test_valid_login(self):
""" Tests a valid login for a real username
"""
@ -308,8 +310,10 @@ class AccessAttemptTest(DefenderTestCase):
""" Tests if can handle a long user agent
"""
long_user_agent = 'ie6' * 1024
response = self._login(username=VALID_USERNAME, password=VALID_PASSWORD,
user_agent=long_user_agent)
response = self._login(
username=VALID_USERNAME, password=VALID_PASSWORD,
user_agent=long_user_agent
)
self.assertNotContains(response, LOGIN_FORM_KEY, status_code=302)
@patch('defender.config.BEHIND_REVERSE_PROXY', True)
@ -541,7 +545,8 @@ class AccessAttemptTest(DefenderTestCase):
@patch('defender.config.DEFENDER_REDIS_NAME', 'bad-key')
def test_get_redis_connection_django_conf_wrong_key(self):
""" see if we get the correct error """
error_msg = 'The cache bad-key was not found on the django cache settings.'
error_msg = ('The cache bad-key was not found on the django '
'cache settings.')
self.assertRaisesMessage(KeyError, error_msg, get_redis_connection)
def test_get_ip_address_from_request(self):
@ -998,7 +1003,8 @@ class TestUtils(DefenderTestCase):
self.assertFalse(utils.is_source_ip_already_locked(ip))
def test_username_argument_precedence(self):
""" test that the optional username argument has highest precedence when provided """
""" test that the optional username argument has highest precedence
when provided """
request_factory = RequestFactory()
request = request_factory.get(ADMIN_LOGIN_URL)
request.user = AnonymousUser()
@ -1010,7 +1016,11 @@ class TestUtils(DefenderTestCase):
self.assertFalse(utils.is_already_locked(request, username=username))
utils.check_request(request, True, username=username)
self.assertEqual(utils.get_user_attempts(request, username=username), 1)
self.assertEqual(
utils.get_user_attempts(request, username=username), 1
)
utils.add_login_attempt_to_db(request, True, username=username)
self.assertEqual(AccessAttempt.objects.filter(username=username).count(), 1)
self.assertEqual(
AccessAttempt.objects.filter(username=username).count(), 1
)

View file

@ -5,6 +5,7 @@ from django.http import HttpResponseRedirect
from django.shortcuts import render
from django.core.validators import validate_ipv46_address
from django.core.exceptions import ValidationError
from django.utils.module_loading import import_string
from .connection import get_redis_connection
from . import config
@ -129,13 +130,18 @@ def increment_key(key):
return new_value
def get_username_from_request(request):
def username_from_request(request):
""" unloads username from default POST request """
if config.USERNAME_FORM_FIELD in request.POST:
return request.POST[config.USERNAME_FORM_FIELD][:255]
return None
get_username_from_request = import_string(
config.GET_USERNAME_FROM_REQUEST_PATH
)
def get_user_attempts(request, get_username=get_username_from_request, username=None):
""" Returns number of access attempts for this ip, username
"""