Skip to content

Commit

Permalink
Improve documentation
Browse files Browse the repository at this point in the history
Fixes #410
  • Loading branch information
aleksihakli committed Apr 27, 2019
1 parent 0a26200 commit d4dc3ba
Show file tree
Hide file tree
Showing 13 changed files with 143 additions and 194 deletions.
26 changes: 10 additions & 16 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,28 @@ Changes
5.0.0 (WIP)
-----------

- Improve managment commands and separate commands for resetting
all access attempts, attempts by IP and attempts by username.
Add tests for the management commands for better coverage.
- Improve management commands and separate commands for resetting
all access attempts, attempts by IP, and attempts by username.
[aleksihakli]

- Add a Django native authentication stack that utilizes
``AUTHENTICATION_BACKENDS``, ``MIDDLEWARE``, and signal handlers
for tracking login attempts and implementing user lockouts.
This results in configuration changes, refer to the documentation.
[aleksihakli]
- Use backend, middleware, and signal handlers for tracking
login attempts and implementing user lockouts.
[aleksihakli, jorlugaqui, joshua-s]

- Add ``AxesDatabaseHandler``, ``AxesCacheHandler``, and ``AxesDummyHandler``
handler backends for processing user login and logout events and failures.
[aleksihakli, jorlugaqui, joshua-s]

- Remove automatic decoration of Django login views and forms.
Leave decorations available for application users who wish to
- Remove automatic decoration and monkey-patching of Django views and forms.
Leave decorators available for application users who wish to
decorate their own login or other views as before.
[aleksihakli]

- Clean up code, tests, and documentation.
Improve test coverage and and raise
Codecov monitoring threshold to 90%.
- Clean up code, documentation, tests, and coverage.
[aleksihakli]

- Drop support for Python 2.7 and Python 3.4.
Require minimum version of Python 3.5+ from now on.
Add support for PyPy 3 in the test matrix.
- Drop support for Python 2.7, 3.4 and 3.5.
Require minimum version of Python 3.6+ from now on.
[aleksihakli]

- Add support for string import for ``AXES_USERNAME_CALLABLE``
Expand Down
31 changes: 14 additions & 17 deletions axes/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,25 @@

class AxesBackend(ModelBackend):
"""
Authentication backend that forbids login attempts for locked out users.
"""
Authentication backend class that forbids login attempts for locked out users.
def authenticate(self, request: AxesHttpRequest, username: str = None, password: str = None, **kwargs):
"""
Check user lock out status and raises PermissionDenied if user is not allowed to log in.
Use this class as the first item of ``AUTHENTICATION_BACKENDS`` to
prevent locked out users from being logged in by the Django authentication flow.
Inserts errors directly to `return_context` that is supplied as a keyword argument.
**Note:** this backend does not log your user in and delegates login to the
backends that are configured after it in the ``AUTHENTICATION_BACKENDS`` list.
"""

Use this on top of your AUTHENTICATION_BACKENDS list to prevent locked out users
from being authenticated in the standard Django authentication flow.
def authenticate(self, request: AxesHttpRequest, username: str = None, password: str = None, **kwargs: dict):
"""
Checks user lockout status and raise a PermissionDenied if user is not allowed to log in.
Note that this method does not log your user in and delegates login to other backends.
This method interrupts the login flow and inserts error message directly to the
``response_context`` attribute that is supplied as a keyword argument.
:param request: see django.contrib.auth.backends.ModelBackend.authenticate
:param username: see django.contrib.auth.backends.ModelBackend.authenticate
:param password: see django.contrib.auth.backends.ModelBackend.authenticate
:param kwargs: see django.contrib.auth.backends.ModelBackend.authenticate
:keyword response_context: context dict that will be updated with error information
:raises AxesBackendRequestParameterRequired: if request parameter is not given correctly
:raises AxesBackendPermissionDenied: if user is already locked out
:return: None
:keyword response_context: kwarg that will be have its ``error`` attribute updated with context.
:raises AxesBackendRequestParameterRequired: if request parameter is not passed.
:raises AxesBackendPermissionDenied: if user is already locked out.
"""

if request is None:
Expand Down
42 changes: 18 additions & 24 deletions axes/handlers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,21 @@

