Added individual attempt expiry feature

This commit is contained in:
kuldeepkhatke 2025-05-28 00:51:50 +05:30 committed by Aleksi Häkli
parent 864dfc2d9a
commit 0fd9ccd1d4
6 changed files with 89 additions and 17 deletions

View file

@ -7,14 +7,26 @@ from axes.models import AccessAttempt, AccessLog, AccessFailureLog
class AccessAttemptAdmin(admin.ModelAdmin):
list_display = (
"attempt_time",
"ip_address",
"user_agent",
"username",
"path_info",
"failures_since_start",
)
if settings.AXES_INDIVIDUAL_ATTEMPT_EXPIRY:
list_display = (
"attempt_time",
"expires_at",
"ip_address",
"user_agent",
"username",
"path_info",
"failures_since_start",
)
else:
list_display = (
"attempt_time",
"ip_address",
"user_agent",
"username",
"path_info",
"failures_since_start",
)
list_filter = ["attempt_time", "path_info"]
@ -23,7 +35,7 @@ class AccessAttemptAdmin(admin.ModelAdmin):
date_hierarchy = "attempt_time"
fieldsets = (
(None, {"fields": ("username", "path_info", "failures_since_start")}),
(None, {"fields": ("username", "path_info", "failures_since_start", "expires_at")}),
(_("Form Data"), {"fields": ("get_data", "post_data")}),
(_("Meta Data"), {"fields": ("user_agent", "ip_address", "http_accept")}),
)

View file

@ -24,3 +24,19 @@ def get_cool_off_threshold(request: Optional[HttpRequest] = None) -> datetime:
if attempt_time is None:
return now() - cool_off
return attempt_time - cool_off
def get_individual_attempt_expiry(request: Optional[HttpRequest] = None) -> datetime:
"""
Get threshold for fetching access attempts from the database.
"""
cool_off = get_cool_off(request)
if cool_off is None:
raise TypeError(
"Cool off threshold can not be calculated with settings.AXES_COOLOFF_TIME set to None"
)
attempt_time = request.axes_attempt_time
if attempt_time is None:
return now() + cool_off
return attempt_time + cool_off

View file

@ -87,6 +87,8 @@ settings.AXES_LOCKOUT_URL = getattr(settings, "AXES_LOCKOUT_URL", None)
settings.AXES_COOLOFF_TIME = getattr(settings, "AXES_COOLOFF_TIME", None)
settings.AXES_INDIVIDUAL_ATTEMPT_EXPIRY = getattr(settings, "AXES_INDIVIDUAL_ATTEMPT_EXPIRY", False)
settings.AXES_VERBOSE = getattr(settings, "AXES_VERBOSE", settings.AXES_ENABLED)
# whitelist and blacklist

View file

@ -7,7 +7,7 @@ from django.db.models.functions import Concat
from django.http import HttpRequest
from django.utils import timezone
from axes.attempts import get_cool_off_threshold
from axes.attempts import get_cool_off_threshold, get_individual_attempt_expiry
from axes.conf import settings
from axes.handlers.base import AbstractAxesHandler, AxesBaseHandler
from axes.helpers import (
@ -185,6 +185,12 @@ class AxesDatabaseHandler(AbstractAxesHandler, AxesBaseHandler):
"path_info": request.axes_path_info,
"failures_since_start": 1,
"attempt_time": request.axes_attempt_time,
# Set the expiry time for the attempt based on the cool off period.
"expires_at": (
get_individual_attempt_expiry(request)
if settings.AXES_INDIVIDUAL_ATTEMPT_EXPIRY
else None
),
},
)
@ -212,6 +218,8 @@ class AxesDatabaseHandler(AbstractAxesHandler, AxesBaseHandler):
attempt.path_info = request.axes_path_info
attempt.failures_since_start = F("failures_since_start") + 1
attempt.attempt_time = request.axes_attempt_time
if settings.AXES_INDIVIDUAL_ATTEMPT_EXPIRY:
attempt.expires_at = max(get_individual_attempt_expiry(request), attempt.expires_at)
attempt.save()
log.warning(
@ -382,13 +390,22 @@ class AxesDatabaseHandler(AbstractAxesHandler, AxesBaseHandler):
)
return 0
threshold = get_cool_off_threshold(request)
count, _ = AccessAttempt.objects.filter(attempt_time__lt=threshold).delete()
log.info(
"AXES: Cleaned up %s expired access attempts from database that were older than %s",
count,
threshold,
)
if settings.AXES_INDIVIDUAL_ATTEMPT_EXPIRY:
threshold = timezone.now()
count, _ = AccessAttempt.objects.filter(expires_at__lt=threshold).delete()
log.info(
"AXES: Cleaned up %s expired access attempts from database that expiry were older than %s",
count,
threshold,
)
else:
threshold = get_cool_off_threshold(request)
count, _ = AccessAttempt.objects.filter(attempt_time__lt=threshold).delete()
log.info(
"AXES: Cleaned up %s expired access attempts from database that were older than %s",
count,
threshold,
)
return count
def reset_user_attempts(

View file

@ -0,0 +1,18 @@
# Generated by Django 4.2.21 on 2025-05-27 18:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('axes', '0009_add_session_hash'),
]
operations = [
migrations.AddField(
model_name='accessattempt',
name='expires_at',
field=models.DateTimeField(blank=True, help_text='The time when this access attempt expires and is no longer valid.', null=True, verbose_name='Expires At'),
),
]

View file

@ -42,6 +42,13 @@ class AccessAttempt(AccessBase):
failures_since_start = models.PositiveIntegerField(_("Failed Logins"))
expires_at = models.DateTimeField(
_("Expires At"),
null=True,
blank=True,
help_text=_("The time when this access attempt expires and is no longer valid."),
)
def __str__(self):
return f"Attempted Access: {self.attempt_time}"