Esempio n. 1
0
    def post_delete_access_attempt(self, instance, **kwargs):  # pylint: disable=unused-argument
        """
        Update cache after deleting AccessAttempts.
        """

        cache_hash_key = get_cache_key(instance)
        get_axes_cache().delete(cache_hash_key)
Esempio n. 2
0
def get_user_attempts(request):
    force_reload = False
    attempts = _query_user_attempts(request)
    cache_hash_key = get_cache_key(request)
    cache_timeout = get_cache_timeout()

    cool_off = settings.AXES_COOLOFF_TIME
    if cool_off:
        if isinstance(cool_off, (int, float)):
            cool_off = timedelta(hours=cool_off)

        for attempt in attempts:
            if attempt.attempt_time + cool_off < timezone.now():
                if attempt.trusted:
                    attempt.failures_since_start = 0
                    attempt.save()
                    get_axes_cache().set(cache_hash_key, 0, cache_timeout)
                else:
                    attempt.delete()
                    force_reload = True
                    failures_cached = get_axes_cache().get(cache_hash_key)
                    if failures_cached is not None:
                        get_axes_cache().set(
                            cache_hash_key, failures_cached - 1, cache_timeout
                        )

    # If objects were deleted, we need to update the queryset to reflect this,
    # so force a reload.
    if force_reload:
        attempts = _query_user_attempts(request)

    return attempts
Esempio n. 3
0
def get_user_attempts(request: HttpRequest,
                      credentials: dict = None) -> QuerySet:
    """
    Get valid user attempts and delete expired attempts which have cool offs in the past.
    """

    attempts = filter_user_attempts(request, credentials)

    # If settings.AXES_COOLOFF_TIME is not configured return the attempts
    cool_off = get_cool_off()
    if cool_off is None:
        return attempts

    # Else AccessAttempts that have expired need to be cleaned up from the database
    num_deleted, _ = attempts.filter(attempt_time__lte=timezone.now() -
                                     cool_off).delete()
    if not num_deleted:
        return attempts

    # If there deletions the cache needs to be updated
    cache_key = get_cache_key(request, credentials)
    num_failures_cached = get_axes_cache().get(cache_key)
    if num_failures_cached is not None:
        get_axes_cache().set(
            cache_key,
            num_failures_cached - num_deleted,
            get_cache_timeout(),
        )

    # AccessAttempts need to be refreshed from the database because of the delete before returning them
    return attempts.all()
Esempio n. 4
0
    def post_save_access_attempt(self, instance, **kwargs):  # pylint: disable=unused-argument
        """
        Update cache after saving AccessAttempts.
        """

        cache_key = get_cache_key(instance)

        if not get_axes_cache().get(cache_key):
            get_axes_cache().set(
                cache_key,
                instance.failures_since_start,
                get_cache_timeout(),
            )
Esempio n. 5
0
def is_already_locked(request):
    ip = get_client_ip(request)

    if (settings.AXES_ONLY_USER_FAILURES
            or settings.AXES_LOCK_OUT_BY_COMBINATION_USER_AND_IP
        ) and request.method == 'GET':
        return False

    if settings.AXES_NEVER_LOCKOUT_WHITELIST and ip_in_whitelist(ip):
        return False

    if settings.AXES_ONLY_WHITELIST and not ip_in_whitelist(ip):
        return True

    if ip_in_blacklist(ip):
        return True

    if not is_user_lockable(request):
        return False

    cache_hash_key = get_cache_key(request)
    failures_cached = get_axes_cache().get(cache_hash_key)
    if failures_cached is not None:
        return (failures_cached >= Settings.objects.first().failure_limit
                and settings.AXES_LOCK_OUT_AT_FAILURE)
    else:
        for attempt in get_user_attempts(request):
            if (attempt.failures_since_start >=
                    Settings.objects.first().failure_limit
                    and settings.AXES_LOCK_OUT_AT_FAILURE):
                return True

    return False
