From 4ea4189e6d70173685a056cc7d6ddfafea368f5a Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Tue, 2 Apr 2024 17:15:16 +0200 Subject: [PATCH] Allow team admins to require two-factor authentication (#4034) * Allow team admins to require two-factor authentication * Add API tests * Improve logic * ADd button tooltip --- doc/admin/config.rst | 5 +- doc/api/resources/teams.rst | 7 ++ src/pretix/api/auth/permission.py | 15 ++++- src/pretix/api/serializers/organizer.py | 2 +- .../base/migrations/0259_team_require_2fa.py | 18 +++++ src/pretix/base/models/organizer.py | 6 ++ src/pretix/control/forms/organizer.py | 2 +- src/pretix/control/middleware.py | 17 ++--- .../pretixcontrol/organizers/team_edit.html | 1 + .../pretixcontrol/user/2fa_leaveteams.html | 30 +++++++++ .../pretixcontrol/user/2fa_main.html | 65 +++++++++++++++---- src/pretix/control/urls.py | 1 + src/pretix/control/views/user.py | 32 +++++++++ src/pretix/helpers/security.py | 24 +++++++ src/pretix/settings.py | 10 ++- src/tests/api/test_auth.py | 21 +++++- src/tests/api/test_teams.py | 6 +- src/tests/control/test_auth.py | 50 +++++++++++++- 18 files changed, 282 insertions(+), 30 deletions(-) create mode 100644 src/pretix/base/migrations/0259_team_require_2fa.py create mode 100644 src/pretix/control/templates/pretixcontrol/user/2fa_leaveteams.html diff --git a/doc/admin/config.rst b/doc/admin/config.rst index bc6169f1bf..5c83dda3e1 100644 --- a/doc/admin/config.rst +++ b/doc/admin/config.rst @@ -97,8 +97,9 @@ Example:: Defaults to ``off``. ``obligatory_2fa`` - Enables or disables obligatory usage of Two-Factor Authentication for users of the pretix backend. - Defaults to ``False`` + Enables or disables obligatory usage of two-factor authentication for users of the pretix backend. + Can be ``True`` to make two-factor authentication obligatory for all users or ``staff`` to make it only + obligatory to users with admin permissions. Defaults to ``False``. ``trust_x_forwarded_for`` Specifies whether the ``X-Forwarded-For`` header can be trusted. Only set to ``on`` if you have a reverse diff --git a/doc/api/resources/teams.rst b/doc/api/resources/teams.rst index 659d6441e4..f1aba2a5fa 100644 --- a/doc/api/resources/teams.rst +++ b/doc/api/resources/teams.rst @@ -22,6 +22,8 @@ id integer Internal ID of name string Team name all_events boolean Whether this team has access to all events limit_events list List of event slugs this team has access to +require_2fa boolean Whether members of this team are required to use + two-factor authentication can_create_events boolean can_change_teams boolean can_change_organizer_settings boolean @@ -122,6 +124,7 @@ Team endpoints "name": "Admin team", "all_events": true, "limit_events": [], + "require_2fa": true, "can_create_events": true, ... } @@ -159,6 +162,7 @@ Team endpoints "name": "Admin team", "all_events": true, "limit_events": [], + "require_2fa": true, "can_create_events": true, ... } @@ -186,6 +190,7 @@ Team endpoints "name": "Admin team", "all_events": true, "limit_events": [], + "require_2fa": true, "can_create_events": true, ... } @@ -203,6 +208,7 @@ Team endpoints "name": "Admin team", "all_events": true, "limit_events": [], + "require_2fa": true, "can_create_events": true, ... } @@ -246,6 +252,7 @@ Team endpoints "name": "Admin team", "all_events": true, "limit_events": [], + "require_2fa": true, "can_create_events": true, ... } diff --git a/src/pretix/api/auth/permission.py b/src/pretix/api/auth/permission.py index dc205312c4..e023a14956 100644 --- a/src/pretix/api/auth/permission.py +++ b/src/pretix/api/auth/permission.py @@ -39,7 +39,8 @@ from pretix.base.models import Device, Event, User from pretix.base.models.auth import SuperuserPermissionSet from pretix.base.models.organizer import TeamAPIToken from pretix.helpers.security import ( - SessionInvalid, SessionReauthRequired, assert_session_valid, + Session2FASetupRequired, SessionInvalid, SessionPasswordChangeRequired, + SessionReauthRequired, assert_session_valid, ) @@ -66,6 +67,10 @@ class EventPermission(BasePermission): return False except SessionReauthRequired: return False + except Session2FASetupRequired: + return False + except SessionPasswordChangeRequired: + return False perm_holder = (request.auth if isinstance(request.auth, (Device, TeamAPIToken)) else request.user) @@ -144,6 +149,10 @@ class ProfilePermission(BasePermission): return False except SessionReauthRequired: return False + except Session2FASetupRequired: + return False + except SessionPasswordChangeRequired: + return False if isinstance(request.auth, OAuthAccessToken): if not (request.auth.allow_scopes(['read']) or request.auth.allow_scopes(['profile'])) and request.method in SAFE_METHODS: @@ -166,5 +175,9 @@ class AnyAuthenticatedClientPermission(BasePermission): return False except SessionReauthRequired: return False + except Session2FASetupRequired: + return False + except SessionPasswordChangeRequired: + return False return True diff --git a/src/pretix/api/serializers/organizer.py b/src/pretix/api/serializers/organizer.py index 5fce6adee8..967d4452e9 100644 --- a/src/pretix/api/serializers/organizer.py +++ b/src/pretix/api/serializers/organizer.py @@ -239,7 +239,7 @@ class TeamSerializer(serializers.ModelSerializer): class Meta: model = Team fields = ( - 'id', 'name', 'all_events', 'limit_events', 'can_create_events', 'can_change_teams', + 'id', 'name', 'require_2fa', 'all_events', 'limit_events', 'can_create_events', 'can_change_teams', 'can_change_organizer_settings', 'can_manage_gift_cards', 'can_change_event_settings', 'can_change_items', 'can_view_orders', 'can_change_orders', 'can_view_vouchers', 'can_change_vouchers', 'can_checkin_orders', 'can_manage_customers', 'can_manage_reusable_media' diff --git a/src/pretix/base/migrations/0259_team_require_2fa.py b/src/pretix/base/migrations/0259_team_require_2fa.py new file mode 100644 index 0000000000..2ba7e5fb86 --- /dev/null +++ b/src/pretix/base/migrations/0259_team_require_2fa.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.10 on 2024-04-02 11:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("pretixbase", "0258_uniq_indx"), + ] + + operations = [ + migrations.AddField( + model_name="team", + name="require_2fa", + field=models.BooleanField(default=False), + ), + ] diff --git a/src/pretix/base/models/organizer.py b/src/pretix/base/models/organizer.py index 8edcc4031f..e45855bbe6 100644 --- a/src/pretix/base/models/organizer.py +++ b/src/pretix/base/models/organizer.py @@ -263,6 +263,12 @@ class Team(LoggedModel): members = models.ManyToManyField(User, related_name="teams", verbose_name=_("Team members")) all_events = models.BooleanField(default=False, verbose_name=_("All events (including newly created ones)")) limit_events = models.ManyToManyField('Event', verbose_name=_("Limit to events"), blank=True) + require_2fa = models.BooleanField( + default=False, verbose_name=_("Require all members of this team to use two-factor authentication"), + help_text=_("If you turn this on, all members of the team will be required to either set up two-factor " + "authentication or leave the team. The setting may take a few minutes to become effective for " + "all users.") + ) can_create_events = models.BooleanField( default=False, diff --git a/src/pretix/control/forms/organizer.py b/src/pretix/control/forms/organizer.py index cfdc1220a7..da5bf0266d 100644 --- a/src/pretix/control/forms/organizer.py +++ b/src/pretix/control/forms/organizer.py @@ -257,7 +257,7 @@ class TeamForm(forms.ModelForm): class Meta: model = Team - fields = ['name', 'all_events', 'limit_events', 'can_create_events', + fields = ['name', 'require_2fa', 'all_events', 'limit_events', 'can_create_events', 'can_change_teams', 'can_change_organizer_settings', 'can_manage_gift_cards', 'can_manage_customers', 'can_manage_reusable_media', diff --git a/src/pretix/control/middleware.py b/src/pretix/control/middleware.py index 7391def2b9..9a176945d1 100644 --- a/src/pretix/control/middleware.py +++ b/src/pretix/control/middleware.py @@ -48,7 +48,8 @@ from pretix.base.models import Event, Organizer from pretix.base.models.auth import SuperuserPermissionSet, User from pretix.helpers.http import redirect_to_url from pretix.helpers.security import ( - SessionInvalid, SessionReauthRequired, assert_session_valid, + Session2FASetupRequired, SessionInvalid, SessionPasswordChangeRequired, + SessionReauthRequired, assert_session_valid, ) @@ -84,6 +85,7 @@ class PermissionMiddleware: "user.settings.2fa.confirm.totp", "user.settings.2fa.confirm.webauthn", "user.settings.2fa.delete", + "user.settings.2fa.leaveteams", "auth.logout", "user.reauth" ) @@ -135,13 +137,12 @@ class PermissionMiddleware: except SessionReauthRequired: if url_name not in ('user.reauth', 'auth.logout'): return redirect_to_url(reverse('control:user.reauth') + '?next=' + quote(request.get_full_path())) - - if request.user.needs_password_change and url_name not in self.EXCEPTIONS_FORCED_PW_CHANGE: - return redirect_to_url(reverse('control:user.settings') + '?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 and not request.user.needs_password_change: - return redirect_to_url(reverse('control:user.settings.2fa')) + except SessionPasswordChangeRequired: + if url_name not in self.EXCEPTIONS_FORCED_PW_CHANGE: + return redirect_to_url(reverse('control:user.settings') + '?next=' + quote(request.get_full_path())) + except Session2FASetupRequired: + if url_name not in self.EXCEPTIONS_2FA: + return redirect_to_url(reverse('control:user.settings.2fa')) if 'event' in url.kwargs and 'organizer' in url.kwargs: if url.kwargs['organizer'] == '-' and url.kwargs['event'] == '-': diff --git a/src/pretix/control/templates/pretixcontrol/organizers/team_edit.html b/src/pretix/control/templates/pretixcontrol/organizers/team_edit.html index 70217e03eb..a5bcb8bd2b 100644 --- a/src/pretix/control/templates/pretixcontrol/organizers/team_edit.html +++ b/src/pretix/control/templates/pretixcontrol/organizers/team_edit.html @@ -18,6 +18,7 @@
{% trans "General information" %} {% bootstrap_field form.name layout="control" %} + {% bootstrap_field form.require_2fa layout="control" %}
{% trans "Organizer permissions" %} diff --git a/src/pretix/control/templates/pretixcontrol/user/2fa_leaveteams.html b/src/pretix/control/templates/pretixcontrol/user/2fa_leaveteams.html new file mode 100644 index 0000000000..16048c075f --- /dev/null +++ b/src/pretix/control/templates/pretixcontrol/user/2fa_leaveteams.html @@ -0,0 +1,30 @@ +{% extends "pretixcontrol/base.html" %} +{% load i18n %} +{% load bootstrap3 %} +{% block title %}{% trans "Two-factor authentication" %}{% endblock %} +{% block content %} +

{% trans "Leave teams that require two-factor authentication" %}

+
+ {% csrf_token %} +

+ {% trans "Do you really want to leave the following teams?" %} +

+
    + {% for t in obligatory_teams %} +
  • + {% blocktrans trimmed with team=t.name organizer=t.organizer.name %} + Team "{{ team }}" of organizer "{{ organizer }}" + {% endblocktrans %} +
  • + {% endfor %} +
+
+ + {% trans "Cancel" %} + + +
+
+{% endblock %} \ No newline at end of file diff --git a/src/pretix/control/templates/pretixcontrol/user/2fa_main.html b/src/pretix/control/templates/pretixcontrol/user/2fa_main.html index 2aa63a79cb..308c57851b 100644 --- a/src/pretix/control/templates/pretixcontrol/user/2fa_main.html +++ b/src/pretix/control/templates/pretixcontrol/user/2fa_main.html @@ -11,23 +11,55 @@ smartphone or a hardware token generator and that changes on a regular basis. {% endblocktrans %}

- {% if settings.PRETIX_OBLIGATORY_2FA %} + {% if obligatory and not user.require_2fa %}
-

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

+

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

+ {% if obligatory == "system" %} +

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

+ {% elif obligatory == "staff" %} +

+ {% trans "As an administrator, you need to use two-factor authentication." %} +

+ {% elif obligatory == "team" %} +

+ {% trans "You are part of one or more organizer teams that require you to use two-factor authentication." %} +

+
    + {% for t in obligatory_teams %} +
  • + {% blocktrans trimmed with team=t.name organizer=t.organizer.name %} + Team "{{ team }}" of organizer "{{ organizer }}" + {% endblocktrans %} +
  • + {% endfor %} +
+ {% endif %}

- {% 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 %} + {% if obligatory == "team" %} + + {% blocktrans trimmed count count=obligatory_teams|length %} + Leave team instead + {% plural %} + Leave {{ count }} teams instead + {% endblocktrans %} + + {% endif %}

- {% 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 %}
@@ -35,7 +67,18 @@

{% trans "Two-factor status" %}

- {% if not settings.PRETIX_OBLIGATORY_2FA %} + {% if obligatory %} + + {% else %} {% trans "Disable" %} @@ -73,7 +116,7 @@ {% for d in devices %}
  • + href="{% url "control:user.settings.2fa.delete" devicetype=d.devicetype device=d.pk %}"> Delete {% if d.devicetype == "totp" %} diff --git a/src/pretix/control/urls.py b/src/pretix/control/urls.py index 230a299f77..877cc219c4 100644 --- a/src/pretix/control/urls.py +++ b/src/pretix/control/urls.py @@ -95,6 +95,7 @@ urlpatterns = [ re_path(r'^settings/oauth/apps/(?P\d+)/roll$', oauth.OAuthApplicationRollView.as_view(), name='user.settings.oauth.app.roll'), re_path(r'^settings/2fa/$', user.User2FAMainView.as_view(), name='user.settings.2fa'), + re_path(r'^settings/2fa/leaveteams$', user.User2FALeaveTeamsView.as_view(), name='user.settings.2fa.leaveteams'), re_path(r'^settings/2fa/add$', user.User2FADeviceAddView.as_view(), name='user.settings.2fa.add'), re_path(r'^settings/2fa/enable', user.User2FAEnableView.as_view(), name='user.settings.2fa.enable'), re_path(r'^settings/2fa/disable', user.User2FADisableView.as_view(), name='user.settings.2fa.disable'), diff --git a/src/pretix/control/views/user.py b/src/pretix/control/views/user.py index a879fbe92d..dba35a3bb6 100644 --- a/src/pretix/control/views/user.py +++ b/src/pretix/control/views/user.py @@ -44,6 +44,7 @@ from django.conf import settings from django.contrib import messages from django.contrib.auth import update_session_auth_hash from django.contrib.contenttypes.models import ContentType +from django.db import transaction from django.shortcuts import get_object_or_404, redirect from django.urls import reverse from django.utils.crypto import get_random_string @@ -323,6 +324,15 @@ class User2FAMainView(RecentAuthenticationRequiredMixin, TemplateView): obj.devicetype = 'webauthn' ctx['devices'] += objs + ctx['obligatory'] = None + if settings.PRETIX_OBLIGATORY_2FA is True: + ctx['obligatory'] = 'system' + elif settings.PRETIX_OBLIGATORY_2FA == "staff" and self.request.user.is_staff: + ctx['obligatory'] = 'staff' + elif teams := self.request.user.teams.filter(require_2fa=True).select_related('organizer'): + ctx['obligatory'] = 'team' + ctx['obligatory_teams'] = teams + return ctx @@ -557,6 +567,28 @@ class User2FADeviceConfirmTOTPView(RecentAuthenticationRequiredMixin, TemplateVi })) +class User2FALeaveTeamsView(RecentAuthenticationRequiredMixin, TemplateView): + template_name = 'pretixcontrol/user/2fa_leaveteams.html' + + @transaction.atomic + def post(self, request, *args, **kwargs): + for team in self.request.user.teams.filter(require_2fa=True).select_related('organizer'): + team.members.remove(self.request.user) + team.log_action( + 'pretix.team.member.removed', user=self.request.user, data={ + 'email': self.request.user.email, + 'user': self.request.user.pk + } + ) + messages.success(request, _('You have left all teams that require two-factor authentication.')) + return redirect(reverse('control:user.settings.2fa')) + + def get_context_data(self, **kwargs): + ctx = super().get_context_data() + ctx['obligatory_teams'] = self.request.user.teams.filter(require_2fa=True).select_related('organizer') + return ctx + + class User2FAEnableView(RecentAuthenticationRequiredMixin, TemplateView): template_name = 'pretixcontrol/user/2fa_enable.html' diff --git a/src/pretix/helpers/security.py b/src/pretix/helpers/security.py index 9d300ade84..d8114697b1 100644 --- a/src/pretix/helpers/security.py +++ b/src/pretix/helpers/security.py @@ -41,6 +41,14 @@ class SessionReauthRequired(Exception): pass +class Session2FASetupRequired(Exception): + pass + + +class SessionPasswordChangeRequired(Exception): + pass + + def get_user_agent_hash(request): return hashlib.sha256(request.headers['User-Agent'].encode()).hexdigest() @@ -94,4 +102,20 @@ def assert_session_valid(request): request.session['pinned_country'] = country request.session['pretix_auth_last_used'] = int(time.time()) + + if request.user.needs_password_change: + raise SessionPasswordChangeRequired() + + force_2fa = not request.user.require_2fa and ( + settings.PRETIX_OBLIGATORY_2FA is True or + (settings.PRETIX_OBLIGATORY_2FA == "staff" and request.user.is_staff) or + cache.get_or_set( + f'user_2fa_team_{request.user.pk}', + lambda: request.user.teams.filter(require_2fa=True).exists(), + timeout=300 + ) + ) + if force_2fa: + raise Session2FASetupRequired() + return True diff --git a/src/pretix/settings.py b/src/pretix/settings.py index 0b34ca45e7..ab939cd8d8 100644 --- a/src/pretix/settings.py +++ b/src/pretix/settings.py @@ -181,7 +181,15 @@ PRETIX_REGISTRATION = config.getboolean('pretix', 'registration', fallback=False 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) + +_obligatory_2fa = config.get('pretix', 'obligatory_2fa', fallback="False") +_mapping = {'1': True, 'yes': True, 'true': True, 'on': True, '0': False, 'no': False, 'false': False, 'off': False, 'staff': 'staff'} +if _obligatory_2fa.lower() not in _mapping: + raise ImproperlyConfigured( + f"Value '{_obligatory_2fa}' not allowed for configuration key pretix.obligatory_2fa." + ) +PRETIX_OBLIGATORY_2FA = _mapping[_obligatory_2fa.lower()] + PRETIX_SESSION_TIMEOUT_RELATIVE = 3600 * 3 PRETIX_SESSION_TIMEOUT_ABSOLUTE = 3600 * 12 diff --git a/src/tests/api/test_auth.py b/src/tests/api/test_auth.py index 52f5f77377..2fb4cb32f9 100644 --- a/src/tests/api/test_auth.py +++ b/src/tests/api/test_auth.py @@ -23,7 +23,7 @@ import time import pytest from bs4 import BeautifulSoup -from django.test import Client +from django.test import Client, override_settings from tests.base import extract_form_fields from pretix.base.models import Organizer @@ -66,6 +66,25 @@ def test_session_auth_relative_timeout(client, user, team): assert resp.status_code == 403 +@pytest.mark.django_db +def test_session_auth_password_change_required(client, user, team): + client.login(email=user.email, password='dummy') + user.needs_password_change = True + user.save() + + resp = client.get('/api/v1/organizers/') + assert resp.status_code == 403 + + +@pytest.mark.django_db +@override_settings(PRETIX_OBLIGATORY_2FA=True) +def test_session_auth_2fa_setup_required(client, user, team): + client.login(email=user.email, password='dummy') + + resp = client.get('/api/v1/organizers/') + assert resp.status_code == 403 + + @pytest.mark.django_db def test_session_auth_csrf(user, team): team.members.add(user) diff --git a/src/tests/api/test_teams.py b/src/tests/api/test_teams.py index 0ebb69819f..71ce6fd2eb 100644 --- a/src/tests/api/test_teams.py +++ b/src/tests/api/test_teams.py @@ -41,7 +41,8 @@ TEST_TEAM_RES = { 'can_change_teams': True, 'can_change_organizer_settings': True, 'can_manage_gift_cards': True, 'can_manage_customers': True, 'can_manage_reusable_media': True, 'can_change_event_settings': True, 'can_change_items': True, 'can_view_orders': True, 'can_change_orders': True, - 'can_view_vouchers': True, 'can_change_vouchers': True, 'can_checkin_orders': False + 'can_view_vouchers': True, 'can_change_vouchers': True, 'can_checkin_orders': False, + 'require_2fa': False, } SECOND_TEAM_RES = { @@ -50,7 +51,8 @@ SECOND_TEAM_RES = { 'can_manage_customers': False, 'can_manage_reusable_media': False, 'can_change_teams': False, 'can_change_organizer_settings': False, 'can_manage_gift_cards': False, 'can_change_event_settings': False, 'can_change_items': False, 'can_view_orders': False, 'can_change_orders': False, - 'can_view_vouchers': False, 'can_change_vouchers': False, 'can_checkin_orders': False + 'can_view_vouchers': False, 'can_change_vouchers': False, 'can_checkin_orders': False, + 'require_2fa': False, } diff --git a/src/tests/control/test_auth.py b/src/tests/control/test_auth.py index 148e0f2cf4..e4cbec939c 100644 --- a/src/tests/control/test_auth.py +++ b/src/tests/control/test_auth.py @@ -49,7 +49,7 @@ from webauthn.authentication.verify_authentication_response import ( VerifiedAuthentication, ) -from pretix.base.models import U2FDevice, User +from pretix.base.models import Organizer, Team, U2FDevice, User from pretix.helpers import security @@ -935,18 +935,19 @@ def test_staff_session_require_staff(user, client): 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') + @override_settings(PRETIX_OBLIGATORY_2FA=True) def test_enabled_2fa_not_setup(self): response = self.client.get('/control/events/') assert response.status_code == 302 assert response.url == '/control/settings/2fa/' + @override_settings(PRETIX_OBLIGATORY_2FA=True) 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 @@ -956,6 +957,7 @@ class Obligatory2FATest(TestCase): assert response.status_code == 302 assert response.url == '/control/settings/2fa/' + @override_settings(PRETIX_OBLIGATORY_2FA=True) def test_enabled_2fa_setup_enabled(self): U2FDevice.objects.create(user=self.user, name='test', json_data="{}", confirmed=True) self.user.require_2fa = True @@ -964,6 +966,50 @@ class Obligatory2FATest(TestCase): response = self.client.get('/control/events/') assert response.status_code == 200 + @override_settings(PRETIX_OBLIGATORY_2FA="staff") + def test_staff_only(self): + self.user.require_2fa = False + self.user.save() + response = self.client.get('/control/events/') + assert response.status_code == 200 + + self.user.is_staff = True + self.user.save() + + response = self.client.get('/control/events/') + assert response.status_code == 302 + assert response.url == '/control/settings/2fa/' + + @override_settings(PRETIX_OBLIGATORY_2FA=False) + def test_by_team(self): + session = self.client.session + session['pretix_auth_long_session'] = True + session['pretix_auth_login_time'] = int(time.time()) + session['pretix_auth_last_used'] = int(time.time()) + session.save() + + organizer = Organizer.objects.create(name='Dummy', slug='dummy') + team = Team.objects.create(organizer=organizer, can_change_teams=True, name='Admin team') + team.members.add(self.user) + self.user.require_2fa = False + self.user.save() + response = self.client.get('/control/events/') + assert response.status_code == 200 + + team.require_2fa = True + team.save() + + response = self.client.get('/control/events/') + assert response.status_code == 302 + assert response.url == '/control/settings/2fa/' + + response = self.client.post('/control/settings/2fa/leaveteams') + assert response.status_code == 302 + assert team.members.count() == 0 + + response = self.client.get('/control/events/') + assert response.status_code == 200 + class PasswordChangeRequiredTest(TestCase): def setUp(self):