From 3031deb7611672eddc2fabbf08580cea4f4da629 Mon Sep 17 00:00:00 2001 From: Andrei Baryshnikov Date: Tue, 29 May 2018 21:32:08 +0700 Subject: [PATCH] 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 --- CHANGES | 5 +++++ README.md | 7 +++++++ defender/config.py | 6 ++++++ defender/decorators.py | 17 ++++++++++------- defender/exampleapp/settings.py | 4 ++++ defender/exampleapp/utils.py | 6 ++++++ defender/tests.py | 26 ++++++++++++++++++-------- defender/utils.py | 8 +++++++- 8 files changed, 63 insertions(+), 16 deletions(-) create mode 100644 defender/exampleapp/utils.py diff --git a/CHANGES b/CHANGES index f61736b..3a2feac 100644 --- a/CHANGES +++ b/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] diff --git a/README.md b/README.md index 53e45f5..7fe8833 100644 --- a/README.md +++ b/README.md @@ -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 -------------------- diff --git a/defender/config.py b/defender/config.py index 9e59c16..a61a88a 100644 --- a/defender/config.py +++ b/defender/config.py @@ -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' +) diff --git a/defender/decorators.py b/defender/decorators.py index 9a1d2f1..bb4b004 100644 --- a/defender/decorators.py +++ b/defender/decorators.py @@ -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) diff --git a/defender/exampleapp/settings.py b/defender/exampleapp/settings.py index c2d7a95..74bbb62 100644 --- a/defender/exampleapp/settings.py +++ b/defender/exampleapp/settings.py @@ -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 diff --git a/defender/exampleapp/utils.py b/defender/exampleapp/utils.py new file mode 100644 index 0000000..ce162f8 --- /dev/null +++ b/defender/exampleapp/utils.py @@ -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 diff --git a/defender/tests.py b/defender/tests.py index b9127ed..c2bc920 100644 --- a/defender/tests.py +++ b/defender/tests.py @@ -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 + ) diff --git a/defender/utils.py b/defender/utils.py index 3299862..1952629 100644 --- a/defender/utils.py +++ b/defender/utils.py @@ -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 """