Esempio n. 6
0
def is_already_locked(request: HttpRequest, credentials: dict = None) -> bool:
    """
    Check if the request or given credentials are already locked by Axes.

    This function is called from

    - function decorators defined in ``axes.decorators``,
    - authentication backends defined in ``axes.backends``, and
    - signal handlers defined in ``axes.handlers``.

    This function checks the following facts for a given request:

    1. Is the request HTTP method _whitelisted_? If it is, return ``False``.
    2. Is the request IP address _blacklisted_? If it is, return ``True``.
    3. Is the request user _whitelisted_? If it is, return ``False``.
    4. Is the request failure count over the attempt limit? If it is, return ``True``.

    Refer to the function source code for the exact implementation.
    """

    if settings.AXES_NEVER_LOCKOUT_GET and request.method == 'GET':
        return False

    if is_ip_blacklisted(request):
        return True

    if not is_user_lockable(request, credentials):
        return False

    if not settings.AXES_LOCK_OUT_AT_FAILURE:
        return False

    # Check failure statistics against cache
    cache_hash_key = get_cache_key(request, credentials)
    num_failures_cached = get_axes_cache().get(cache_hash_key)

    # Do not hit the database if we have an answer in the cache
    if num_failures_cached is not None:
        return num_failures_cached >= settings.AXES_FAILURE_LIMIT

    # Check failure statistics against database
    attempts = get_user_attempts(request, credentials)
    failures = attempts.filter(
        failures_since_start__gte=settings.AXES_FAILURE_LIMIT, )

    return failures.exists()
Esempio n. 7
0
    def user_login_failed(self, sender, credentials, request, **kwargs):  # pylint: disable=unused-argument
        """
        When user login fails, save AccessAttempt record in database and lock user out if necessary.

        :raises AxesSignalPermissionDenied: if user should is locked out
        """

        if request is None:
            log.warning(
                'AxesHandler.user_login_failed does not function without a request.'
            )
            return

        username = get_client_username(request, credentials)
        ip_address = get_client_ip(request)
        user_agent = get_client_user_agent(request)
        path_info = request.META.get('PATH_INFO', '<unknown>')[:255]
        http_accept = request.META.get('HTTP_ACCEPT', '<unknown>')[:1025]
        client_str = get_client_str(username, ip_address, user_agent,
                                    path_info)

        if settings.AXES_NEVER_LOCKOUT_WHITELIST and ip_in_whitelist(
                ip_address):
            log.info('Login failed from whitelisted IP %s.', ip_address)
            return

        attempts = get_user_attempts(request, credentials)
        cache_key = get_cache_key(request, credentials)
        num_failures_cached = get_axes_cache().get(cache_key)

        if num_failures_cached:
            failures = num_failures_cached
        elif attempts:
            failures = attempts.aggregate(
                Max('failures_since_start'), )['failures_since_start__max']
        else:
            failures = 0

        # add a failed attempt for this user
        failures += 1
        get_axes_cache().set(
            cache_key,
            failures,
            get_cache_timeout(),
        )

        if attempts:
            # Update existing attempt information but do not touch the username, ip_address, or user_agent fields,
            # because attackers can request the site with multiple different usernames, addresses, or programs.
            for attempt in attempts:
                template = '{}\n---------\n{}'

                attempt.get_data = template.format(
                    attempt.get_data,
                    query2str(request.GET),
                )
                attempt.post_data = template.format(attempt.post_data,
                                                    query2str(request.POST))
                attempt.http_accept = http_accept
                attempt.path_info = path_info
                attempt.failures_since_start = failures
                attempt.attempt_time = now()
                attempt.save()

                log.info(
                    'AXES: Repeated login failure by %s. Count = %d of %d',
                    client_str,
                    failures,
                    settings.AXES_FAILURE_LIMIT,
                )
        else:
            # Record failed attempt. Whether or not the IP address or user agent is
            # used in counting failures is handled elsewhere, so we just record # everything here.
            AccessAttempt.objects.create(
                username=username,
                ip_address=ip_address,
                user_agent=user_agent,
                get_data=query2str(request.GET),
                post_data=query2str(request.POST),
                http_accept=http_accept,
                path_info=path_info,
                failures_since_start=failures,
            )

            log.info(
                'AXES: New login failure by %s. Creating access record.',
                client_str,
            )

        if is_already_locked(request, credentials):
            log.warning(
                'AXES: Locked out %s after repeated login failures.',
                client_str,
            )

            user_locked_out.send(
                'axes',
                request=request,
                username=username,
                ip_address=ip_address,
            )

            raise AxesSignalPermissionDenied(
                'Locked out due to repeated login failures.')
