mirror of
https://github.com/jazzband/django-defender.git
synced 2026-03-16 22:10:32 +00:00
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:
parent
825afb3209
commit
3031deb761
8 changed files with 63 additions and 16 deletions
5
CHANGES
5
CHANGES
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
--------------------
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
6
defender/exampleapp/utils.py
Normal file
6
defender/exampleapp/utils.py
Normal 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
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
"""
|
||||
|
|
|
|||
Loading…
Reference in a new issue