Add auditable superuser mode (#824)

* Remove is_superuser everywhere

* Session handling

* List of sessions, relative timeout

* Absolute timeout

* Optionally pseudo-force audit comments

* Fix failing tests

* Add tests

* Add docs

* Rebsae migration

* Typos

* Fix tests
This commit is contained in:
Raphael Michel
2018-03-28 14:16:58 +02:00
committed by GitHub
parent 558c920181
commit a284e0c2f7
56 changed files with 965 additions and 130 deletions

View File

@@ -3,8 +3,10 @@ from importlib import import_module
from django.conf import settings
from django.core.urlresolvers import Resolver404, get_script_prefix, resolve
from django.db.models import Q
from django.utils.translation import get_language
from pretix.base.models.auth import StaffSession
from pretix.base.settings import GlobalSettingsObject
from ..helpers.i18n import get_javascript_format, get_moment_locale
@@ -93,10 +95,19 @@ def contextprocessor(request):
ctx['warning_update_check_active'] = False
gs = GlobalSettingsObject()
ctx['global_settings'] = gs.settings
if request.user.is_superuser:
if request.user.is_staff:
if gs.settings.update_check_result_warning:
ctx['warning_update_available'] = True
if not gs.settings.update_check_ack and 'runserver' not in sys.argv:
ctx['warning_update_check_active'] = True
if request.user.is_authenticated:
ctx['staff_session'] = request.user.has_active_staff_session(request.session.session_key)
ctx['staff_need_to_explain'] = (
StaffSession.objects.filter(user=request.user, date_end__isnull=False).filter(
Q(comment__isnull=True) | Q(comment="")
)
if request.user.is_staff and settings.PRETIX_ADMIN_AUDIT_COMMENTS else StaffSession.objects.none()
)
return ctx

View File

@@ -234,7 +234,7 @@ class OrderSearchFilterForm(OrderFilterForm):
def __init__(self, *args, **kwargs):
request = kwargs.pop('request')
super().__init__(*args, **kwargs)
if request.user.is_superuser:
if request.user.has_active_staff_session(request.session.session_key):
self.fields['organizer'].queryset = Organizer.objects.all()
else:
self.fields['organizer'].queryset = Organizer.objects.filter(
@@ -393,7 +393,7 @@ class EventFilterForm(FilterForm):
def __init__(self, *args, **kwargs):
request = kwargs.pop('request')
super().__init__(*args, **kwargs)
if request.user.is_superuser:
if request.user.has_active_staff_session(request.session.session_key):
self.fields['organizer'].queryset = Organizer.objects.all()
else:
self.fields['organizer'].queryset = Organizer.objects.filter(
@@ -583,9 +583,9 @@ class UserFilterForm(FilterForm):
qs = qs.filter(is_active=False)
if fdata.get('superuser') == 'yes':
qs = qs.filter(is_superuser=True)
qs = qs.filter(is_staff=True)
elif fdata.get('superuser') == 'no':
qs = qs.filter(is_superuser=False)
qs = qs.filter(is_staff=False)
if fdata.get('query'):
qs = qs.filter(

View File

@@ -8,6 +8,13 @@ from django.utils.translation import ugettext_lazy as _
from pytz import common_timezones
from pretix.base.models import User
from pretix.base.models.auth import StaffSession
class StaffSessionForm(forms.ModelForm):
class Meta:
model = StaffSession
fields = ['comment']
class UserEditForm(forms.ModelForm):
@@ -41,7 +48,7 @@ class UserEditForm(forms.ModelForm):
'email',
'require_2fa',
'is_active',
'is_superuser'
'is_staff'
]
def __init__(self, *args, **kwargs):

View File

@@ -4,12 +4,14 @@ from django.conf import settings
from django.contrib.auth import REDIRECT_FIELD_NAME, logout
from django.core.urlresolvers import get_script_prefix, resolve, reverse
from django.http import Http404
from django.shortcuts import redirect, resolve_url
from django.shortcuts import get_object_or_404, redirect, resolve_url
from django.utils.deprecation import MiddlewareMixin
from django.utils.encoding import force_str
from django.utils.translation import ugettext as _
from hijack.templatetags.hijack_tags import is_hijacked
from pretix.base.models import Event, Organizer
from pretix.base.models.auth import SuperuserPermissionSet, User
from pretix.helpers.security import (
SessionInvalid, SessionReauthRequired, assert_session_valid,
)
@@ -81,16 +83,52 @@ class PermissionMiddleware(MiddlewareMixin):
slug=url.kwargs['event'],
organizer__slug=url.kwargs['organizer'],
).select_related('organizer').first()
if not request.event or not request.user.has_event_permission(request.event.organizer, request.event):
if not request.event or not request.user.has_event_permission(request.event.organizer, request.event,
request=request):
raise Http404(_("The selected event was not found or you "
"have no permission to administrate it."))
request.organizer = request.event.organizer
request.eventpermset = request.user.get_event_permission_set(request.organizer, request.event)
if request.user.has_active_staff_session(request.session.session_key):
request.eventpermset = SuperuserPermissionSet()
else:
request.eventpermset = request.user.get_event_permission_set(request.organizer, request.event)
elif 'organizer' in url.kwargs:
request.organizer = Organizer.objects.filter(
slug=url.kwargs['organizer'],
).first()
if not request.organizer or not request.user.has_organizer_permission(request.organizer):
if not request.organizer or not request.user.has_organizer_permission(request.organizer, request=request):
raise Http404(_("The selected organizer was not found or you "
"have no permission to administrate it."))
request.orgapermset = request.user.get_organizer_permission_set(request.organizer)
if request.user.has_active_staff_session(request.session.session_key):
request.orgapermset = SuperuserPermissionSet()
else:
request.orgapermset = request.user.get_organizer_permission_set(request.organizer)
class AuditLogMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
if request.path.startswith(get_script_prefix() + 'control') and request.user.is_authenticated:
if is_hijacked(request):
hijack_history = request.session.get('hijack_history', False)
hijacker = get_object_or_404(User, pk=hijack_history[0])
ss = hijacker.get_active_staff_session(request.session.get('hijacker_session'))
if ss:
ss.logs.create(
url=request.path,
method=request.method,
impersonating=request.user
)
else:
ss = request.user.get_active_staff_session(request.session.session_key)
if ss:
ss.logs.create(
url=request.path,
method=request.method
)
response = self.get_response(request)
return response

View File

@@ -1,4 +1,7 @@
from django.core.exceptions import PermissionDenied
from django.shortcuts import redirect
from django.urls import reverse
from django.utils.http import urlquote
from django.utils.translation import ugettext as _
@@ -18,8 +21,7 @@ def event_permission_required(permission):
raise PermissionDenied()
allowed = (
request.user.is_superuser
or request.user.has_event_permission(request.organizer, request.event, permission)
request.user.has_event_permission(request.organizer, request.event, permission, request=request)
)
if allowed:
return function(request, *args, **kw)
@@ -57,10 +59,7 @@ def organizer_permission_required(permission):
# just a double check, should not ever happen
raise PermissionDenied()
allowed = (
request.user.is_superuser
or request.user.has_organizer_permission(request.organizer, permission)
)
allowed = request.user.has_organizer_permission(request.organizer, permission, request=request)
if allowed:
return function(request, *args, **kw)
@@ -85,14 +84,33 @@ class OrganizerPermissionRequiredMixin:
def administrator_permission_required():
"""
This view decorator rejects all requests with a 403 response which are not from
users with the is_superuser flag.
users with a current staff member session.
"""
def decorator(function):
def wrapper(request, *args, **kw):
if not request.user.is_authenticated: # NOQA
# just a double check, should not ever happen
raise PermissionDenied()
if not request.user.is_superuser:
if not request.user.has_active_staff_session(request.session.session_key):
if request.user.is_staff:
return redirect(reverse('control:user.sudo') + '?next=' + urlquote(request.path))
raise PermissionDenied(_('You do not have permission to view this content.'))
return function(request, *args, **kw)
return wrapper
return decorator
def staff_member_required():
"""
This view decorator rejects all requests with a 403 response which are not staff
members (but do not need to have an active session).
"""
def decorator(function):
def wrapper(request, *args, **kw):
if not request.user.is_authenticated: # NOQA
# just a double check, should not ever happen
raise PermissionDenied()
if not request.user.is_staff:
raise PermissionDenied(_('You do not have permission to view this content.'))
return function(request, *args, **kw)
return wrapper
@@ -108,3 +126,14 @@ class AdministratorPermissionRequiredMixin:
def as_view(cls, **initkwargs):
view = super(AdministratorPermissionRequiredMixin, cls).as_view(**initkwargs)
return administrator_permission_required()(view)
class StaffMemberRequiredMixin:
"""
This mixin is equivalent to the staff_memer_required view decorator but
is in a form suitable for class-based views.
"""
@classmethod
def as_view(cls, **initkwargs):
view = super(StaffMemberRequiredMixin, cls).as_view(**initkwargs)
return staff_member_required()(view)

View File

@@ -151,6 +151,22 @@
</li>
{% endfor %}
{% if request.user.is_staff and not staff_session %}
<li>
<form action="{% url 'control:user.sudo' %}?next={{ request.path|urlencode }}" method="post">
{% csrf_token %}
<button type="submit" class="btn btn-link" id="button-sudo">
<i class="fa fa-id-card"></i> {% trans "Admin mode" %}
</button>
</form>
</li>
{% elif request.user.is_staff and staff_session %}
<li>
<a href="{% url 'control:user.sudo.stop' %}" class="danger">
<i class="fa fa-id-card"></i> {% trans "End admin session" %}
</a>
</li>
{% endif %}
{% if warning_update_available %}
<li>
<a href="{% url 'control:global.update' %}" class="danger">
@@ -191,7 +207,7 @@
{% trans "Dashboard" %}
</a>
</li>
{% if request.user.is_superuser %}
{% if staff_session %}
<li>
<a href="{% url 'control:global.settings' %}"
{% if "global.settings" in url_name %}class="active"{% endif %}>
@@ -219,14 +235,21 @@
{% trans "Order search" %}
</a>
</li>
{% if request.user.is_superuser %}
{% if staff_session %}
<li>
<a href="{% url 'control:users' %}"
{% if "users" in url_name %}class="active"{% endif %}>
{% if "users" in url_name %}class="active"{% endif %}>
<i class="fa fa-user fa-fw"></i>
{% trans "Users" %}
</a>
</li>
<li>
<a href="{% url 'control:user.sudo.list' %}"
{% if "sudo" in url_name %}class="active"{% endif %}>
<i class="fa fa-id-card fa-fw"></i>
{% trans "Admin sessions" %}
</a>
</li>
{% endif %}
{% for nav in nav_global %}
<li>
@@ -260,6 +283,21 @@
</div>
</div>
</nav>
{% if staff_need_to_explain %}
<div class="impersonate-warning">
<span class="fa fa-id-card"></span>
{% blocktrans trimmed %}
Please leave a short comment on what you did in the following admin sessions:
{% endblocktrans %}
<ul>
{% for s in staff_need_to_explain %}
<li>
<a href="{% url "control:user.sudo.edit" id=s.pk %}">#{{ s.pk }}</a>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if request|is_hijacked %}
<div class="impersonate-warning">
<span class="fa fa-user-secret"></span>

View File

@@ -109,7 +109,7 @@
</div>
<div class="col-lg-2 col-sm-6 col-xs-12">
{% if log.user %}
{% if log.user.is_superuser %}
{% if log.user.is_staff %}
<span class="fa fa-id-card fa-danger fa-fw"
data-toggle="tooltip"
title="{% trans "This change was performed by a pretix administrator." %}">

View File

@@ -34,7 +34,7 @@
</div>
<div class="col-lg-2 col-sm-6 col-xs-12">
{% if log.user %}
{% if log.user.is_superuser %}
{% if log.user.is_staff %}
<span class="fa fa-id-card fa-danger fa-fw"
data-toggle="tooltip"
title="{% trans "This change was performed by a pretix administrator." %}">

View File

@@ -23,7 +23,7 @@
<div class="col-sm-4">
{% if plugin.app.compatibility_errors %}
<button class="btn disabled btn-block btn-default" disabled="disabled">{% trans "Incompatible" %}</button>
{% elif plugin.restricted and not request.user.is_superuser %}
{% elif plugin.restricted and not request.user.is_staff %}
<button class="btn disabled btn-block btn-default" disabled="disabled">{% trans "Not available" %}</button>
{% elif plugin.module in plugins_active %}
<button class="btn btn-default btn-block" name="plugin:{{ plugin.module }}" value="disable">{% trans "Disable" %}</button>
@@ -44,7 +44,7 @@
{% endblocktrans %}</p>
{% endif %}
<p>{{ plugin.description }}</p>
{% if plugin.restricted and not request.user.is_superuser %}
{% if plugin.restricted and not request.user.is_staff %}
<div class="alert alert-warning">
{% trans "This plugin needs to be enabled by a system administrator for your event." %}
</div>

View File

@@ -29,7 +29,7 @@
<div class="alert alert-danger">
{% trans "Every event needs to be created as part of an organizer account. Currently, you do not have access to any organizer accounts." %}
</div>
{% if request.user.is_superuser %}
{% if staff_session %}
<a href="{% url "control:organizers.add" %}" class="btn btn-default">
{% trans "Create a new organizer" %}
</a>

View File

@@ -6,7 +6,7 @@
<p class="meta">
<span class="fa fa-clock-o"></span> {{ log.datetime|date:"SHORT_DATETIME_FORMAT" }}
{% if log.user %}
{% if log.user.is_superuser %}
{% if log.user.is_staff %}
<span class="fa fa-id-card fa-danger fa-fw"
data-toggle="tooltip"
title="{% trans "This change was performed by a pretix administrator." %}">

View File

@@ -20,7 +20,7 @@
</button>
</div>
</form>
{% if request.user.is_superuser %}
{% if staff_session %}
<p>
<a href="{% url "control:organizers.add" %}" class="btn btn-default">
<span class="fa fa-plus"></span>

View File

@@ -0,0 +1,47 @@
{% extends "pretixcontrol/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Staff session" %}{% endblock %}
{% block content %}
<h1>{% trans "Session notes" %}</h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
{% bootstrap_form_errors form %}
{% bootstrap_field form.comment layout='horizontal' %}
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
<h1>{% trans "Audit log" %}</h1>
<dl class="dl-horizontal">
<dt>{% trans "Start date" %}</dt>
<dd>{{ session.date_start|date:"SHORT_DATETIME_FORMAT" }}</dd>
<dt>{% trans "End date" %}</dt>
<dd>{{ session.date_end|date:"SHORT_DATETIME_FORMAT" }}</dd>
<dt>{% trans "User" %}</dt>
<dd>{{ session.user.email }}</dd>
</dl>
<table class="table table-condensed">
<thead>
<tr>
<th>{% trans "Timestamp" %}</th>
<th>{% trans "Method" %}</th>
<th>{% trans "URL" %}</th>
<th>{% trans "On behalf of" %}</th>
</tr>
</thead>
<tbody>
{% for log in logs %}
<tr>
<td>{{ log.datetime|date:"SHORT_DATETIME_FORMAT" }}</td>
<td>{{ log.method }}</td>
<td>{{ log.url }}</td>
<td>{{ log.impersonating|default:"" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

View File

@@ -0,0 +1,58 @@
{% extends "pretixcontrol/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% load urlreplace %}
{% block title %}{% trans "Admin sessions" %}{% endblock %}
{% block content %}
<h1>{% trans "Admin sessions" %}</h1>
<table class="table table-condensed table-hover">
<thead>
<tr>
<th>
#
</th>
<th>
{% trans "User" %}
</th>
<th>
{% trans "Start date" %}
</th>
<th>{% trans "End date" %}</th>
<th>{% trans "Comment" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for s in sessions %}
<tr>
<td><strong>
<a href="{% url "control:user.sudo.edit" id=s.pk %}">{{ s.pk }}</a>
</strong></td>
<td><strong>
<a href="{% url "control:users.edit" id=s.user.pk %}">{{ s.user.email }}</a>
</strong></td>
<td>
{{ s.date_start|date:"SHORT_DATETIME_FORMAT" }}
</td>
<td>
{% if s.date_end %}
{{ s.date_end|date:"SHORT_DATETIME_FORMAT" }}
{% endif %}
</td>
<td>
{% if s.comment %}
<span class="fa fa-check"></span>
{% else %}
<span class="fa fa-times text-danger"></span>
{% endif %}
</td>
<td class="text-right">
<a href="{% url "control:user.sudo.edit" id=s.id %}" class="btn btn-default btn-sm"><i
class="fa fa-edit"></i></a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% include "pretixcontrol/pagination.html" %}
{% endblock %}

View File

@@ -0,0 +1,22 @@
{% extends "pretixcontrol/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Admin mode" %}{% endblock %}
{% block content %}
<h1>{% trans "Admin mode" %}</h1>
<p>
{% blocktrans trimmed %}
To perform this action, you need to start an administrative session. Everything you do in that session
will be logged and you will later be asked to fill in a comment on what you did in your session for later
reference.
{% endblocktrans %}
</p>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Start session" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -25,7 +25,7 @@
{% bootstrap_field form.fullname layout='control' %}
{% bootstrap_field form.locale layout='control' %}
{% bootstrap_field form.timezone layout='control' %}
{% bootstrap_field form.is_superuser layout='control' %}
{% bootstrap_field form.is_staff layout='control' %}
</fieldset>
<fieldset>
<legend>{% trans "Log-in settings" %}</legend>

View File

@@ -56,7 +56,7 @@
</strong></td>
<td>{{ u.fullname|default_if_none:"" }}</td>
<td>{% if u.is_active %}<span class="fa fa-check-circle"></span>{% endif %}</td>
<td>{% if u.is_superuser %}<span class="fa fa-check-circle"></span>{% endif %}</td>
<td>{% if u.is_staff %}<span class="fa fa-check-circle"></span>{% endif %}</td>
<td class="text-right">
<a href="{% url "control:users.edit" id=u.id %}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
</td>

View File

@@ -19,6 +19,10 @@ urlpatterns = [
url(r'^global/update/$', global_settings.UpdateCheckView.as_view(), name='global.update'),
url(r'^global/message/$', global_settings.MessageView.as_view(), name='global.message'),
url(r'^reauth/$', user.ReauthView.as_view(), name='user.reauth'),
url(r'^sudo/$', user.StartStaffSession.as_view(), name='user.sudo'),
url(r'^sudo/stop/$', user.StopStaffSession.as_view(), name='user.sudo.stop'),
url(r'^sudo/(?P<id>\d+)/$', user.EditStaffSession.as_view(), name='user.sudo.edit'),
url(r'^sudo/sessions/$', user.StaffSessionList.as_view(), name='user.sudo.list'),
url(r'^users/$', users.UserListView.as_view(), name='users'),
url(r'^users/select2$', typeahead.users_select2, name='users.select2'),
url(r'^users/add$', users.UserCreateView.as_view(), name='users.add'),

View File

@@ -248,12 +248,13 @@ def event_index(request, organizer, event):
for r, result in event_dashboard_widgets.send(sender=request.event, subevent=subevent):
widgets.extend(result)
can_change_orders = request.user.has_event_permission(request.organizer, request.event, 'can_change_orders')
can_change_orders = request.user.has_event_permission(request.organizer, request.event, 'can_change_orders',
request=request)
qs = request.event.logentry_set.all().select_related('user', 'content_type').order_by('-datetime')
qs = qs.exclude(action_type__in=OVERVIEW_BLACKLIST)
if not request.user.has_event_permission(request.organizer, request.event, 'can_view_orders'):
if not request.user.has_event_permission(request.organizer, request.event, 'can_view_orders', request=request):
qs = qs.exclude(content_type=ContentType.objects.get_for_model(Order))
if not request.user.has_event_permission(request.organizer, request.event, 'can_view_vouchers'):
if not request.user.has_event_permission(request.organizer, request.event, 'can_view_vouchers', request=request):
qs = qs.exclude(content_type=ContentType.objects.get_for_model(Voucher))
a_qs = request.event.requiredaction_set.filter(done=False)
@@ -271,7 +272,7 @@ def event_index(request, organizer, event):
return render(request, 'pretixcontrol/event/index.html', ctx)
def annotated_event_query(user):
def annotated_event_query(request):
active_orders = Order.objects.filter(
event=OuterRef('pk'),
status__in=[Order.STATUS_PENDING, Order.STATUS_PAID]
@@ -285,7 +286,7 @@ def annotated_event_query(user):
event=OuterRef('pk'),
done=False
)
qs = user.get_events_with_any_permission().annotate(
qs = request.user.get_events_with_any_permission(request).annotate(
order_count=Subquery(active_orders, output_field=IntegerField()),
has_ra=Exists(required_actions)
).annotate(
@@ -299,7 +300,7 @@ def annotated_event_query(user):
return qs
def widgets_for_event_qs(qs, user, nmax):
def widgets_for_event_qs(request, qs, user, nmax):
widgets = []
# Get set of events where we have the permission to show the # of orders
@@ -370,7 +371,7 @@ def widgets_for_event_qs(qs, user, nmax):
orders_text=ungettext('{num} order', '{num} orders', event.order_count or 0).format(
num=event.order_count or 0
)
) if user.is_superuser or event.pk in events_with_orders else ''
) if user.has_active_staff_session(request.session.session_key) or event.pk in events_with_orders else ''
),
daterange=dr,
status=status[1],
@@ -402,7 +403,8 @@ def user_index(request):
ctx = {
'widgets': rearrange(widgets),
'upcoming': widgets_for_event_qs(
annotated_event_query(request.user).filter(
request,
annotated_event_query(request).filter(
Q(has_subevents=False) &
Q(
Q(Q(date_to__isnull=True) & Q(date_from__gte=now()))
@@ -413,7 +415,8 @@ def user_index(request):
7
),
'past': widgets_for_event_qs(
annotated_event_query(request.user).filter(
request,
annotated_event_query(request).filter(
Q(has_subevents=False) &
Q(
Q(Q(date_to__isnull=True) & Q(date_from__lt=now()))
@@ -424,7 +427,8 @@ def user_index(request):
8
),
'series': widgets_for_event_qs(
annotated_event_query(request.user).filter(
request,
annotated_event_query(request).filter(
has_subevents=True
).order_by('-order_to'),
request.user,

View File

@@ -212,7 +212,7 @@ class EventPlugins(EventSettingsViewMixin, EventPermissionRequiredMixin, Templat
module = key.split(":")[1]
if value == "enable" and module in plugins_available:
if getattr(plugins_available[module], 'restricted', False):
if not request.user.is_superuser:
if not request.user.has_active_staff_session(request.session.session_key):
continue
if hasattr(plugins_available[module].app, 'installed'):
@@ -854,9 +854,11 @@ class EventLog(EventPermissionRequiredMixin, ListView):
def get_queryset(self):
qs = self.request.event.logentry_set.all().select_related('user', 'content_type').order_by('-datetime')
qs = qs.exclude(action_type__in=OVERVIEW_BLACKLIST)
if not self.request.user.has_event_permission(self.request.organizer, self.request.event, 'can_view_orders'):
if not self.request.user.has_event_permission(self.request.organizer, self.request.event, 'can_view_orders',
request=self.request):
qs = qs.exclude(content_type=ContentType.objects.get_for_model(Order))
if not self.request.user.has_event_permission(self.request.organizer, self.request.event, 'can_view_vouchers'):
if not self.request.user.has_event_permission(self.request.organizer, self.request.event, 'can_view_vouchers',
request=self.request):
qs = qs.exclude(content_type=ContentType.objects.get_for_model(Voucher))
if self.request.GET.get('user') == 'yes':

View File

@@ -8,7 +8,9 @@ from pretix.base.settings import GlobalSettingsObject
from pretix.control.forms.global_settings import (
GlobalSettingsForm, UpdateSettingsForm,
)
from pretix.control.permissions import AdministratorPermissionRequiredMixin
from pretix.control.permissions import (
AdministratorPermissionRequiredMixin, StaffMemberRequiredMixin,
)
class GlobalSettingsView(AdministratorPermissionRequiredMixin, FormView):
@@ -28,7 +30,7 @@ class GlobalSettingsView(AdministratorPermissionRequiredMixin, FormView):
return reverse('control:global.settings')
class UpdateCheckView(AdministratorPermissionRequiredMixin, FormView):
class UpdateCheckView(StaffMemberRequiredMixin, FormView):
template_name = 'pretixcontrol/global_update.html'
form_class = UpdateSettingsForm

View File

@@ -31,7 +31,7 @@ class EventList(PaginationMixin, ListView):
template_name = 'pretixcontrol/events/index.html'
def get_queryset(self):
qs = self.request.user.get_events_with_any_permission().select_related('organizer').prefetch_related(
qs = self.request.user.get_events_with_any_permission(self.request).select_related('organizer').prefetch_related(
'_settings_objects', 'organizer___settings_objects'
).order_by('-date_from')

View File

@@ -38,7 +38,7 @@ class OrganizerList(PaginationMixin, ListView):
qs = Organizer.objects.all()
if self.filter_form.is_valid():
qs = self.filter_form.filter_qs(qs)
if self.request.user.is_superuser:
if self.request.user.has_active_staff_session(self.request.session.session_key):
return qs
else:
return qs.filter(pk__in=self.request.user.teams.values_list('organizer', flat=True))
@@ -219,7 +219,7 @@ class OrganizerUpdate(OrganizerPermissionRequiredMixin, UpdateView):
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
if self.request.user.is_superuser:
if self.request.user.has_active_staff_session(self.request.session.session_key):
kwargs['domain'] = True
return kwargs
@@ -271,7 +271,7 @@ class OrganizerCreate(CreateView):
context_object_name = 'organizer'
def dispatch(self, request, *args, **kwargs):
if not request.user.is_superuser:
if not request.user.has_active_staff_session(self.request.session.session_key):
raise PermissionDenied() # TODO
return super().dispatch(request, *args, **kwargs)

View File

@@ -24,7 +24,7 @@ class OrderSearch(PaginationMixin, ListView):
def get_queryset(self):
qs = Order.objects.select_related('invoice_address')
if not self.request.user.is_superuser:
if not self.request.user.has_active_staff_session(self.request.session.session_key):
qs = qs.filter(
Q(event__organizer_id__in=self.request.user.teams.filter(
all_events=True, can_view_orders=True).values_list('organizer', flat=True))

View File

@@ -18,7 +18,7 @@ def event_list(request):
page = int(request.GET.get('page', '1'))
except ValueError:
page = 1
qs = request.user.get_events_with_any_permission().filter(
qs = request.user.get_events_with_any_permission(request).filter(
Q(name__icontains=i18ncomp(query)) | Q(slug__icontains=query) |
Q(organizer__name__icontains=i18ncomp(query)) | Q(organizer__slug__icontains=query)
).annotate(
@@ -107,7 +107,7 @@ def organizer_select2(request):
qs = Organizer.objects.all()
if term:
qs = qs.filter(Q(name__icontains=term) | Q(slug__icontains=term))
if not request.user.is_superuser:
if not request.user.has_active_staff_session(request.session.session_key):
qs = qs.filter(pk__in=request.user.teams.values_list('organizer', flat=True))
total = qs.count()
@@ -130,7 +130,7 @@ def organizer_select2(request):
def users_select2(request):
if not request.user.is_superuser:
if not request.user.has_active_staff_session(request.session.session_key):
raise PermissionDenied()
term = request.GET.get('query', '')

View File

@@ -12,8 +12,10 @@ from django.shortcuts import get_object_or_404, redirect
from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
from django.utils.http import is_safe_url
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from django.views.generic import FormView, TemplateView, UpdateView
from django.views import View
from django.views.generic import FormView, ListView, TemplateView, UpdateView
from django_otp.plugins.otp_static.models import StaticDevice
from django_otp.plugins.otp_totp.models import TOTPDevice
from u2flib_server import u2f
@@ -21,7 +23,12 @@ from u2flib_server.jsapi import DeviceRegistration
from pretix.base.forms.user import User2FADeviceAddForm, UserSettingsForm
from pretix.base.models import Event, NotificationSetting, U2FDevice, User
from pretix.base.models.auth import StaffSession
from pretix.base.notifications import get_all_notification_types
from pretix.control.forms.users import StaffSessionForm
from pretix.control.permissions import (
AdministratorPermissionRequiredMixin, StaffMemberRequiredMixin,
)
from pretix.control.views.auth import get_u2f_appid
REAL_DEVICE_TYPES = (TOTPDevice, U2FDevice)
@@ -472,3 +479,67 @@ class UserNotificationsEditView(TemplateView):
if self.event:
ctx['permset'] = self.request.user.get_event_permission_set(self.event.organizer, self.event)
return ctx
class StartStaffSession(StaffMemberRequiredMixin, RecentAuthenticationRequiredMixin, TemplateView):
template_name = 'pretixcontrol/user/staff_session_start.html'
def post(self, request, *args, **kwargs):
if not request.user.has_active_staff_session(request.session.session_key):
StaffSession.objects.create(
user=request.user,
session_key=request.session.session_key
)
if "next" in request.GET and is_safe_url(request.GET.get("next")):
return redirect(request.GET.get("next"))
else:
return redirect(reverse("control:index"))
class StopStaffSession(StaffMemberRequiredMixin, View):
def get(self, request, *args, **kwargs):
session = StaffSession.objects.filter(
date_end__isnull=True, session_key=request.session.session_key, user=request.user,
).first()
if not session:
return redirect(reverse("control:index"))
session.date_end = now()
session.save()
return redirect(reverse("control:user.sudo.edit", kwargs={'id': session.pk}))
class StaffSessionList(AdministratorPermissionRequiredMixin, ListView):
context_object_name = 'sessions'
template_name = 'pretixcontrol/user/staff_session_list.html'
paginate_by = 25
model = StaffSession
def get_queryset(self):
return StaffSession.objects.select_related('user').order_by('-date_start')
class EditStaffSession(StaffMemberRequiredMixin, UpdateView):
context_object_name = 'session'
template_name = 'pretixcontrol/user/staff_session_edit.html'
form_class = StaffSessionForm
def get_success_url(self):
return reverse("control:user.sudo.edit", kwargs={'id': self.object.pk})
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['logs'] = self.object.logs.select_related('impersonating')
return ctx
def form_valid(self, form):
messages.success(self.request, _('Your comment has been saved.'))
return super().form_valid(form)
def get_object(self, queryset=None):
if self.request.user.has_active_staff_session(self.request.session.session_key):
return get_object_or_404(StaffSession, pk=self.kwargs['id'])
else:
return get_object_or_404(StaffSession, pk=self.kwargs['id'], user=self.request.user)

View File

@@ -112,7 +112,9 @@ class UserImpersonateView(AdministratorPermissionRequiredMixin, RecentAuthentica
'other': self.kwargs.get("id"),
'other_email': self.object.email
})
oldkey = request.session.session_key
login_user(request, self.object)
request.session['hijacker_session'] = oldkey
return redirect(reverse('control:index'))
@@ -120,7 +122,14 @@ class UserImpersonateStopView(LoginRequiredMixin, View):
def post(self, request, *args, **kwargs):
impersonated = request.user
hijs = request.session['hijacker_session']
release_hijack(request)
ss = request.user.get_active_staff_session(hijs)
if ss:
request.session.save()
ss.session_key = request.session.session_key
ss.save()
request.user.log_action('pretix.control.auth.user.impersonate_stopped',
user=request.user,
data={

View File

@@ -48,7 +48,8 @@ class WaitingListView(EventPermissionRequiredMixin, PaginationMixin, ListView):
def post(self, request, *args, **kwargs):
if 'assign' in request.POST:
if not request.user.has_event_permission(request.organizer, request.event, 'can_change_orders'):
if not request.user.has_event_permission(request.organizer, request.event, 'can_change_orders',
request=request):
messages.error(request, _('You do not have permission to do this'))
return redirect(reverse('control:event.orders.waitinglist', kwargs={
'event': request.event.slug,