Esempio n. 8
0
def log_user_login_failed(sender, credentials, request, **kwargs):  # pylint: disable=unused-argument
    """ Create an AccessAttempt record if the login wasn't successful
    """
    if request is None:
        log.warning('Attempt to authenticate with a custom backend failed.')
        return

    ip_address = get_client_ip(request)
    username = get_client_username(request)
    user_agent = request.META.get('HTTP_USER_AGENT', '<unknown>')[:255]
    path_info = request.META.get('PATH_INFO', '<unknown>')[:255]
    http_accept = request.META.get('HTTP_ACCEPT', '<unknown>')[:1025]

    if settings.AXES_NEVER_LOCKOUT_WHITELIST and ip_in_whitelist(ip_address):
        return

    failures = 0
    attempts = get_user_attempts(request)
    cache_hash_key = get_cache_key(request)
    cache_timeout = get_cache_timeout()

    failures_cached = get_axes_cache().get(cache_hash_key)
    if failures_cached is not None:
        failures = failures_cached
    else:
        for attempt in attempts:
            failures = max(failures, attempt.failures_since_start)

    # add a failed attempt for this user
    failures += 1
    get_axes_cache().set(cache_hash_key, failures, cache_timeout)

    # has already attempted, update the info
    if attempts:
        for attempt in attempts:
            attempt.get_data = '%s\n---------\n%s' % (
                attempt.get_data,
                query2str(request.GET),
            )
            attempt.post_data = '%s\n---------\n%s' % (attempt.post_data,
                                                       query2str(request.POST))
            attempt.http_accept = http_accept
            attempt.path_info = path_info
            attempt.failures_since_start = failures
            attempt.attempt_time = timezone.now()
            attempt.save()

            log.info(
                'AXES: Repeated login failure by %s. Count = %d of %d',
                get_client_str(username, ip_address, user_agent, path_info),
                failures, settings.AXES_FAILURE_LIMIT)
    else:
        # Record failed attempt. Whether or not the IP address or user agent is
        # used in counting failures is handled elsewhere, so we just record
        # everything here.
        AccessAttempt.objects.create(
            user_agent=user_agent,
            ip_address=ip_address,
            username=username,
            get_data=query2str(request.GET),
            post_data=query2str(request.POST),
            http_accept=http_accept,
            path_info=path_info,
            failures_since_start=failures,
        )

        log.info('AXES: New login failure by %s. Creating access record.',
                 get_client_str(username, ip_address, user_agent, path_info))

    # no matter what, we want to lock them out if they're past the number of
    # attempts allowed, unless the user is set to notlockable
    if (failures >= settings.AXES_FAILURE_LIMIT
            and settings.AXES_LOCK_OUT_AT_FAILURE
            and is_user_lockable(request)):
        log.warning(
            'AXES: locked out %s after repeated login attempts.',
            get_client_str(username, ip_address, user_agent, path_info))

        # send signal when someone is locked out.
        user_locked_out.send('axes',
                             request=request,
                             username=username,
                             ip_address=ip_address)
Esempio n. 9
0
def delete_cache_after_delete(instance, **kwargs):  # pylint: disable=unused-argument
    cache_hash_key = get_cache_key(instance)
    get_axes_cache().delete(cache_hash_key)
Esempio n. 10
0
def update_cache_after_save(instance, **kwargs):  # pylint: disable=unused-argument
    cache_hash_key = get_cache_key(instance)
    if not get_axes_cache().get(cache_hash_key):
        cache_timeout = get_cache_timeout()
        get_axes_cache().set(cache_hash_key, instance.failures_since_start,
                             cache_timeout)
Esempio n. 11
0
def delete_cache_after_delete(instance, **kwargs):
    cache_hash_key = get_cache_key(instance)
    get_axes_cache().delete(cache_hash_key)