Compare commits

..

23 commits

Author SHA1 Message Date
Rodrigo Nogueira
b14b78a16e docs: narrow cool-off docs changes and keep table format
Some checks failed
Test / build (Python 3.10, Django 4.2) (push) Has been cancelled
Test / build (Python 3.11, Django 4.2) (push) Has been cancelled
Test / build (Python 3.12, Django 4.2) (push) Has been cancelled
Test / build (Python 3.14, Django 4.2) (push) Has been cancelled
Test / build (Python 3.10, Django 5.2) (push) Has been cancelled
Test / build (Python 3.11, Django 5.2) (push) Has been cancelled
Test / build (Python 3.12, Django 5.2) (push) Has been cancelled
Test / build (Python 3.13, Django 5.2) (push) Has been cancelled
Test / build (Python 3.14, Django 5.2) (push) Has been cancelled
Test / build (Python 3.12, Django 6.0) (push) Has been cancelled
Test / build (Python 3.13, Django 6.0) (push) Has been cancelled
Test / build (Python 3.14, Django 6.0) (push) Has been cancelled
Test / build (Python 3.14, Django main) (push) Has been cancelled
Test / build (Python 3.14, Django qa) (push) Has been cancelled
2026-03-20 12:24:11 +02:00
rodrigo.nogueira
e4cdd72231 docs: Fix typo in AXES_LOCKOUT_PARAMETERS description and add a blank line. 2026-03-20 12:24:11 +02:00
rodrigo.nogueira
3fc256c8d2 docs: Update lockout configuration table title and column widths. 2026-03-20 12:24:11 +02:00
rodrigo.nogueira
1aa8509cdc docs: clarify AXES_COOLOFF_TIME and AXES_USE_ATTEMPT_EXPIRATION descriptions and add common configuration examples. 2026-03-20 12:24:11 +02:00
rodrigo.nogueira
46e206af49 docs: Revamp and expand the configuration options documentation. 2026-03-20 12:24:11 +02:00
dependabot[bot]
0d7f4bdb43 chore(deps): bump coverage from 7.13.4 to 7.13.5
Bumps [coverage](https://github.com/coveragepy/coveragepy) from 7.13.4 to 7.13.5.
- [Release notes](https://github.com/coveragepy/coveragepy/releases)
- [Changelog](https://github.com/coveragepy/coveragepy/blob/main/CHANGES.rst)
- [Commits](https://github.com/coveragepy/coveragepy/compare/7.13.4...7.13.5)

---
updated-dependencies:
- dependency-name: coverage
  dependency-version: 7.13.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-20 10:08:26 +02:00
dependabot[bot]
cc0387ae60 chore(deps): bump tox from 4.49.1 to 4.50.1
Bumps [tox](https://github.com/tox-dev/tox) from 4.49.1 to 4.50.1.
- [Release notes](https://github.com/tox-dev/tox/releases)
- [Changelog](https://github.com/tox-dev/tox/blob/main/docs/changelog.rst)
- [Commits](https://github.com/tox-dev/tox/compare/4.49.1...4.50.1)

---
updated-dependencies:
- dependency-name: tox
  dependency-version: 4.50.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-20 10:08:24 +02:00
Enrico Tröger
fdd7b22cd3 Clarify and/or conditions in AXES_LOCKOUT_PARAMETERS examples
Some checks failed
Test / build (Python 3.10, Django 4.2) (push) Has been cancelled
Test / build (Python 3.11, Django 4.2) (push) Has been cancelled
Test / build (Python 3.12, Django 4.2) (push) Has been cancelled
Test / build (Python 3.14, Django 4.2) (push) Has been cancelled
Test / build (Python 3.10, Django 5.2) (push) Has been cancelled
Test / build (Python 3.11, Django 5.2) (push) Has been cancelled
Test / build (Python 3.12, Django 5.2) (push) Has been cancelled
Test / build (Python 3.13, Django 5.2) (push) Has been cancelled
Test / build (Python 3.14, Django 5.2) (push) Has been cancelled
Test / build (Python 3.12, Django 6.0) (push) Has been cancelled
Test / build (Python 3.13, Django 6.0) (push) Has been cancelled
Test / build (Python 3.14, Django 6.0) (push) Has been cancelled
Test / build (Python 3.14, Django main) (push) Has been cancelled
Test / build (Python 3.14, Django qa) (push) Has been cancelled
2026-03-16 18:55:20 +02:00
dependabot[bot]
a5d14cd630 chore(deps): bump black from 26.1.0 to 26.3.1
Bumps [black](https://github.com/psf/black) from 26.1.0 to 26.3.1.
- [Release notes](https://github.com/psf/black/releases)
- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
- [Commits](https://github.com/psf/black/compare/26.1.0...26.3.1)

---
updated-dependencies:
- dependency-name: black
  dependency-version: 26.3.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-16 18:54:27 +02:00
dependabot[bot]
2a31c0133f chore(deps): bump tox from 4.34.1 to 4.49.1
Bumps [tox](https://github.com/tox-dev/tox) from 4.34.1 to 4.49.1.
- [Release notes](https://github.com/tox-dev/tox/releases)
- [Changelog](https://github.com/tox-dev/tox/blob/main/docs/changelog.rst)
- [Commits](https://github.com/tox-dev/tox/compare/4.34.1...4.49.1)

---
updated-dependencies:
- dependency-name: tox
  dependency-version: 4.49.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-16 18:54:17 +02:00
dependabot[bot]
4624eed684 chore(deps): bump pytest-django from 4.11.1 to 4.12.0
Bumps [pytest-django](https://github.com/pytest-dev/pytest-django) from 4.11.1 to 4.12.0.
- [Release notes](https://github.com/pytest-dev/pytest-django/releases)
- [Changelog](https://github.com/pytest-dev/pytest-django/blob/main/docs/changelog.rst)
- [Commits](https://github.com/pytest-dev/pytest-django/compare/v4.11.1...v4.12.0)

---
updated-dependencies:
- dependency-name: pytest-django
  dependency-version: 4.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-16 18:54:13 +02:00
Aleksi Häkli
e27ce891ea
Version 8.3.1
Some checks failed
Test / build (Python 3.10, Django 4.2) (push) Has been cancelled
Test / build (Python 3.11, Django 4.2) (push) Has been cancelled
Test / build (Python 3.12, Django 4.2) (push) Has been cancelled
Test / build (Python 3.14, Django 4.2) (push) Has been cancelled
Test / build (Python 3.10, Django 5.2) (push) Has been cancelled
Test / build (Python 3.11, Django 5.2) (push) Has been cancelled
Test / build (Python 3.12, Django 5.2) (push) Has been cancelled
Test / build (Python 3.13, Django 5.2) (push) Has been cancelled
Test / build (Python 3.14, Django 5.2) (push) Has been cancelled
Test / build (Python 3.12, Django 6.0) (push) Has been cancelled
Test / build (Python 3.13, Django 6.0) (push) Has been cancelled
Test / build (Python 3.14, Django 6.0) (push) Has been cancelled
Test / build (Python 3.14, Django main) (push) Has been cancelled
Test / build (Python 3.14, Django qa) (push) Has been cancelled
2026-02-11 22:15:47 +02:00
dependabot[bot]
c3dcd1ba51 chore(deps): bump coverage from 7.13.3 to 7.13.4
Bumps [coverage](https://github.com/coveragepy/coveragepy) from 7.13.3 to 7.13.4.
- [Release notes](https://github.com/coveragepy/coveragepy/releases)
- [Changelog](https://github.com/coveragepy/coveragepy/blob/main/CHANGES.rst)
- [Commits](https://github.com/coveragepy/coveragepy/compare/7.13.3...7.13.4)

---
updated-dependencies:
- dependency-name: coverage
  dependency-version: 7.13.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-11 22:14:42 +02:00
Aleksi Häkli
41ebdc3063 Try to run all tox QA commands even if some fail 2026-02-11 22:14:31 +02:00
Aleksi Häkli
31c69dbea5 Simplify black formatting rules 2026-02-11 22:14:31 +02:00
Aleksi Häkli
bdd0c9546a Fix prospector errors 2026-02-11 22:14:31 +02:00
Aleksi Häkli
4b77eb69ee Run black autoformatting 2026-02-11 22:14:31 +02:00
Aleksi Häkli
5acae054b4 Update Black formatting rules 2026-02-11 22:14:31 +02:00
Aleksi Häkli
d59a289407 Suppress mypy type errors
Update Mypy Python version to 3.14
2026-02-11 22:14:31 +02:00
Aleksi Häkli
23ee2fca44 Update version support matrix to run tox QA tests properly on GitHub 2026-02-11 22:14:31 +02:00
Aleksi Häkli
4ea615811b Implement custom lazy object to avoid JSON errors with Celery
Fixes jazzband/django-axes#1391
2026-02-11 22:14:31 +02:00
Aleksi Häkli
b4fb3088b4
Version 8.3.0
Some checks failed
Test / build (Python 3.10, Django 4.2) (push) Has been cancelled
Test / build (Python 3.11, Django 4.2) (push) Has been cancelled
Test / build (Python 3.12, Django 4.2) (push) Has been cancelled
Test / build (Python 3.14, Django 4.2) (push) Has been cancelled
Test / build (Python 3.10, Django 5.2) (push) Has been cancelled
Test / build (Python 3.11, Django 5.2) (push) Has been cancelled
Test / build (Python 3.12, Django 5.2) (push) Has been cancelled
Test / build (Python 3.13, Django 5.2) (push) Has been cancelled
Test / build (Python 3.14, Django 5.2) (push) Has been cancelled
Test / build (Python 3.12, Django 6.0) (push) Has been cancelled
Test / build (Python 3.13, Django 6.0) (push) Has been cancelled
Test / build (Python 3.14, Django 6.0) (push) Has been cancelled
Test / build (Python 3.14, Django main) (push) Has been cancelled
Test / build (Python 3.14, Django qa) (push) Has been cancelled
2026-02-09 10:37:38 +02:00
Hugo van Kemenade
6c8feada83 Replace removed pkg_resources with stdlib 2026-02-09 10:35:54 +02:00
15 changed files with 126 additions and 48 deletions

View file

@ -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)
------------------ ------------------

View file

@ -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
@ -34,9 +36,9 @@ class AccessAttemptAdmin(admin.ModelAdmin):
"path_info", "path_info",
"failures_since_start", "failures_since_start",
] ]
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."))
@ -85,10 +90,15 @@ class AccessAttemptAdmin(admin.ModelAdmin):
def expiration(self, obj: AccessAttempt): def expiration(self, obj: AccessAttempt):
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 = (

View file

@ -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

View file

@ -235,4 +235,4 @@ def is_valid_callable(value) -> bool:
except ImportError: except ImportError:
return False return False
return True return True

View file

@ -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)

View file

@ -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,

View file

@ -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"

View file

@ -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

View file

@ -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}"

View file

@ -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.

View file

@ -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
------------------------------------- -------------------------------------

View file

@ -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])

View file

@ -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.*]

View file

@ -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
""" """

View file

@ -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