slightly refactor Factor View, add more unittests
This commit is contained in:
parent
0fa1fc86da
commit
37aeeea239
|
@ -39,35 +39,41 @@ class AuthenticationView(UserPassesTestMixin, View):
|
||||||
|
|
||||||
# Allow only not authenticated users to login
|
# Allow only not authenticated users to login
|
||||||
def test_func(self):
|
def test_func(self):
|
||||||
return self.request.user.is_authenticated is False
|
return AuthenticationView.SESSION_PENDING_USER in self.request.session
|
||||||
|
|
||||||
def handle_no_permission(self):
|
def handle_no_permission(self):
|
||||||
# Function from UserPassesTestMixin
|
# Function from UserPassesTestMixin
|
||||||
if 'next' in self.request.GET:
|
if 'next' in self.request.GET:
|
||||||
return redirect(self.request.GET.get('next'))
|
return redirect(self.request.GET.get('next'))
|
||||||
|
if self.request.user.is_authenticated:
|
||||||
return _redirect_with_qs('passbook_core:overview', self.request.GET)
|
return _redirect_with_qs('passbook_core:overview', self.request.GET)
|
||||||
|
return _redirect_with_qs('passbook_core:auth-login', self.request.GET)
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def get_pending_factors(self):
|
||||||
# Extract pending user from session (only remember uid)
|
"""Loading pending factors from Database or load from session variable"""
|
||||||
if AuthenticationView.SESSION_PENDING_USER in request.session:
|
|
||||||
self.pending_user = get_object_or_404(
|
|
||||||
User, id=self.request.session[AuthenticationView.SESSION_PENDING_USER])
|
|
||||||
else:
|
|
||||||
# No Pending user, redirect to login screen
|
|
||||||
return _redirect_with_qs('passbook_core:auth-login', request.GET)
|
|
||||||
# Write pending factors to session
|
# Write pending factors to session
|
||||||
if AuthenticationView.SESSION_PENDING_FACTORS in request.session:
|
if AuthenticationView.SESSION_PENDING_FACTORS in self.request.session:
|
||||||
self.pending_factors = request.session[AuthenticationView.SESSION_PENDING_FACTORS]
|
return self.request.session[AuthenticationView.SESSION_PENDING_FACTORS]
|
||||||
else:
|
|
||||||
# Get an initial list of factors which are currently enabled
|
# Get an initial list of factors which are currently enabled
|
||||||
# and apply to the current user. We check policies here and block the request
|
# and apply to the current user. We check policies here and block the request
|
||||||
_all_factors = Factor.objects.filter(enabled=True).order_by('order').select_subclasses()
|
_all_factors = Factor.objects.filter(enabled=True).order_by('order').select_subclasses()
|
||||||
self.pending_factors = []
|
pending_factors = []
|
||||||
for factor in _all_factors:
|
for factor in _all_factors:
|
||||||
policy_engine = PolicyEngine(factor.policies.all())
|
policy_engine = PolicyEngine(factor.policies.all())
|
||||||
policy_engine.for_user(self.pending_user).with_request(request).build()
|
policy_engine.for_user(self.pending_user).with_request(self.request).build()
|
||||||
if policy_engine.passing:
|
if policy_engine.passing:
|
||||||
self.pending_factors.append((factor.uuid.hex, factor.type))
|
pending_factors.append((factor.uuid.hex, factor.type))
|
||||||
|
return pending_factors
|
||||||
|
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
# Check if user passes test (i.e. SESSION_PENDING_USER is set)
|
||||||
|
user_test_result = self.get_test_func()()
|
||||||
|
if not user_test_result:
|
||||||
|
return self.handle_no_permission()
|
||||||
|
# Extract pending user from session (only remember uid)
|
||||||
|
self.pending_user = get_object_or_404(
|
||||||
|
User, id=self.request.session[AuthenticationView.SESSION_PENDING_USER])
|
||||||
|
self.pending_factors = self.get_pending_factors()
|
||||||
# Read and instantiate factor from session
|
# Read and instantiate factor from session
|
||||||
factor_uuid, factor_class = None, None
|
factor_uuid, factor_class = None, None
|
||||||
if AuthenticationView.SESSION_FACTOR not in request.session:
|
if AuthenticationView.SESSION_FACTOR not in request.session:
|
||||||
|
@ -107,11 +113,11 @@ class AuthenticationView(UserPassesTestMixin, View):
|
||||||
next_factor = None
|
next_factor = None
|
||||||
if self.pending_factors:
|
if self.pending_factors:
|
||||||
next_factor = self.pending_factors.pop()
|
next_factor = self.pending_factors.pop()
|
||||||
|
# Save updated pening_factor list to session
|
||||||
self.request.session[AuthenticationView.SESSION_PENDING_FACTORS] = \
|
self.request.session[AuthenticationView.SESSION_PENDING_FACTORS] = \
|
||||||
self.pending_factors
|
self.pending_factors
|
||||||
self.request.session[AuthenticationView.SESSION_FACTOR] = next_factor
|
self.request.session[AuthenticationView.SESSION_FACTOR] = next_factor
|
||||||
LOGGER.debug("Rendering Factor is %s", next_factor)
|
LOGGER.debug("Rendering Factor is %s", next_factor)
|
||||||
# return _redirect_with_qs('passbook_core:auth-process', kwargs={'factor': next_factor})
|
|
||||||
return _redirect_with_qs('passbook_core:auth-process', self.request.GET)
|
return _redirect_with_qs('passbook_core:auth-process', self.request.GET)
|
||||||
# User passed all factors
|
# User passed all factors
|
||||||
LOGGER.debug("User passed all factors, logging in")
|
LOGGER.debug("User passed all factors, logging in")
|
||||||
|
@ -126,7 +132,6 @@ class AuthenticationView(UserPassesTestMixin, View):
|
||||||
|
|
||||||
def _user_passed(self):
|
def _user_passed(self):
|
||||||
"""User Successfully passed all factors"""
|
"""User Successfully passed all factors"""
|
||||||
# user = authenticate(request=self.request, )
|
|
||||||
backend = self.request.session[AuthenticationView.SESSION_USER_BACKEND]
|
backend = self.request.session[AuthenticationView.SESSION_USER_BACKEND]
|
||||||
login(self.request, self.pending_user, backend=backend)
|
login(self.request, self.pending_user, backend=backend)
|
||||||
LOGGER.debug("Logged in user %s", self.pending_user)
|
LOGGER.debug("Logged in user %s", self.pending_user)
|
||||||
|
|
|
@ -0,0 +1,130 @@
|
||||||
|
"""passbook Core Authentication Test"""
|
||||||
|
import string
|
||||||
|
from random import SystemRandom
|
||||||
|
|
||||||
|
from django.contrib.auth.models import AnonymousUser
|
||||||
|
from django.contrib.sessions.middleware import SessionMiddleware
|
||||||
|
from django.test import RequestFactory, TestCase
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from passbook.core.auth.view import AuthenticationView
|
||||||
|
from passbook.core.models import DummyFactor, PasswordFactor, User
|
||||||
|
|
||||||
|
|
||||||
|
class TestFactorAuthentication(TestCase):
|
||||||
|
"""passbook Core Authentication Test"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.password = ''.join(SystemRandom().choice(
|
||||||
|
string.ascii_uppercase + string.digits) for _ in range(8))
|
||||||
|
self.factor, _ = PasswordFactor.objects.get_or_create(name='password',
|
||||||
|
slug='password',
|
||||||
|
backends=[])
|
||||||
|
self.user = User.objects.create_user(username='test',
|
||||||
|
email='test@test.test',
|
||||||
|
password=self.password)
|
||||||
|
|
||||||
|
def test_unauthenticated_raw(self):
|
||||||
|
"""test direct call to AuthenticationView"""
|
||||||
|
response = self.client.get(reverse('passbook_core:auth-process'))
|
||||||
|
# Response should be 302 since no pending user is set
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.url, reverse('passbook_core:auth-login'))
|
||||||
|
|
||||||
|
def test_unauthenticated_prepared(self):
|
||||||
|
"""test direct call but with pending_uesr in session"""
|
||||||
|
request = RequestFactory().get(reverse('passbook_core:auth-process'))
|
||||||
|
request.user = AnonymousUser()
|
||||||
|
request.session = {}
|
||||||
|
request.session[AuthenticationView.SESSION_PENDING_USER] = self.user.pk
|
||||||
|
|
||||||
|
response = AuthenticationView.as_view()(request)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_no_factors(self):
|
||||||
|
"""Test with all factors disabled"""
|
||||||
|
self.factor.enabled = False
|
||||||
|
self.factor.save()
|
||||||
|
request = RequestFactory().get(reverse('passbook_core:auth-process'))
|
||||||
|
request.user = AnonymousUser()
|
||||||
|
request.session = {}
|
||||||
|
request.session[AuthenticationView.SESSION_PENDING_USER] = self.user.pk
|
||||||
|
|
||||||
|
response = AuthenticationView.as_view()(request)
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.url, reverse('passbook_core:auth-denied'))
|
||||||
|
self.factor.enabled = True
|
||||||
|
self.factor.save()
|
||||||
|
|
||||||
|
def test_authenticated(self):
|
||||||
|
"""Test with already logged in user"""
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
response = self.client.get(reverse('passbook_core:auth-process'))
|
||||||
|
# Response should be 302 since no pending user is set
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.url, reverse('passbook_core:overview'))
|
||||||
|
self.client.logout()
|
||||||
|
|
||||||
|
def test_unauthenticated_post(self):
|
||||||
|
"""Test post request as unauthenticated user"""
|
||||||
|
request = RequestFactory().post(reverse('passbook_core:auth-process'), data={
|
||||||
|
'password': self.password
|
||||||
|
})
|
||||||
|
request.user = AnonymousUser()
|
||||||
|
middleware = SessionMiddleware()
|
||||||
|
middleware.process_request(request)
|
||||||
|
request.session.save()
|
||||||
|
request.session[AuthenticationView.SESSION_PENDING_USER] = self.user.pk
|
||||||
|
|
||||||
|
response = AuthenticationView.as_view()(request)
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.url, reverse('passbook_core:overview'))
|
||||||
|
self.client.logout()
|
||||||
|
|
||||||
|
def test_unauthenticated_post_invalid(self):
|
||||||
|
"""Test post request as unauthenticated user"""
|
||||||
|
request = RequestFactory().post(reverse('passbook_core:auth-process'), data={
|
||||||
|
'password': self.password + 'a'
|
||||||
|
})
|
||||||
|
request.user = AnonymousUser()
|
||||||
|
middleware = SessionMiddleware()
|
||||||
|
middleware.process_request(request)
|
||||||
|
request.session.save()
|
||||||
|
request.session[AuthenticationView.SESSION_PENDING_USER] = self.user.pk
|
||||||
|
|
||||||
|
response = AuthenticationView.as_view()(request)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.client.logout()
|
||||||
|
|
||||||
|
def test_multifactor(self):
|
||||||
|
"""Test view with multiple active factors"""
|
||||||
|
DummyFactor.objects.get_or_create(name='dummy',
|
||||||
|
slug='dummy',
|
||||||
|
order=1)
|
||||||
|
request = RequestFactory().post(reverse('passbook_core:auth-process'), data={
|
||||||
|
'password': self.password
|
||||||
|
})
|
||||||
|
request.user = AnonymousUser()
|
||||||
|
middleware = SessionMiddleware()
|
||||||
|
middleware.process_request(request)
|
||||||
|
request.session.save()
|
||||||
|
request.session[AuthenticationView.SESSION_PENDING_USER] = self.user.pk
|
||||||
|
|
||||||
|
response = AuthenticationView.as_view()(request)
|
||||||
|
session_copy = request.session.items()
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
# Verify view redirects to itself after auth
|
||||||
|
self.assertEqual(response.url, reverse('passbook_core:auth-process'))
|
||||||
|
|
||||||
|
# Run another request with same session which should result in a logged in user
|
||||||
|
request = RequestFactory().post(reverse('passbook_core:auth-process'))
|
||||||
|
request.user = AnonymousUser()
|
||||||
|
middleware = SessionMiddleware()
|
||||||
|
middleware.process_request(request)
|
||||||
|
for key, value in session_copy:
|
||||||
|
request.session[key] = value
|
||||||
|
request.session.save()
|
||||||
|
response = AuthenticationView.as_view()(request)
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.url, reverse('passbook_core:overview'))
|
Reference in New Issue