Skip to content

Commit

Permalink
Add settings flag for disabling Axes
Browse files Browse the repository at this point in the history
AXES_ENABLED = False can be used to toggle
the plugin off in tests which use the built-in
Django test client login, force_login and logout
methods which do not supply a request views.

Fixes #433
  • Loading branch information
aleksihakli committed May 7, 2019
1 parent f61e8ef commit 816676f
Show file tree
Hide file tree
Showing 8 changed files with 63 additions and 3 deletions.
3 changes: 2 additions & 1 deletion axes/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from axes.exceptions import AxesBackendPermissionDenied, AxesBackendRequestParameterRequired
from axes.handlers.proxy import AxesProxyHandler
from axes.helpers import get_credentials, get_lockout_message
from axes.helpers import get_credentials, get_lockout_message, toggleable
from axes.request import AxesHttpRequest


Expand All @@ -17,6 +17,7 @@ class AxesBackend(ModelBackend):
Authentication is handled by the following backends that are configured in ``AUTHENTICATION_BACKENDS``.
"""

@toggleable
def authenticate(self, request: AxesHttpRequest, username: str = None, password: str = None, **kwargs: dict):
"""
Checks user lockout status and raises an exception if user is not allowed to log in.
Expand Down
3 changes: 3 additions & 0 deletions axes/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@


class AxesAppConf(AppConf):
# disable plugin when set to False
ENABLED = True

# see if the user has overridden the failure limit
FAILURE_LIMIT = 3

Expand Down
5 changes: 5 additions & 0 deletions axes/handlers/proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from axes.conf import settings
from axes.handlers.base import AxesHandler
from axes.helpers import toggleable
from axes.request import AxesHttpRequest

log = getLogger(settings.AXES_LOGGER)
Expand Down Expand Up @@ -44,18 +45,22 @@ def is_allowed(cls, request: AxesHttpRequest, credentials: dict = None) -> bool:
return cls.get_implementation().is_allowed(request, credentials)

@classmethod
@toggleable
def user_login_failed(cls, sender, credentials: dict, request: AxesHttpRequest = None, **kwargs):
return cls.get_implementation().user_login_failed(sender, credentials, request, **kwargs)

@classmethod
@toggleable
def user_logged_in(cls, sender, request: AxesHttpRequest, user, **kwargs):
return cls.get_implementation().user_logged_in(sender, request, user, **kwargs)

@classmethod
@toggleable
def user_logged_out(cls, sender, request: AxesHttpRequest, user, **kwargs):
return cls.get_implementation().user_logged_out(sender, request, user, **kwargs)

@classmethod
@toggleable
def post_save_access_attempt(cls, instance, **kwargs):
return cls.get_implementation().post_save_access_attempt(instance, **kwargs)

Expand Down
19 changes: 18 additions & 1 deletion axes/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from hashlib import md5
from ipaddress import ip_address
from logging import getLogger
from typing import Any, Optional, Type, Union
from typing import Any, Callable, Optional, Type, Union

from django.core.cache import caches, BaseCache
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect, JsonResponse, QueryDict
Expand Down Expand Up @@ -376,3 +376,20 @@ def get_client_cache_key(request_or_attempt: Union[HttpRequest, Any], credential
cache_key = f'axes-{cache_key_digest}'

return cache_key


def toggleable(func) -> Callable:
"""
Decorator that toggles function execution based on settings.
If the ``settings.AXES_ENABLED`` flag is set to ``False``
the decorated function never runs and a None is returned.
This decorator is only suitable for functions that do not
require return values to be passed back to callers.
"""

def inner(*args, **kwargs): # pylint: disable=inconsistent-return-statements
if settings.AXES_ENABLED:
return func(*args, **kwargs)
return inner
3 changes: 3 additions & 0 deletions axes/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
get_client_path_info,
get_client_http_accept,
get_lockout_response,
toggleable,
)
from axes.request import AxesHttpRequest

Expand Down Expand Up @@ -45,6 +46,7 @@ def __call__(self, request: HttpRequest):
self.update_request(request)
return self.get_response(request)

@toggleable
def update_request(self, request: HttpRequest):
"""
Construct an ``AxesHttpRequest`` from the given ``HttpRequest``
Expand All @@ -60,6 +62,7 @@ def update_request(self, request: HttpRequest):
request.axes_path_info = get_client_path_info(request)
request.axes_http_accept = get_client_http_accept(request)

@toggleable
def process_exception(self, request: AxesHttpRequest, exception): # pylint: disable=inconsistent-return-statements
"""
Handle exceptions raised by the Axes signal handler class when requests fail checks.
Expand Down
23 changes: 22 additions & 1 deletion axes/tests/test_login.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
TODO: Clean up the tests in this module.
"""

from django.test import override_settings
from django.test import override_settings, TestCase
from django.urls import reverse
from django.contrib.auth import get_user_model

Expand All @@ -13,6 +13,27 @@
from axes.tests.base import AxesTestCase


@override_settings(AXES_ENABLED=False)
class DjangoLoginTestCase(TestCase):
def setUp(self):
self.username = 'john.doe'
self.password = 'hunter2'

self.user = get_user_model().objects.create(username=self.username)
self.user.set_password(self.password)
self.user.save()

def test_login(self):
self.client.login(username=self.username, password=self.password)

def test_logout(self):
self.client.login(username=self.username, password=self.password)
self.client.logout()

def test_force_login(self):
self.client.force_login(self.user)


class LoginTestCase(AxesTestCase):
"""
Test for lockouts under different configurations and circumstances to prevent false positives and false negatives.
Expand Down
8 changes: 8 additions & 0 deletions docs/2_installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,11 @@ of your regular CI workflows to verify that your project is not misconfigured.
Axes uses checks to verify your Django settings configuration for security and functionality.
Many people have different configurations for their development and production environments,
and running the application with misconfigured settings can prevent security features from working.

If you get errors when running tests or other configurations, try setting the ``AXES_ENABLED``
flag to ``False`` in your project or test settings configuration file::

AXES_ENABLED = False

This disables the Axes middleware, authentication backend and signal handlers
which might produce errors with exotic test configurations.
2 changes: 2 additions & 0 deletions docs/4_configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ Configuring project settings

The following ``settings.py`` options are available for customizing Axes behaviour.

* ``AXES_ENABLED``: Enable or disable Axes plugin functionality,
for example in test runner setup. Default: ``True``
* ``AXES_FAILURE_LIMIT``: The number of login attempts allowed before a
record is created for the failed logins. Default: ``3``
* ``AXES_LOCK_OUT_AT_FAILURE``: After the number of allowed login attempts
Expand Down

0 comments on commit 816676f

Please sign in to comment.