-
-
Notifications
You must be signed in to change notification settings - Fork 449
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Redirect admin users to setup TOTP
When TOTP is required on an admin view and a user does not have a TOTP device configured, redirect them to the TOTP setup view.
- Loading branch information
1 parent
54170a4
commit 32db4b8
Showing
11 changed files
with
272 additions
and
88 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,3 +10,5 @@ example/settings_private.py | |
.eggs/ | ||
|
||
.idea/ | ||
|
||
venv/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,7 +14,6 @@ django-bootstrap-form | |
django-user-sessions | ||
|
||
# Testing | ||
|
||
coverage | ||
flake8 | ||
tox | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,7 @@ | ||
from django.conf import settings | ||
from django.shortcuts import resolve_url | ||
|
||
from unittest import mock | ||
|
||
from django.shortcuts import reverse | ||
from django.test import TestCase | ||
from django.test.utils import override_settings | ||
|
||
|
@@ -9,7 +11,7 @@ | |
|
||
|
||
@override_settings(ROOT_URLCONF='tests.urls_admin') | ||
class AdminPatchTest(TestCase): | ||
class TwoFactorAdminSiteTest(UserMixin, TestCase): | ||
|
||
def setUp(self): | ||
patch_admin() | ||
|
@@ -19,50 +21,98 @@ def tearDown(self): | |
|
||
def test(self): | ||
response = self.client.get('/admin/', follow=True) | ||
redirect_to = '%s?next=/admin/' % resolve_url(settings.LOGIN_URL) | ||
self.assertRedirects(response, redirect_to) | ||
|
||
@override_settings(LOGIN_URL='two_factor:login') | ||
def test_named_url(self): | ||
response = self.client.get('/admin/', follow=True) | ||
redirect_to = '%s?next=/admin/' % resolve_url(settings.LOGIN_URL) | ||
redirect_to = '%s?next=/admin/' % reverse('admin:login') | ||
self.assertRedirects(response, redirect_to) | ||
|
||
|
||
@override_settings(ROOT_URLCONF='tests.urls_admin') | ||
class AdminSiteTest(UserMixin, TestCase): | ||
|
||
def setUp(self): | ||
super().setUp() | ||
self.user = self.create_superuser() | ||
self.login_user() | ||
class AdminPatchTest(TestCase): | ||
""" | ||
otp_admin is admin console that needs OTP for access. | ||
Only admin users (is_staff and is_active) | ||
with OTP can access it. | ||
""" | ||
|
||
def test_anonymous_get_admin_index_redirects_to_admin_login(self): | ||
index_url = reverse('admin:index') | ||
login_url = reverse('admin:login') | ||
response = self.client.get(index_url, follow=True) | ||
redirect_to = '%s?next=%s' % (login_url, index_url) | ||
self.assertRedirects(response, redirect_to) | ||
|
||
def test_default_admin(self): | ||
response = self.client.get('/admin/') | ||
def test_anonymous_get_admin_logout_redirects_to_admin_index(self): | ||
# see: django.tests.admin_views.test_client_logout_url_can_be_used_to_login | ||
index_url = reverse('admin:index') | ||
logout_url = reverse('admin:logout') | ||
response = self.client.get(logout_url) | ||
self.assertEqual( | ||
response.status_code, 302 | ||
) | ||
self.assertEqual(response.get('Location'), index_url) | ||
|
||
def test_anonymous_get_admin_login(self): | ||
login_url = reverse('admin:login') | ||
response = self.client.get(login_url, follow=True) | ||
self.assertEqual(response.status_code, 200) | ||
|
||
|
||
@override_settings(ROOT_URLCONF='tests.urls_otp_admin') | ||
class OTPAdminSiteTest(UserMixin, TestCase): | ||
|
||
def setUp(self): | ||
super().setUp() | ||
def test_is_staff_not_verified_not_setup_get_admin_index_redirects_to_setup(self): | ||
""" | ||
admins without MFA setup should be redirected to the setup page. | ||
""" | ||
index_url = reverse('admin:index') | ||
setup_url = reverse('two_factor:setup') | ||
self.user = self.create_superuser() | ||
self.login_user() | ||
|
||
def test_otp_admin_without_otp(self): | ||
response = self.client.get('/otp_admin/', follow=True) | ||
redirect_to = '%s?next=/otp_admin/' % resolve_url(settings.LOGIN_URL) | ||
response = self.client.get(index_url, follow=True) | ||
redirect_to = '%s?next=%s' % (setup_url, index_url) | ||
self.assertRedirects(response, redirect_to) | ||
|
||
@override_settings(LOGIN_URL='two_factor:login') | ||
def test_otp_admin_without_otp_named_url(self): | ||
response = self.client.get('/otp_admin/', follow=True) | ||
redirect_to = '%s?next=/otp_admin/' % resolve_url(settings.LOGIN_URL) | ||
def test_is_staff_not_verified_not_setup_get_admin_login_redirects_to_setup(self): | ||
index_url = reverse('admin:index') | ||
login_url = reverse('admin:login') | ||
setup_url = reverse('two_factor:setup') | ||
self.user = self.create_superuser() | ||
self.login_user() | ||
response = self.client.get(login_url, follow=True) | ||
redirect_to = '%s?next=%s' % (setup_url, index_url) | ||
self.assertRedirects(response, redirect_to) | ||
|
||
def test_otp_admin_with_otp(self): | ||
self.enable_otp() | ||
def test_is_staff_is_verified_get_admin_index(self): | ||
index_url = reverse('admin:index') | ||
self.user = self.create_superuser() | ||
self.enable_otp(self.user) | ||
self.login_user() | ||
response = self.client.get('/otp_admin/') | ||
response = self.client.get(index_url) | ||
self.assertEqual(response.status_code, 200) | ||
|
||
def test_is_staff_is_verified_get_admin_password_change(self): | ||
password_change_url = reverse('admin:password_change') | ||
self.user = self.create_superuser() | ||
self.enable_otp(self.user) | ||
self.login_user() | ||
response = self.client.get(password_change_url) | ||
self.assertEqual(response.status_code, 200) | ||
|
||
def test_is_staff_is_verified_get_admin_login_redirects_to_admin_index(self): | ||
login_url = reverse('admin:login') | ||
index_url = reverse('admin:index') | ||
self.user = self.create_superuser() | ||
self.enable_otp(self.user) | ||
self.login_user() | ||
response = self.client.get(login_url) | ||
self.assertEqual(response.get('Location'), index_url) | ||
|
||
@mock.patch('two_factor.views.core.signals.user_verified.send') | ||
def test_valid_login(self, mock_signal): | ||
login_url = reverse('admin:login') | ||
self.user = self.create_user() | ||
self.enable_otp(self.user) | ||
data = {'auth-username': '[email protected]', | ||
'auth-password': 'secret', | ||
'login_view-current_step': 'auth'} | ||
response = self.client.post(login_url, data=data) | ||
self.assertEqual(response.status_code, 200) | ||
|
||
# No signal should be fired for non-verified user logins. | ||
self.assertFalse(mock_signal.called) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,9 @@ | ||
from django.contrib import admin | ||
from django.urls import path | ||
|
||
from two_factor.admin import TwoFactorAdminSite | ||
|
||
from .urls import urlpatterns | ||
|
||
urlpatterns += [ | ||
path('admin/', admin.site.urls), | ||
path('admin/', TwoFactorAdminSite().urls), | ||
] |
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.