class AxesHandler: # pylint: disable=unused-argument
"""
Handler API definition for subclassing handlers that can be used with the AxesProxyHandler.
Public API methods for this class are:
- is_allowed
- user_login_failed
- user_logged_in
- user_logged_out
- post_save_access_attempt
- post_delete_access_attempt
Other API methods are considered internal and do not have fixed signatures.
Virtual handler API definition for subclassing handlers that can be used with the ``AxesProxyHandler``.
If you wish to implement your own handler class just override the methods you wish to specialize
and define the class to be used with ``settings.AXES_HANDLER = 'dotted.full.path.to.YourClass'``.
and define the class to be used with ``settings.AXES_HANDLER = 'path.to.YourClass'``.
The default implementation that is actually used by Axes is the ``axes.handlers.database.AxesDatabaseHandler``.
"""

def is_allowed(self, request: AxesHttpRequest, credentials: dict = None) -> bool:
"""
Check if the user is allowed to access or use given functionality such as a login view or authentication.
Checks if the user is allowed to access or use given functionality such as a login view or authentication.
This method is abstract and other backends can specialize it as needed, but the default implementation
checks if the user has attempted to authenticate into the site too many times through the
Django authentication backends and returns false if user exceeds the configured Axes thresholds.
Django authentication backends and returns ``False``if user exceeds the configured Axes thresholds.
This checker can implement arbitrary checks such as IP whitelisting or blacklisting,
request frequency checking, failed attempt monitoring or similar functions.
Expand All @@ -54,32 +45,32 @@ def is_allowed(self, request: AxesHttpRequest, credentials: dict = None) -> bool

def user_login_failed(self, sender, credentials: dict, request: AxesHttpRequest = None, **kwargs):
"""
Handle the Django user_login_failed authentication signal.
Handles the Django ``django.contrib.auth.signals.user_login_failed`` authentication signal.
"""

def user_logged_in(self, sender, request: AxesHttpRequest, user, **kwargs):
"""
Handle the Django user_logged_in authentication signal.
Handles the Django ``django.contrib.auth.signals.user_logged_in`` authentication signal.
"""

def user_logged_out(self, sender, request: AxesHttpRequest, user, **kwargs):
"""
Handle the Django user_logged_out authentication signal.
Handles the Django ``django.contrib.auth.signals.user_logged_out`` authentication signal.
"""

def post_save_access_attempt(self, instance, **kwargs):
"""
Handle the Axes AccessAttempt object post save signal.
Handles the ``axes.models.AccessAttempt`` object post save signal.
"""

def post_delete_access_attempt(self, instance, **kwargs):
"""
Handle the Axes AccessAttempt object post delete signal.
Handles the ``axes.models.AccessAttempt`` object post delete signal.
"""

def is_blacklisted(self, request: AxesHttpRequest, credentials: dict = None) -> bool: # pylint: disable=unused-argument
"""
Check if the request or given credentials are blacklisted from access.
Checks if the request or given credentials are blacklisted from access.
"""

if is_client_ip_address_blacklisted(request):
Expand All @@ -89,7 +80,7 @@ def is_blacklisted(self, request: AxesHttpRequest, credentials: dict = None) ->

def is_whitelisted(self, request: AxesHttpRequest, credentials: dict = None) -> bool: # pylint: disable=unused-argument
"""
Check if the request or given credentials are whitelisted for access.
Checks if the request or given credentials are whitelisted for access.
"""

if is_client_ip_address_whitelisted(request):
Expand All @@ -102,7 +93,7 @@ def is_whitelisted(self, request: AxesHttpRequest, credentials: dict = None) ->

def is_locked(self, request: AxesHttpRequest, credentials: dict = None) -> bool:
"""
Check if the request or given credentials are locked.
Checks if the request or given credentials are locked.
"""

if settings.AXES_LOCK_OUT_AT_FAILURE:
Expand All @@ -112,7 +103,10 @@ def is_locked(self, request: AxesHttpRequest, credentials: dict = None) -> bool:

def get_failures(self, request: AxesHttpRequest, credentials: dict = None) -> int:
"""
Check the number of failures associated to the given request and credentials.
Checks the number of failures associated to the given request and credentials.
This is a virtual method that needs an implementation in the handler subclass
if the ``settings.AXES_LOCK_OUT_AT_FAILURE`` flag is set to ``True``.
"""

raise NotImplementedError('The Axes handler class needs a method definition for get_failures')
36 changes: 20 additions & 16 deletions axes/middleware.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import Callable

from django.http import HttpRequest
from django.utils.timezone import now

Expand All @@ -17,38 +19,40 @@ class AxesMiddleware:
Middleware that maps lockout signals into readable HTTP 403 Forbidden responses.
Without this middleware the backend returns HTTP 403 errors with the
django.views.defaults.permission_denied view that renders the 403.html
``django.views.defaults.permission_denied`` view that renders the ``403.html``
template from the root template directory if found.
This middleware uses the ``axes.helpers.get_lockout_response`` handler
for returning a context aware lockout message to the end user.
Refer to the Django documentation for further information:
https://docs.djangoproject.com/en/dev/ref/views/#the-403-http-forbidden-view
To customize the error rendering, you can for example inherit this middleware
and change the process_exception handler to your own liking.
To customize the error rendering, you can subclass this middleware
and change the ``process_exception`` handler to your own liking.
"""

