diff --git a/axes/admin.py b/axes/admin.py index b5a9581..6ab6690 100644 --- a/axes/admin.py +++ b/axes/admin.py @@ -19,7 +19,9 @@ class IsLockedOutFilter(admin.SimpleListFilter): def queryset(self, request, queryset): if self.value() == "yes": - return queryset.filter(failures_since_start__gte=settings.AXES_FAILURE_LIMIT) + return queryset.filter( + failures_since_start__gte=settings.AXES_FAILURE_LIMIT + ) elif self.value() == "no": return queryset.filter(failures_since_start__lt=settings.AXES_FAILURE_LIMIT) return queryset @@ -34,9 +36,9 @@ class AccessAttemptAdmin(admin.ModelAdmin): "path_info", "failures_since_start", ] - + if settings.AXES_USE_ATTEMPT_EXPIRATION: - list_display.append('expiration') + list_display.append("expiration") list_filter = ["attempt_time", "path_info"] @@ -51,7 +53,10 @@ class AccessAttemptAdmin(admin.ModelAdmin): date_hierarchy = "attempt_time" 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")}), (_("Meta Data"), {"fields": ("user_agent", "ip_address", "http_accept")}), ) @@ -69,9 +74,9 @@ class AccessAttemptAdmin(admin.ModelAdmin): "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): count = self.handler.clean_expired_user_attempts(request=request) self.message_user(request, _(f"Cleaned up {count} expired access attempts.")) @@ -85,10 +90,15 @@ class AccessAttemptAdmin(admin.ModelAdmin): def expiration(self, obj: AccessAttempt): return obj.expiration.expires_at if hasattr(obj, "expiration") else _("Not set") - + def status(self, obj: AccessAttempt): - return f"{settings.AXES_FAILURE_LIMIT - obj.failures_since_start} "+_("Attempt Remaining") if \ - obj.failures_since_start < settings.AXES_FAILURE_LIMIT else _("Locked Out") + return ( + 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): list_display = ( diff --git a/axes/checks.py b/axes/checks.py index e6009b5..b16e879 100644 --- a/axes/checks.py +++ b/axes/checks.py @@ -235,4 +235,4 @@ def is_valid_callable(value) -> bool: except ImportError: return False - return True \ No newline at end of file + return True diff --git a/axes/conf.py b/axes/conf.py index 6c3a59f..e908401 100644 --- a/axes/conf.py +++ b/axes/conf.py @@ -11,6 +11,7 @@ class JSONSerializableLazyObject(SimpleLazyObject): Fixes jazzband/django-axes#1391 """ + def __json__(self): return str(self) @@ -55,6 +56,7 @@ settings.AXES_ONLY_ADMIN_SITE = getattr(settings, "AXES_ONLY_ADMIN_SITE", False) # show Axes logs in admin settings.AXES_ENABLE_ADMIN = getattr(settings, "AXES_ENABLE_ADMIN", True) + # use a specific username field to retrieve from login POST data def _get_username_field_default(): return get_user_model().USERNAME_FIELD @@ -106,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_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) diff --git a/axes/handlers/database.py b/axes/handlers/database.py index c344300..d9ce3f1 100644 --- a/axes/handlers/database.py +++ b/axes/handlers/database.py @@ -21,7 +21,12 @@ from axes.helpers import ( get_query_str, 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 log = getLogger(__name__) @@ -223,15 +228,17 @@ class AxesDatabaseHandler(AbstractAxesHandler, AxesBaseHandler): if settings.AXES_USE_ATTEMPT_EXPIRATION: if not hasattr(attempt, "expiration") or attempt.expiration is None: log.debug( - "AXES: Creating new AccessAttemptExpiration for %s", client_str + "AXES: Creating new AccessAttemptExpiration for %s", + client_str, ) attempt.expiration = AccessAttemptExpiration.objects.create( access_attempt=attempt, - expires_at=get_attempt_expiration(request) + expires_at=get_attempt_expiration(request), ) else: attempt.expiration.expires_at = max( - get_attempt_expiration(request), attempt.expiration.expires_at + get_attempt_expiration(request), + attempt.expiration.expires_at, ) attempt.expiration.save() @@ -400,7 +407,9 @@ class AxesDatabaseHandler(AbstractAxesHandler, AxesBaseHandler): if settings.AXES_USE_ATTEMPT_EXPIRATION: 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( "AXES: Cleaned up %s expired access attempts from database that expiry were older than %s", count, @@ -408,7 +417,9 @@ class AxesDatabaseHandler(AbstractAxesHandler, AxesBaseHandler): ) else: 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( "AXES: Cleaned up %s expired access attempts from database that were older than %s", count, diff --git a/axes/helpers.py b/axes/helpers.py index d526566..a7ccf60 100644 --- a/axes/helpers.py +++ b/axes/helpers.py @@ -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}" + def get_attempt_expiration(request: Optional[HttpRequest] = None) -> datetime: """ Get threshold for fetching access attempts from the database. @@ -116,6 +117,7 @@ def get_attempt_expiration(request: Optional[HttpRequest] = None) -> datetime: return datetime.now() + cool_off return attempt_time + cool_off + def get_credentials(username: Optional[str] = None, **kwargs) -> dict: """ Calculate credentials for Axes to use internally from given username and kwargs. diff --git a/axes/middleware.py b/axes/middleware.py index 189ee78..dd3824a 100644 --- a/axes/middleware.py +++ b/axes/middleware.py @@ -60,6 +60,8 @@ class AxesMiddleware: credentials = getattr(request, "axes_credentials", None) response = await sync_to_async( get_lockout_response, thread_sensitive=True - )(request, credentials) # type: ignore + )( + request, credentials + ) # type: ignore return response diff --git a/axes/models.py b/axes/models.py index 5658cab..fe2251b 100644 --- a/axes/models.py +++ b/axes/models.py @@ -68,9 +68,12 @@ class AccessAttemptExpiration(models.Model): verbose_name = _("access attempt expiration") verbose_name_plural = _("access attempt expirations") + class AccessLog(AccessBase): 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): return f"Access Log for {self.username} @ {self.attempt_time}"