From cd950ddfef23bc5ff4cfe24de0904fc71509e565 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleksi=20H=C3=A4kli?= Date: Thu, 27 Apr 2023 22:53:22 +0300 Subject: [PATCH] Make ipware an optional dependency Relates to #1038 --- axes/helpers.py | 45 ++++++++++++++++++++++++++++++----------- docs/2_installation.rst | 3 ++- requirements-test.txt | 1 + setup.py | 5 ++++- tests/test_helpers.py | 10 +++++++++ 5 files changed, 50 insertions(+), 14 deletions(-) diff --git a/axes/helpers.py b/axes/helpers.py index 778d582..8c58411 100644 --- a/axes/helpers.py +++ b/axes/helpers.py @@ -5,7 +5,6 @@ from string import Template from typing import Callable, Optional, Type, Union from urllib.parse import urlencode -import ipware.ip from django.core.cache import BaseCache, caches from django.http import HttpRequest, HttpResponse, JsonResponse, QueryDict from django.shortcuts import redirect, render @@ -16,6 +15,13 @@ from axes.models import AccessBase log = getLogger(__name__) +try: + import ipware.ip + + IPWARE_INSTALLED = True +except ImportError: + IPWARE_INSTALLED = False + def get_cache() -> BaseCache: """ @@ -148,20 +154,24 @@ def get_client_username( return request_data.get(settings.AXES_USERNAME_FORM_FIELD, None) -def get_client_ip_address(request: HttpRequest) -> str: +def get_client_ip_address( + request: HttpRequest, + use_ipware: Optional[bool] = None, +) -> Optional[str]: """ Get client IP address as configured by the user. The order of preference for address resolution is as follows: 1. If configured, use ``AXES_CLIENT_IP_CALLABLE``, and supply ``request`` as argument - 2. Use django-ipware package (parameters can be configured in the Axes package) + 2. If available, use django-ipware package (parameters can be configured in the Axes package) + 3. Use ``request.META.get('REMOTE_ADDR', None)`` as a fallback :param request: incoming Django ``HttpRequest`` or similar object from authentication backend or other source """ if settings.AXES_CLIENT_IP_CALLABLE: - log.debug("Using settings.AXES_CLIENT_IP_CALLABLE to get username") + log.debug("Using settings.AXES_CLIENT_IP_CALLABLE to get client IP address") if callable(settings.AXES_CLIENT_IP_CALLABLE): return settings.AXES_CLIENT_IP_CALLABLE( # pylint: disable=not-callable @@ -173,15 +183,26 @@ def get_client_ip_address(request: HttpRequest) -> str: "settings.AXES_CLIENT_IP_CALLABLE needs to be a string, callable, or None." ) - client_ip_address, _ = ipware.ip.get_client_ip( - request, - proxy_order=settings.AXES_PROXY_ORDER, - proxy_count=settings.AXES_PROXY_COUNT, - proxy_trusted_ips=settings.AXES_PROXY_TRUSTED_IPS, - request_header_order=settings.AXES_META_PRECEDENCE_ORDER, - ) + # Resolve using django-ipware from a configuration flag that can be set to False to explicitly disable + # this is added to both enable or disable the branch when ipware is installed in the test environment + if use_ipware is None: + use_ipware = IPWARE_INSTALLED + if use_ipware: + log.debug("Using django-ipware to get client IP address") - return client_ip_address + client_ip_address, _ = ipware.ip.get_client_ip( + request, + proxy_order=settings.AXES_PROXY_ORDER, + proxy_count=settings.AXES_PROXY_COUNT, + proxy_trusted_ips=settings.AXES_PROXY_TRUSTED_IPS, + request_header_order=settings.AXES_META_PRECEDENCE_ORDER, + ) + return client_ip_address + + log.debug( + "Using request.META.get('REMOTE_ADDR', None) fallback method to get client IP address" + ) + return request.META.get("REMOTE_ADDR", None) def get_client_user_agent(request: HttpRequest) -> str: diff --git a/docs/2_installation.rst b/docs/2_installation.rst index e001768..27d1f45 100644 --- a/docs/2_installation.rst +++ b/docs/2_installation.rst @@ -5,7 +5,8 @@ Installation Axes is easy to install from the PyPI package:: - $ pip install django-axes + $ pip install django-axes[ipware] # use django-ipware for resolving client IP addresses OR + $ pip install django-axes # implement and configure custom AXES_CLIENT_IP_CALLABLE After installing the package, the project settings need to be configured. diff --git a/requirements-test.txt b/requirements-test.txt index 7c28881..a7a440f 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,4 +1,5 @@ -e . +django-ipware>=3 coverage==7.2.3 pytest==7.3.1 pytest-cov==4.0.0 diff --git a/setup.py b/setup.py index c7a6d1f..aca2304 100644 --- a/setup.py +++ b/setup.py @@ -36,7 +36,10 @@ setup( use_scm_version=True, setup_requires=["setuptools_scm"], python_requires=">=3.7", - install_requires=["django>=3.2", "django-ipware>=3", "setuptools"], + install_requires=["django>=3.2", "setuptools"], + extras_require={ + "ipware": "django-ipware>=3", + }, include_package_data=True, packages=find_packages(exclude=["tests"]), classifiers=[ diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 15e0746..2677a17 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -614,6 +614,16 @@ class ClientIpAddressTestCase(AxesTestCase): with self.assertRaises(TypeError): get_client_ip_address(HttpRequest()) + def test_get_client_ip_address_with_ipware(self): + request = HttpRequest() + request.META["REMOTE_ADDR"] = "127.0.0.2" + self.assertEqual(get_client_ip_address(request, use_ipware=True), "127.0.0.2") + + def test_get_client_ip_address_without_ipware(self): + request = HttpRequest() + request.META["REMOTE_ADDR"] = "127.0.0.3" + self.assertEqual(get_client_ip_address(request, use_ipware=False), "127.0.0.3") + class IPWhitelistTestCase(AxesTestCase): def setUp(self):