Repository: jazzband/django-axes

Issue:
Extra database requests always made with DRF integration
Hi,

I experienced issues with one extra database query in my tests suite after the update to version 5.8.0. The request is:
```sql
SELECT SUM("axes_accessattempt"."failures_since_start") AS "failures_since_start__sum"
FROM "axes_accessattempt"
WHERE ("axes_accessattempt"."ip_address" = '127.0.0.1'::inet AND "axes_accessattempt"."username" IS NULL AND "axes_accessattempt"."attempt_time" >= '2020-11-03T14:32:57.279609+00:00'::timestamptz)
```

I did some digging and it seems to come from the work done in #659 to fix integration with DRF. From what I understand, we normally just track the login attempts when a user fail to login with `user_login_failed`. So, unless we are on a login view, no extra database requests are made. However, with this update, we always do one extra request, even if we are not on a login view (a plain `ListView` in may case) because of the call to `get_failure_limit` which is always done in the middleware.

Is this the desired behavior?


Discussion:
Good catch @Jenselme!

The behaviour should probably be so that Axes only checks the information on login views, reducing extra database load from normal, unrelated requests.

What do you think would be the best way to realize this?
> What do you think would be the best way to realize this?

Would it make more sense to move this code out of the middleware to handle DRF in https://github.com/jazzband/django-axes/blob/22c564743db7c2abf3051e6ba10ec28fdb70b027/axes/handlers/database.py#L86?
I think the problem relates to this https://github.com/jazzband/django-axes/issues/570#issuecomment-709292733. The handler gets the wrapped request, the middleware gets the original request.

One valid option would be to check if the view is an authentication view, but I'm not sure where that information would be available from?
Do you mean implementing something like this in the middleware:
```python
    def __call__(self, request):
        if "rest_framework" in settings.INSTALLED_APPS:
            def deny_access(**kwargs):
                raise PermissionDenied('Too many failed login attempts')

            user_locked_out.connect(deny_access)

        response = self.get_response(request)
```
As suggested in https://github.com/jazzband/django-axes/issues/570#issuecomment-708713546?

I confess I don't know much about signals yet.
One way in which this issue has bitten us is now some tests that inherited from `SimpleTestCase` now require inheriting from `TestCase`.

Similarly some pytest-style function tests that previously didn't require DB access now require the `@pytest.mark.django_db` annotation to pass.

Resolving patch:
diff --git a/axes/conf.py b/axes/conf.py
--- a/axes/conf.py
+++ b/axes/conf.py
@@ -115,8 +115,3 @@
     "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
-)
diff --git a/axes/middleware.py b/axes/middleware.py
--- a/axes/middleware.py
+++ b/axes/middleware.py
@@ -43,20 +43,6 @@ def __call__(self, request):
         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
 


Test patch:
diff --git a/axes/tests/test_middleware.py b/axes/tests/test_middleware.py
--- a/axes/tests/test_middleware.py
+++ b/axes/tests/test_middleware.py
@@ -39,74 +39,3 @@ def get_response(request):
 
         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)