예제 #1
0
def is_user_lockable(request):
    """Check if the user has a profile with nolockout
    If so, then return the value to see if this user is special
    and doesn't get their account locked out
    """
    if hasattr(request.user, 'nolockout'):
        return not request.user.nolockout

    if request.method != 'POST':
        return True

    try:
        field = getattr(get_user_model(), 'USERNAME_FIELD', 'username')
        kwargs = {
            field: get_client_username(request)
        }
        user = get_user_model().objects.get(**kwargs)

        if hasattr(user, 'nolockout'):
            # need to invert since we need to return
            # false for users that can't be blocked
            return not user.nolockout

    except get_user_model().DoesNotExist:
        # not a valid user
        return True

    # Default behavior for a user to be lockable
    return True
예제 #2
0
def is_user_lockable(request: HttpRequest, credentials: dict = None) -> bool:
    """
    Check if the given request or credentials refer to a whitelisted user object

    A whitelisted user has the magic ``nolockout`` property set.

    If the property is unknown or False or the user can not be found,
    this implementation fails gracefully and returns True.
    """

    username_field = getattr(get_user_model(), 'USERNAME_FIELD', 'username')
    username_value = get_client_username(request, credentials)
    kwargs = {
        username_field: username_value
    }

    UserModel = get_user_model()

    try:
        user = UserModel.objects.get(**kwargs)
        return not user.nolockout
    except (UserModel.DoesNotExist, AttributeError):
        pass

    return True
예제 #3
0
def get_cache_key(request_or_attempt, credentials: dict = None) -> str:
    """
    Build cache key name from request or AccessAttempt object.

    :param request_or_attempt: HttpRequest or AccessAttempt object
    :param credentials: credentials containing user information
    :return cache_key: Hash key that is usable for Django cache backends
    """

    if isinstance(request_or_attempt, AccessAttempt):
        username = request_or_attempt.username
        ip_address = request_or_attempt.ip_address
        user_agent = request_or_attempt.user_agent
    else:
        username = get_client_username(request_or_attempt, credentials)
        ip_address = get_client_ip(request_or_attempt)
        user_agent = get_client_user_agent(request_or_attempt)

    filter_kwargs = get_filter_kwargs(username, ip_address, user_agent)

    cache_key_components = ''.join(filter_kwargs.values())
    cache_key_digest = md5(cache_key_components.encode()).hexdigest()
    cache_key = 'axes-{}'.format(cache_key_digest)

    return cache_key
예제 #4
0
def get_cache_key(request_or_obj, credentials=None):
    """
    Build cache key name from request or AccessAttempt object.
    :param  request_or_obj: Request or AccessAttempt object
    :return cache-key: String, key to be used in cache system
    """
    if isinstance(request_or_obj, AccessAttempt):
        ip = request_or_obj.ip_address
        un = request_or_obj.username
        ua = request_or_obj.user_agent
    else:
        ip = get_client_ip(request_or_obj)
        un = get_client_username(request_or_obj, credentials)
        ua = request_or_obj.META.get('HTTP_USER_AGENT', '<unknown>')[:255]

    ip = ip.encode('utf-8') if ip else ''.encode('utf-8')
    un = un.encode('utf-8') if un else ''.encode('utf-8')
    ua = ua.encode('utf-8') if ua else ''.encode('utf-8')

    if settings.AXES_ONLY_USER_FAILURES:
        attributes = un
    elif settings.AXES_LOCK_OUT_BY_COMBINATION_USER_AND_IP:
        attributes = ip + un
    else:
        attributes = ip

    if settings.AXES_USE_USER_AGENT and not settings.AXES_ONLY_USER_FAILURES:
        attributes += ua

    cache_hash_key = 'axes-{}'.format(md5(attributes).hexdigest())

    return cache_hash_key
