mirror of
https://github.com/jazzband/django-axes.git
synced 2026-03-16 22:30:23 +00:00
Added individual attempt expiry feature
This commit is contained in:
parent
864dfc2d9a
commit
5b2a483bfa
6 changed files with 89 additions and 17 deletions
|
|
@ -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")}),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
18
axes/migrations/0010_accessattempt_expires_at.py
Normal file
18
axes/migrations/0010_accessattempt_expires_at.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
|
|
@ -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}"
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue