diff --git a/CHANGES.rst b/CHANGES.rst index beb1f14..1cd9e34 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -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`` diff --git a/axes/backends.py b/axes/backends.py index bcf8ae1..1146c74 100644 --- a/axes/backends.py +++ b/axes/backends.py @@ -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: diff --git a/axes/handlers/base.py b/axes/handlers/base.py index 6b38981..ad0d4a4 100644 --- a/axes/handlers/base.py +++ b/axes/handlers/base.py @@ -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') diff --git a/axes/middleware.py b/axes/middleware.py index 1bbd13b..f05a357 100644 --- a/axes/middleware.py +++ b/axes/middleware.py @@ -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): diff --git a/axes/tests/test_handlers.py b/axes/tests/test_handlers.py index 07fea84..f5cc249 100644 --- a/axes/tests/test_handlers.py +++ b/axes/tests/test_handlers.py @@ -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() diff --git a/docs/10_reference.rst b/docs/10_reference.rst deleted file mode 100644 index 3b732e7..0000000 --- a/docs/10_reference.rst +++ /dev/null @@ -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: diff --git a/docs/2_installation.rst b/docs/2_installation.rst index 903eb1b..37e38c3 100644 --- a/docs/2_installation.rst +++ b/docs/2_installation.rst @@ -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. \ No newline at end of file +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. diff --git a/docs/9_architecture.rst b/docs/7_architecture.rst similarity index 98% rename from docs/9_architecture.rst rename to docs/7_architecture.rst index 4ffadb2..54ea028 100644 --- a/docs/9_architecture.rst +++ b/docs/7_architecture.rst @@ -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. diff --git a/docs/7_upgrading.rst b/docs/7_upgrading.rst deleted file mode 100644 index bf87b9d..0000000 --- a/docs/7_upgrading.rst +++ /dev/null @@ -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. diff --git a/docs/8_development.rst b/docs/8_development.rst deleted file mode 100644 index e7ebf2a..0000000 --- a/docs/8_development.rst +++ /dev/null @@ -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 diff --git a/docs/8_reference.rst b/docs/8_reference.rst new file mode 100644 index 0000000..6503eac --- /dev/null +++ b/docs/8_reference.rst @@ -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: diff --git a/docs/9_development.rst b/docs/9_development.rst new file mode 100644 index 0000000..89ba195 --- /dev/null +++ b/docs/9_development.rst @@ -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:/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. diff --git a/docs/index.rst b/docs/index.rst index 7383065..4ba0ac9 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -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 ------------------