def __init__(self, get_response):
def __init__(self, get_response: Callable):
self.get_response = get_response

def __call__(self, request: HttpRequest):
self.update_request(request)
return self.get_response(request)

def update_request(self, request: HttpRequest):
"""
Update given Django ``HttpRequest`` with necessary attributes
before passing it on the ``get_response`` for further
Django middleware and view processing.
"""

request.axes_attempt_time = now()
request.axes_ip_address = get_client_ip_address(request)
request.axes_user_agent = get_client_user_agent(request)
request.axes_path_info = get_client_path_info(request)
request.axes_http_accept = get_client_http_accept(request)

return self.get_response(request)

def process_exception(self, request: AxesHttpRequest, exception): # pylint: disable=inconsistent-return-statements
"""
Exception handler that processes exceptions raised by the axes signal handler when request fails with login.
Refer to axes.signals.log_user_login_failed for the error code.
Exception handler that processes exceptions raised by the Axes signal handler when request fails with login.
:param request: HTTPRequest that will be locked out.
:param exception: Exception raised by Django views or signals. Only AxesSignalPermissionDenied will be handled.
:return: HTTPResponse that indicates the lockout or None.
Only ``axes.exceptions.AxesSignalPermissionDenied`` exception is handled by this middleware.
"""

if isinstance(exception, AxesSignalPermissionDenied):
Expand Down
8 changes: 4 additions & 4 deletions axes/tests/test_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def test_post_delete_access_attempt(self, handler):
self.assertTrue(handler.post_delete_access_attempt.called)


class AxesHandlerTestCase(AxesTestCase):
class AxesHandlerBaseTestCase(AxesTestCase):
def check_whitelist(self, log):
with override_settings(
AXES_NEVER_LOCKOUT_WHITELIST=True,
Expand All @@ -102,7 +102,7 @@ def check_empty_request(self, log, handler):
AXES_COOLOFF_TIME=timedelta(seconds=1),
AXES_RESET_ON_SUCCESS=True,
)
class AxesDatabaseHandlerTestCase(AxesHandlerTestCase):
class AxesDatabaseHandlerTestCase(AxesHandlerBaseTestCase):
@override_settings(AXES_RESET_ON_SUCCESS=True)
def test_handler(self):
self.check_handler()
Expand All @@ -129,7 +129,7 @@ def test_user_whitelisted(self, is_whitelisted):
AXES_HANDLER='axes.handlers.cache.AxesCacheHandler',
AXES_COOLOFF_TIME=timedelta(seconds=1),
)
class AxesCacheHandlerTestCase(AxesHandlerTestCase):
class AxesCacheHandlerTestCase(AxesHandlerBaseTestCase):
@override_settings(AXES_RESET_ON_SUCCESS=True)
def test_handler(self):
self.check_handler()
Expand All @@ -150,7 +150,7 @@ def test_whitelist(self, log):
@override_settings(
AXES_HANDLER='axes.handlers.dummy.AxesDummyHandler',
)
class AxesDummyHandlerTestCase(AxesHandlerTestCase):
class AxesDummyHandlerTestCase(AxesHandlerBaseTestCase):
def test_handler(self):
for _ in range(settings.AXES_FAILURE_LIMIT):
self.login()
Expand Down
30 changes: 0 additions & 30 deletions docs/10_reference.rst

This file was deleted.

Loading

0 comments on commit d4dc3ba

Please sign in to comment.