From 7473220705fadda876e869cdbbe9e1b881252570 Mon Sep 17 00:00:00 2001 From: Markus Holtermann Date: Thu, 28 Jul 2016 01:40:40 +0200 Subject: [PATCH] Added admin themed views --- tests/test_admin.py | 11 +- two_factor/admin.py | 139 +++++++++++++++++- two_factor/templates/admin/base.html | 8 + .../templates/two_factor/admin/_style.html | 56 +++++++ .../two_factor/admin/_wizard_actions.html | 14 ++ .../two_factor/admin/_wizard_form_auth.html | 21 +++ .../two_factor/admin/_wizard_form_backup.html | 8 + .../admin/_wizard_form_generator.html | 8 + .../two_factor/admin/_wizard_form_method.html | 9 ++ .../admin/_wizard_form_phone_number.html | 8 + .../two_factor/admin/_wizard_form_token.html | 8 + .../admin/_wizard_form_validation.html | 8 + .../admin/_wizard_form_yubikey.html | 8 + .../two_factor/admin/backup_tokens.html | 33 +++++ .../templates/two_factor/admin/login.html | 65 ++++++++ .../templates/two_factor/admin/profile.html | 56 +++++++ .../templates/two_factor/admin/setup.html | 58 ++++++++ 17 files changed, 505 insertions(+), 13 deletions(-) create mode 100644 two_factor/templates/admin/base.html create mode 100644 two_factor/templates/two_factor/admin/_style.html create mode 100644 two_factor/templates/two_factor/admin/_wizard_actions.html create mode 100644 two_factor/templates/two_factor/admin/_wizard_form_auth.html create mode 100644 two_factor/templates/two_factor/admin/_wizard_form_backup.html create mode 100644 two_factor/templates/two_factor/admin/_wizard_form_generator.html create mode 100644 two_factor/templates/two_factor/admin/_wizard_form_method.html create mode 100644 two_factor/templates/two_factor/admin/_wizard_form_phone_number.html create mode 100644 two_factor/templates/two_factor/admin/_wizard_form_token.html create mode 100644 two_factor/templates/two_factor/admin/_wizard_form_validation.html create mode 100644 two_factor/templates/two_factor/admin/_wizard_form_yubikey.html create mode 100644 two_factor/templates/two_factor/admin/backup_tokens.html create mode 100644 two_factor/templates/two_factor/admin/login.html create mode 100644 two_factor/templates/two_factor/admin/profile.html create mode 100644 two_factor/templates/two_factor/admin/setup.html diff --git a/tests/test_admin.py b/tests/test_admin.py index 317c5b51e..50912ca8c 100644 --- a/tests/test_admin.py +++ b/tests/test_admin.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- -from django.conf import settings -from django.shortcuts import resolve_url +from django.core.urlresolvers import reverse from django.test import TestCase from django.test.utils import override_settings @@ -21,13 +20,13 @@ def tearDown(self): def test(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(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) @@ -54,13 +53,13 @@ def setUp(self): 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) + redirect_to = '%s?next=/otp_admin/' % reverse('admin:login') 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) + redirect_to = '%s?next=/otp_admin/' % reverse('admin:login') self.assertRedirects(response, redirect_to) def test_otp_admin_with_otp(self): diff --git a/two_factor/admin.py b/two_factor/admin.py index 60a46f714..2b7059b72 100644 --- a/two_factor/admin.py +++ b/two_factor/admin.py @@ -1,13 +1,116 @@ +from functools import update_wrapper + from django.conf import settings from django.contrib import admin from django.contrib.admin import AdminSite from django.contrib.auth import REDIRECT_FIELD_NAME from django.contrib.auth.views import redirect_to_login +from django.core.urlresolvers import reverse from django.shortcuts import resolve_url from django.utils.http import is_safe_url +from django.utils.translation import ugettext from .models import PhoneDevice from .utils import monkeypatch_method +from .views import BackupTokensView, LoginView, ProfileView, SetupView + + +class AdminLoginView(LoginView): + form_templates = { + 'auth': 'two_factor/admin/_wizard_form_auth.html', + 'token': 'two_factor/admin/_wizard_form_token.html', + 'backup': 'two_factor/admin/_wizard_form_backup.html', + } + redirect_url = 'admin:two_factor:setup' + template_name = 'two_factor/admin/login.html' + + def get_context_data(self, form, **kwargs): + context = super(AdminLoginView, self).get_context_data(form, **kwargs) + if self.kwargs['extra_context']: + context.update(self.kwargs['extra_context']) + user_is_validated = getattr(self.request.user, 'is_verified', None) + context.update({ + 'cancel_url': reverse('admin:index' if user_is_validated else 'admin:login'), + 'site_header': ugettext("Enable Two-Factor Authentication"), + 'wizard_form_template': self.form_templates.get(self.steps.current), + }) + return context + + def get_redirect_url(self): + redirect_to = self.request.GET.get(self.redirect_field_name, '') + url_is_safe = is_safe_url(url=redirect_to, host=self.request.get_host()) + if url_is_safe: + self.request.session[REDIRECT_FIELD_NAME] = redirect_to + user_is_validated = getattr(self.request.user, 'is_verified', None) + if not url_is_safe or not user_is_validated: + redirect_to = resolve_url(self.redirect_url) + return redirect_to + + +admin_login_view = AdminLoginView.as_view() + + +class AdminSetupView(SetupView): + form_templates = { + 'method': 'two_factor/admin/_wizard_form_method.html', + 'generator': 'two_factor/admin/_wizard_form_generator.html', + 'sms': 'two_factor/admin/_wizard_form_phone_number.html', + 'call': 'two_factor/admin/_wizard_form_phone_number.html', + 'validation': 'two_factor/admin/_wizard_form_validation.html', + 'yubikey': 'two_factor/admin/_wizard_form_yubikey.html', + } + redirect_url = 'admin:two_factor:profile' + template_name = 'two_factor/admin/setup.html' + + def get_context_data(self, form, **kwargs): + context = super(AdminSetupView, self).get_context_data(form, **kwargs) + user_is_validated = getattr(self.request.user, 'is_verified', None) + context.update({ + 'cancel_url': reverse('admin:two_factor:profile' if user_is_validated else 'admin:login'), + 'site_header': ugettext("Enable Two-Factor Authentication"), + 'title': ugettext("Enable Two-Factor Authentication"), + 'wizard_form_template': self.form_templates.get(self.steps.current), + }) + return context + + def get_redirect_url(self): + redirect_to = self.request.session.pop(REDIRECT_FIELD_NAME, '') + url_is_safe = is_safe_url(url=redirect_to, host=self.request.get_host()) + user_is_validated = self.request.user.is_verified() + if url_is_safe and user_is_validated: + return redirect_to + return super(AdminSetupView, self).get_redirect_url() + +admin_setup_view = AdminSetupView.as_view() + + +class AdminBackupTokensView(BackupTokensView): + redirect_url = 'admin:two_factor:backup_tokens' + template_name = 'two_factor/admin/backup_tokens.html' + + def get_context_data(self, **kwargs): + context = super(AdminBackupTokensView, self).get_context_data(**kwargs) + context.update({ + 'site_header': ugettext("Backup Tokens"), + 'title': ugettext("Backup Tokens"), + }) + return context + +admin_backup_tokens_view = AdminBackupTokensView.as_view() + + +class AdminProfileView(ProfileView): + template_name = 'two_factor/admin/profile.html' + + def get_context_data(self, **kwargs): + context = super(AdminProfileView, self).get_context_data(**kwargs) + context.update({ + 'site_header': ugettext("Account Security"), + 'title': ugettext("Account Security"), + }) + return context + +admin_profile_view = AdminProfileView.as_view() class AdminSiteOTPRequiredMixin(object): @@ -27,16 +130,38 @@ def has_permission(self, request): return False return request.user.is_verified() + def get_urls(self): + from django.conf.urls import include, url + + def wrap(view, cacheable=False): + def wrapper(*args, **kwargs): + return self.admin_view(view, cacheable)(*args, **kwargs) + wrapper.admin_site = self + return update_wrapper(wrapper, view) + + urlpatterns_2fa = [ + url(r'^profile/$', wrap(self.two_factor_profile), name='profile'), + url(r'^setup/$', self.two_factor_setup, name='setup'), + url(r'^backup/tokens/$', wrap(self.two_factor_backup_tokens), name='backup_tokens'), + ] + + urlpatterns = [ + url(r'^two_factor/', include(urlpatterns_2fa, namespace='two_factor')) + ] + urlpatterns += super(AdminSiteOTPRequiredMixin, self).get_urls() + return urlpatterns + def login(self, request, extra_context=None): - """ - Redirects to the site login page for the given HttpRequest. - """ - redirect_to = request.POST.get(REDIRECT_FIELD_NAME, request.GET.get(REDIRECT_FIELD_NAME)) + return admin_login_view(request, extra_context=extra_context) - if not redirect_to or not is_safe_url(url=redirect_to, host=request.get_host()): - redirect_to = resolve_url(settings.LOGIN_REDIRECT_URL) + def two_factor_profile(self, request): + return admin_profile_view(request) - return redirect_to_login(redirect_to) + def two_factor_setup(self, request): + return admin_setup_view(request) + + def two_factor_backup_tokens(self, request): + return admin_backup_tokens_view(request) class AdminSiteOTPRequired(AdminSiteOTPRequiredMixin, AdminSite): diff --git a/two_factor/templates/admin/base.html b/two_factor/templates/admin/base.html new file mode 100644 index 000000000..2ae0fd2cd --- /dev/null +++ b/two_factor/templates/admin/base.html @@ -0,0 +1,8 @@ +{% extends "admin/base.html" %} + +{% load i18n %} + +{% block userlinks %} +{% trans 'Two Factor' %} / +{{ block.super }} +{% endblock %} diff --git a/two_factor/templates/two_factor/admin/_style.html b/two_factor/templates/two_factor/admin/_style.html new file mode 100644 index 000000000..b7c7cb699 --- /dev/null +++ b/two_factor/templates/two_factor/admin/_style.html @@ -0,0 +1,56 @@ + diff --git a/two_factor/templates/two_factor/admin/_wizard_actions.html b/two_factor/templates/two_factor/admin/_wizard_actions.html new file mode 100644 index 000000000..8eb2674f2 --- /dev/null +++ b/two_factor/templates/two_factor/admin/_wizard_actions.html @@ -0,0 +1,14 @@ +{% load i18n %} + +{# hidden submit button to enable [enter] key #} +
+ +
+ {% trans "Cancel" %} + {% if wizard.steps.prev %} + + {% else %} + + {% endif %} + +
diff --git a/two_factor/templates/two_factor/admin/_wizard_form_auth.html b/two_factor/templates/two_factor/admin/_wizard_form_auth.html new file mode 100644 index 000000000..5187fdc36 --- /dev/null +++ b/two_factor/templates/two_factor/admin/_wizard_form_auth.html @@ -0,0 +1,21 @@ +{% load i18n %} +
+ {% if not form.this_is_the_login_form.errors %}{{ form.username.errors }}{% endif %} + {{ form.username }} +
+
+ {% if not form.this_is_the_login_form.errors %}{{ form.password.errors }}{% endif %} + {{ form.password }} + + +
+{% url 'admin_password_reset' as password_reset_url %} +{% if password_reset_url %} + +{% endif %} + + diff --git a/two_factor/templates/two_factor/admin/_wizard_form_backup.html b/two_factor/templates/two_factor/admin/_wizard_form_backup.html new file mode 100644 index 000000000..eb2a8c855 --- /dev/null +++ b/two_factor/templates/two_factor/admin/_wizard_form_backup.html @@ -0,0 +1,8 @@ +
+ {% if not form.this_is_the_login_form.errors %}{{ form.otp_token.errors }}{% endif %} + {{ form.otp_token }} +
+ + diff --git a/two_factor/templates/two_factor/admin/_wizard_form_generator.html b/two_factor/templates/two_factor/admin/_wizard_form_generator.html new file mode 100644 index 000000000..3af4bda26 --- /dev/null +++ b/two_factor/templates/two_factor/admin/_wizard_form_generator.html @@ -0,0 +1,8 @@ +
+ {% if not form.this_is_the_login_form.errors %}{{ form.token.errors }}{% endif %} + {{ form.token }} +
+ + diff --git a/two_factor/templates/two_factor/admin/_wizard_form_method.html b/two_factor/templates/two_factor/admin/_wizard_form_method.html new file mode 100644 index 000000000..9d07b86be --- /dev/null +++ b/two_factor/templates/two_factor/admin/_wizard_form_method.html @@ -0,0 +1,9 @@ +{% load i18n %} +
+ {% if not form.this_is_the_login_form.errors %}{{ form.method.errors }}{% endif %} + {{ form.method }} +
+ + diff --git a/two_factor/templates/two_factor/admin/_wizard_form_phone_number.html b/two_factor/templates/two_factor/admin/_wizard_form_phone_number.html new file mode 100644 index 000000000..6ea791219 --- /dev/null +++ b/two_factor/templates/two_factor/admin/_wizard_form_phone_number.html @@ -0,0 +1,8 @@ +
+ {% if not form.this_is_the_login_form.errors %}{{ form.number.errors }}{% endif %} + {{ form.number }} +
+ + diff --git a/two_factor/templates/two_factor/admin/_wizard_form_token.html b/two_factor/templates/two_factor/admin/_wizard_form_token.html new file mode 100644 index 000000000..715e1a643 --- /dev/null +++ b/two_factor/templates/two_factor/admin/_wizard_form_token.html @@ -0,0 +1,8 @@ +
+ {% if not form.this_is_the_login_form.errors %}{{ form.otp_token.errors }}{% endif %} + {{ form.otp_token }} +
+ + diff --git a/two_factor/templates/two_factor/admin/_wizard_form_validation.html b/two_factor/templates/two_factor/admin/_wizard_form_validation.html new file mode 100644 index 000000000..1300e4acf --- /dev/null +++ b/two_factor/templates/two_factor/admin/_wizard_form_validation.html @@ -0,0 +1,8 @@ +
+ {% if not form.this_is_the_login_form.errors %}{{ form.token.errors }}{% endif %} + {{ form.token }} +
+ + diff --git a/two_factor/templates/two_factor/admin/_wizard_form_yubikey.html b/two_factor/templates/two_factor/admin/_wizard_form_yubikey.html new file mode 100644 index 000000000..cd3d83c80 --- /dev/null +++ b/two_factor/templates/two_factor/admin/_wizard_form_yubikey.html @@ -0,0 +1,8 @@ +
+ {% if not form.this_is_the_login_form.errors %}{{ form.token.errors }}{% endif %} + {{ form.token }} +
+ + diff --git a/two_factor/templates/two_factor/admin/backup_tokens.html b/two_factor/templates/two_factor/admin/backup_tokens.html new file mode 100644 index 000000000..70a60d77f --- /dev/null +++ b/two_factor/templates/two_factor/admin/backup_tokens.html @@ -0,0 +1,33 @@ +{% extends "admin/login.html" %} + +{% load i18n %} + +{% block content %} +
+
{% csrf_token %} +
+ {% blocktrans %}Backup tokens can be used when your primary and backup phone numbers aren't available. The backup tokens below can be used for login verification. If you've used up all your backup tokens, you can generate a new set of backup tokens. Only the backup tokens shown below will be valid.{% endblocktrans %} +
+ +
+ {% if device.token_set.count %} +
    + {% for token in device.token_set.all %} +
  • {{ token.token }}
  • + {% endfor %} +
+

{% blocktrans %}Print these tokens and keep them somewhere safe.{% endblocktrans %}

+ {% else %} +

{% trans "You don't have any backup codes yet." %}

+ {% endif %} +
+ + + +
+ +
+{% endblock %} diff --git a/two_factor/templates/two_factor/admin/login.html b/two_factor/templates/two_factor/admin/login.html new file mode 100644 index 000000000..6fd804b5b --- /dev/null +++ b/two_factor/templates/two_factor/admin/login.html @@ -0,0 +1,65 @@ +{% extends "admin/login.html" %} + +{% load i18n admin_static two_factor %} + +{% block extrastyle %}{{ block.super }}{% include "two_factor/admin/_style.html" %}{% endblock %} + +{% block content %} +{% if form.errors and not form.non_field_errors and not form.this_is_the_login_form.errors %} +

+{% if form.errors.items|length == 1 %}{% trans "Please correct the error below." %}{% else %}{% trans "Please correct the errors below." %}{% endif %} +

+{% endif %} + +{% if form.non_field_errors or form.this_is_the_login_form.errors %} +{% for error in form.non_field_errors|add:form.this_is_the_login_form.errors %} +

+ {{ error }} +

+{% endfor %} +{% endif %} + +
+
{% csrf_token %} +
+ {% if wizard.steps.current == 'auth' %} + {% blocktrans %}Enter your credentials.{% endblocktrans %} + {% elif wizard.steps.current == 'token' %} + {% if device.method == 'call' %} + {% blocktrans %}We are calling your phone right now, please enter the digits you hear.{% endblocktrans %} + {% elif device.method == 'sms' %} + {% blocktrans %}We sent you a text message, please enter the tokens we sent.{% endblocktrans %} + {% else %} + {% blocktrans %}Please enter the tokens generated by your token generator.{% endblocktrans %} + {% endif %} + {% elif wizard.steps.current == 'backup' %} + {% blocktrans %}Use this form for entering backup tokens for logging in. These tokens have been generated for you to print and keep safe. Please enter one of these backup tokens to login to your account.{% endblocktrans %} + {% endif %} +
+ + {% include "two_factor/_wizard_forms.html" %} + + {% include "two_factor/admin/_wizard_actions.html" %} + + {% if other_devices %} +
+ {% trans "Or, alternatively, use one of your backup phones:" %}

+
+
+ {% for other in other_devices %} + {{ other|device_action }} + {% endfor %} +
+ {% endif %} + + {% if backup_tokens %} +
+ + +
+ {% endif %} + +
+ +
+{% endblock %} diff --git a/two_factor/templates/two_factor/admin/profile.html b/two_factor/templates/two_factor/admin/profile.html new file mode 100644 index 000000000..f98086bd8 --- /dev/null +++ b/two_factor/templates/two_factor/admin/profile.html @@ -0,0 +1,56 @@ +{% extends "admin/login.html" %} + +{% load i18n two_factor %} + +{% block content %} +
+
+ {% if default_device %} + {% if default_device_type == 'TOTPDevice' %} + {% blocktrans %}Tokens will be generated by your token generator.{% endblocktrans %} + {% elif default_device_type == 'PhoneDevice' %} + {% blocktrans with primary=default_device|device_action %}Primary method: {{ primary }}{% endblocktrans %} + {% elif default_device_type == 'RemoteYubikeyDevice' %} + {% blocktrans %}Tokens will be generated by your YubiKey.{% endblocktrans %} + {% endif %} + {% else %} +

{% blocktrans %}Two-factor authentication is not enabled for your account. Enable two-factor authentication for enhanced account security.{% endblocktrans %}

+

{% trans "Enable Two-Factor Authentication" %}

+ {% endif %} +
+ +
+

{% trans "Backup Phone Numbers" %}

+ TODO + {% comment %} +

{% blocktrans %}If your primary method is not available, we are able to send backup tokens to the phone numbers listed below.{% endblocktrans %}

+
    + {% for phone in backup_phones %} +
  • + {{ phone|device_action }} +
    + {% csrf_token %} + +
    +
  • + {% endfor %} +
+

{% trans "Add Phone Number" %}

+ {% endcomment %} +
+ +
+

{% trans "Backup Tokens" %}

+

+ {% blocktrans %}If you don't have any device with you, you can access your account using backup tokens.{% endblocktrans %} + {% blocktrans count counter=backup_tokens %}You have only one backup token remaining.{% plural %}You have {{ counter }} backup tokens remaining.{% endblocktrans %} +

+

{% trans "Show Codes" %}

+
+ + + +
+{% endblock %} diff --git a/two_factor/templates/two_factor/admin/setup.html b/two_factor/templates/two_factor/admin/setup.html new file mode 100644 index 000000000..97c771ac2 --- /dev/null +++ b/two_factor/templates/two_factor/admin/setup.html @@ -0,0 +1,58 @@ +{% extends "admin/login.html" %} + +{% load i18n admin_static two_factor %} + +{% block extrastyle %}{{ block.super }}{% include "two_factor/admin/_style.html" %}{% endblock %} + +{% block content %} +{% if form.errors and not form.non_field_errors and not form.this_is_the_login_form.errors %} +

+{% if form.errors.items|length == 1 %}{% trans "Please correct the error below." %}{% else %}{% trans "Please correct the errors below." %}{% endif %} +

+{% endif %} + +{% if form.non_field_errors or form.this_is_the_login_form.errors %} +{% for error in form.non_field_errors|add:form.this_is_the_login_form.errors %} +

+ {{ error }} +

+{% endfor %} +{% endif %} + +
+
{% csrf_token %} +
+ {% if wizard.steps.current == 'welcome' %} + {% blocktrans %}You are about to take your account security to the next level. Follow the steps in this wizard to enable two-factor authentication.{% endblocktrans %} + {% elif wizard.steps.current == 'method' %} + {% blocktrans %}Please select which authentication method you would like to use.{% endblocktrans %} + {% elif wizard.steps.current == 'generator' %} + {% blocktrans %}To start using a token generator, please use your smartphone to scan the QR code below. For example, use Google Authenticator. Then, enter the token generated by the app. {% endblocktrans %} + QR Code + {% elif wizard.steps.current == 'sms' %} + {% blocktrans %}Please enter the phone number you wish to receive the text messages on. This number will be validated in the next step. {% endblocktrans %} + {% elif wizard.steps.current == 'call' %} + {% blocktrans %}Please enter the phone number you wish to be called on. This number will be validated in the next step. {% endblocktrans %} + {% elif wizard.steps.current == 'validation' %} + {% if challenge_succeeded %} + {% if device.method == 'call' %} + {% blocktrans %}We are calling your phone right now, please enter the digits you hear.{% endblocktrans %} + {% elif device.method == 'sms' %} + {% blocktrans %}We sent you a text message, please enter the tokens we sent.{% endblocktrans %} + {% endif %} + {% else %} +
+ + {% include "two_factor/_wizard_forms.html" %} + + {% include "two_factor/admin/_wizard_actions.html" %} + +
+ +
+{% endblock %}