mirror of
https://github.com/jazzband/django-axes.git
synced 2026-05-08 15:44:46 +00:00
Merge 694dafc092 into b14b78a16e
This commit is contained in:
commit
f1c8aaabc4
3 changed files with 126 additions and 3 deletions
|
|
@ -5,6 +5,7 @@ from string import Template
|
|||
from typing import Callable, Optional, Type, Union, List
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.cache import BaseCache, caches
|
||||
from django.http import HttpRequest, HttpResponse, JsonResponse, QueryDict
|
||||
from django.shortcuts import redirect, render
|
||||
|
|
@ -131,6 +132,32 @@ def get_credentials(username: Optional[str] = None, **kwargs) -> dict:
|
|||
return credentials
|
||||
|
||||
|
||||
def get_username_from_data(data: dict) -> Optional[str]:
|
||||
"""
|
||||
Extract username from a dict-like object (credentials or request data).
|
||||
|
||||
Tries AXES_USERNAME_FORM_FIELD first, then falls back to Django's USERNAME_FIELD
|
||||
for compatibility with Django's user_login_failed signal.
|
||||
|
||||
See: https://github.com/jazzband/django-axes/issues/1159
|
||||
|
||||
:param data: dict-like object containing username (credentials or request.POST)
|
||||
:return: username string or None if not found
|
||||
"""
|
||||
username = data.get(settings.AXES_USERNAME_FORM_FIELD, None)
|
||||
if username is None:
|
||||
# Fallback to Django's USERNAME_FIELD for compatibility with
|
||||
# Django's user_login_failed signal which uses USERNAME_FIELD as the key
|
||||
username_field = get_user_model().USERNAME_FIELD
|
||||
if username_field != str(settings.AXES_USERNAME_FORM_FIELD):
|
||||
log.debug(
|
||||
"Falling back to USERNAME_FIELD '%s' for data lookup",
|
||||
username_field,
|
||||
)
|
||||
username = data.get(username_field, None)
|
||||
return username
|
||||
|
||||
|
||||
def get_client_username(
|
||||
request: HttpRequest, credentials: Optional[dict] = None
|
||||
) -> str:
|
||||
|
|
@ -164,14 +191,14 @@ def get_client_username(
|
|||
log.debug(
|
||||
"Using parameter credentials to get username with key settings.AXES_USERNAME_FORM_FIELD"
|
||||
)
|
||||
return credentials.get(settings.AXES_USERNAME_FORM_FIELD, None) # type: ignore[return-value]
|
||||
return get_username_from_data(credentials) # type: ignore[return-value]
|
||||
|
||||
log.debug(
|
||||
"Using parameter request.POST to get username with key settings.AXES_USERNAME_FORM_FIELD"
|
||||
)
|
||||
|
||||
request_data = getattr(request, "data", request.POST)
|
||||
return request_data.get(settings.AXES_USERNAME_FORM_FIELD, None)
|
||||
return get_username_from_data(request_data) # type: ignore[return-value]
|
||||
|
||||
|
||||
def get_client_ip_address(
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ The following ``settings.py`` options are available for customizing Axes behavio
|
|||
+------------------------------------------------------+----------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
||||
| AXES_VERBOSE | True | If ``True``, you'll see slightly more logging for Axes. |
|
||||
+------------------------------------------------------+----------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
||||
| AXES_USERNAME_FORM_FIELD | 'settings.AUTH_USER_MODEL.USERNAME_FIELD' | The name of the form field that contains your users usernames. |
|
||||
| AXES_USERNAME_FORM_FIELD | 'settings.AUTH_USER_MODEL.USERNAME_FIELD' | The name of the form field that contains your users usernames. When looking up the username from credentials or request data, Axes will first try this field, then fall back to Django's ``USERNAME_FIELD`` if not found. This ensures compatibility with Django's ``user_login_failed`` signal which uses ``USERNAME_FIELD``. |
|
||||
+------------------------------------------------------+----------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
||||
| AXES_USERNAME_CALLABLE | None | A callable or a string path to callable that takes two arguments for user lookups: ``def get_username(request: HttpRequest, credentials: dict) -> str: ...``. This can be any callable such as ``AXES_USERNAME_CALLABLE = lambda request, credentials: 'username'`` or a full Python module path to callable such as ``AXES_USERNAME_CALLABLE = 'example.get_username``. The ``request`` is a HttpRequest like object and the ``credentials`` is a dictionary like object. ``credentials`` are the ones that were passed to Django ``authenticate()`` in the login flow. If no function is supplied, Axes fetches the username from the ``credentials`` or ``request.POST`` dictionaries based on ``AXES_USERNAME_FORM_FIELD``. |
|
||||
+------------------------------------------------------+----------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ from axes.helpers import (
|
|||
get_cool_off,
|
||||
get_cool_off_iso8601,
|
||||
get_lockout_response,
|
||||
get_username_from_data,
|
||||
is_client_ip_address_blacklisted,
|
||||
is_client_ip_address_whitelisted,
|
||||
is_client_method_whitelisted,
|
||||
|
|
@ -792,6 +793,101 @@ class UsernameTestCase(AxesTestCase):
|
|||
def test_get_client_username_str(self):
|
||||
self.assertEqual(get_client_username(HttpRequest(), {}), "username")
|
||||
|
||||
@override_settings(AXES_USERNAME_FORM_FIELD="auth-username")
|
||||
def test_get_client_username_fallback_to_username_field_from_credentials(self):
|
||||
"""
|
||||
Test that when AXES_USERNAME_FORM_FIELD is set to a custom value that
|
||||
doesn't exist in credentials, we fallback to Django's USERNAME_FIELD.
|
||||
|
||||
This fixes https://github.com/jazzband/django-axes/issues/1159
|
||||
"""
|
||||
expected = "test-username"
|
||||
|
||||
request = HttpRequest()
|
||||
# Credentials from Django's user_login_failed signal use USERNAME_FIELD (e.g., "username")
|
||||
# not the custom AXES_USERNAME_FORM_FIELD value
|
||||
credentials = {"username": expected}
|
||||
|
||||
actual = get_client_username(request, credentials)
|
||||
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
@override_settings(AXES_USERNAME_FORM_FIELD="auth-username")
|
||||
def test_get_client_username_fallback_to_username_field_from_request(self):
|
||||
"""
|
||||
Test that when AXES_USERNAME_FORM_FIELD is set to a custom value that
|
||||
doesn't exist in request.POST, we fallback to Django's USERNAME_FIELD.
|
||||
|
||||
This fixes https://github.com/jazzband/django-axes/issues/1159
|
||||
"""
|
||||
expected = "test-username"
|
||||
|
||||
request = HttpRequest()
|
||||
# POST data might use Django's USERNAME_FIELD instead of custom form field
|
||||
request.POST["username"] = expected
|
||||
|
||||
actual = get_client_username(request)
|
||||
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
@override_settings(AXES_USERNAME_FORM_FIELD="auth-username")
|
||||
def test_get_client_username_custom_field_takes_priority(self):
|
||||
"""
|
||||
Test that AXES_USERNAME_FORM_FIELD takes priority when it exists in credentials.
|
||||
"""
|
||||
expected = "custom-field-username"
|
||||
fallback = "username-field-value"
|
||||
|
||||
request = HttpRequest()
|
||||
credentials = {
|
||||
"auth-username": expected, # Custom field - should be used
|
||||
"username": fallback, # Django's USERNAME_FIELD - should be ignored
|
||||
}
|
||||
|
||||
actual = get_client_username(request, credentials)
|
||||
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
|
||||
class GetUsernameFromDataTestCase(AxesTestCase):
|
||||
"""Unit tests for the get_username_from_data helper function."""
|
||||
|
||||
@override_settings(AXES_USERNAME_FORM_FIELD="username")
|
||||
def test_get_username_from_data_with_matching_field(self):
|
||||
"""Test that username is returned when AXES_USERNAME_FORM_FIELD matches."""
|
||||
data = {"username": "test-user"}
|
||||
self.assertEqual(get_username_from_data(data), "test-user")
|
||||
|
||||
@override_settings(AXES_USERNAME_FORM_FIELD="custom-field")
|
||||
def test_get_username_from_data_fallback_to_username_field(self):
|
||||
"""
|
||||
Test that when AXES_USERNAME_FORM_FIELD doesn't exist in data,
|
||||
we fallback to Django's USERNAME_FIELD.
|
||||
"""
|
||||
# Django's default USERNAME_FIELD is "username"
|
||||
data = {"username": "fallback-user"}
|
||||
self.assertEqual(get_username_from_data(data), "fallback-user")
|
||||
|
||||
@override_settings(AXES_USERNAME_FORM_FIELD="custom-field")
|
||||
def test_get_username_from_data_custom_field_takes_priority(self):
|
||||
"""Test that AXES_USERNAME_FORM_FIELD takes priority over USERNAME_FIELD."""
|
||||
data = {
|
||||
"custom-field": "custom-user",
|
||||
"username": "fallback-user",
|
||||
}
|
||||
self.assertEqual(get_username_from_data(data), "custom-user")
|
||||
|
||||
@override_settings(AXES_USERNAME_FORM_FIELD="nonexistent")
|
||||
def test_get_username_from_data_returns_none_when_not_found(self):
|
||||
"""Test that None is returned when username is not found in any field."""
|
||||
data = {"other-field": "some-value"}
|
||||
self.assertIsNone(get_username_from_data(data))
|
||||
|
||||
@override_settings(AXES_USERNAME_FORM_FIELD="username")
|
||||
def test_get_username_from_data_with_empty_dict(self):
|
||||
"""Test that None is returned for empty dict."""
|
||||
self.assertIsNone(get_username_from_data({}))
|
||||
|
||||
|
||||
def get_username(request, credentials: dict) -> str:
|
||||
return "username"
|
||||
|
|
|
|||
Loading…
Reference in a new issue