forked from CGM_Public/pretix_original
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
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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'] == '-':
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
<fieldset>
|
||||
<legend>{% trans "General information" %}</legend>
|
||||
{% bootstrap_field form.name layout="control" %}
|
||||
{% bootstrap_field form.require_2fa layout="control" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Organizer permissions" %}</legend>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
{% extends "pretixcontrol/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}{% trans "Two-factor authentication" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{% trans "Leave teams that require two-factor authentication" %}</h1>
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
<p>
|
||||
<strong>{% trans "Do you really want to leave the following teams?" %}</strong>
|
||||
</p>
|
||||
<ul>
|
||||
{% for t in obligatory_teams %}
|
||||
<li>
|
||||
{% blocktrans trimmed with team=t.name organizer=t.organizer.name %}
|
||||
Team "{{ team }}" of organizer "{{ organizer }}"
|
||||
{% endblocktrans %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="form-group submit-group">
|
||||
<a href="{% url "control:user.settings.2fa" %}" class="btn btn-default btn-cancel">
|
||||
{% trans "Cancel" %}
|
||||
</a>
|
||||
<button type="submit" class="btn btn-danger btn-save">
|
||||
{% trans "Leave" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -11,23 +11,55 @@
|
||||
smartphone or a hardware token generator and that changes on a regular basis.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% if settings.PRETIX_OBLIGATORY_2FA %}
|
||||
{% if obligatory and not user.require_2fa %}
|
||||
<div class="panel panel-warning">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{% trans "Obligatory usage of two-factor authentication" %}</h3>
|
||||
<h3 class="panel-title">
|
||||
<span class="fa fa-warning"></span>
|
||||
{% trans "Obligatory usage of two-factor authentication" %}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{% if obligatory == "system" %}
|
||||
<p>
|
||||
<strong>{% trans "This system enforces the usage of two-factor authentication!" %}</strong>
|
||||
</p>
|
||||
{% elif obligatory == "staff" %}
|
||||
<p>
|
||||
<strong>{% trans "As an administrator, you need to use two-factor authentication." %}</strong>
|
||||
</p>
|
||||
{% elif obligatory == "team" %}
|
||||
<p>
|
||||
<strong>{% trans "You are part of one or more organizer teams that require you to use two-factor authentication." %}</strong>
|
||||
</p>
|
||||
<ul>
|
||||
{% for t in obligatory_teams %}
|
||||
<li>
|
||||
{% blocktrans trimmed with team=t.name organizer=t.organizer.name %}
|
||||
Team "{{ team }}" of organizer "{{ organizer }}"
|
||||
{% endblocktrans %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
<p>
|
||||
<strong>{% trans "This system enforces the usage of two-factor authentication!" %}</strong>
|
||||
{% 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" %}
|
||||
<a href="{% url "control:user.settings.2fa.leaveteams" %}">
|
||||
{% blocktrans trimmed count count=obligatory_teams|length %}
|
||||
Leave team instead
|
||||
{% plural %}
|
||||
Leave {{ count }} teams instead
|
||||
{% endblocktrans %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</p>
|
||||
{% if not devices %}
|
||||
<p>{% trans "Please set up at least one device below." %}</p>
|
||||
{% elif not user.require_2fa %}
|
||||
<p>{% trans "Please activate two-factor authentication using the button below." %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
{% if user.require_2fa %}
|
||||
<div class="panel panel-success">
|
||||
@@ -35,7 +67,18 @@
|
||||
<h3 class="panel-title">{% trans "Two-factor status" %}</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{% if not settings.PRETIX_OBLIGATORY_2FA %}
|
||||
{% if obligatory %}
|
||||
<button disabled class="btn btn-primary pull-right flip" data-toggle="tooltip"
|
||||
title="{% spaceless %}{% 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." %}
|
||||
{% endif %}{% endspaceless %}">
|
||||
{% trans "Disable" %}
|
||||
</button>
|
||||
{% else %}
|
||||
<a href="{% url "control:user.settings.2fa.disable" %}" class="btn btn-primary pull-right flip">
|
||||
{% trans "Disable" %}
|
||||
</a>
|
||||
@@ -73,7 +116,7 @@
|
||||
{% for d in devices %}
|
||||
<li class="list-group-item">
|
||||
<a class="btn btn-danger btn-xs pull-right flip"
|
||||
href="{% url "control:user.settings.2fa.delete" devicetype=d.devicetype device=d.pk %}">
|
||||
href="{% url "control:user.settings.2fa.delete" devicetype=d.devicetype device=d.pk %}">
|
||||
Delete
|
||||
</a>
|
||||
{% if d.devicetype == "totp" %}
|
||||
|
||||
@@ -95,6 +95,7 @@ urlpatterns = [
|
||||
re_path(r'^settings/oauth/apps/(?P<pk>\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'),
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user