mirror of
https://github.com/jazzband/django-axes.git
synced 2026-03-16 22:30:23 +00:00
Move DRF integration into signals
Add documentation on how to enable the integration and remove the logic from global middleware. Fixes #673
This commit is contained in:
parent
9acf904568
commit
59bcbd8816
5 changed files with 37 additions and 121 deletions
13
CHANGES.rst
13
CHANGES.rst
|
|
@ -3,6 +3,14 @@ Changes
|
|||
=======
|
||||
|
||||
|
||||
5.10.0 (2020-12-16)
|
||||
-------------------
|
||||
|
||||
- Deprecate stock DRF support, require users to set it up per project.
|
||||
Check the documentation for more information.
|
||||
[aleksihakli]
|
||||
|
||||
|
||||
5.9.1 (2020-12-02)
|
||||
------------------
|
||||
|
||||
|
|
@ -17,9 +25,8 @@ Changes
|
|||
|
||||
- Add Python 3.9 support.
|
||||
[hramezani]
|
||||
- Prevent ``AccessAttempt`` creation with database handler
|
||||
when username is not set
|
||||
and ``AXES_ONLY_USER_FAILURES`` setting is not set.
|
||||
- Prevent ``AccessAttempt`` creation with database handler when
|
||||
username is not set and ``AXES_ONLY_USER_FAILURES`` setting is not set.
|
||||
[hramezani]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -115,8 +115,3 @@ settings.AXES_META_PRECEDENCE_ORDER = getattr(
|
|||
"AXES_META_PRECEDENCE_ORDER",
|
||||
getattr(settings, "IPWARE_META_PRECEDENCE_ORDER", ("REMOTE_ADDR",)),
|
||||
)
|
||||
|
||||
# set to `True` if using with Django REST Framework
|
||||
settings.AXES_REST_FRAMEWORK_ACTIVE = getattr(
|
||||
settings, "AXES_REST_FRAMEWORK_ACTIVE", False
|
||||
)
|
||||
|
|
|
|||
|
|
@ -43,20 +43,6 @@ class AxesMiddleware:
|
|||
response = self.get_response(request)
|
||||
|
||||
if settings.AXES_ENABLED:
|
||||
if "rest_framework" in settings.INSTALLED_APPS:
|
||||
AxesProxyHandler.update_request(request)
|
||||
username = get_client_username(request)
|
||||
credentials = get_credentials(username)
|
||||
failures_since_start = AxesProxyHandler.get_failures(
|
||||
request, credentials
|
||||
)
|
||||
if (
|
||||
settings.AXES_LOCK_OUT_AT_FAILURE
|
||||
and failures_since_start >= get_failure_limit(request, credentials)
|
||||
):
|
||||
|
||||
request.axes_locked_out = True
|
||||
|
||||
if getattr(request, "axes_locked_out", None):
|
||||
response = get_lockout_response(request) # type: ignore
|
||||
|
||||
|
|
|
|||
|
|
@ -39,74 +39,3 @@ class MiddlewareTestCase(AxesTestCase):
|
|||
|
||||
response = AxesMiddleware(get_response)(self.request)
|
||||
self.assertEqual(response.status_code, self.STATUS_SUCCESS)
|
||||
|
||||
@mock.patch("django.conf.settings.INSTALLED_APPS", ["rest_framework"])
|
||||
def test_response_contains_required_attrs_with_drf_integration(self):
|
||||
def get_response(request):
|
||||
return HttpResponse()
|
||||
|
||||
self.assertFalse(hasattr(self.request, "axes_locked_out"))
|
||||
self.assertFalse(hasattr(self.request, "axes_attempt_time"))
|
||||
self.assertFalse(hasattr(self.request, "axes_ip_address"))
|
||||
self.assertFalse(hasattr(self.request, "axes_user_agent"))
|
||||
self.assertFalse(hasattr(self.request, "axes_path_info"))
|
||||
self.assertFalse(hasattr(self.request, "axes_http_accept"))
|
||||
self.assertFalse(hasattr(self.request, "axes_updated"))
|
||||
|
||||
AxesMiddleware(get_response)(self.request)
|
||||
|
||||
self.assertTrue(hasattr(self.request, "axes_locked_out"))
|
||||
self.assertTrue(hasattr(self.request, "axes_attempt_time"))
|
||||
self.assertTrue(hasattr(self.request, "axes_ip_address"))
|
||||
self.assertTrue(hasattr(self.request, "axes_user_agent"))
|
||||
self.assertTrue(hasattr(self.request, "axes_path_info"))
|
||||
self.assertTrue(hasattr(self.request, "axes_http_accept"))
|
||||
self.assertTrue(hasattr(self.request, "axes_updated"))
|
||||
|
||||
def test_response_does_not_contain_extra_attrs_without_drf_integration(
|
||||
self,
|
||||
):
|
||||
def get_response(request):
|
||||
return HttpResponse()
|
||||
|
||||
self.assertNotIn("rest_framework", settings.INSTALLED_APPS)
|
||||
|
||||
AxesMiddleware(get_response)(self.request)
|
||||
|
||||
self.assertFalse(hasattr(self.request, "axes_locked_out"))
|
||||
self.assertFalse(hasattr(self.request, "axes_attempt_time"))
|
||||
self.assertFalse(hasattr(self.request, "axes_ip_address"))
|
||||
self.assertFalse(hasattr(self.request, "axes_user_agent"))
|
||||
self.assertFalse(hasattr(self.request, "axes_path_info"))
|
||||
self.assertFalse(hasattr(self.request, "axes_http_accept"))
|
||||
self.assertFalse(hasattr(self.request, "axes_updated"))
|
||||
|
||||
@mock.patch("axes.middleware.get_failure_limit", return_value=5)
|
||||
@mock.patch("axes.middleware.AxesProxyHandler.get_failures", return_value=5)
|
||||
@mock.patch("django.conf.settings.INSTALLED_APPS", ["rest_framework"])
|
||||
@override_settings(AXES_LOCK_OUT_AT_FAILURE=True)
|
||||
def test_lockout_response_with_drf_integration(
|
||||
self, mock_get_failure_limit, mock_get_failures
|
||||
):
|
||||
def get_response(request):
|
||||
return HttpResponse()
|
||||
|
||||
response = AxesMiddleware(get_response)(self.request)
|
||||
self.assertTrue(hasattr(self.request, "axes_locked_out"))
|
||||
self.assertTrue(self.request.axes_locked_out)
|
||||
self.assertEqual(response.status_code, self.STATUS_LOCKOUT)
|
||||
|
||||
@mock.patch("axes.middleware.get_failure_limit", return_value=5)
|
||||
@mock.patch("axes.middleware.AxesProxyHandler.get_failures", return_value=3)
|
||||
@mock.patch("django.conf.settings.INSTALLED_APPS", ["rest_framework"])
|
||||
@override_settings(AXES_LOCK_OUT_AT_FAILURE=True)
|
||||
def test_success_response_with_drf_integration(
|
||||
self, mock_get_failure_limit, mock_get_failures
|
||||
):
|
||||
def get_response(request):
|
||||
return HttpResponse()
|
||||
|
||||
response = AxesMiddleware(get_response)(self.request)
|
||||
self.assertTrue(hasattr(self.request, "axes_locked_out"))
|
||||
self.assertFalse(self.request.axes_locked_out)
|
||||
self.assertEqual(response.status_code, self.STATUS_SUCCESS)
|
||||
|
|
|
|||
|
|
@ -100,45 +100,44 @@ Integration with Django REST Framework
|
|||
--------------------------------------
|
||||
|
||||
.. note::
|
||||
Modern versions of Django REST Framework after 3.7.0 work normally with Axes
|
||||
out-of-the-box and require no customization in DRF.
|
||||
DRF versions prior to 3.7.0 will not function properly.
|
||||
|
||||
|
||||
Django REST Framework versions prior to 3.7.0
|
||||
require the request object to be passed for authentication
|
||||
by a customized DRF authentication class::
|
||||
Django Axes requires REST Framework to be connected
|
||||
via lockout signals for correct functionality.
|
||||
|
||||
from rest_framework.authentication import BasicAuthentication
|
||||
You can use the following snippet in your project signals such as ``example/signals.py``::
|
||||
|
||||
class AxesBasicAuthentication(BasicAuthentication):
|
||||
"""
|
||||
Extended basic authentication backend class that supplies the
|
||||
request object into the authentication call for Axes compatibility.
|
||||
from django.dispatch import receiver
|
||||
|
||||
NOTE: This patch is only needed for DRF versions < 3.7.0.
|
||||
"""
|
||||
from axes.signals import user_locked_out
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
|
||||
def authenticate(self, request):
|
||||
# NOTE: Request is added as an instance attribute in here
|
||||
self._current_request = request
|
||||
return super().authenticate(request)
|
||||
@receiver(user_locked_out)
|
||||
def raise_permission_denied(*args, **kwargs):
|
||||
raise PermissionDenied("Too many failed login attempts")
|
||||
|
||||
def authenticate_credentials(self, userid, password, request=None):
|
||||
credentials = {
|
||||
get_user_model().USERNAME_FIELD: userid,
|
||||
'password': password
|
||||
}
|
||||
And then configure your application to load it in ``examples/apps.py``::
|
||||
|
||||
# NOTE: Request is added as an argument to the authenticate call here
|
||||
user = authenticate(request=request or self._current_request, **credentials)
|
||||
from django import apps
|
||||
|
||||
if user is None:
|
||||
raise exceptions.AuthenticationFailed(_('Invalid username/password.'))
|
||||
|
||||
if not user.is_active:
|
||||
raise exceptions.AuthenticationFailed(_('User inactive or deleted.'))
|
||||
class AppConfig(apps.AppConfig):
|
||||
name = "example"
|
||||
|
||||
return (user, None)
|
||||
def ready(self):
|
||||
from example import signals # noqa
|
||||
|
||||
Please check the Django signals documentation for more information:
|
||||
|
||||
https://docs.djangoproject.com/en/3.1/topics/signals/
|
||||
|
||||
When a user login fails a signal is emitted and PermissionDenied
|
||||
raises a HTTP 403 reply which interrupts the login process.
|
||||
|
||||
This functionality was handled in the middleware for a time,
|
||||
but that resulted in extra database requests being made for
|
||||
each and every web request, and was migrated to signals.
|
||||
|
||||
|
||||
Integration with Django Simple Captcha
|
||||
|
|
|
|||
Loading…
Reference in a new issue