Repository: jazzband/django-axes
Pull request #1023 (2023-03-29 12:58:13)

Bump black from 23.1.0 to 23.3.0
Bumps [black](https://github.com/psf/black) from 23.1.0 to 23.3.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/psf/black/releases">black's releases</a>.</em></p>
<blockquote>
<h2>23.3.0</h2>
<h3>Highlights</h3>
<p>This release fixes a longstanding confusing behavior in Black's GitHub action, where the
version of the action did not determine the version of Black being run (issue <a href="https://redirect.github.com/psf/black/issues/3382">#3382</a>). In
addition, there is a small bug fix around imports and a number of improvements to the
preview style.</p>
<p>Please try out the
<a href="https://black.readthedocs.io/en/stable/the_black_code_style/future_style.html#preview-style">preview style</a>
with <code>black --preview</code> and tell us your feedback. All changes in the preview style are
expected to become part of Black's stable style in January 2024.</p>
<h3>Stable style</h3>
<ul>
<li>Import lines with <code># fmt: skip</code> and <code># fmt: off</code> no longer have an extra blank line
added when they are right after another import line (<a href="https://redirect.github.com/psf/black/issues/3610">#3610</a>)</li>
</ul>
<h3>Preview style</h3>
<ul>
<li>Add trailing commas to collection literals even if there's a comment after the last
entry (<a href="https://redirect.github.com/psf/black/issues/3393">#3393</a>)</li>
<li><code>async def</code>, <code>async for</code>, and <code>async with</code> statements are now formatted consistently
compared to their non-async version. (<a href="https://redirect.github.com/psf/black/issues/3609">#3609</a>)</li>
<li><code>with</code> statements that contain two context managers will be consistently wrapped in
parentheses (<a href="https://redirect.github.com/psf/black/issues/3589">#3589</a>)</li>
<li>Let string splitters respect <a href="https://www.unicode.org/reports/tr11/">East Asian Width</a>
(<a href="https://redirect.github.com/psf/black/issues/3445">#3445</a>)</li>
<li>Now long string literals can be split after East Asian commas and periods (<code>、</code> U+3001
IDEOGRAPHIC COMMA, <code>。</code> U+3002 IDEOGRAPHIC FULL STOP, &amp; <code>，</code> U+FF0C FULLWIDTH COMMA)
besides before spaces (<a href="https://redirect.github.com/psf/black/issues/3445">#3445</a>)</li>
<li>For stubs, enforce one blank line after a nested class with a body other than just
<code>...</code> (<a href="https://redirect.github.com/psf/black/issues/3564">#3564</a>)</li>
<li>Improve handling of multiline strings by changing line split behavior (<a href="https://redirect.github.com/psf/black/issues/1879">#1879</a>)</li>
</ul>
<h3>Parser</h3>
<ul>
<li>Added support for formatting files with invalid type comments (<a href="https://redirect.github.com/psf/black/issues/3594">#3594</a>)</li>
</ul>
<h3>Integrations</h3>
<ul>
<li>Update GitHub Action to use the version of Black equivalent to action's version if
version input is not specified (<a href="https://redirect.github.com/psf/black/issues/3543">#3543</a>)</li>
<li>Fix missing Python binary path in autoload script for vim (<a href="https://redirect.github.com/psf/black/issues/3508">#3508</a>)</li>
</ul>
<h3>Documentation</h3>
<ul>
<li>Document that only the most recent release is supported for security issues;
vulnerabilities should be reported through Tidelift (<a href="https://redirect.github.com/psf/black/issues/3612">#3612</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/psf/black/blob/main/CHANGES.md">black's changelog</a>.</em></p>
<blockquote>
<h2>23.3.0</h2>
<h3>Highlights</h3>
<p>This release fixes a longstanding confusing behavior in Black's GitHub action, where the
version of the action did not determine the version of Black being run (issue <a href="https://redirect.github.com/psf/black/issues/3382">#3382</a>). In
addition, there is a small bug fix around imports and a number of improvements to the
preview style.</p>
<p>Please try out the
<a href="https://black.readthedocs.io/en/stable/the_black_code_style/future_style.html#preview-style">preview style</a>
with <code>black --preview</code> and tell us your feedback. All changes in the preview style are
expected to become part of Black's stable style in January 2024.</p>
<h3>Stable style</h3>
<ul>
<li>Import lines with <code># fmt: skip</code> and <code># fmt: off</code> no longer have an extra blank line
added when they are right after another import line (<a href="https://redirect.github.com/psf/black/issues/3610">#3610</a>)</li>
</ul>
<h3>Preview style</h3>
<ul>
<li>Add trailing commas to collection literals even if there's a comment after the last
entry (<a href="https://redirect.github.com/psf/black/issues/3393">#3393</a>)</li>
<li><code>async def</code>, <code>async for</code>, and <code>async with</code> statements are now formatted consistently
compared to their non-async version. (<a href="https://redirect.github.com/psf/black/issues/3609">#3609</a>)</li>
<li><code>with</code> statements that contain two context managers will be consistently wrapped in
parentheses (<a href="https://redirect.github.com/psf/black/issues/3589">#3589</a>)</li>
<li>Let string splitters respect <a href="https://www.unicode.org/reports/tr11/">East Asian Width</a>
(<a href="https://redirect.github.com/psf/black/issues/3445">#3445</a>)</li>
<li>Now long string literals can be split after East Asian commas and periods (<code>、</code> U+3001
IDEOGRAPHIC COMMA, <code>。</code> U+3002 IDEOGRAPHIC FULL STOP, &amp; <code>，</code> U+FF0C FULLWIDTH COMMA)
besides before spaces (<a href="https://redirect.github.com/psf/black/issues/3445">#3445</a>)</li>
<li>For stubs, enforce one blank line after a nested class with a body other than just
<code>...</code> (<a href="https://redirect.github.com/psf/black/issues/3564">#3564</a>)</li>
<li>Improve handling of multiline strings by changing line split behavior (<a href="https://redirect.github.com/psf/black/issues/1879">#1879</a>)</li>
</ul>
<h3>Parser</h3>
<ul>
<li>Added support for formatting files with invalid type comments (<a href="https://redirect.github.com/psf/black/issues/3594">#3594</a>)</li>
</ul>
<h3>Integrations</h3>
<ul>
<li>Update GitHub Action to use the version of Black equivalent to action's version if
version input is not specified (<a href="https://redirect.github.com/psf/black/issues/3543">#3543</a>)</li>
<li>Fix missing Python binary path in autoload script for vim (<a href="https://redirect.github.com/psf/black/issues/3508">#3508</a>)</li>
</ul>
<h3>Documentation</h3>
<ul>
<li>Document that only the most recent release is supported for security issues;
vulnerabilities should be reported through Tidelift (<a href="https://redirect.github.com/psf/black/issues/3612">#3612</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/psf/black/commit/bf7a16254ec96b084a6caf3d435ec18f0f245cc7"><code>bf7a162</code></a> Fixup the changelog (<a href="https://redirect.github.com/psf/black/issues/3628">#3628</a>)</li>
<li><a href="https://github.com/psf/black/commit/9b2b048692f0bd642f38230b4e61c778a4653f91"><code>9b2b048</code></a> Prepare release 23.3.0 (<a href="https://redirect.github.com/psf/black/issues/3625">#3625</a>)</li>
<li><a href="https://github.com/psf/black/commit/bf5abdb0b6cf6644628977230736a0a6478c1bff"><code>bf5abdb</code></a> Specify Python exec path with minor version if available (<a href="https://redirect.github.com/psf/black/issues/3508">#3508</a>)</li>
<li><a href="https://github.com/psf/black/commit/b542f589a5c4041f54847591104cd51684849f2e"><code>b542f58</code></a> Use GH action version when version argument not specified (<a href="https://redirect.github.com/psf/black/issues/3543">#3543</a>)</li>
<li><a href="https://github.com/psf/black/commit/f3b1a3b9d2fc6de8f0845399cb80d8bdfd6400fd"><code>f3b1a3b</code></a> Bump furo from 2022.12.7 to 2023.3.23 in /docs (<a href="https://redirect.github.com/psf/black/issues/3624">#3624</a>)</li>
<li><a href="https://github.com/psf/black/commit/ef6e079901d53a42dfae4ab10b081ce7a73a47b5"><code>ef6e079</code></a> Let string splitters respect <code>East_Asian_Width</code> property (<a href="https://redirect.github.com/psf/black/issues/3445">#3445</a>)</li>
<li><a href="https://github.com/psf/black/commit/5c064a986c388e2be1e448c3e4b28e5f5ba7ce5a"><code>5c064a9</code></a> Bump sphinx from 5.3.0 to 6.1.3 in /docs (<a href="https://redirect.github.com/psf/black/issues/3499">#3499</a>)</li>
<li><a href="https://github.com/psf/black/commit/3a9d6f0a5f9013b97676f3d24246bd34d93fce4c"><code>3a9d6f0</code></a> Bump myst-parser from 0.18.1 to 1.0.0 in /docs (<a href="https://redirect.github.com/psf/black/issues/3601">#3601</a>)</li>
<li><a href="https://github.com/psf/black/commit/53c23e62df9b182edf9e7ccf726acdcf8c25846f"><code>53c23e6</code></a> Support files with type comment syntax errors (<a href="https://redirect.github.com/psf/black/issues/3594">#3594</a>)</li>
<li><a href="https://github.com/psf/black/commit/dba3c2695c59fdb11825dbdf8f3b0ab6e0b368b2"><code>dba3c26</code></a> Fix bug introduced in <a href="https://redirect.github.com/psf/black/issues/3564">#3564</a>. (<a href="https://redirect.github.com/psf/black/issues/3615">#3615</a>)</li>
<li>Additional commits viewable in <a href="https://github.com/psf/black/compare/23.1.0...23.3.0">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=black&package-manager=pip&previous-version=23.1.0&new-version=23.3.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>

codecov[bot]: ## [Codecov](https://codecov.io/gh/jazzband/django-axes/pull/1023?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=jazzband) Report
> Merging [#1023](https://codecov.io/gh/jazzband/django-axes/pull/1023?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=jazzband) (d5bd8e7) into [master](https://codecov.io/gh/jazzband/django-axes/commit/dccbba7764d66837aa1c965dd515a92c3b609697?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=jazzband) (dccbba7) will **not change** coverage.
> The diff coverage is `n/a`.

```diff
@@           Coverage Diff           @@
##           master    #1023   +/-   ##
=======================================
  Coverage   96.17%   96.17%           
=======================================
  Files          34       34           
  Lines        1046     1046           
  Branches      185      185           
=======================================
  Hits         1006     1006           
  Misses         34       34           
  Partials        6        6           
```



:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=jazzband)

github-code-scanning[bot]: You have successfully added a new CodeQL configuration `.github/workflows/codeql.yml:CodeQL-Build`. As part of the setup process, we have scanned this repository and found no existing alerts. In the future, you will see all code scanning alerts on the repository [Security tab](/jazzband/django-axes/security/code-scanning?query=pr%3A1023+tool%3ACodeQL+is%3Aopen).

Patch:
diff --git a/axes/conf.py b/axes/conf.py
index 745024f..c3999e5 100644
--- a/axes/conf.py
+++ b/axes/conf.py
@@ -1,7 +1,6 @@
 from django.conf import settings
 from django.utils.translation import gettext_lazy as _
 
-
 # disable plugin when set to False
 settings.AXES_ENABLED = getattr(settings, "AXES_ENABLED", True)
 
@@ -52,6 +51,9 @@ settings.AXES_WHITELIST_CALLABLE = getattr(settings, "AXES_WHITELIST_CALLABLE",
 # return custom lockout response if configured
 settings.AXES_LOCKOUT_CALLABLE = getattr(settings, "AXES_LOCKOUT_CALLABLE", None)
 
+# use a provided callable to get client ip address
+settings.AXES_CLIENT_IP_CALLABLE = getattr(settings, "AXES_CLIENT_IP_CALLABLE", None)
+
 # reset the number of failed attempts after one successful attempt
 settings.AXES_RESET_ON_SUCCESS = getattr(settings, "AXES_RESET_ON_SUCCESS", False)
 
diff --git a/axes/helpers.py b/axes/helpers.py
index e40a2b3..457354d 100644
--- a/axes/helpers.py
+++ b/axes/helpers.py
@@ -6,9 +6,9 @@ from typing import Callable, Optional, Type, Union
 from urllib.parse import urlencode
 
 import ipware.ip
-from django.core.cache import caches, BaseCache
+from django.core.cache import BaseCache, caches
 from django.http import HttpRequest, HttpResponse, JsonResponse, QueryDict
-from django.shortcuts import render, redirect
+from django.shortcuts import redirect, render
 from django.utils.module_loading import import_string
 
 from axes.conf import settings
@@ -152,10 +152,27 @@ def get_client_ip_address(request: HttpRequest) -> str:
     """
     Get client IP address as configured by the user.
 
-    The django-ipware package is used for address resolution
-    and parameters can be configured in the Axes package.
+    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)
+
+    :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")
+
+        if callable(settings.AXES_CLIENT_IP_CALLABLE):
+            return settings.AXES_CLIENT_IP_CALLABLE(  # pylint: disable=not-callable
+                request
+            )
+        if isinstance(settings.AXES_CLIENT_IP_CALLABLE, str):
+            return import_string(settings.AXES_CLIENT_IP_CALLABLE)(request)
+        raise TypeError(
+            "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,
diff --git a/docs/4_configuration.rst b/docs/4_configuration.rst
index 6228d9e..d9071a6 100644
--- a/docs/4_configuration.rst
+++ b/docs/4_configuration.rst
@@ -55,6 +55,8 @@ The following ``settings.py`` options are available for customizing Axes behavio
 +------------------------------------------------------+----------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
 | AXES_LOCKOUT_CALLABLE                                | None                                         | A callable or a string path to callable that takes two arguments returns a response. For example: ``def generate_lockout_response(request: HttpRequest, credentials: dict) -> HttpResponse: ...``. This can be any callable similarly to ``AXES_USERNAME_CALLABLE``. If not callable is defined, then the default implementation in ``axes.helpers.get_lockout_response`` is used for determining the correct lockout response that is sent to the requesting client.                                                                                                                                                                                                                                                                     |
 +------------------------------------------------------+----------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
+| AXES_CLIENT_IP_CALLABLE                              | None                                         | A callable or a string path to callable that takes two arguments returns a response. For example: ``def get_ip(request: HttpRequest) -> str: ...``. This can be any callable similarly to ``AXES_USERNAME_CALLABLE``. If not callable is defined, then the default implementation in ``axes.helpers.get_client_ip_address`` is used.                                                                                                                                                                                                                                                                                                                                                                                                      |
++------------------------------------------------------+----------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
 | AXES_PASSWORD_FORM_FIELD                             | 'password'                                   | The name of the form or credentials field that contains your users password.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
 +------------------------------------------------------+----------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
 | AXES_SENSITIVE_PARAMETERS                            | []                                           | Configures POST and GET parameter values (in addition to the value of ``AXES_PASSWORD_FORM_FIELD``) to mask in login attempt logging.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
diff --git a/docs/5_customization.rst b/docs/5_customization.rst
index 44b3836..47f2fd5 100644
--- a/docs/5_customization.rst
+++ b/docs/5_customization.rst
@@ -173,3 +173,18 @@ An example of usage could be e.g. a custom view for processing lockouts.
 ``settings.py``::
 
     AXES_LOCKOUT_CALLABLE = "example.views.lockout"
+
+
+Customizing client ip address lookups
+-----------------------------
+
+Axes can be configured with ``AXES_CLIENT_IP_CALLABLE`` to use custom client ip address lookup logic.
+
+``example/utils.py``::
+
+    def get_client_ip(request):
+        return request.META.get("REMOTE_ADDR")
+
+``settings.py``::
+
+    AXES_LOCKOUT_CALLABLE = "example.utils.get_client_ip"
diff --git a/requirements-qa.txt b/requirements-qa.txt
index ee7af78..88ba230 100644
--- a/requirements-qa.txt
+++ b/requirements-qa.txt
@@ -1,4 +1,4 @@
-black==23.1.0
+black==23.3.0
 mypy==1.1.1
 prospector==1.9.0
 types-pkg_resources  # Type stub


Test patch:
diff --git a/tests/test_helpers.py b/tests/test_helpers.py
index 1da372d..fc4b5a3 100644
--- a/tests/test_helpers.py
+++ b/tests/test_helpers.py
@@ -3,16 +3,18 @@ from hashlib import sha256
 from unittest.mock import patch
 
 from django.contrib.auth import get_user_model
-from django.http import JsonResponse, HttpResponseRedirect, HttpResponse, HttpRequest
-from django.test import override_settings, RequestFactory
+from django.http import HttpRequest, HttpResponse, HttpResponseRedirect, JsonResponse
+from django.test import RequestFactory, override_settings
 
 from axes.apps import AppConfig
 from axes.helpers import (
+    cleanse_parameters,
     get_cache_timeout,
-    get_client_str,
-    get_client_username,
     get_client_cache_key,
+    get_client_ip_address,
     get_client_parameters,
+    get_client_str,
+    get_client_username,
     get_cool_off,
     get_cool_off_iso8601,
     get_lockout_response,
@@ -23,7 +25,6 @@ from axes.helpers import (
     is_ip_address_in_whitelist,
     is_user_attempt_whitelisted,
     toggleable,
-    cleanse_parameters,
 )
 from axes.models import AccessAttempt
 from tests.base import AxesTestCase
@@ -341,7 +342,11 @@ class ClientParametersTestCase(AxesTestCase):
         self.assertEqual(
             get_client_parameters(self.username, self.ip_address, self.user_agent),
             [
-                {"username": self.username, "ip_address": self.ip_address, "user_agent": self.user_agent},
+                {
+                    "username": self.username,
+                    "ip_address": self.ip_address,
+                    "user_agent": self.user_agent,
+                },
             ],
         )
 
@@ -553,6 +558,43 @@ def get_username(request, credentials: dict) -> str:
     return "username"
 
 
+def get_ip(request: HttpRequest) -> str:
+    return "127.0.0.1"
+
+
+class ClientIpAddressTestCase(AxesTestCase):
+    @override_settings(AXES_CLIENT_IP_CALLABLE=get_ip)
+    def test_get_client_ip_address(self):
+        self.assertEqual(get_client_ip_address(HttpRequest()), "127.0.0.1")
+
+    @override_settings(AXES_CLIENT_IP_CALLABLE="tests.test_helpers.get_ip")
+    def test_get_client_ip_address_str(self):
+        self.assertEqual(get_client_ip_address(HttpRequest()), "127.0.0.1")
+
+    @override_settings(
+        AXES_CLIENT_IP_CALLABLE=lambda request: "127.0.0.1"
+    )  # pragma: no cover
+    def test_get_client_ip_address_lambda(self):
+        self.assertEqual(get_client_ip_address(HttpRequest()), "127.0.0.1")
+
+    @override_settings(AXES_CLIENT_IP_CALLABLE=True)
+    def test_get_client_ip_address_not_callable(self):
+        with self.assertRaises(TypeError):
+            get_client_ip_address(HttpRequest())
+
+    @override_settings(AXES_CLIENT_IP_CALLABLE=lambda: None)  # pragma: no cover
+    def test_get_client_ip_address_invalid_callable_too_few_arguments(self):
+        with self.assertRaises(TypeError):
+            get_client_ip_address(HttpRequest())
+
+    @override_settings(
+        AXES_CLIENT_IP_CALLABLE=lambda request, extra: None
+    )  # pragma: no cover
+    def test_get_client_ip_address_invalid_callable_too_many_arguments(self):
+        with self.assertRaises(TypeError):
+            get_client_ip_address(HttpRequest())
+
+
 class IPWhitelistTestCase(AxesTestCase):
     def setUp(self):
         self.request = HttpRequest()