Improve documentation

Fixes #410
This commit is contained in:
Aleksi Häkli 2019-04-27 17:55:28 +03:00
parent 0a2620095a
commit d4dc3ba246
13 changed files with 143 additions and 194 deletions

View file

@ -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``

View file

@ -8,28 +8,25 @@ from axes.request import AxesHttpRequest
class AxesBackend(ModelBackend):
"""
Authentication backend that forbids login attempts for locked out users.
Authentication backend class that forbids login attempts for locked out users.
Use this class as the first item of ``AUTHENTICATION_BACKENDS`` to
prevent locked out users from being logged in by the Django authentication flow.
**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.
"""
def authenticate(self, request: AxesHttpRequest, username: str = None, password: str = None, **kwargs):
def authenticate(self, request: AxesHttpRequest, username: str = None, password: str = None, **kwargs: dict):
"""
Check user lock out status and raises PermissionDenied if user is not allowed to log in.
Checks user lockout status and raise a PermissionDenied if user is not allowed to log in.
Inserts errors directly to `return_context` that is supplied as a keyword argument.
This method interrupts the login flow and inserts error message directly to the
``response_context`` attribute that is supplied as a keyword argument.
Use this on top of your AUTHENTICATION_BACKENDS list to prevent locked out users
from being authenticated in the standard Django authentication flow.
Note that this method does not log your user in and delegates login to other backends.
: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:

View file

@ -9,30 +9,21 @@ from axes.request import AxesHttpRequest
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.
@ -54,32 +45,32 @@ class AxesHandler: # pylint: disable=unused-argument
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):
@ -89,7 +80,7 @@ class AxesHandler: # pylint: disable=unused-argument
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):
@ -102,7 +93,7 @@ class AxesHandler: # pylint: disable=unused-argument
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:
@ -112,7 +103,10 @@ class AxesHandler: # pylint: disable=unused-argument
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')

View file

@ -1,3 +1,5 @@
from typing import Callable
from django.http import HttpRequest
from django.utils.timezone import now
@ -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.
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.
: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):

View file

@ -82,7 +82,7 @@ class AxesProxyHandlerTestCase(AxesTestCase):
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,
@ -102,7 +102,7 @@ class AxesHandlerTestCase(AxesTestCase):
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()
@ -129,7 +129,7 @@ class AxesDatabaseHandlerTestCase(AxesHandlerTestCase):
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()
@ -150,7 +150,7 @@ class AxesCacheHandlerTestCase(AxesHandlerTestCase):
@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()

View file

@ -1,30 +0,0 @@
.. _reference:
10. API reference
=================
Axes offers extendable APIs that you can customize to your liking.
You can specialize the following base classes or alternatively
implement your own classes based on the following base implementations.
AxesBackend
-----------
.. automodule:: axes.backends
:members:
AxesHandler
---------------
.. automodule:: axes.handlers.base
:members:
AxesMiddleware
--------------
.. automodule:: axes.middleware
:members:

View file

@ -7,13 +7,9 @@ Axes is easy to install from the PyPI package::
$ pip install django-axes
After installing the package, the project settings need to be configured.
Configuring settings
--------------------
After installing the package, the project ``settings.py`` needs to be configured.
1. add ``axes`` to your ``INSTALLED_APPS``::
**1.** Add ``axes`` to your ``INSTALLED_APPS``::
INSTALLED_APPS = [
'django.contrib.admin',
@ -23,25 +19,21 @@ After installing the package, the project ``settings.py`` needs to be configured
'django.contrib.messages',
'django.contrib.staticfiles',
# ... other applications per your preference.
# Axes app can be in any position in the INSTALLED_APPS list.
'axes',
]
2. add ``axes.backends.AxesBackend`` to the top of ``AUTHENTICATION_BACKENDS``::
**2.** Add ``axes.backends.AxesBackend`` to the top of ``AUTHENTICATION_BACKENDS``::
AUTHENTICATION_BACKENDS = [
# AxesBackend should be the first backend in the list.
# It stops the authentication flow when a user is locked out.
# AxesBackend should be the first backend in the AUTHENTICATION_BACKENDS list.
'axes.backends.AxesBackend',
# ... other authentication backends per your preference.
# Django ModelBackend is the default authentication backend.
'django.contrib.auth.backends.ModelBackend',
]
3. add ``axes.middleware.AxesMiddleware`` to your list of ``MIDDLEWARE``::
**3.** Add ``axes.middleware.AxesMiddleware`` to your list of ``MIDDLEWARE``::
MIDDLEWARE = [
# The following is the list of default middleware in new Django projects.
@ -53,26 +45,21 @@ After installing the package, the project ``settings.py`` needs to be configured
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
# ... other middleware per your preference.
# AxesMiddleware should be the last middleware in the list.
# It pretty formats authentication errors into readable responses.
# AxesMiddleware should be the last middleware in the MIDDLEWARE list.
'axes.middleware.AxesMiddleware',
]
4. Run ``python manage.py migrate`` to sync the database.
**4.** Run ``python manage.py check`` to check the configuration.
**5.** Run ``python manage.py migrate`` to sync the database.
Axes is now functional with the default settings and is saving user attempts
into your database and locking users out if they exceed the maximum attempts.
Running Django system checks
----------------------------
Use the ``python manage.py check`` command to verify the correct configuration in both
development and production environments. It is probably best to use this step as part
You should use the ``python manage.py check`` command to verify the correct configuration in both
development, staging, and production environments. It is probably best to use this step as part
of your regular CI workflows to verify that your project is not misconfigured.
Axes uses the checks to verify your cache configuration to see that your caches
should be functional with the configuration of Axes. Many people have different configurations
for their development and production environments.
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.

View file

@ -1,6 +1,6 @@
.. _architecture:
9. Architecture
7. Architecture
================
Axes is based on the existing Django authentication backend
@ -116,6 +116,7 @@ Axes implements the lockout flow as follows:
8. AxesSignalPermissionDenied exception is raised
if appropriate and it bubbles up the middleware stack.
The exception aborts the Django authentication flow.
9. AxesMiddleware processes the exception
and returns a readable lockout message to the user.

View file

@ -1,40 +0,0 @@
.. _upgrading:
7. Upgrading
============
This page contains upgrade instructions between different Axes
versions so that users might more confidently upgrade their installations.
Upgrading from Axes version 4 to 5
----------------------------------
Axes version 5 has a few differences compared to Axes 4.
- Python 2.7, 3.4 and 3.5 support has been dropped.
This is to facilitate new language features and typing support,
as maintainers opted for the use of new tools in development.
- Login and logout view monkey-patching was removed.
Login monitoring is now implemented with signal handlers
and locking users out is implemented with a combination
of a custom authentication backend, middleware, and signals.
This does not change existing logic, but is good to know.
- The old decorators function as before and their behaviour is the same.
- ``AXES_USERNAME_CALLABLE`` is now always called with two arguments,
(``request``, ``credentials``) instead of just ``request``.
If you have implemented a custom callable, you need to add
the second ``credentials`` argument to the function signature.
- ``AXES_USERNAME_CALLABLE`` now supports string paths in addition to callables.
- ``axes.backends.AxesBackend.RequestParameterRequired``
exception was renamed and retyped from ``Exception`` to ``ValueError``.
Exception was moved to ``axes.exception.AxesBackendRequestParameterRequired``.
- ``AxesBackend`` now raises a
``axes.exceptions.AxesBackendPermissionDenied``
exception when user is locked out, which triggers signal handler
to run on failed logins, checking user lockout statuses.
- Axes lockout signal handler now raises a
``axes.exceptions.AxesHandlerPermissionDenied`` exception on lockouts.
- ``AxesMiddleware`` was added to process lockout events.
The middleware handles the ``axes.exception.AxesHandlerPermissionDenied``
exception and converts it to a lockout response.

View file

@ -1,14 +0,0 @@
.. _development:
8. Development
==============
You can contribute to this project forking it from GitHub and sending pull requests.
Running tests
-------------
Clone the repository and install the Django version you want. Then run::
$ tox

17
docs/8_reference.rst Normal file
View file

@ -0,0 +1,17 @@
.. _reference:
8. API reference
================
Axes offers extendable APIs that you can customize to your liking.
You can specialize the following base classes or alternatively implement
your own classes based on top of the following base implementations.
.. automodule:: axes.backends
:members:
.. automodule:: axes.middleware
:members:
.. automodule:: axes.handlers.base
:members:

39
docs/9_development.rst Normal file
View file

@ -0,0 +1,39 @@
.. _development:
9. Development
==============
You can contribute to this project forking it from GitHub and sending pull requests.
Setting up a development environment
------------------------------------
Fork and clone the repository, initialize a virtual environment and install the requirements::
$ git clone git@github.com:<fork>/django-axes.git
$ cd django-axes
$ mkdir ~/.virtualenvs
$ python3 -m venv ~/.virtualenvs/django-axes
$ source ~/.virtualenvs/bin/activate
$ pip install -r requirements.txt
Unit tests that are in the `axes/tests` folder can be run easily with the ``axes.tests.settings`` configuration::
$ pytest
Prospector runs a number of source code style, safety, and complexity checks::
$ prospector
Mypy runs static typing checks to verify the source code type annotations and correctness::
$ mypy .
Before committing, you can run all the tests against all supported Django versions with tox::
$ tox
Tox runs the same tests that are run by Travis, and your code should be good to go if it passes.
After you have made your changes, open a pull request on GitHub for getting your code upstreamed.

View file

@ -15,10 +15,10 @@ Contents
4_configuration
5_customization
6_integration
7_upgrading
8_development
9_architecture
10_reference
7_architecture
8_reference
9_development
Indices and tables
------------------