diff --git a/doc/admin/config.rst b/doc/admin/config.rst index 829dc172a..46bc1bf4a 100644 --- a/doc/admin/config.rst +++ b/doc/admin/config.rst @@ -78,6 +78,10 @@ Example:: Enables or disables nagging staff users for leaving comments on their sessions for auditability. Defaults to ``off``. +``obligatory_2fa`` + Enables or disables obligatory usage of Two-Factor Authentication for users of the pretix backend. + Defaults to ``False`` + Locale settings --------------- diff --git a/src/pretix/control/middleware.py b/src/pretix/control/middleware.py index 4eb26ed0b..a53de2e78 100644 --- a/src/pretix/control/middleware.py +++ b/src/pretix/control/middleware.py @@ -35,6 +35,19 @@ class PermissionMiddleware: "user.settings.notifications.off", ) + EXCEPTIONS_2FA = ( + "user.settings.2fa", + "user.settings.2fa.add", + "user.settings.2fa.enable", + "user.settings.2fa.disable", + "user.settings.2fa.regenemergency", + "user.settings.2fa.confirm.totp", + "user.settings.2fa.confirm.u2f", + "user.settings.2fa.delete", + "auth.logout", + "user.reauth" + ) + def __init__(self, get_response=None): self.get_response = get_response super().__init__() @@ -83,6 +96,10 @@ class PermissionMiddleware: if url_name not in ('user.reauth', 'auth.logout'): return redirect(reverse('control:user.reauth') + '?next=' + quote(request.get_full_path())) + if not request.user.require_2fa and settings.PRETIX_OBLIGATORY_2FA \ + and url_name not in self.EXCEPTIONS_2FA: + return redirect(reverse('control:user.settings.2fa')) + if 'event' in url.kwargs and 'organizer' in url.kwargs: with scope(organizer=None): request.event = Event.objects.filter( diff --git a/src/pretix/control/templates/pretixcontrol/user/2fa_main.html b/src/pretix/control/templates/pretixcontrol/user/2fa_main.html index 3fb117d67..b9db919b3 100644 --- a/src/pretix/control/templates/pretixcontrol/user/2fa_main.html +++ b/src/pretix/control/templates/pretixcontrol/user/2fa_main.html @@ -11,15 +11,35 @@ smartphone or a hardware token generator and that changes on a regular basis. {% endblocktrans %}

+ {% if settings.PRETIX_OBLIGATORY_2FA %} +
+
+

{% trans "Obligatory usage of two-factor authentication" %}

+
+
+

+ {% trans "This system enforces the usage of two-factor authentication!" %} +

+ {% if not devices %} +

{% trans "Please set up at least one device below." %}

+ {% elif not user.require_2fa %} +

{% trans "Please activate two-factor authentication using the button below." %}

+ {% endif %} +
+
+ + {% endif %} {% if user.require_2fa %}

{% trans "Two-factor status" %}

- - {% trans "Disable" %} - + {% if not settings.PRETIX_OBLIGATORY_2FA %} + + {% trans "Disable" %} + + {% endif %}

{% trans "Two-factor authentication is currently enabled." %}

diff --git a/src/pretix/settings.py b/src/pretix/settings.py index ecefc5244..2393691b0 100644 --- a/src/pretix/settings.py +++ b/src/pretix/settings.py @@ -120,6 +120,7 @@ PRETIX_REGISTRATION = config.getboolean('pretix', 'registration', fallback=True) PRETIX_PASSWORD_RESET = config.getboolean('pretix', 'password_reset', fallback=True) PRETIX_LONG_SESSIONS = config.getboolean('pretix', 'long_sessions', fallback=True) PRETIX_ADMIN_AUDIT_COMMENTS = config.getboolean('pretix', 'audit_comments', fallback=False) +PRETIX_OBLIGATORY_2FA = config.getboolean('pretix', 'obligatory_2fa', fallback=False) PRETIX_SESSION_TIMEOUT_RELATIVE = 3600 * 3 PRETIX_SESSION_TIMEOUT_ABSOLUTE = 3600 * 12 diff --git a/src/tests/control/test_auth.py b/src/tests/control/test_auth.py index 340668a2f..0d03935ef 100644 --- a/src/tests/control/test_auth.py +++ b/src/tests/control/test_auth.py @@ -769,3 +769,33 @@ def test_staff_session_require_staff(user, client): session.save() response = client.post('/control/sudo/') assert response.status_code == 403 + + +@override_settings(PRETIX_OBLIGATORY_2FA=True) +class Obligatory2FATest(TestCase): + def setUp(self): + super().setUp() + self.user = User.objects.create_user('demo@demo.dummy', 'demo') + self.client.login(email='demo@demo.dummy', password='demo') + + def test_enabled_2fa_not_setup(self): + response = self.client.get('/control/events/') + assert response.status_code == 302 + assert response.url == '/control/settings/2fa/' + + def test_enabled_2fa_setup_not_enabled(self): + U2FDevice.objects.create(user=self.user, name='test', json_data="{}", confirmed=True) + self.user.require_2fa = False + self.user.save() + + response = self.client.get('/control/events/') + assert response.status_code == 302 + assert response.url == '/control/settings/2fa/' + + def test_enabled_2fa_setup_enabled(self): + U2FDevice.objects.create(user=self.user, name='test', json_data="{}", confirmed=True) + self.user.require_2fa = True + self.user.save() + + response = self.client.get('/control/events/') + assert response.status_code == 200