예제 #5
0
def lockout_response(request):
    context = {
        'failure_limit': Settings.objects.first().failure_limit,
        'username': get_client_username(request) or ''
    }

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

        context.update({'cooloff_time': iso8601(cool_off)})

    if request.is_ajax():
        return HttpResponse(
            json.dumps(context),
            content_type='application/json',
            status=403,
        )

    elif settings.AXES_LOCKOUT_TEMPLATE:
        return render(request,
                      settings.AXES_LOCKOUT_TEMPLATE,
                      context,
                      status=403)

    elif settings.AXES_LOCKOUT_URL:
        return HttpResponseRedirect(settings.AXES_LOCKOUT_URL)

    return HttpResponse(get_lockout_message(), status=403)
예제 #6
0
def get_cache_key(request_or_attempt, credentials: dict = None) -> str:
    """
    Build cache key name from request or AccessAttempt object.

    :param request_or_attempt: HttpRequest or AccessAttempt object
    :param credentials: credentials containing user information
    :return cache_key: Hash key that is usable for Django cache backends
    """

    if isinstance(request_or_attempt, AccessAttempt):
        username = request_or_attempt.username
        ip_address = request_or_attempt.ip_address
        user_agent = request_or_attempt.user_agent
    else:
        username = get_client_username(request_or_attempt, credentials)
        ip_address = get_client_ip(request_or_attempt)
        user_agent = get_client_user_agent(request_or_attempt)

    filter_kwargs = get_filter_kwargs(username, ip_address, user_agent)

    cache_key_components = ''.join(filter_kwargs.values())
    cache_key_digest = md5(cache_key_components.encode()).hexdigest()
    cache_key = 'axes-{}'.format(cache_key_digest)

    return cache_key
예제 #7
0
    def test_default_get_client_username(self):
        expected = 'test-username'

        request = HttpRequest()
        request.POST['username'] = expected

        actual = get_client_username(request)

        self.assertEqual(expected, actual)
예제 #8
0
    def test_default_get_client_username(self):
        expected = 'test-username'

        request = HttpRequest()
        request.POST['username'] = expected

        actual = get_client_username(request)

        self.assertEqual(expected, actual)
예제 #9
0
    def test_custom_get_client_username(self):
        provided = 'test-username'
        expected = 'prefixed-' + provided

        request = HttpRequest()
        request.POST['username'] = provided

        actual = get_client_username(request)

        self.assertEqual(expected, actual)
예제 #10
0
    def test_default_get_client_username_credentials(self):
        expected = 'test-username'
        expected_in_credentials = 'test-credentials-username'

        request = HttpRequest()
        request.POST['username'] = expected
        credentials = {'username': expected_in_credentials}

        actual = get_client_username(request, credentials)

        self.assertEqual(expected_in_credentials, actual)
예제 #11
0
def query_user_attempts(request: HttpRequest, credentials: dict = None) -> QuerySet:
    """
    Return a queryset of AccessAttempts that match the given request and credentials.
    """

    username = get_client_username(request, credentials)
    ip_address = get_client_ip(request)
    user_agent = get_client_user_agent(request)

    filter_kwargs = get_filter_kwargs(username, ip_address, user_agent)

    return AccessAttempt.objects.filter(**filter_kwargs)
예제 #12
0
    def test_custom_get_client_username_from_credentials(self):
        provided = 'test-username'
        expected = 'prefixed-' + provided
        provided_in_credentials = 'test-credentials-username'
        expected_in_credentials = 'prefixed-' + provided_in_credentials

        request = HttpRequest()
        request.POST['username'] = provided
        credentials = {'username': provided_in_credentials}

        actual = get_client_username(request, credentials)

        self.assertEqual(expected_in_credentials, actual)
예제 #13
0
    def test_default_get_client_username_credentials(self):
        expected = 'test-username'
        expected_in_credentials = 'test-credentials-username'

        request = HttpRequest()
        request.POST['username'] = expected
        credentials = {
            'username': expected_in_credentials
        }

        actual = get_client_username(request, credentials)

        self.assertEqual(expected_in_credentials, actual)
