mirror of
https://github.com/jazzband/django-axes.git
synced 2026-05-10 08:35:01 +00:00
Compare commits
23 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b14b78a16e | ||
|
|
e4cdd72231 | ||
|
|
3fc256c8d2 | ||
|
|
1aa8509cdc | ||
|
|
46e206af49 | ||
|
|
0d7f4bdb43 | ||
|
|
cc0387ae60 | ||
|
|
fdd7b22cd3 | ||
|
|
a5d14cd630 | ||
|
|
2a31c0133f | ||
|
|
4624eed684 | ||
|
|
e27ce891ea | ||
|
|
c3dcd1ba51 | ||
|
|
41ebdc3063 | ||
|
|
31c69dbea5 | ||
|
|
bdd0c9546a | ||
|
|
4b77eb69ee | ||
|
|
5acae054b4 | ||
|
|
d59a289407 | ||
|
|
23ee2fca44 | ||
|
|
4ea615811b | ||
|
|
b4fb3088b4 | ||
|
|
6c8feada83 |
15 changed files with 126 additions and 48 deletions
12
CHANGES.rst
12
CHANGES.rst
|
|
@ -2,6 +2,18 @@
|
||||||
Changes
|
Changes
|
||||||
=======
|
=======
|
||||||
|
|
||||||
|
8.3.1 (2026-02-11)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
- Fix configuration JSON serialization errors for Celery.
|
||||||
|
[aleksihakli]
|
||||||
|
|
||||||
|
8.3.0 (2026-02-09)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
- Remove deprecated pkg_resources in favour of new importlib.
|
||||||
|
[hugovk]
|
||||||
|
|
||||||
8.2.0 (2026-02-06)
|
8.2.0 (2026-02-06)
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,10 @@ class IsLockedOutFilter(admin.SimpleListFilter):
|
||||||
|
|
||||||
def queryset(self, request, queryset):
|
def queryset(self, request, queryset):
|
||||||
if self.value() == "yes":
|
if self.value() == "yes":
|
||||||
return queryset.filter(failures_since_start__gte=settings.AXES_FAILURE_LIMIT)
|
return queryset.filter(
|
||||||
elif self.value() == "no":
|
failures_since_start__gte=settings.AXES_FAILURE_LIMIT
|
||||||
|
)
|
||||||
|
if self.value() == "no":
|
||||||
return queryset.filter(failures_since_start__lt=settings.AXES_FAILURE_LIMIT)
|
return queryset.filter(failures_since_start__lt=settings.AXES_FAILURE_LIMIT)
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
@ -36,7 +38,7 @@ class AccessAttemptAdmin(admin.ModelAdmin):
|
||||||
]
|
]
|
||||||
|
|
||||||
if settings.AXES_USE_ATTEMPT_EXPIRATION:
|
if settings.AXES_USE_ATTEMPT_EXPIRATION:
|
||||||
list_display.append('expiration')
|
list_display.append("expiration")
|
||||||
|
|
||||||
list_filter = ["attempt_time", "path_info"]
|
list_filter = ["attempt_time", "path_info"]
|
||||||
|
|
||||||
|
|
@ -44,14 +46,17 @@ class AccessAttemptAdmin(admin.ModelAdmin):
|
||||||
# This will only add the status field if AXES_FAILURE_LIMIT is set to a positive integer
|
# This will only add the status field if AXES_FAILURE_LIMIT is set to a positive integer
|
||||||
# Because callable failure limit requires scope of request object
|
# Because callable failure limit requires scope of request object
|
||||||
list_display.append("status")
|
list_display.append("status")
|
||||||
list_filter.append(IsLockedOutFilter)
|
list_filter.append(IsLockedOutFilter) # type: ignore[arg-type]
|
||||||
|
|
||||||
search_fields = ["ip_address", "username", "user_agent", "path_info"]
|
search_fields = ["ip_address", "username", "user_agent", "path_info"]
|
||||||
|
|
||||||
date_hierarchy = "attempt_time"
|
date_hierarchy = "attempt_time"
|
||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, {"fields": ("username", "path_info", "failures_since_start", "expiration")}),
|
(
|
||||||
|
None,
|
||||||
|
{"fields": ("username", "path_info", "failures_since_start", "expiration")},
|
||||||
|
),
|
||||||
(_("Form Data"), {"fields": ("get_data", "post_data")}),
|
(_("Form Data"), {"fields": ("get_data", "post_data")}),
|
||||||
(_("Meta Data"), {"fields": ("user_agent", "ip_address", "http_accept")}),
|
(_("Meta Data"), {"fields": ("user_agent", "ip_address", "http_accept")}),
|
||||||
)
|
)
|
||||||
|
|
@ -69,10 +74,10 @@ class AccessAttemptAdmin(admin.ModelAdmin):
|
||||||
"expiration",
|
"expiration",
|
||||||
]
|
]
|
||||||
|
|
||||||
actions = ['cleanup_expired_attempts']
|
actions = ["cleanup_expired_attempts"]
|
||||||
|
|
||||||
@admin.action(description=_('Clean up expired attempts'))
|
@admin.action(description=_("Clean up expired attempts"))
|
||||||
def cleanup_expired_attempts(self, request, queryset):
|
def cleanup_expired_attempts(self, request, queryset): # noqa
|
||||||
count = self.handler.clean_expired_user_attempts(request=request)
|
count = self.handler.clean_expired_user_attempts(request=request)
|
||||||
self.message_user(request, _(f"Cleaned up {count} expired access attempts."))
|
self.message_user(request, _(f"Cleaned up {count} expired access attempts."))
|
||||||
|
|
||||||
|
|
@ -87,8 +92,13 @@ class AccessAttemptAdmin(admin.ModelAdmin):
|
||||||
return obj.expiration.expires_at if hasattr(obj, "expiration") else _("Not set")
|
return obj.expiration.expires_at if hasattr(obj, "expiration") else _("Not set")
|
||||||
|
|
||||||
def status(self, obj: AccessAttempt):
|
def status(self, obj: AccessAttempt):
|
||||||
return f"{settings.AXES_FAILURE_LIMIT - obj.failures_since_start} "+_("Attempt Remaining") if \
|
return (
|
||||||
obj.failures_since_start < settings.AXES_FAILURE_LIMIT else _("Locked Out")
|
f"{settings.AXES_FAILURE_LIMIT - obj.failures_since_start} "
|
||||||
|
+ _("Attempt Remaining")
|
||||||
|
if obj.failures_since_start < settings.AXES_FAILURE_LIMIT
|
||||||
|
else _("Locked Out")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AccessLogAdmin(admin.ModelAdmin):
|
class AccessLogAdmin(admin.ModelAdmin):
|
||||||
list_display = (
|
list_display = (
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ def get_cool_off_threshold(request: Optional[HttpRequest] = None) -> datetime:
|
||||||
"Cool off threshold can not be calculated with settings.AXES_COOLOFF_TIME set to None"
|
"Cool off threshold can not be calculated with settings.AXES_COOLOFF_TIME set to None"
|
||||||
)
|
)
|
||||||
|
|
||||||
attempt_time = request.axes_attempt_time
|
attempt_time = request.axes_attempt_time # type: ignore[union-attr]
|
||||||
if attempt_time is None:
|
if attempt_time is None:
|
||||||
return now() - cool_off
|
return now() - cool_off
|
||||||
return attempt_time - cool_off
|
return attempt_time - cool_off
|
||||||
|
|
|
||||||
20
axes/conf.py
20
axes/conf.py
|
|
@ -3,6 +3,19 @@ from django.contrib.auth import get_user_model
|
||||||
from django.utils.functional import SimpleLazyObject
|
from django.utils.functional import SimpleLazyObject
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
class JSONSerializableLazyObject(SimpleLazyObject):
|
||||||
|
"""
|
||||||
|
Celery/Kombu config inspection may JSON-encode Django settings.
|
||||||
|
Provide a JSON-friendly representation for lazy values.
|
||||||
|
|
||||||
|
Fixes jazzband/django-axes#1391
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __json__(self):
|
||||||
|
return str(self)
|
||||||
|
|
||||||
|
|
||||||
# disable plugin when set to False
|
# disable plugin when set to False
|
||||||
settings.AXES_ENABLED = getattr(settings, "AXES_ENABLED", True)
|
settings.AXES_ENABLED = getattr(settings, "AXES_ENABLED", True)
|
||||||
|
|
||||||
|
|
@ -43,6 +56,7 @@ settings.AXES_ONLY_ADMIN_SITE = getattr(settings, "AXES_ONLY_ADMIN_SITE", False)
|
||||||
# show Axes logs in admin
|
# show Axes logs in admin
|
||||||
settings.AXES_ENABLE_ADMIN = getattr(settings, "AXES_ENABLE_ADMIN", True)
|
settings.AXES_ENABLE_ADMIN = getattr(settings, "AXES_ENABLE_ADMIN", True)
|
||||||
|
|
||||||
|
|
||||||
# use a specific username field to retrieve from login POST data
|
# use a specific username field to retrieve from login POST data
|
||||||
def _get_username_field_default():
|
def _get_username_field_default():
|
||||||
return get_user_model().USERNAME_FIELD
|
return get_user_model().USERNAME_FIELD
|
||||||
|
|
@ -51,7 +65,7 @@ def _get_username_field_default():
|
||||||
settings.AXES_USERNAME_FORM_FIELD = getattr(
|
settings.AXES_USERNAME_FORM_FIELD = getattr(
|
||||||
settings,
|
settings,
|
||||||
"AXES_USERNAME_FORM_FIELD",
|
"AXES_USERNAME_FORM_FIELD",
|
||||||
SimpleLazyObject(_get_username_field_default),
|
JSONSerializableLazyObject(_get_username_field_default),
|
||||||
)
|
)
|
||||||
|
|
||||||
# use a specific password field to retrieve from login POST data
|
# use a specific password field to retrieve from login POST data
|
||||||
|
|
@ -94,7 +108,9 @@ settings.AXES_LOCKOUT_URL = getattr(settings, "AXES_LOCKOUT_URL", None)
|
||||||
|
|
||||||
settings.AXES_COOLOFF_TIME = getattr(settings, "AXES_COOLOFF_TIME", None)
|
settings.AXES_COOLOFF_TIME = getattr(settings, "AXES_COOLOFF_TIME", None)
|
||||||
|
|
||||||
settings.AXES_USE_ATTEMPT_EXPIRATION = getattr(settings, "AXES_USE_ATTEMPT_EXPIRATION", False)
|
settings.AXES_USE_ATTEMPT_EXPIRATION = getattr(
|
||||||
|
settings, "AXES_USE_ATTEMPT_EXPIRATION", False
|
||||||
|
)
|
||||||
|
|
||||||
settings.AXES_VERBOSE = getattr(settings, "AXES_VERBOSE", settings.AXES_ENABLED)
|
settings.AXES_VERBOSE = getattr(settings, "AXES_VERBOSE", settings.AXES_ENABLED)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,12 @@ from axes.helpers import (
|
||||||
get_query_str,
|
get_query_str,
|
||||||
get_attempt_expiration,
|
get_attempt_expiration,
|
||||||
)
|
)
|
||||||
from axes.models import AccessAttempt, AccessAttemptExpiration, AccessFailureLog, AccessLog
|
from axes.models import (
|
||||||
|
AccessAttempt,
|
||||||
|
AccessAttemptExpiration,
|
||||||
|
AccessFailureLog,
|
||||||
|
AccessLog,
|
||||||
|
)
|
||||||
from axes.signals import user_locked_out
|
from axes.signals import user_locked_out
|
||||||
|
|
||||||
log = getLogger(__name__)
|
log = getLogger(__name__)
|
||||||
|
|
@ -223,15 +228,17 @@ class AxesDatabaseHandler(AbstractAxesHandler, AxesBaseHandler):
|
||||||
if settings.AXES_USE_ATTEMPT_EXPIRATION:
|
if settings.AXES_USE_ATTEMPT_EXPIRATION:
|
||||||
if not hasattr(attempt, "expiration") or attempt.expiration is None:
|
if not hasattr(attempt, "expiration") or attempt.expiration is None:
|
||||||
log.debug(
|
log.debug(
|
||||||
"AXES: Creating new AccessAttemptExpiration for %s", client_str
|
"AXES: Creating new AccessAttemptExpiration for %s",
|
||||||
|
client_str,
|
||||||
)
|
)
|
||||||
attempt.expiration = AccessAttemptExpiration.objects.create(
|
attempt.expiration = AccessAttemptExpiration.objects.create(
|
||||||
access_attempt=attempt,
|
access_attempt=attempt,
|
||||||
expires_at=get_attempt_expiration(request)
|
expires_at=get_attempt_expiration(request),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
attempt.expiration.expires_at = max(
|
attempt.expiration.expires_at = max(
|
||||||
get_attempt_expiration(request), attempt.expiration.expires_at
|
get_attempt_expiration(request),
|
||||||
|
attempt.expiration.expires_at,
|
||||||
)
|
)
|
||||||
attempt.expiration.save()
|
attempt.expiration.save()
|
||||||
|
|
||||||
|
|
@ -365,7 +372,7 @@ class AxesDatabaseHandler(AbstractAxesHandler, AxesBaseHandler):
|
||||||
return attempts_list
|
return attempts_list
|
||||||
|
|
||||||
def get_user_attempts(
|
def get_user_attempts(
|
||||||
self, request: HttpRequest, credentials: Optional[dict] = None
|
self, request: HttpRequest, credentials: Optional[dict] = None # noqa
|
||||||
) -> List[QuerySet]:
|
) -> List[QuerySet]:
|
||||||
"""
|
"""
|
||||||
Get list of querysets with valid user attempts that match the given request and credentials.
|
Get list of querysets with valid user attempts that match the given request and credentials.
|
||||||
|
|
@ -386,7 +393,9 @@ class AxesDatabaseHandler(AbstractAxesHandler, AxesBaseHandler):
|
||||||
]
|
]
|
||||||
|
|
||||||
def clean_expired_user_attempts(
|
def clean_expired_user_attempts(
|
||||||
self, request: Optional[HttpRequest] = None, credentials: Optional[dict] = None
|
self,
|
||||||
|
request: Optional[HttpRequest] = None,
|
||||||
|
credentials: Optional[dict] = None, # noqa
|
||||||
) -> int:
|
) -> int:
|
||||||
"""
|
"""
|
||||||
Clean expired user attempts from the database.
|
Clean expired user attempts from the database.
|
||||||
|
|
@ -400,7 +409,9 @@ class AxesDatabaseHandler(AbstractAxesHandler, AxesBaseHandler):
|
||||||
|
|
||||||
if settings.AXES_USE_ATTEMPT_EXPIRATION:
|
if settings.AXES_USE_ATTEMPT_EXPIRATION:
|
||||||
threshold = timezone.now()
|
threshold = timezone.now()
|
||||||
count, _ = AccessAttempt.objects.filter(expiration__expires_at__lte=threshold).delete()
|
count, _ = AccessAttempt.objects.filter(
|
||||||
|
expiration__expires_at__lte=threshold
|
||||||
|
).delete()
|
||||||
log.info(
|
log.info(
|
||||||
"AXES: Cleaned up %s expired access attempts from database that expiry were older than %s",
|
"AXES: Cleaned up %s expired access attempts from database that expiry were older than %s",
|
||||||
count,
|
count,
|
||||||
|
|
@ -408,7 +419,9 @@ class AxesDatabaseHandler(AbstractAxesHandler, AxesBaseHandler):
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
threshold = get_cool_off_threshold(request)
|
threshold = get_cool_off_threshold(request)
|
||||||
count, _ = AccessAttempt.objects.filter(attempt_time__lte=threshold).delete()
|
count, _ = AccessAttempt.objects.filter(
|
||||||
|
attempt_time__lte=threshold
|
||||||
|
).delete()
|
||||||
log.info(
|
log.info(
|
||||||
"AXES: Cleaned up %s expired access attempts from database that were older than %s",
|
"AXES: Cleaned up %s expired access attempts from database that were older than %s",
|
||||||
count,
|
count,
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,7 @@ def get_cool_off_iso8601(delta: timedelta) -> str:
|
||||||
return f"P{days_str}T{time_str}"
|
return f"P{days_str}T{time_str}"
|
||||||
return f"P{days_str}"
|
return f"P{days_str}"
|
||||||
|
|
||||||
|
|
||||||
def get_attempt_expiration(request: Optional[HttpRequest] = None) -> datetime:
|
def get_attempt_expiration(request: Optional[HttpRequest] = None) -> datetime:
|
||||||
"""
|
"""
|
||||||
Get threshold for fetching access attempts from the database.
|
Get threshold for fetching access attempts from the database.
|
||||||
|
|
@ -111,11 +112,12 @@ def get_attempt_expiration(request: Optional[HttpRequest] = None) -> datetime:
|
||||||
"Cool off threshold can not be calculated with settings.AXES_COOLOFF_TIME set to None"
|
"Cool off threshold can not be calculated with settings.AXES_COOLOFF_TIME set to None"
|
||||||
)
|
)
|
||||||
|
|
||||||
attempt_time = request.axes_attempt_time
|
attempt_time = request.axes_attempt_time # type: ignore[union-attr]
|
||||||
if attempt_time is None:
|
if attempt_time is None:
|
||||||
return datetime.now() + cool_off
|
return datetime.now() + cool_off
|
||||||
return attempt_time + cool_off
|
return attempt_time + cool_off
|
||||||
|
|
||||||
|
|
||||||
def get_credentials(username: Optional[str] = None, **kwargs) -> dict:
|
def get_credentials(username: Optional[str] = None, **kwargs) -> dict:
|
||||||
"""
|
"""
|
||||||
Calculate credentials for Axes to use internally from given username and kwargs.
|
Calculate credentials for Axes to use internally from given username and kwargs.
|
||||||
|
|
@ -162,7 +164,7 @@ def get_client_username(
|
||||||
log.debug(
|
log.debug(
|
||||||
"Using parameter credentials to get username with key settings.AXES_USERNAME_FORM_FIELD"
|
"Using parameter credentials to get username with key settings.AXES_USERNAME_FORM_FIELD"
|
||||||
)
|
)
|
||||||
return credentials.get(settings.AXES_USERNAME_FORM_FIELD, None)
|
return credentials.get(settings.AXES_USERNAME_FORM_FIELD, None) # type: ignore[return-value]
|
||||||
|
|
||||||
log.debug(
|
log.debug(
|
||||||
"Using parameter request.POST to get username with key settings.AXES_USERNAME_FORM_FIELD"
|
"Using parameter request.POST to get username with key settings.AXES_USERNAME_FORM_FIELD"
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,8 @@ class AxesMiddleware:
|
||||||
credentials = getattr(request, "axes_credentials", None)
|
credentials = getattr(request, "axes_credentials", None)
|
||||||
response = await sync_to_async(
|
response = await sync_to_async(
|
||||||
get_lockout_response, thread_sensitive=True
|
get_lockout_response, thread_sensitive=True
|
||||||
)(request, credentials) # type: ignore
|
)(
|
||||||
|
request, credentials
|
||||||
|
) # type: ignore
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
|
||||||
|
|
@ -68,9 +68,12 @@ class AccessAttemptExpiration(models.Model):
|
||||||
verbose_name = _("access attempt expiration")
|
verbose_name = _("access attempt expiration")
|
||||||
verbose_name_plural = _("access attempt expirations")
|
verbose_name_plural = _("access attempt expirations")
|
||||||
|
|
||||||
|
|
||||||
class AccessLog(AccessBase):
|
class AccessLog(AccessBase):
|
||||||
logout_time = models.DateTimeField(_("Logout Time"), null=True, blank=True)
|
logout_time = models.DateTimeField(_("Logout Time"), null=True, blank=True)
|
||||||
session_hash = models.CharField(_("Session key hash (sha256)"), default="", blank=True, max_length=64)
|
session_hash = models.CharField(
|
||||||
|
_("Session key hash (sha256)"), default="", blank=True, max_length=64
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"Access Log for {self.username} @ {self.attempt_time}"
|
return f"Access Log for {self.username} @ {self.attempt_time}"
|
||||||
|
|
|
||||||
|
|
@ -19,11 +19,13 @@ The following ``settings.py`` options are available for customizing Axes behavio
|
||||||
+======================================================+==============================================+===========================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================+
|
+======================================================+==============================================+===========================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================+
|
||||||
| AXES_ENABLED | True | Enable or disable Axes plugin functionality, for example in test runner setup |
|
| AXES_ENABLED | True | Enable or disable Axes plugin functionality, for example in test runner setup |
|
||||||
+------------------------------------------------------+----------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
+------------------------------------------------------+----------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
||||||
| AXES_FAILURE_LIMIT | 3 | The integer number of login attempts allowed before a record is created for the failed logins. This can also be a callable or a dotted path to callable that returns an integer and all of the following are valid: ``AXES_FAILURE_LIMIT = 42``, ``AXES_FAILURE_LIMIT = lambda *args: 42``, and ``AXES_FAILURE_LIMIT = 'project.app.get_login_failure_limit'``. |
|
| AXES_FAILURE_LIMIT | 3 | The integer number of login attempts allowed before the request is considered locked. This can also be a callable or a dotted path to callable that returns an integer and all of the following are valid: ``AXES_FAILURE_LIMIT = 42``, ``AXES_FAILURE_LIMIT = lambda *args: 42``, and ``AXES_FAILURE_LIMIT = 'project.app.get_login_failure_limit'``. |
|
||||||
+------------------------------------------------------+----------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
+------------------------------------------------------+----------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
||||||
| AXES_LOCK_OUT_AT_FAILURE | True | After the number of allowed login attempts are exceeded, should we lock out this IP (and optional user agent)? |
|
| AXES_LOCK_OUT_AT_FAILURE | True | After the number of allowed login attempts are exceeded, should we lock out this IP (and optional user agent)? |
|
||||||
+------------------------------------------------------+----------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
+------------------------------------------------------+----------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
||||||
| AXES_COOLOFF_TIME | None | If set, defines a period of inactivity after which old failed login attempts will be cleared. Can be set to a Python timedelta object, an integer, a float, a callable, or a string path to a callable which takes the request as argument. If an integer or float, will be interpreted as a number of hours: ``AXES_COOLOFF_TIME = 2`` 2 hours, ``AXES_COOLOFF_TIME = 2.0`` 2 hours, 120 minutes, ``AXES_COOLOFF_TIME = 1.7`` 1.7 hours, 102 minutes, 6120 seconds |
|
| AXES_COOLOFF_TIME | None | If set, defines the cool-off period after which old failed login attempts are cleared. If ``None``, lockout is permanent until attempts are manually reset. Can be set to a Python timedelta object, an integer, a float, a callable, or a string path to a callable that takes the request as argument. If an integer or float, this is interpreted as hours (``1`` is 1 hour, ``0.5`` is 30 minutes, ``1.7`` is 6120 seconds). ``timedelta`` is recommended for clarity. See also ``AXES_USE_ATTEMPT_EXPIRATION`` for rolling-window behavior. |
|
||||||
|
+------------------------------------------------------+----------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
||||||
|
| AXES_USE_ATTEMPT_EXPIRATION | False | If ``True``, changes ``AXES_COOLOFF_TIME`` to a rolling window where each failed attempt expires individually after the cool-off time. This enables policies like "3 failed login attempts per 15 minutes". If ``False``, ``AXES_COOLOFF_TIME`` acts as an inactivity period where attempts are cleared only after no new failures occur within the cool-off limit. |
|
||||||
+------------------------------------------------------+----------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
+------------------------------------------------------+----------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
||||||
| AXES_ONLY_ADMIN_SITE | False | If ``True``, lock is only enabled for admin site. Admin site is determined by checking request path against the path of ``"admin:index"`` view. If admin urls are not registered in current urlconf, all requests will not be locked. |
|
| AXES_ONLY_ADMIN_SITE | False | If ``True``, lock is only enabled for admin site. Admin site is determined by checking request path against the path of ``"admin:index"`` view. If admin urls are not registered in current urlconf, all requests will not be locked. |
|
||||||
+------------------------------------------------------+----------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
+------------------------------------------------------+----------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
||||||
|
|
@ -81,11 +83,28 @@ The following ``settings.py`` options are available for customizing Axes behavio
|
||||||
+------------------------------------------------------+----------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
+------------------------------------------------------+----------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
||||||
| AXES_HTTP_RESPONSE_CODE | 429 | Sets the http response code returned when ``AXES_FAILURE_LIMIT`` is reached. For example: ``AXES_HTTP_RESPONSE_CODE = 403`` |
|
| AXES_HTTP_RESPONSE_CODE | 429 | Sets the http response code returned when ``AXES_FAILURE_LIMIT`` is reached. For example: ``AXES_HTTP_RESPONSE_CODE = 403`` |
|
||||||
+------------------------------------------------------+----------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
+------------------------------------------------------+----------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
||||||
| AXES_RESET_COOL_OFF_ON_FAILURE_DURING_LOCKOUT | True | If ``True``, a failed login attempt during lockout will reset the cool off period. |
|
| AXES_RESET_COOL_OFF_ON_FAILURE_DURING_LOCKOUT | True | If ``True``, any failed login attempt during lockout resets the cool-off timer to ``now() + AXES_COOLOFF_TIME``. Repeated failed attempts keep extending the lockout period. |
|
||||||
+------------------------------------------------------+----------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
+------------------------------------------------------+----------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
||||||
| AXES_LOCKOUT_PARAMETERS | ["ip_address"] | A list of parameters that Axes uses to lock out users. It can also be callable, which takes an http request or AccesAttempt object and credentials and returns a list of parameters. Each parameter can be a string (a single parameter) or a list of strings (a combined parameter). For example, if you configure ``AXES_LOCKOUT_PARAMETERS = ["ip_address", ["username", "user_agent"]]``, axes will block clients by ip and/or username and user agent combination. See :ref:`customizing-lockout-parameters` for more details. |
|
| AXES_LOCKOUT_PARAMETERS | ["ip_address"] | A list of parameters that Axes uses to lock out users. It can also be callable, which takes an http request or AccesAttempt object and credentials and returns a list of parameters. Each parameter can be a string (a single parameter) or a list of strings (a combined parameter). For example, if you configure ``AXES_LOCKOUT_PARAMETERS = ["ip_address", ["username", "user_agent"]]``, axes will block clients by ip and/or username and user agent combination. See :ref:`customizing-lockout-parameters` for more details. |
|
||||||
+------------------------------------------------------+----------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
+------------------------------------------------------+----------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
||||||
|
|
||||||
|
**Common configurations**
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# Classic: 3 failures -> 30 min lockout
|
||||||
|
AXES_FAILURE_LIMIT = 3
|
||||||
|
AXES_COOLOFF_TIME = timedelta(minutes=30)
|
||||||
|
|
||||||
|
# Rolling window: max 5 failures in any 15-minute period
|
||||||
|
AXES_FAILURE_LIMIT = 5
|
||||||
|
AXES_COOLOFF_TIME = timedelta(minutes=15)
|
||||||
|
AXES_USE_ATTEMPT_EXPIRATION = True
|
||||||
|
|
||||||
|
# Hard lockout (manual reset only)
|
||||||
|
AXES_FAILURE_LIMIT = 5
|
||||||
|
AXES_COOLOFF_TIME = None
|
||||||
|
|
||||||
The configuration option precedences for the access attempt monitoring are:
|
The configuration option precedences for the access attempt monitoring are:
|
||||||
|
|
||||||
1. Default: only use IP address.
|
1. Default: only use IP address.
|
||||||
|
|
|
||||||
|
|
@ -188,7 +188,7 @@ Example ``AXES_LOCKOUT_PARAMETERS`` configuration:
|
||||||
|
|
||||||
AXES_LOCKOUT_PARAMETERS = ["ip_address", ["username", "user_agent"]]
|
AXES_LOCKOUT_PARAMETERS = ["ip_address", ["username", "user_agent"]]
|
||||||
|
|
||||||
This way, axes will lock out users using ip_address and/or combination of username and user agent
|
This way, axes will lock out users using ip_address or combination of username and user_agent
|
||||||
|
|
||||||
Example of callable ``AXES_LOCKOUT_PARAMETERS``:
|
Example of callable ``AXES_LOCKOUT_PARAMETERS``:
|
||||||
|
|
||||||
|
|
@ -213,7 +213,7 @@ Example of callable ``AXES_LOCKOUT_PARAMETERS``:
|
||||||
|
|
||||||
AXES_LOCKOUT_PARAMETERS = "example.utils.get_lockout_parameters"
|
AXES_LOCKOUT_PARAMETERS = "example.utils.get_lockout_parameters"
|
||||||
|
|
||||||
This way, if client ip_address is localhost, axes will lockout client only by username. In other case, axes will lockout client by username and/or ip_address.
|
This way, if client ip_address is localhost, axes will lockout client only by username. In other case, axes will lockout client by username or ip_address.
|
||||||
|
|
||||||
Customizing client ip address lookups
|
Customizing client ip address lookups
|
||||||
-------------------------------------
|
-------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ More information on the configuration options is available at:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# import sphinx_rtd_theme
|
# import sphinx_rtd_theme
|
||||||
from pkg_resources import get_distribution
|
from importlib.metadata import version as get_version
|
||||||
|
|
||||||
import django
|
import django
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
@ -43,7 +43,7 @@ copyright = "2016, Jazzband"
|
||||||
author = "Jazzband"
|
author = "Jazzband"
|
||||||
|
|
||||||
# The full version, including alpha/beta/rc tags.
|
# The full version, including alpha/beta/rc tags.
|
||||||
release = get_distribution("django-axes").version
|
release = get_version("django-axes")
|
||||||
|
|
||||||
# The short X.Y version.
|
# The short X.Y version.
|
||||||
version = ".".join(release.split(".")[:2])
|
version = ".".join(release.split(".")[:2])
|
||||||
|
|
|
||||||
2
mypy.ini
2
mypy.ini
|
|
@ -1,5 +1,5 @@
|
||||||
[mypy]
|
[mypy]
|
||||||
python_version = 3.12
|
python_version = 3.14
|
||||||
ignore_missing_imports = True
|
ignore_missing_imports = True
|
||||||
|
|
||||||
[mypy-axes.migrations.*]
|
[mypy-axes.migrations.*]
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,8 @@ envlist =
|
||||||
py{310,311,312}-dj42
|
py{310,311,312}-dj42
|
||||||
py{310,311,312,313}-dj52
|
py{310,311,312,313}-dj52
|
||||||
py{312,313,314}-dj60
|
py{312,313,314}-dj60
|
||||||
py312-djmain
|
py314-djmain
|
||||||
py312-djqa
|
py314-djqa
|
||||||
|
|
||||||
[gh-actions]
|
[gh-actions]
|
||||||
python =
|
python =
|
||||||
|
|
@ -36,9 +36,9 @@ DJANGO =
|
||||||
[testenv]
|
[testenv]
|
||||||
deps =
|
deps =
|
||||||
-r requirements.txt
|
-r requirements.txt
|
||||||
dj42: django>=4.2,<5
|
dj42: django>=4.2,<4.3
|
||||||
dj52: django>=5.2,<6
|
dj52: django>=5.2,<5.3
|
||||||
dj60: django>=6.0,<7
|
dj60: django>=6.0,<6.1
|
||||||
djmain: https://github.com/django/django/archive/main.tar.gz
|
djmain: https://github.com/django/django/archive/main.tar.gz
|
||||||
usedevelop = true
|
usedevelop = true
|
||||||
commands = pytest
|
commands = pytest
|
||||||
|
|
@ -51,10 +51,11 @@ ignore_errors =
|
||||||
djmain: True
|
djmain: True
|
||||||
|
|
||||||
# QA runs type checks, linting, and code formatting checks
|
# QA runs type checks, linting, and code formatting checks
|
||||||
[testenv:py312-djqa]
|
[testenv:py314-djqa]
|
||||||
|
stoponfail = false
|
||||||
deps = -r requirements.txt
|
deps = -r requirements.txt
|
||||||
commands =
|
commands =
|
||||||
mypy axes
|
mypy axes
|
||||||
prospector
|
prospector axes
|
||||||
black -t py312 --check --diff axes
|
black --check --diff axes
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
-e .
|
-e .
|
||||||
black==26.1.0
|
black==26.3.1
|
||||||
coverage==7.13.3
|
coverage==7.13.5
|
||||||
django-ipware>=3
|
django-ipware>=3
|
||||||
mypy==1.19.1
|
mypy==1.19.1
|
||||||
prospector==1.18.0
|
prospector==1.18.0
|
||||||
pytest-cov==7.0.0
|
pytest-cov==7.0.0
|
||||||
pytest-django==4.11.1
|
pytest-django==4.12.0
|
||||||
pytest-subtests==0.15.0
|
pytest-subtests==0.15.0
|
||||||
pytest==9.0.2
|
pytest==9.0.2
|
||||||
sphinx_rtd_theme==3.1.0
|
sphinx_rtd_theme==3.1.0
|
||||||
tox==4.34.1
|
tox==4.50.1
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue