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 @@
- {% 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):