forked from CGM_Public/pretix_original
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:
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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." %}">
|
||||
|
||||
@@ -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." %}">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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." %}">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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', '')
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user