예제 #14
0
def filter_user_attempts(request: HttpRequest,
                         credentials: dict = None) -> QuerySet:
    """
    Return a queryset of AccessAttempts that match the given request and credentials.
    """

    username = get_client_username(request, credentials)
    ip_address = get_client_ip(request)
    user_agent = get_client_user_agent(request)

    filter_kwargs = get_filter_kwargs(username, ip_address, user_agent)

    return AccessAttempt.objects.filter(**filter_kwargs)
예제 #15
0
    def test_custom_get_client_username_from_credentials(self):
        provided = 'test-username'
        expected = 'prefixed-' + provided
        provided_in_credentials = 'test-credentials-username'
        expected_in_credentials = 'prefixed-' + provided_in_credentials

        request = HttpRequest()
        request.POST['username'] = provided
        credentials = {'username': provided_in_credentials}

        actual = get_client_username(request, credentials)

        self.assertEqual(expected_in_credentials, actual)
예제 #16
0
def is_user_lockable(request: HttpRequest, credentials: dict = None) -> bool:
    """
    Check if the given request or credentials refer to a whitelisted user object

    A whitelisted user has the magic ``nolockout`` property set.

    If the property is unknown or False or the user can not be found,
    this implementation fails gracefully and returns True.
    """

    username_field = getattr(get_user_model(), 'USERNAME_FIELD', 'username')
    username_value = get_client_username(request, credentials)
    kwargs = {username_field: username_value}

    UserModel = get_user_model()

    try:
        user = UserModel.objects.get(**kwargs)
        return not user.nolockout
    except (UserModel.DoesNotExist, AttributeError):
        pass

    return True
예제 #17
0
def _query_user_attempts(request):
    """Returns access attempt record if it exists.
    Otherwise return None.
    """
    ip = get_client_ip(request)
    username = get_client_username(request)

    if settings.AXES_ONLY_USER_FAILURES:
        attempts = AccessAttempt.objects.filter(username=username)
    elif settings.AXES_USE_USER_AGENT:
        ua = request.META.get('HTTP_USER_AGENT', '<unknown>')[:255]
        attempts = AccessAttempt.objects.filter(user_agent=ua,
                                                ip_address=ip,
                                                username=username,
                                                trusted=True)
    else:
        attempts = AccessAttempt.objects.filter(ip_address=ip,
                                                username=username,
                                                trusted=True)

    if not attempts:
        params = {'trusted': False}

        if settings.AXES_ONLY_USER_FAILURES:
            params['username'] = username
        elif settings.AXES_LOCK_OUT_BY_COMBINATION_USER_AND_IP:
            params['username'] = username
            params['ip_address'] = ip
        else:
            params['ip_address'] = ip

        if settings.AXES_USE_USER_AGENT and not settings.AXES_ONLY_USER_FAILURES:
            params['user_agent'] = ua

        attempts = AccessAttempt.objects.filter(**params)

    return attempts
예제 #18
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)
예제 #19
0
 def test_get_client_username(self):
     self.assertEqual(get_client_username(HttpRequest(), {}), 'example')
예제 #20
0
 def test_get_client_username_invalid_callable_too_many_arguments(self):
     with self.assertRaises(TypeError):
         get_client_username(HttpRequest(), {})
예제 #21
0
 def test_get_client_username_too_many_arguments_invalid_callable(self):
     with self.assertRaises(TypeError):
         actual = get_client_username(HttpRequest(), {})
예제 #22
0
 def test_get_client_username_invalid_callable_too_few_arguments(self):
     with self.assertRaises(TypeError):
         get_client_username(HttpRequest(), {})
예제 #23
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

        ip_address = get_client_ip(request)
        username = get_client_username(request, credentials)
        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):
            log.info('Login failed from whitelisted IP %s.', ip_address)
            return

        failures = 0
        attempts = get_user_attempts(request, credentials)
        cache_hash_key = get_cache_key(request, credentials)
        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 = 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, credentials)
        ):
            log.warning(
                'AXES: Locked out %s after repeated login failures.',
                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,
            )

            raise AxesSignalPermissionDenied('Locked out due to repeated login failures.')
예제 #24
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.')
예제 #25
0
 def test_get_client_username(self):
     self.assertEqual(get_client_username(HttpRequest(), {}), 'example')