2019-02-22 23:22:11 +00:00
|
|
|
from random import choice
|
|
|
|
|
from string import ascii_letters, digits
|
|
|
|
|
from time import sleep
|
|
|
|
|
|
|
|
|
|
from django.contrib.auth import get_user_model
|
|
|
|
|
from django.http import HttpRequest
|
|
|
|
|
from django.test import TestCase
|
|
|
|
|
from django.urls import reverse
|
2019-03-03 19:56:57 +00:00
|
|
|
from django.utils.timezone import now
|
2019-02-22 23:22:11 +00:00
|
|
|
|
2019-02-25 20:54:40 +00:00
|
|
|
from axes.utils import reset
|
2019-02-22 23:22:11 +00:00
|
|
|
from axes.conf import settings
|
2019-03-03 19:56:57 +00:00
|
|
|
from axes.helpers import (
|
|
|
|
|
get_cache,
|
|
|
|
|
get_client_http_accept,
|
|
|
|
|
get_client_ip_address,
|
|
|
|
|
get_client_path_info,
|
|
|
|
|
get_client_user_agent,
|
|
|
|
|
get_cool_off,
|
|
|
|
|
get_credentials,
|
2019-06-12 20:43:44 +00:00
|
|
|
get_failure_limit,
|
2019-03-03 19:56:57 +00:00
|
|
|
)
|
|
|
|
|
from axes.models import AccessAttempt
|
2019-02-22 23:22:11 +00:00
|
|
|
|
|
|
|
|
|
2019-06-12 20:53:36 +00:00
|
|
|
def custom_failure_limit(request, credentials):
|
|
|
|
|
return 3
|
|
|
|
|
|
|
|
|
|
|
2019-02-22 23:22:11 +00:00
|
|
|
class AxesTestCase(TestCase):
|
|
|
|
|
"""
|
|
|
|
|
Test case using custom settings for testing.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
VALID_USERNAME = 'axes-valid-username'
|
|
|
|
|
VALID_PASSWORD = 'axes-valid-password'
|
|
|
|
|
VALID_EMAIL = 'axes-valid-email@example.com'
|
|
|
|
|
VALID_USER_AGENT = 'axes-user-agent'
|
|
|
|
|
VALID_IP_ADDRESS = '127.0.0.1'
|
|
|
|
|
|
|
|
|
|
INVALID_USERNAME = 'axes-invalid-username'
|
|
|
|
|
INVALID_PASSWORD = 'axes-invalid-password'
|
|
|
|
|
INVALID_EMAIL = 'axes-invalid-email@example.com'
|
|
|
|
|
|
|
|
|
|
LOCKED_MESSAGE = 'Account locked: too many login attempts.'
|
|
|
|
|
LOGOUT_MESSAGE = 'Logged out'
|
|
|
|
|
LOGIN_FORM_KEY = '<input type="submit" value="Log in" />'
|
|
|
|
|
|
2019-05-19 12:54:27 +00:00
|
|
|
STATUS_SUCCESS = 200
|
2019-02-22 23:22:11 +00:00
|
|
|
ALLOWED = 302
|
|
|
|
|
BLOCKED = 403
|
|
|
|
|
|
|
|
|
|
def setUp(self):
|
|
|
|
|
"""
|
|
|
|
|
Create a valid user for login.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
self.username = self.VALID_USERNAME
|
|
|
|
|
self.password = self.VALID_PASSWORD
|
|
|
|
|
self.email = self.VALID_EMAIL
|
|
|
|
|
|
|
|
|
|
self.ip_address = self.VALID_IP_ADDRESS
|
|
|
|
|
self.user_agent = self.VALID_USER_AGENT
|
|
|
|
|
self.path_info = reverse('admin:login')
|
|
|
|
|
|
|
|
|
|
self.user = get_user_model().objects.create_superuser(
|
|
|
|
|
username=self.username,
|
|
|
|
|
password=self.password,
|
|
|
|
|
email=self.email,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
self.request = HttpRequest()
|
|
|
|
|
self.request.method = 'POST'
|
|
|
|
|
self.request.META['REMOTE_ADDR'] = self.ip_address
|
|
|
|
|
self.request.META['HTTP_USER_AGENT'] = self.user_agent
|
|
|
|
|
self.request.META['PATH_INFO'] = self.path_info
|
|
|
|
|
|
2019-03-03 19:56:57 +00:00
|
|
|
self.request.axes_attempt_time = now()
|
|
|
|
|
self.request.axes_ip_address = get_client_ip_address(self.request)
|
|
|
|
|
self.request.axes_user_agent = get_client_user_agent(self.request)
|
|
|
|
|
self.request.axes_path_info = get_client_path_info(self.request)
|
|
|
|
|
self.request.axes_http_accept = get_client_http_accept(self.request)
|
|
|
|
|
|
2019-02-22 23:22:11 +00:00
|
|
|
self.credentials = get_credentials(self.username)
|
|
|
|
|
|
|
|
|
|
def tearDown(self):
|
2019-02-25 14:45:56 +00:00
|
|
|
get_cache().clear()
|
2019-02-22 23:22:11 +00:00
|
|
|
|
|
|
|
|
def get_kwargs_with_defaults(self, **kwargs):
|
|
|
|
|
defaults = {
|
|
|
|
|
'user_agent': self.user_agent,
|
|
|
|
|
'ip_address': self.ip_address,
|
|
|
|
|
'username': self.username,
|
|
|
|
|
'failures_since_start': 1,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
defaults.update(kwargs)
|
|
|
|
|
return defaults
|
|
|
|
|
|
|
|
|
|
def create_attempt(self, **kwargs):
|
|
|
|
|
return AccessAttempt.objects.create(**self.get_kwargs_with_defaults(**kwargs))
|
|
|
|
|
|
|
|
|
|
def reset(self, ip=None, username=None):
|
|
|
|
|
return reset(ip, username)
|
|
|
|
|
|
|
|
|
|
def login(self, is_valid_username=False, is_valid_password=False, **kwargs):
|
|
|
|
|
"""
|
|
|
|
|
Login a user.
|
|
|
|
|
|
|
|
|
|
A valid credential is used when is_valid_username is True,
|
|
|
|
|
otherwise it will use a random string to make a failed login.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
if is_valid_username:
|
|
|
|
|
username = self.VALID_USERNAME
|
|
|
|
|
else:
|
|
|
|
|
username = ''.join(
|
|
|
|
|
choice(ascii_letters + digits)
|
|
|
|
|
for _ in range(10)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if is_valid_password:
|
|
|
|
|
password = self.VALID_PASSWORD
|
|
|
|
|
else:
|
|
|
|
|
password = self.INVALID_PASSWORD
|
|
|
|
|
|
|
|
|
|
post_data = {
|
|
|
|
|
'username': username,
|
|
|
|
|
'password': password,
|
|
|
|
|
**kwargs
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return self.client.post(
|
|
|
|
|
reverse('admin:login'),
|
|
|
|
|
post_data,
|
|
|
|
|
REMOTE_ADDR=self.ip_address,
|
|
|
|
|
HTTP_USER_AGENT=self.user_agent,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def logout(self):
|
|
|
|
|
return self.client.post(
|
|
|
|
|
reverse('admin:logout'),
|
|
|
|
|
REMOTE_ADDR=self.ip_address,
|
|
|
|
|
HTTP_USER_AGENT=self.user_agent,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def check_login(self):
|
|
|
|
|
response = self.login(is_valid_username=True, is_valid_password=True)
|
|
|
|
|
self.assertNotContains(response, self.LOGIN_FORM_KEY, status_code=self.ALLOWED, html=True)
|
|
|
|
|
|
|
|
|
|
def almost_lockout(self):
|
2019-06-12 20:43:44 +00:00
|
|
|
for _ in range(1, get_failure_limit(None, None)):
|
2019-02-22 23:22:11 +00:00
|
|
|
response = self.login()
|
|
|
|
|
self.assertContains(response, self.LOGIN_FORM_KEY, html=True)
|
|
|
|
|
|
|
|
|
|
def lockout(self):
|
|
|
|
|
self.almost_lockout()
|
|
|
|
|
return self.login()
|
|
|
|
|
|
|
|
|
|
def check_lockout(self):
|
|
|
|
|
response = self.lockout()
|
2019-06-13 23:29:26 +00:00
|
|
|
if settings.AXES_LOCK_OUT_AT_FAILURE == True:
|
|
|
|
|
self.assertContains(response, self.LOCKED_MESSAGE, status_code=self.BLOCKED)
|
|
|
|
|
else:
|
|
|
|
|
self.assertNotContains(response, self.LOCKED_MESSAGE, status_code=self.STATUS_SUCCESS)
|
2019-02-22 23:22:11 +00:00
|
|
|
|
|
|
|
|
def cool_off(self):
|
|
|
|
|
sleep(get_cool_off().total_seconds())
|
|
|
|
|
|
|
|
|
|
def check_logout(self):
|
|
|
|
|
response = self.logout()
|
2019-05-19 12:54:27 +00:00
|
|
|
self.assertContains(response, self.LOGOUT_MESSAGE, status_code=self.STATUS_SUCCESS)
|
2019-02-22 23:22:11 +00:00
|
|
|
|
|
|
|
|
def check_handler(self):
|
|
|
|
|
"""
|
|
|
|
|
Check a handler and its basic functionality with lockouts, cool offs, login, and logout.
|
|
|
|
|
|
|
|
|
|
This is a check that is intended to successfully run for each and every new handler.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
self.check_lockout()
|
|
|
|
|
self.cool_off()
|
|
|
|
|
self.check_login()
|
|
|
|
|
self.check_logout()
|
|
|
|
|
|