From d5e4268f5c0a46cd36a5c1918b797ea44625d73d Mon Sep 17 00:00:00 2001 From: kuldeepkhatke Date: Sat, 7 Jun 2025 18:43:14 +0530 Subject: [PATCH] Shifted epired_at filed to new model --- axes/admin.py | 10 ++-- axes/handlers/database.py | 31 ++++++++---- .../0010_accessattempt_expires_at.py | 18 ------- .../0010_accessattemptexpiration.py | 50 +++++++++++++++++++ axes/models.py | 25 +++++++--- 5 files changed, 95 insertions(+), 39 deletions(-) delete mode 100644 axes/migrations/0010_accessattempt_expires_at.py create mode 100644 axes/migrations/0010_accessattemptexpiration.py diff --git a/axes/admin.py b/axes/admin.py index 7c3cfab..d21b076 100644 --- a/axes/admin.py +++ b/axes/admin.py @@ -7,8 +7,8 @@ from axes.models import AccessAttempt, AccessLog, AccessFailureLog class AccessAttemptAdmin(admin.ModelAdmin): - if settings.AXES_INDIVIDUAL_ATTEMPT_EXPIRY: - list_display = ( + if settings.AXES_USE_ATTEMPT_EXPIRATION: + list_display = ( "attempt_time", "expires_at", "ip_address", @@ -27,7 +27,6 @@ class AccessAttemptAdmin(admin.ModelAdmin): "failures_since_start", ) - list_filter = ["attempt_time", "path_info"] search_fields = ["ip_address", "username", "user_agent", "path_info"] @@ -50,11 +49,16 @@ class AccessAttemptAdmin(admin.ModelAdmin): "get_data", "post_data", "failures_since_start", + "expires_at", ] def has_add_permission(self, request: HttpRequest) -> bool: return False + def expires_at(self, obj: AccessAttempt): + if hasattr(obj, "expiration") and obj.expiration.expires_at: + return obj.expiration.expires_at #.strftime("%Y-%m-%d %H:%M:%S") + return _("Not set") class AccessLogAdmin(admin.ModelAdmin): list_display = ( diff --git a/axes/handlers/database.py b/axes/handlers/database.py index 13d1e04..2f26ae0 100644 --- a/axes/handlers/database.py +++ b/axes/handlers/database.py @@ -20,7 +20,7 @@ from axes.helpers import ( get_lockout_parameters, get_query_str, ) -from axes.models import AccessAttempt, AccessFailureLog, AccessLog +from axes.models import AccessAttempt, AccessAttemptExpiration, AccessFailureLog, AccessLog from axes.signals import user_locked_out log = getLogger(__name__) @@ -184,13 +184,7 @@ class AxesDatabaseHandler(AbstractAxesHandler, AxesBaseHandler): "http_accept": request.axes_http_accept, "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_USE_ATTEMPT_EXPIRATION - else None - ), + "attempt_time": request.axes_attempt_time }, ) @@ -218,8 +212,6 @@ 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_USE_ATTEMPT_EXPIRATION: - attempt.expires_at = max(get_individual_attempt_expiry(request), attempt.expires_at) attempt.save() log.warning( @@ -227,6 +219,23 @@ class AxesDatabaseHandler(AbstractAxesHandler, AxesBaseHandler): client_str, ) + # Set the expiration time for the attempt based on the cool off period. + 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 + ) + # Create a new AccessAttemptExpiration if it does not exist + attempt.expiration = AccessAttemptExpiration( + access_attempt=attempt + ) + attempt.expiration.expires_at = get_individual_attempt_expiry(request) + else: + attempt.expiration.expires_at = max( + get_individual_attempt_expiry(request), attempt.expiration.expires_at + ) + attempt.expiration.save() + # 3. or 4. database query: Calculate the current maximum failure number from the existing attempts failures_since_start = self.get_failures(request, credentials) request.axes_failures_since_start = failures_since_start @@ -392,7 +401,7 @@ class AxesDatabaseHandler(AbstractAxesHandler, AxesBaseHandler): if settings.AXES_USE_ATTEMPT_EXPIRATION: threshold = timezone.now() - count, _ = AccessAttempt.objects.filter(expires_at__lt=threshold).delete() + count, _ = AccessAttempt.objects.filter(expiration__expires_at__lt=threshold).delete() log.info( "AXES: Cleaned up %s expired access attempts from database that expiry were older than %s", count, diff --git a/axes/migrations/0010_accessattempt_expires_at.py b/axes/migrations/0010_accessattempt_expires_at.py deleted file mode 100644 index 86af0a6..0000000 --- a/axes/migrations/0010_accessattempt_expires_at.py +++ /dev/null @@ -1,18 +0,0 @@ -# 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'), - ), - ] diff --git a/axes/migrations/0010_accessattemptexpiration.py b/axes/migrations/0010_accessattemptexpiration.py new file mode 100644 index 0000000..509a154 --- /dev/null +++ b/axes/migrations/0010_accessattemptexpiration.py @@ -0,0 +1,50 @@ +# Generated by Django 5.2.1 on 2025-06-07 17:37 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("axes", "0009_add_session_hash"), + ] + + operations = [ + migrations.CreateModel( + name="AccessAttemptExpiration", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "expires_at", + models.DateTimeField( + blank=True, + help_text="The time when access attempt expires and is no longer valid.", + null=True, + verbose_name="Expires At", + ), + ), + ( + "access_attempt", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="expiration", + to="axes.accessattempt", + verbose_name="Access Attempt", + ), + ), + ], + options={ + "verbose_name": "access attempt expiration", + "verbose_name_plural": "access attempt expirations", + }, + ), + ] diff --git a/axes/models.py b/axes/models.py index b1a7458..26f4857 100644 --- a/axes/models.py +++ b/axes/models.py @@ -42,13 +42,6 @@ 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}" @@ -58,6 +51,24 @@ class AccessAttempt(AccessBase): unique_together = [["username", "ip_address", "user_agent"]] +class AccessAttemptExpiration(models.Model): + access_attempt = models.OneToOneField( + AccessAttempt, + on_delete=models.CASCADE, + related_name="expiration", + verbose_name=_("Access Attempt"), + ) + expires_at = models.DateTimeField( + _("Expires At"), + null=True, + blank=True, + help_text=_("The time when access attempt expires and is no longer valid."), + ) + + class Meta: + 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)