2019-02-13 12:05:24 +00:00
|
|
|
from logging import getLogger
|
2025-05-03 13:40:04 +00:00
|
|
|
from typing import List, Optional
|
2019-02-07 18:20:49 +00:00
|
|
|
|
2023-08-31 17:42:44 +00:00
|
|
|
from django.db import router, transaction
|
2025-05-03 13:40:04 +00:00
|
|
|
from django.db.models import F, Q, QuerySet, Sum, Value
|
2019-02-20 20:18:27 +00:00
|
|
|
from django.db.models.functions import Concat
|
2025-04-24 03:15:14 +00:00
|
|
|
from django.http import HttpRequest
|
2019-07-11 12:56:28 +00:00
|
|
|
from django.utils import timezone
|
2019-02-07 18:20:49 +00:00
|
|
|
|
2025-06-08 09:01:08 +00:00
|
|
|
from axes.attempts import get_cool_off_threshold
|
2019-02-10 20:01:08 +00:00
|
|
|
from axes.conf import settings
|
2025-05-03 13:40:04 +00:00
|
|
|
from axes.handlers.base import AbstractAxesHandler, AxesBaseHandler
|
2019-02-25 20:54:40 +00:00
|
|
|
from axes.helpers import (
|
2025-05-03 13:40:04 +00:00
|
|
|
get_client_parameters,
|
2024-04-30 14:22:50 +00:00
|
|
|
get_client_session_hash,
|
2019-02-16 17:05:59 +00:00
|
|
|
get_client_str,
|
|
|
|
|
get_client_username,
|
|
|
|
|
get_credentials,
|
2019-06-12 20:43:44 +00:00
|
|
|
get_failure_limit,
|
2023-05-04 09:42:42 +00:00
|
|
|
get_lockout_parameters,
|
2019-02-16 17:05:59 +00:00
|
|
|
get_query_str,
|
2025-06-08 09:01:08 +00:00
|
|
|
get_attempt_expiration,
|
2019-02-16 17:05:59 +00:00
|
|
|
)
|
2026-02-11 19:54:13 +00:00
|
|
|
from axes.models import (
|
|
|
|
|
AccessAttempt,
|
|
|
|
|
AccessAttemptExpiration,
|
|
|
|
|
AccessFailureLog,
|
|
|
|
|
AccessLog,
|
|
|
|
|
)
|
2021-01-07 12:05:48 +00:00
|
|
|
from axes.signals import user_locked_out
|
2019-02-07 18:20:49 +00:00
|
|
|
|
2020-09-26 14:50:13 +00:00
|
|
|
log = getLogger(__name__)
|
2019-02-07 18:20:49 +00:00
|
|
|
|
|
|
|
|
|
2020-07-26 23:01:33 +00:00
|
|
|
class AxesDatabaseHandler(AbstractAxesHandler, AxesBaseHandler):
|
2019-02-07 18:20:49 +00:00
|
|
|
"""
|
|
|
|
|
Signal handler implementation that records user login attempts to database and locks users out if necessary.
|
2019-12-01 12:23:16 +00:00
|
|
|
|
|
|
|
|
.. note:: The get_user_attempts function is called several time during the authentication and lockout
|
|
|
|
|
process, caching its output can be dangerous.
|
2019-02-07 18:20:49 +00:00
|
|
|
"""
|
|
|
|
|
|
2020-07-07 08:46:22 +00:00
|
|
|
def reset_attempts(
|
|
|
|
|
self,
|
|
|
|
|
*,
|
2022-04-11 12:05:09 +00:00
|
|
|
ip_address: Optional[str] = None,
|
|
|
|
|
username: Optional[str] = None,
|
2020-07-07 08:46:22 +00:00
|
|
|
ip_or_username: bool = False,
|
|
|
|
|
) -> int:
|
2019-07-11 12:56:28 +00:00
|
|
|
attempts = AccessAttempt.objects.all()
|
|
|
|
|
|
2020-07-07 08:46:22 +00:00
|
|
|
if ip_or_username:
|
|
|
|
|
attempts = attempts.filter(Q(ip_address=ip_address) | Q(username=username))
|
|
|
|
|
else:
|
|
|
|
|
if ip_address:
|
|
|
|
|
attempts = attempts.filter(ip_address=ip_address)
|
|
|
|
|
if username:
|
|
|
|
|
attempts = attempts.filter(username=username)
|
2019-07-11 12:56:28 +00:00
|
|
|
|
|
|
|
|
count, _ = attempts.delete()
|
2019-09-28 16:27:50 +00:00
|
|
|
log.info("AXES: Reset %d access attempts from database.", count)
|
2019-07-11 12:56:28 +00:00
|
|
|
|
|
|
|
|
return count
|
|
|
|
|
|
2022-04-11 12:05:09 +00:00
|
|
|
def reset_logs(self, *, age_days: Optional[int] = None) -> int:
|
2019-07-11 12:56:28 +00:00
|
|
|
if age_days is None:
|
|
|
|
|
count, _ = AccessLog.objects.all().delete()
|
2019-09-28 16:27:50 +00:00
|
|
|
log.info("AXES: Reset all %d access logs from database.", count)
|
2019-07-11 12:56:28 +00:00
|
|
|
else:
|
|
|
|
|
limit = timezone.now() - timezone.timedelta(days=age_days)
|
|
|
|
|
count, _ = AccessLog.objects.filter(attempt_time__lte=limit).delete()
|
2019-09-28 16:27:50 +00:00
|
|
|
log.info(
|
|
|
|
|
"AXES: Reset %d access logs older than %d days from database.",
|
|
|
|
|
count,
|
|
|
|
|
age_days,
|
|
|
|
|
)
|
2019-07-11 12:56:28 +00:00
|
|
|
|
|
|
|
|
return count
|
|
|
|
|
|
2022-04-11 12:05:09 +00:00
|
|
|
def reset_failure_logs(self, *, age_days: Optional[int] = None) -> int:
|
2022-03-15 09:42:24 +00:00
|
|
|
if age_days is None:
|
|
|
|
|
count, _ = AccessFailureLog.objects.all().delete()
|
|
|
|
|
log.info("AXES: Reset all %d access failure logs from database.", count)
|
|
|
|
|
else:
|
|
|
|
|
limit = timezone.now() - timezone.timedelta(days=age_days)
|
|
|
|
|
count, _ = AccessFailureLog.objects.filter(attempt_time__lte=limit).delete()
|
|
|
|
|
log.info(
|
|
|
|
|
"AXES: Reset %d access failure logs older than %d days from database.",
|
|
|
|
|
count,
|
|
|
|
|
age_days,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return count
|
|
|
|
|
|
|
|
|
|
def remove_out_of_limit_failure_logs(
|
2022-05-16 07:31:46 +00:00
|
|
|
self,
|
|
|
|
|
*,
|
|
|
|
|
username: str,
|
|
|
|
|
limit: Optional[int] = settings.AXES_ACCESS_FAILURE_LOG_PER_USER_LIMIT,
|
|
|
|
|
) -> int:
|
2022-03-15 09:42:24 +00:00
|
|
|
count = 0
|
|
|
|
|
failures = AccessFailureLog.objects.filter(username=username)
|
|
|
|
|
out_of_limit_failures_logs = failures.count() - limit
|
|
|
|
|
if out_of_limit_failures_logs > 0:
|
|
|
|
|
for failure in failures[:out_of_limit_failures_logs]:
|
|
|
|
|
failure.delete()
|
|
|
|
|
count += 1
|
|
|
|
|
return count
|
|
|
|
|
|
2022-04-11 12:05:09 +00:00
|
|
|
def get_failures(self, request, credentials: Optional[dict] = None) -> int:
|
2025-05-03 13:40:04 +00:00
|
|
|
attempts_list = self.get_user_attempts(request, credentials)
|
2020-07-07 08:46:22 +00:00
|
|
|
attempt_count = max(
|
|
|
|
|
(
|
|
|
|
|
attempts.aggregate(Sum("failures_since_start"))[
|
|
|
|
|
"failures_since_start__sum"
|
|
|
|
|
]
|
|
|
|
|
or 0
|
|
|
|
|
)
|
|
|
|
|
for attempts in attempts_list
|
2019-09-28 16:27:50 +00:00
|
|
|
)
|
2020-07-07 08:46:22 +00:00
|
|
|
return attempt_count
|
2019-02-17 21:56:48 +00:00
|
|
|
|
2021-11-30 12:48:53 +00:00
|
|
|
def user_login_failed(self, sender, credentials: dict, request=None, **kwargs):
|
2025-04-23 13:38:41 +00:00
|
|
|
"""
|
|
|
|
|
When user login fails, save AccessFailureLog record in database,
|
2022-03-15 09:42:24 +00:00
|
|
|
save AccessAttempt record in database, mark request with
|
|
|
|
|
lockout attribute and emit lockout signal.
|
2019-02-07 18:20:49 +00:00
|
|
|
"""
|
|
|
|
|
|
2021-06-29 14:05:43 +00:00
|
|
|
log.info("AXES: User login failed, running database handler for failure.")
|
|
|
|
|
|
2019-02-07 18:20:49 +00:00
|
|
|
if request is None:
|
2019-09-28 16:27:50 +00:00
|
|
|
log.error(
|
|
|
|
|
"AXES: AxesDatabaseHandler.user_login_failed does not function without a request."
|
|
|
|
|
)
|
2019-02-07 18:20:49 +00:00
|
|
|
return
|
|
|
|
|
|
2019-03-03 19:56:57 +00:00
|
|
|
# 1. database query: Clean up expired user attempts from the database before logging new attempts
|
2025-04-24 03:15:14 +00:00
|
|
|
self.clean_expired_user_attempts(request, credentials)
|
2019-03-03 19:56:57 +00:00
|
|
|
|
2019-02-07 18:20:49 +00:00
|
|
|
username = get_client_username(request, credentials)
|
2019-09-28 16:27:50 +00:00
|
|
|
client_str = get_client_str(
|
|
|
|
|
username,
|
|
|
|
|
request.axes_ip_address,
|
|
|
|
|
request.axes_user_agent,
|
|
|
|
|
request.axes_path_info,
|
2021-09-02 12:55:40 +00:00
|
|
|
request,
|
2019-09-28 16:27:50 +00:00
|
|
|
)
|
2019-02-07 18:20:49 +00:00
|
|
|
|
2022-03-22 10:11:34 +00:00
|
|
|
# If axes denied access, don't record the failed attempt as that would reset the lockout time.
|
2022-03-22 12:58:23 +00:00
|
|
|
if (
|
|
|
|
|
not settings.AXES_RESET_COOL_OFF_ON_FAILURE_DURING_LOCKOUT
|
|
|
|
|
and request.axes_locked_out
|
|
|
|
|
):
|
2022-03-22 10:11:34 +00:00
|
|
|
request.axes_credentials = credentials
|
|
|
|
|
user_locked_out.send(
|
|
|
|
|
"axes",
|
|
|
|
|
request=request,
|
|
|
|
|
username=username,
|
|
|
|
|
ip_address=request.axes_ip_address,
|
|
|
|
|
)
|
|
|
|
|
return
|
|
|
|
|
|
2021-06-29 14:05:43 +00:00
|
|
|
# This replaces null byte chars that crash saving failures.
|
2021-04-23 22:06:03 +00:00
|
|
|
get_data = get_query_str(request.GET).replace("\0", "0x00")
|
|
|
|
|
post_data = get_query_str(request.POST).replace("\0", "0x00")
|
2019-02-20 20:52:12 +00:00
|
|
|
|
2019-02-22 23:22:11 +00:00
|
|
|
if self.is_whitelisted(request, credentials):
|
2019-09-28 16:27:50 +00:00
|
|
|
log.info("AXES: Login failed from whitelisted client %s.", client_str)
|
2019-02-07 18:20:49 +00:00
|
|
|
return
|
|
|
|
|
|
2021-06-29 14:05:43 +00:00
|
|
|
# 2. database query: Get or create access record with the new failure data
|
2023-05-10 17:06:55 +00:00
|
|
|
lockout_parameters = get_lockout_parameters(request, credentials)
|
2023-05-04 09:42:42 +00:00
|
|
|
if lockout_parameters == ["username"] and username is None:
|
2021-06-29 12:17:44 +00:00
|
|
|
log.warning(
|
2023-05-04 09:42:42 +00:00
|
|
|
"AXES: Username is None and username is the only one lockout parameter, new record will NOT be created."
|
2021-06-29 12:17:44 +00:00
|
|
|
)
|
|
|
|
|
else:
|
2023-08-31 17:42:44 +00:00
|
|
|
with transaction.atomic(using=router.db_for_write(AccessAttempt)):
|
2021-09-08 11:02:06 +00:00
|
|
|
(
|
|
|
|
|
attempt,
|
|
|
|
|
created,
|
|
|
|
|
) = AccessAttempt.objects.select_for_update().get_or_create(
|
|
|
|
|
username=username,
|
|
|
|
|
ip_address=request.axes_ip_address,
|
|
|
|
|
user_agent=request.axes_user_agent,
|
|
|
|
|
defaults={
|
|
|
|
|
"get_data": get_data,
|
|
|
|
|
"post_data": post_data,
|
|
|
|
|
"http_accept": request.axes_http_accept,
|
|
|
|
|
"path_info": request.axes_path_info,
|
|
|
|
|
"failures_since_start": 1,
|
2025-06-22 08:02:57 +00:00
|
|
|
"attempt_time": request.axes_attempt_time,
|
2021-09-08 11:02:06 +00:00
|
|
|
},
|
2020-10-24 17:43:19 +00:00
|
|
|
)
|
2021-06-29 14:05:43 +00:00
|
|
|
|
2021-09-08 11:02:06 +00:00
|
|
|
# Record failed attempt with all the relevant information.
|
|
|
|
|
# Filtering based on username, IP address and user agent handled elsewhere,
|
|
|
|
|
# and this handler just records the available information for further use.
|
|
|
|
|
if created:
|
|
|
|
|
log.warning(
|
|
|
|
|
"AXES: New login failure by %s. Created new record in the database.",
|
|
|
|
|
client_str,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# 3. database query if there were previous attempts in the database
|
|
|
|
|
# Update failed attempt information but do not touch the username, IP address, or user agent fields,
|
|
|
|
|
# because attackers can request the site with multiple different configurations
|
|
|
|
|
# in order to bypass the defense mechanisms that are used by the site.
|
|
|
|
|
else:
|
|
|
|
|
separator = "\n---------\n"
|
|
|
|
|
|
|
|
|
|
attempt.get_data = Concat("get_data", Value(separator + get_data))
|
|
|
|
|
attempt.post_data = Concat(
|
|
|
|
|
"post_data", Value(separator + post_data)
|
|
|
|
|
)
|
|
|
|
|
attempt.http_accept = request.axes_http_accept
|
|
|
|
|
attempt.path_info = request.axes_path_info
|
|
|
|
|
attempt.failures_since_start = F("failures_since_start") + 1
|
|
|
|
|
attempt.attempt_time = request.axes_attempt_time
|
|
|
|
|
attempt.save()
|
|
|
|
|
|
|
|
|
|
log.warning(
|
|
|
|
|
"AXES: Repeated login failure by %s. Updated existing record in the database.",
|
|
|
|
|
client_str,
|
|
|
|
|
)
|
2021-06-29 14:05:43 +00:00
|
|
|
|
2025-06-07 13:13:14 +00:00
|
|
|
if settings.AXES_USE_ATTEMPT_EXPIRATION:
|
|
|
|
|
if not hasattr(attempt, "expiration") or attempt.expiration is None:
|
|
|
|
|
log.debug(
|
2026-02-11 19:54:13 +00:00
|
|
|
"AXES: Creating new AccessAttemptExpiration for %s",
|
|
|
|
|
client_str,
|
2025-06-07 13:13:14 +00:00
|
|
|
)
|
2025-06-08 08:50:14 +00:00
|
|
|
attempt.expiration = AccessAttemptExpiration.objects.create(
|
|
|
|
|
access_attempt=attempt,
|
2026-02-11 19:54:13 +00:00
|
|
|
expires_at=get_attempt_expiration(request),
|
2025-06-07 13:13:14 +00:00
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
attempt.expiration.expires_at = max(
|
2026-02-11 19:54:13 +00:00
|
|
|
get_attempt_expiration(request),
|
|
|
|
|
attempt.expiration.expires_at,
|
2025-06-07 13:13:14 +00:00
|
|
|
)
|
2025-06-08 08:50:14 +00:00
|
|
|
attempt.expiration.save()
|
2025-06-07 13:13:14 +00:00
|
|
|
|
2021-06-29 14:05:43 +00:00
|
|
|
# 3. or 4. database query: Calculate the current maximum failure number from the existing attempts
|
|
|
|
|
failures_since_start = self.get_failures(request, credentials)
|
2021-08-25 01:38:15 +00:00
|
|
|
request.axes_failures_since_start = failures_since_start
|
2021-06-29 14:05:43 +00:00
|
|
|
|
2019-09-28 16:27:50 +00:00
|
|
|
if (
|
|
|
|
|
settings.AXES_LOCK_OUT_AT_FAILURE
|
2021-06-29 12:21:56 +00:00
|
|
|
and failures_since_start >= get_failure_limit(request, credentials)
|
2019-09-28 16:27:50 +00:00
|
|
|
):
|
|
|
|
|
log.warning(
|
|
|
|
|
"AXES: Locking out %s after repeated login failures.", client_str
|
|
|
|
|
)
|
2019-02-07 18:20:49 +00:00
|
|
|
|
2019-05-19 12:54:27 +00:00
|
|
|
request.axes_locked_out = True
|
2021-10-07 22:10:29 +00:00
|
|
|
request.axes_credentials = credentials
|
2019-02-07 18:20:49 +00:00
|
|
|
user_locked_out.send(
|
2019-09-28 16:27:50 +00:00
|
|
|
"axes",
|
2019-02-07 18:20:49 +00:00
|
|
|
request=request,
|
|
|
|
|
username=username,
|
2019-03-03 19:56:57 +00:00
|
|
|
ip_address=request.axes_ip_address,
|
2019-02-07 18:20:49 +00:00
|
|
|
)
|
|
|
|
|
|
2022-03-15 09:42:24 +00:00
|
|
|
# 5. database entry: Log for ever the attempt in the AccessFailureLog
|
|
|
|
|
if settings.AXES_ENABLE_ACCESS_FAILURE_LOG:
|
2023-08-31 17:42:44 +00:00
|
|
|
with transaction.atomic(using=router.db_for_write(AccessFailureLog)):
|
2022-03-15 09:42:24 +00:00
|
|
|
AccessFailureLog.objects.create(
|
|
|
|
|
username=username,
|
|
|
|
|
ip_address=request.axes_ip_address,
|
|
|
|
|
user_agent=request.axes_user_agent,
|
|
|
|
|
http_accept=request.axes_http_accept,
|
|
|
|
|
path_info=request.axes_path_info,
|
|
|
|
|
attempt_time=request.axes_attempt_time,
|
|
|
|
|
locked_out=request.axes_locked_out,
|
|
|
|
|
)
|
|
|
|
|
self.remove_out_of_limit_failure_logs(username=username)
|
|
|
|
|
|
2021-11-30 12:48:53 +00:00
|
|
|
def user_logged_in(self, sender, request, user, **kwargs):
|
2019-02-07 18:20:49 +00:00
|
|
|
"""
|
|
|
|
|
When user logs in, update the AccessLog related to the user.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
username = user.get_username()
|
|
|
|
|
credentials = get_credentials(username)
|
2019-09-28 16:27:50 +00:00
|
|
|
client_str = get_client_str(
|
|
|
|
|
username,
|
|
|
|
|
request.axes_ip_address,
|
|
|
|
|
request.axes_user_agent,
|
|
|
|
|
request.axes_path_info,
|
2021-09-02 12:55:40 +00:00
|
|
|
request,
|
2019-09-28 16:27:50 +00:00
|
|
|
)
|
2019-02-07 18:20:49 +00:00
|
|
|
|
2019-09-28 16:27:50 +00:00
|
|
|
log.info("AXES: Successful login by %s.", client_str)
|
2019-02-07 18:20:49 +00:00
|
|
|
|
2025-04-23 13:38:41 +00:00
|
|
|
# 1. database query: Clean up expired user attempts from the database
|
2025-04-24 03:15:14 +00:00
|
|
|
self.clean_expired_user_attempts(request, credentials)
|
2025-04-23 13:38:41 +00:00
|
|
|
|
2019-05-25 17:23:18 +00:00
|
|
|
if not settings.AXES_DISABLE_ACCESS_LOG:
|
2019-02-22 23:22:11 +00:00
|
|
|
# 2. database query: Insert new access logs with login time
|
2019-02-07 18:20:49 +00:00
|
|
|
AccessLog.objects.create(
|
|
|
|
|
username=username,
|
2019-03-03 19:56:57 +00:00
|
|
|
ip_address=request.axes_ip_address,
|
|
|
|
|
user_agent=request.axes_user_agent,
|
|
|
|
|
http_accept=request.axes_http_accept,
|
|
|
|
|
path_info=request.axes_path_info,
|
|
|
|
|
attempt_time=request.axes_attempt_time,
|
2024-04-30 14:22:50 +00:00
|
|
|
# evaluate session hash here to ensure having the correct
|
|
|
|
|
# value which is stored on the backend
|
|
|
|
|
session_hash=get_client_session_hash(request),
|
2019-02-07 18:20:49 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if settings.AXES_RESET_ON_SUCCESS:
|
2019-02-22 23:22:11 +00:00
|
|
|
# 3. database query: Reset failed attempts for the logging in user
|
2025-05-03 13:40:04 +00:00
|
|
|
count = self.reset_user_attempts(request, credentials)
|
2019-09-28 16:27:50 +00:00
|
|
|
log.info(
|
|
|
|
|
"AXES: Deleted %d failed login attempts by %s from database.",
|
|
|
|
|
count,
|
|
|
|
|
client_str,
|
|
|
|
|
)
|
2019-02-07 18:20:49 +00:00
|
|
|
|
2021-11-30 12:48:53 +00:00
|
|
|
def user_logged_out(self, sender, request, user, **kwargs):
|
2019-02-07 18:20:49 +00:00
|
|
|
"""
|
|
|
|
|
When user logs out, update the AccessLog related to the user.
|
|
|
|
|
"""
|
|
|
|
|
|
2019-05-08 10:28:22 +00:00
|
|
|
username = user.get_username() if user else None
|
2025-04-23 13:38:41 +00:00
|
|
|
credentials = get_credentials(username) if username else None
|
2019-09-28 16:27:50 +00:00
|
|
|
client_str = get_client_str(
|
|
|
|
|
username,
|
|
|
|
|
request.axes_ip_address,
|
|
|
|
|
request.axes_user_agent,
|
|
|
|
|
request.axes_path_info,
|
2021-09-02 12:55:40 +00:00
|
|
|
request,
|
2019-09-28 16:27:50 +00:00
|
|
|
)
|
2019-02-16 17:05:59 +00:00
|
|
|
|
2025-04-23 13:38:41 +00:00
|
|
|
# 1. database query: Clean up expired user attempts from the database
|
2025-04-24 03:15:14 +00:00
|
|
|
self.clean_expired_user_attempts(request, credentials)
|
2025-04-23 13:38:41 +00:00
|
|
|
|
2019-09-28 16:27:50 +00:00
|
|
|
log.info("AXES: Successful logout by %s.", client_str)
|
2019-02-07 18:20:49 +00:00
|
|
|
|
2019-02-22 23:22:11 +00:00
|
|
|
if username and not settings.AXES_DISABLE_ACCESS_LOG:
|
|
|
|
|
# 2. database query: Update existing attempt logs with logout time
|
2019-02-07 18:20:49 +00:00
|
|
|
AccessLog.objects.filter(
|
2024-04-30 14:22:50 +00:00
|
|
|
username=username,
|
|
|
|
|
logout_time__isnull=True,
|
|
|
|
|
# update only access log for given session
|
|
|
|
|
session_hash=get_client_session_hash(request),
|
2019-09-28 16:27:50 +00:00
|
|
|
).update(logout_time=request.axes_attempt_time)
|
2020-07-19 16:36:54 +00:00
|
|
|
|
2025-05-03 13:40:04 +00:00
|
|
|
def filter_user_attempts(
|
|
|
|
|
self, request: HttpRequest, credentials: Optional[dict] = None
|
|
|
|
|
) -> List[QuerySet]:
|
|
|
|
|
"""
|
|
|
|
|
Return a list querysets of AccessAttempts that match the given request and credentials.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
username = get_client_username(request, credentials)
|
|
|
|
|
|
|
|
|
|
filter_kwargs_list = get_client_parameters(
|
|
|
|
|
username,
|
|
|
|
|
request.axes_ip_address,
|
|
|
|
|
request.axes_user_agent,
|
|
|
|
|
request,
|
|
|
|
|
credentials,
|
|
|
|
|
)
|
|
|
|
|
attempts_list = [
|
|
|
|
|
AccessAttempt.objects.filter(**filter_kwargs)
|
|
|
|
|
for filter_kwargs in filter_kwargs_list
|
|
|
|
|
]
|
|
|
|
|
return attempts_list
|
|
|
|
|
|
|
|
|
|
def get_user_attempts(
|
2026-02-11 20:06:59 +00:00
|
|
|
self, request: HttpRequest, credentials: Optional[dict] = None # noqa
|
2025-05-03 13:40:04 +00:00
|
|
|
) -> List[QuerySet]:
|
|
|
|
|
"""
|
|
|
|
|
Get list of querysets with valid user attempts that match the given request and credentials.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
attempts_list = self.filter_user_attempts(request, credentials)
|
|
|
|
|
|
|
|
|
|
if settings.AXES_COOLOFF_TIME is None:
|
|
|
|
|
log.debug(
|
|
|
|
|
"AXES: Getting all access attempts from database because no AXES_COOLOFF_TIME is configured"
|
|
|
|
|
)
|
|
|
|
|
return attempts_list
|
|
|
|
|
|
|
|
|
|
threshold = get_cool_off_threshold(request)
|
|
|
|
|
log.debug("AXES: Getting access attempts that are newer than %s", threshold)
|
|
|
|
|
return [
|
|
|
|
|
attempts.filter(attempt_time__gte=threshold) for attempts in attempts_list
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
def clean_expired_user_attempts(
|
2026-02-11 20:06:59 +00:00
|
|
|
self,
|
|
|
|
|
request: Optional[HttpRequest] = None,
|
|
|
|
|
credentials: Optional[dict] = None, # noqa
|
2025-05-03 13:40:04 +00:00
|
|
|
) -> int:
|
|
|
|
|
"""
|
|
|
|
|
Clean expired user attempts from the database.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
if settings.AXES_COOLOFF_TIME is None:
|
|
|
|
|
log.debug(
|
|
|
|
|
"AXES: Skipping clean for expired access attempts because no AXES_COOLOFF_TIME is configured"
|
|
|
|
|
)
|
|
|
|
|
return 0
|
|
|
|
|
|
2025-06-07 11:48:54 +00:00
|
|
|
if settings.AXES_USE_ATTEMPT_EXPIRATION:
|
2025-05-27 19:21:50 +00:00
|
|
|
threshold = timezone.now()
|
2026-02-11 19:54:13 +00:00
|
|
|
count, _ = AccessAttempt.objects.filter(
|
|
|
|
|
expiration__expires_at__lte=threshold
|
|
|
|
|
).delete()
|
2025-05-27 19:21:50 +00:00
|
|
|
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)
|
2026-02-11 19:54:13 +00:00
|
|
|
count, _ = AccessAttempt.objects.filter(
|
|
|
|
|
attempt_time__lte=threshold
|
|
|
|
|
).delete()
|
2025-05-27 19:21:50 +00:00
|
|
|
log.info(
|
|
|
|
|
"AXES: Cleaned up %s expired access attempts from database that were older than %s",
|
|
|
|
|
count,
|
|
|
|
|
threshold,
|
|
|
|
|
)
|
2025-05-03 13:40:04 +00:00
|
|
|
return count
|
|
|
|
|
|
|
|
|
|
def reset_user_attempts(
|
|
|
|
|
self, request: HttpRequest, credentials: Optional[dict] = None
|
|
|
|
|
) -> int:
|
|
|
|
|
"""
|
|
|
|
|
Reset all user attempts that match the given request and credentials.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
attempts_list = self.filter_user_attempts(request, credentials)
|
|
|
|
|
|
|
|
|
|
count = 0
|
|
|
|
|
for attempts in attempts_list:
|
|
|
|
|
_count, _ = attempts.delete()
|
|
|
|
|
count += _count
|
|
|
|
|
log.info("AXES: Reset %s access attempts from database.", count)
|
|
|
|
|
|
|
|
|
|
return count
|
|
|
|
|
|
2020-07-19 16:36:54 +00:00
|
|
|
def post_save_access_attempt(self, instance, **kwargs):
|
|
|
|
|
"""
|
|
|
|
|
Handles the ``axes.models.AccessAttempt`` object post save signal.
|
2020-07-19 22:33:16 +00:00
|
|
|
|
|
|
|
|
When needed, all post_save actions for this backend should be located
|
|
|
|
|
here.
|
2020-07-19 16:36:54 +00:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def post_delete_access_attempt(self, instance, **kwargs):
|
|
|
|
|
"""
|
|
|
|
|
Handles the ``axes.models.AccessAttempt`` object post delete signal.
|
|
|
|
|
|
2020-07-19 22:33:16 +00:00
|
|
|
When needed, all post_delete actions for this backend should be located
|
|
|
|
|
here.
|
2020-07-26 23:01:33 +00:00
|
|
|
"""
|