diff --git a/src/pretix/control/logdisplay.py b/src/pretix/control/logdisplay.py index d6a434dc7e..11ebd15ee6 100644 --- a/src/pretix/control/logdisplay.py +++ b/src/pretix/control/logdisplay.py @@ -458,6 +458,7 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs): 'pretix.user.settings.2fa.enabled': _('Two-factor authentication has been enabled.'), 'pretix.user.settings.2fa.disabled': _('Two-factor authentication has been disabled.'), 'pretix.user.settings.2fa.regenemergency': _('Your two-factor emergency codes have been regenerated.'), + 'pretix.user.settings.2fa.emergency': _('A two-factor emergency code has been generated.'), 'pretix.user.settings.2fa.device.added': _('A new two-factor authentication device "{name}" has been added to ' 'your account.'), 'pretix.user.settings.2fa.device.deleted': _('The two-factor authentication device "{name}" has been removed ' diff --git a/src/pretix/control/templates/pretixcontrol/users/form.html b/src/pretix/control/templates/pretixcontrol/users/form.html index 658eeeb939..6e0443b117 100644 --- a/src/pretix/control/templates/pretixcontrol/users/form.html +++ b/src/pretix/control/templates/pretixcontrol/users/form.html @@ -11,6 +11,12 @@ {% endif %} + {% if user.require_2fa %} +
+ {% csrf_token %} + +
+ {% endif %}
{% csrf_token %} diff --git a/src/pretix/control/urls.py b/src/pretix/control/urls.py index 877cc219c4..9960c31949 100644 --- a/src/pretix/control/urls.py +++ b/src/pretix/control/urls.py @@ -72,8 +72,9 @@ urlpatterns = [ re_path(r'^users/impersonate/stop', users.UserImpersonateStopView.as_view(), name='users.impersonate.stop'), re_path(r'^users/(?P\d+)/$', users.UserEditView.as_view(), name='users.edit'), re_path(r'^users/(?P\d+)/reset$', users.UserResetView.as_view(), name='users.reset'), - re_path(r'^users/(?P\d+)/impersonate', users.UserImpersonateView.as_view(), name='users.impersonate'), - re_path(r'^users/(?P\d+)/anonymize', users.UserAnonymizeView.as_view(), name='users.anonymize'), + re_path(r'^users/(?P\d+)/impersonate$', users.UserImpersonateView.as_view(), name='users.impersonate'), + re_path(r'^users/(?P\d+)/anonymize$', users.UserAnonymizeView.as_view(), name='users.anonymize'), + re_path(r'^users/(?P\d+)/emergencytoken$', users.UserEmergencyTokenView.as_view(), name='users.emergencytoken'), re_path(r'^pdf/editor/webfonts.css', pdf.FontsCSSView.as_view(), name='pdf.css'), re_path(r'^settings/?$', user.UserSettings.as_view(), name='user.settings'), re_path(r'^settings/history/$', user.UserHistoryView.as_view(), name='user.settings.history'), diff --git a/src/pretix/control/views/users.py b/src/pretix/control/views/users.py index b53dce156e..4e7fa2cdd3 100644 --- a/src/pretix/control/views/users.py +++ b/src/pretix/control/views/users.py @@ -30,10 +30,12 @@ from django.contrib.auth import ( from django.contrib.auth.mixins import LoginRequiredMixin from django.shortcuts import get_object_or_404, redirect from django.urls import reverse +from django.utils.crypto import get_random_string from django.utils.functional import cached_property from django.utils.translation import gettext_lazy as _ from django.views import View from django.views.generic import ListView, TemplateView +from django_otp.plugins.otp_static.models import StaticDevice from hijack import signals from pretix.base.auth import get_auth_backends @@ -150,6 +152,32 @@ class UserResetView(AdministratorPermissionRequiredMixin, RecentAuthenticationRe return reverse('control:users.edit', kwargs=self.kwargs) +class UserEmergencyTokenView(AdministratorPermissionRequiredMixin, RecentAuthenticationRequiredMixin, View): + + def get(self, request, *args, **kwargs): + return redirect(reverse('control:users.edit', kwargs=self.kwargs)) + + def post(self, request, *args, **kwargs): + self.object = get_object_or_404(User, pk=self.kwargs.get("id")) + + d, __ = StaticDevice.objects.get_or_create(user=self.object, name='emergency') + token = d.token_set.create(token=get_random_string(length=12, allowed_chars='1234567890')) + self.object.log_action('pretix.user.settings.2fa.emergency', user=self.request.user) + + messages.success(request, _( + 'The emergency token for this user is "{token}". It can only be used once. Please make sure to transmit ' + 'this code only over an authenticated channel (other than email, if possible). Any previous emergency ' + 'tokens for this user remain active.' + ).format( + token=token.token + )) + + return redirect(self.get_success_url()) + + def get_success_url(self): + return reverse('control:users.edit', kwargs=self.kwargs) + + class UserAnonymizeView(AdministratorPermissionRequiredMixin, RecentAuthenticationRequiredMixin, TemplateView): template_name = "pretixcontrol/users/anonymize.html" diff --git a/src/tests/control/test_permissions.py b/src/tests/control/test_permissions.py index 9e6a6f0fb6..dbb392a8f2 100644 --- a/src/tests/control/test_permissions.py +++ b/src/tests/control/test_permissions.py @@ -71,6 +71,8 @@ superuser_urls = [ "users/1/", "users/1/impersonate", "users/1/reset", + "users/1/anonymize", + "users/1/emergencytoken", "sudo/sessions/", ]