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:
@@ -70,6 +70,10 @@ Example::
|
||||
that are used to print tax amounts in the customer currency on invoices for some currencies. Set to ``off`` to
|
||||
disable this feature. Defaults to ``on``.
|
||||
|
||||
``audit_comments``
|
||||
Enables or disables nagging staff users for leaving comments on their sessions for auditability.
|
||||
Defaults to ``off``.
|
||||
|
||||
|
||||
Locale settings
|
||||
---------------
|
||||
|
||||
@@ -16,4 +16,5 @@ Contents:
|
||||
settings
|
||||
background
|
||||
email
|
||||
permissions
|
||||
logging
|
||||
|
||||
@@ -31,6 +31,9 @@ Organizers and events
|
||||
.. autoclass:: pretix.base.models.Team
|
||||
:members:
|
||||
|
||||
.. autoclass:: pretix.base.models.TeamAPIToken
|
||||
:members:
|
||||
|
||||
.. autoclass:: pretix.base.models.RequiredAction
|
||||
:members:
|
||||
|
||||
|
||||
194
doc/development/implementation/permissions.rst
Normal file
194
doc/development/implementation/permissions.rst
Normal file
@@ -0,0 +1,194 @@
|
||||
Permissions
|
||||
===========
|
||||
|
||||
pretix uses a fine-grained permission system to control who is allowed to control what parts of the system.
|
||||
The central concept here is the concept of *Teams*. You can read more on `configuring teams and permissions <user-teams>`_
|
||||
and the :class:`pretix.base.models.Team` model in the respective parts of the documentation. The basic digest is:
|
||||
An organizer account can have any number of teams, and any number of users can be part of a team. A team can be
|
||||
assigned a set of permissions and connected to some or all of the events of the organizer.
|
||||
|
||||
A second way to access pretix is via the REST API, which allows authentication via tokens that are bound to a team,
|
||||
but not to a user. You can read more at :class:`pretix.base.models.TeamAPIToken`. This page will show you how to
|
||||
work with permissions in plugins and within the pretix code base.
|
||||
|
||||
Requiring permissions for a view
|
||||
--------------------------------
|
||||
|
||||
pretix provides a number of useful mixins and decorators that allow you to specify that a user needs a certain
|
||||
permission level to access a view::
|
||||
|
||||
from pretix.control.permissions import (
|
||||
OrganizerPermissionRequiredMixin, organizer_permission_required
|
||||
)
|
||||
|
||||
|
||||
class MyOrgaView(OrganizerPermissionRequiredMixin, View):
|
||||
permission = 'can_change_organizer_settings'
|
||||
# Only users with the permission ``can_change_organizer_settings`` on
|
||||
# this organizer can access this
|
||||
|
||||
|
||||
class MyOtherOrgaView(OrganizerPermissionRequiredMixin, View):
|
||||
permission = None
|
||||
# Only users with *any* permission on this organizer can access this
|
||||
|
||||
|
||||
@organizer_permission_required('can_change_organizer_settings')
|
||||
def my_orga_view(request, organizer, **kwargs):
|
||||
# Only users with the permission ``can_change_organizer_settings`` on
|
||||
# this organizer can access this
|
||||
|
||||
|
||||
@organizer_permission_required()
|
||||
def my_other_orga_view(request, organizer, **kwargs):
|
||||
# Only users with *any* permission on this organizer can access this
|
||||
|
||||
|
||||
Of course, the same is available on event level::
|
||||
|
||||
from pretix.control.permissions import (
|
||||
EventPermissionRequiredMixin, event_permission_required
|
||||
)
|
||||
|
||||
|
||||
class MyEventView(EventPermissionRequiredMixin, View):
|
||||
permission = 'can_change_event_settings'
|
||||
# Only users with the permission ``can_change_event_settings`` on
|
||||
# this event can access this
|
||||
|
||||
|
||||
class MyOtherEventView(EventPermissionRequiredMixin, View):
|
||||
permission = None
|
||||
# Only users with *any* permission on this event can access this
|
||||
|
||||
|
||||
@event_permission_required('can_change_event_settings')
|
||||
def my_event_view(request, organizer, **kwargs):
|
||||
# Only users with the permission ``can_change_event_settings`` on
|
||||
# this event can access this
|
||||
|
||||
|
||||
@event_permission_required()
|
||||
def my_other_event_view(request, organizer, **kwargs):
|
||||
# Only users with *any* permission on this event can access this
|
||||
|
||||
You can also require that this view is only accessible by system administrators with an active "admin session"
|
||||
(see below for what this means)::
|
||||
|
||||
from pretix.control.permissions import (
|
||||
AdministratorPermissionRequiredMixin, administrator_permission_required
|
||||
)
|
||||
|
||||
|
||||
class MyGlobalView(AdministratorPermissionRequiredMixin, View):
|
||||
# ...
|
||||
|
||||
|
||||
@administrator_permission_required
|
||||
def my_global_view(request, organizer, **kwargs):
|
||||
# ...
|
||||
|
||||
In rare cases it might also be useful to expose a feature only to people who have a staff account but do not
|
||||
necessarily have an active admin session::
|
||||
|
||||
from pretix.control.permissions import (
|
||||
StaffMemberRequiredMixin, staff_member_required
|
||||
)
|
||||
|
||||
|
||||
class MyGlobalView(StaffMemberRequiredMixin, View):
|
||||
# ...
|
||||
|
||||
|
||||
@staff_member_required
|
||||
def my_global_view(request, organizer, **kwargs):
|
||||
# ...
|
||||
|
||||
|
||||
|
||||
Requiring permissions in the REST API
|
||||
-------------------------------------
|
||||
|
||||
When creating your own ``viewset`` using Django REST framework, you just need to set the ``permission`` attribute
|
||||
and pretix will check it automatically for you::
|
||||
|
||||
class MyModelViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
permission = 'can_view_orders'
|
||||
|
||||
Checking permission in code
|
||||
---------------------------
|
||||
|
||||
If you need to work with permissions manually, there are a couple of useful helper methods on the :class:`pretix.base.models.Event`,
|
||||
:class:`pretix.base.models.User` and :class:`pretix.base.models.TeamAPIToken` classes. Here's a quick overview.
|
||||
|
||||
Return all users that are in any team that is connected to this event::
|
||||
|
||||
>>> event.get_users_with_any_permission()
|
||||
<QuerySet: …>
|
||||
|
||||
Return all users that are in a team with a specific permission for this event::
|
||||
|
||||
>>> event.get_users_with_permission('can_change_event_settings')
|
||||
<QuerySet: …>
|
||||
|
||||
Determine if a user has a certain permission for a specific event::
|
||||
|
||||
>>> user.has_event_permission(organizer, event, 'can_change_event_settings', request=request)
|
||||
True
|
||||
|
||||
Determine if a user has any permission for a specific event::
|
||||
|
||||
>>> user.has_event_permission(organizer, event, request=request)
|
||||
True
|
||||
|
||||
In the two previous commands, the ``request`` argument is optional, but required to support staff sessions (see below).
|
||||
|
||||
The same method exists for organizer-level permissions::
|
||||
|
||||
>>> user.has_organizer_permission(organizer, 'can_change_event_settings', request=request)
|
||||
True
|
||||
|
||||
Sometimes, it might be more useful to get the set of permissions at once::
|
||||
|
||||
>>> user.get_event_permission_set(organizer, event)
|
||||
{'can_change_event_settings', 'can_view_orders', 'can_change_orders'}
|
||||
|
||||
>>> user.get_organizer_permission_set(organizer, event)
|
||||
{'can_change_organizer_settings', 'can_create_events'}
|
||||
|
||||
Within a view on the ``/control`` subpath, the results of these two methods are already available in the
|
||||
``request.eventpermset`` and ``request.orgapermset`` properties. This makes it convenient to query them in templates::
|
||||
|
||||
{% if "can_change_orders" in request.eventpermset %}
|
||||
…
|
||||
{% endif %}
|
||||
|
||||
You can also do the reverse to get any events a user has access to::
|
||||
|
||||
>>> user.get_events_with_permission('can_change_event_settings', request=request)
|
||||
<QuerySet: …>
|
||||
|
||||
>>> user.get_events_with_any_permission(request=request)
|
||||
<QuerySet: …>
|
||||
|
||||
Most of these methods work identically on :class:`pretix.base.models.TeamAPIToken`.
|
||||
|
||||
Staff sessions
|
||||
--------------
|
||||
|
||||
.. versionchanged:: 1.14
|
||||
|
||||
In 1.14, the ``User.is_superuser`` attribute has been deprecated and statically set to return ``False``. Staff
|
||||
sessions have been newly introduced.
|
||||
|
||||
System administrators of a pretix instance are identified by the ``is_staff`` attribute on the user model. By default,
|
||||
the regular permission rules apply for users with ``is_staff = True``. The only difference is that such users can
|
||||
temporarily turn on "staff mode" via a button in the user interface that grants them **all permissions** as long as
|
||||
staff mode is active. You can check if a user is in staff mode using their session key:
|
||||
|
||||
>>> user.has_active_staff_session(request.session.session_key)
|
||||
False
|
||||
|
||||
Staff mode has a hard time limit and during staff mode, a middleware will log all requests made by that user. Later,
|
||||
the user is able to also save a message to comment on what they did in their administrative session. This feature is
|
||||
intended to help compliance with data protection rules as imposed e.g. by GDPR.
|
||||
@@ -1,6 +1,7 @@
|
||||
addon
|
||||
addons
|
||||
api
|
||||
auditability
|
||||
auth
|
||||
autobuild
|
||||
backend
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
.. _user-teams:
|
||||
|
||||
Teams
|
||||
=====
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ class OrganizerViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
|
||||
def get_queryset(self):
|
||||
if self.request.user.is_authenticated():
|
||||
if self.request.user.is_superuser:
|
||||
if self.request.user.has_active_staff_session(self.request.session.session_key):
|
||||
return Organizer.objects.all()
|
||||
else:
|
||||
return Organizer.objects.filter(pk__in=self.request.user.teams.values_list('organizer', flat=True))
|
||||
|
||||
@@ -12,7 +12,7 @@ class PretixBaseConfig(AppConfig):
|
||||
from . import exporters # NOQA
|
||||
from . import invoice # NOQA
|
||||
from . import notifications # NOQA
|
||||
from .services import export, mail, tickets, cart, orders, invoices, cleanup, update_check, quotas, notifications # NOQA
|
||||
from .services import auth, export, mail, tickets, cart, orders, invoices, cleanup, update_check, quotas, notifications # NOQA
|
||||
|
||||
try:
|
||||
from .celery_app import app as celery_app # NOQA
|
||||
|
||||
64
src/pretix/base/migrations/0087_auto_20180317_1952.py
Normal file
64
src/pretix/base/migrations/0087_auto_20180317_1952.py
Normal file
@@ -0,0 +1,64 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.10 on 2018-03-17 19:52
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def set_is_staff(apps, schema_editor):
|
||||
User = apps.get_model('pretixbase', 'User')
|
||||
User.objects.filter(is_superuser=True).update(is_staff=True)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('pretixbase', '0086_auto_20180320_1219'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
set_is_staff,
|
||||
migrations.RunPython.noop,
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='user',
|
||||
name='is_superuser',
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='StaffSession',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('date_start', models.DateTimeField(auto_now_add=True)),
|
||||
('date_end', models.DateTimeField(blank=True, null=True)),
|
||||
('session_key', models.CharField(max_length=255)),
|
||||
('comment', models.TextField()),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='StaffSessionAuditLog',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('datetime', models.DateTimeField(auto_now_add=True)),
|
||||
('url', models.CharField(max_length=255)),
|
||||
('session', models.ForeignKey(on_delete=models.deletion.CASCADE, related_name='logs', to='pretixbase.StaffSession')),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='staffsession',
|
||||
name='user',
|
||||
field=models.ForeignKey(default=None, on_delete=models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='staffsessionauditlog',
|
||||
name='impersonating',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='staffsessionauditlog',
|
||||
name='method',
|
||||
field=models.CharField(default='GET', max_length=255),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
@@ -19,7 +19,9 @@ from .orders import (
|
||||
cachedcombinedticket_name, cachedticket_name, generate_position_secret,
|
||||
generate_secret,
|
||||
)
|
||||
from .organizer import Organizer, Organizer_SettingsStore, Team, TeamInvite
|
||||
from .organizer import (
|
||||
Organizer, Organizer_SettingsStore, Team, TeamAPIToken, TeamInvite,
|
||||
)
|
||||
from .tax import TaxRule
|
||||
from .vouchers import Voucher
|
||||
from .waitinglist import WaitingListEntry
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Union
|
||||
from datetime import timedelta
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import (
|
||||
@@ -9,6 +9,7 @@ from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django_otp.models import Device
|
||||
|
||||
@@ -36,7 +37,6 @@ class UserManager(BaseUserManager):
|
||||
raise Exception("You must provide a password")
|
||||
user = self.model(email=email)
|
||||
user.is_staff = True
|
||||
user.is_superuser = True
|
||||
user.set_password(password)
|
||||
user.save()
|
||||
return user
|
||||
@@ -46,6 +46,11 @@ def generate_notifications_token():
|
||||
return get_random_string(length=32)
|
||||
|
||||
|
||||
class SuperuserPermissionSet:
|
||||
def __contains__(self, item):
|
||||
return True
|
||||
|
||||
|
||||
class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
"""
|
||||
This is the user model used by pretix for authentication.
|
||||
@@ -114,6 +119,10 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
def __str__(self):
|
||||
return self.email
|
||||
|
||||
@property
|
||||
def is_superuser(self):
|
||||
return False
|
||||
|
||||
def get_short_name(self) -> str:
|
||||
"""
|
||||
Returns the first of the following user properties that is found to exist:
|
||||
@@ -194,40 +203,36 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
))
|
||||
return self._teamcache['e{}'.format(event.pk)]
|
||||
|
||||
class SuperuserPermissionSet:
|
||||
def __contains__(self, item):
|
||||
return True
|
||||
|
||||
def get_event_permission_set(self, organizer, event) -> Union[set, SuperuserPermissionSet]:
|
||||
def get_event_permission_set(self, organizer, event) -> set:
|
||||
"""
|
||||
Gets a set of permissions (as strings) that a user holds for a particular event
|
||||
|
||||
:param organizer: The organizer of the event
|
||||
:param event: The event to check
|
||||
:return: set in case of a normal user and a SuperuserPermissionSet in case of a superuser (fake object where
|
||||
a in b always returns true).
|
||||
:return: set
|
||||
"""
|
||||
if self.is_superuser:
|
||||
return self.SuperuserPermissionSet()
|
||||
|
||||
teams = self._get_teams_for_event(organizer, event)
|
||||
return set.union(*[t.permission_set() for t in teams])
|
||||
sets = [t.permission_set() for t in teams]
|
||||
if sets:
|
||||
return set.union(*sets)
|
||||
else:
|
||||
return set()
|
||||
|
||||
def get_organizer_permission_set(self, organizer) -> Union[set, SuperuserPermissionSet]:
|
||||
def get_organizer_permission_set(self, organizer) -> set:
|
||||
"""
|
||||
Gets a set of permissions (as strings) that a user holds for a particular organizer
|
||||
|
||||
:param organizer: The organizer of the event
|
||||
:return: set in case of a normal user and a SuperuserPermissionSet in case of a superuser (fake object where
|
||||
a in b always returns true).
|
||||
:return: set
|
||||
"""
|
||||
if self.is_superuser:
|
||||
return self.SuperuserPermissionSet()
|
||||
|
||||
teams = self._get_teams_for_organizer(organizer)
|
||||
return set.union(*[t.permission_set() for t in teams])
|
||||
sets = [t.permission_set() for t in teams]
|
||||
if sets:
|
||||
return set.union(*sets)
|
||||
else:
|
||||
return set()
|
||||
|
||||
def has_event_permission(self, organizer, event, perm_name=None) -> bool:
|
||||
def has_event_permission(self, organizer, event, perm_name=None, request=None) -> bool:
|
||||
"""
|
||||
Checks if this user is part of any team that grants access of type ``perm_name``
|
||||
to the event ``event``.
|
||||
@@ -235,9 +240,10 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
:param organizer: The organizer of the event
|
||||
:param event: The event to check
|
||||
:param perm_name: The permission, e.g. ``can_change_teams``
|
||||
:param request: The current request (optional). Required to detect staff sessions properly.
|
||||
:return: bool
|
||||
"""
|
||||
if self.is_superuser:
|
||||
if request and self.has_active_staff_session(request.session.session_key):
|
||||
return True
|
||||
teams = self._get_teams_for_event(organizer, event)
|
||||
if teams:
|
||||
@@ -246,16 +252,17 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
return True
|
||||
return False
|
||||
|
||||
def has_organizer_permission(self, organizer, perm_name=None):
|
||||
def has_organizer_permission(self, organizer, perm_name=None, request=None):
|
||||
"""
|
||||
Checks if this user is part of any team that grants access of type ``perm_name``
|
||||
to the organizer ``organizer``.
|
||||
|
||||
:param organizer: The organizer to check
|
||||
:param perm_name: The permission, e.g. ``can_change_teams``
|
||||
:param request: The current request (optional). Required to detect staff sessions properly.
|
||||
:return: bool
|
||||
"""
|
||||
if self.is_superuser:
|
||||
if request and self.has_active_staff_session(request.session.session_key):
|
||||
return True
|
||||
teams = self._get_teams_for_organizer(organizer)
|
||||
if teams:
|
||||
@@ -263,15 +270,16 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_events_with_any_permission(self):
|
||||
def get_events_with_any_permission(self, request=None):
|
||||
"""
|
||||
Returns a queryset of events the user has any permissions to.
|
||||
|
||||
:param request: The current request (optional). Required to detect staff sessions properly.
|
||||
:return: Iterable of Events
|
||||
"""
|
||||
from .event import Event
|
||||
|
||||
if self.is_superuser:
|
||||
if request and self.has_active_staff_session(request.session.session_key):
|
||||
return Event.objects.all()
|
||||
|
||||
return Event.objects.filter(
|
||||
@@ -279,15 +287,16 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
| Q(id__in=self.teams.values_list('limit_events__id', flat=True))
|
||||
)
|
||||
|
||||
def get_events_with_permission(self, permission):
|
||||
def get_events_with_permission(self, permission, request=None):
|
||||
"""
|
||||
Returns a queryset of events the user has a specific permissions to.
|
||||
|
||||
:param request: The current request (optional). Required to detect staff sessions properly.
|
||||
:return: Iterable of Events
|
||||
"""
|
||||
from .event import Event
|
||||
|
||||
if self.is_superuser:
|
||||
if request and self.has_active_staff_session(request.session.session_key):
|
||||
return Event.objects.all()
|
||||
|
||||
kwargs = {permission: True}
|
||||
@@ -297,6 +306,56 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
| Q(id__in=self.teams.filter(**kwargs).values_list('limit_events__id', flat=True))
|
||||
)
|
||||
|
||||
def has_active_staff_session(self, session_key=None):
|
||||
"""
|
||||
Returns whether or not a user has an active staff session (formerly known as superuser session)
|
||||
with the given session key.
|
||||
"""
|
||||
return self.get_active_staff_session(session_key) is not None
|
||||
|
||||
def get_active_staff_session(self, session_key=None):
|
||||
if not self.is_staff:
|
||||
return None
|
||||
if not hasattr(self, '_staff_session_cache'):
|
||||
self._staff_session_cache = {}
|
||||
if session_key not in self._staff_session_cache:
|
||||
qs = StaffSession.objects.filter(
|
||||
user=self, date_end__isnull=True
|
||||
)
|
||||
if session_key:
|
||||
qs = qs.filter(session_key=session_key)
|
||||
sess = qs.first()
|
||||
if sess:
|
||||
if sess.date_start < now() - timedelta(seconds=settings.PRETIX_SESSION_TIMEOUT_ABSOLUTE):
|
||||
sess.date_end = now()
|
||||
sess.save()
|
||||
sess = None
|
||||
|
||||
self._staff_session_cache[session_key] = sess
|
||||
return self._staff_session_cache[session_key]
|
||||
|
||||
|
||||
class StaffSession(models.Model):
|
||||
user = models.ForeignKey('User')
|
||||
date_start = models.DateTimeField(auto_now_add=True)
|
||||
date_end = models.DateTimeField(null=True, blank=True)
|
||||
session_key = models.CharField(max_length=255)
|
||||
comment = models.TextField()
|
||||
|
||||
class Meta:
|
||||
ordering = ('date_start',)
|
||||
|
||||
|
||||
class StaffSessionAuditLog(models.Model):
|
||||
session = models.ForeignKey('StaffSession', related_name='logs')
|
||||
datetime = models.DateTimeField(auto_now_add=True)
|
||||
url = models.CharField(max_length=255)
|
||||
method = models.CharField(max_length=255)
|
||||
impersonating = models.ForeignKey('User', null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ('datetime',)
|
||||
|
||||
|
||||
class U2FDevice(Device):
|
||||
json_data = models.TextField()
|
||||
|
||||
@@ -554,9 +554,7 @@ class Event(EventMixin, LoggedModel):
|
||||
Q(all_events=True) | Q(limit_events__pk=self.pk)
|
||||
)
|
||||
|
||||
return User.objects.annotate(twp=Exists(team_with_perm)).filter(
|
||||
Q(is_superuser=True) | Q(twp=True)
|
||||
)
|
||||
return User.objects.annotate(twp=Exists(team_with_perm)).filter(twp=True)
|
||||
|
||||
def allow_delete(self):
|
||||
return not self.orders.exists() and not self.invoices.exists()
|
||||
|
||||
@@ -264,7 +264,7 @@ class TeamAPIToken(models.Model):
|
||||
"""
|
||||
return self.team.permission_set() if self.team.organizer == organizer else set()
|
||||
|
||||
def has_event_permission(self, organizer, event, perm_name=None) -> bool:
|
||||
def has_event_permission(self, organizer, event, perm_name=None, request=None) -> bool:
|
||||
"""
|
||||
Checks if this token is part of a team that grants access of type ``perm_name``
|
||||
to the event ``event``.
|
||||
@@ -272,6 +272,7 @@ class TeamAPIToken(models.Model):
|
||||
:param organizer: The organizer of the event
|
||||
:param event: The event to check
|
||||
:param perm_name: The permission, e.g. ``can_change_teams``
|
||||
:param request: This parameter is ignored and only defined for compatibility reasons.
|
||||
:return: bool
|
||||
"""
|
||||
has_event_access = (self.team.all_events and organizer == self.team.organizer) or (
|
||||
@@ -279,13 +280,14 @@ class TeamAPIToken(models.Model):
|
||||
)
|
||||
return has_event_access and (not perm_name or self.team.has_permission(perm_name))
|
||||
|
||||
def has_organizer_permission(self, organizer, perm_name=None):
|
||||
def has_organizer_permission(self, organizer, perm_name=None, request=None):
|
||||
"""
|
||||
Checks if this token is part of a team that grants access of type ``perm_name``
|
||||
to the organizer ``organizer``.
|
||||
|
||||
:param organizer: The organizer to check
|
||||
:param perm_name: The permission, e.g. ``can_change_teams``
|
||||
:param request: This parameter is ignored and only defined for compatibility reasons.
|
||||
:return: bool
|
||||
"""
|
||||
return organizer == self.team.organizer and (not perm_name or self.team.has_permission(perm_name))
|
||||
|
||||
19
src/pretix/base/services/auth.py
Normal file
19
src/pretix/base/services/auth.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from datetime import timedelta
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.models import Max, Q
|
||||
from django.dispatch import receiver
|
||||
from django.utils.timezone import now
|
||||
|
||||
from pretix.base.models.auth import StaffSession
|
||||
|
||||
from ..signals import periodic_task
|
||||
|
||||
|
||||
@receiver(signal=periodic_task)
|
||||
def close_inactive_staff_sessions(sender, **kwargs):
|
||||
StaffSession.objects.annotate(last_used=Max('logs__datetime')).filter(
|
||||
Q(last_used__lte=now() - timedelta(seconds=settings.PRETIX_SESSION_TIMEOUT_RELATIVE)) & Q(date_end__isnull=True)
|
||||
).update(
|
||||
date_end=now()
|
||||
)
|
||||
@@ -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
|
||||
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."))
|
||||
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,7 +235,7 @@
|
||||
{% 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 %}>
|
||||
@@ -227,6 +243,13 @@
|
||||
{% 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,
|
||||
|
||||
@@ -35,7 +35,7 @@ def control_nav_import(sender, request=None, **kwargs):
|
||||
@receiver(nav_organizer, dispatch_uid="payment_banktransfer_organav")
|
||||
def control_nav_orga_import(sender, request=None, **kwargs):
|
||||
url = resolve(request.path_info)
|
||||
if not request.user.has_organizer_permission(request.organizer, 'can_change_orders'):
|
||||
if not request.user.has_organizer_permission(request.organizer, 'can_change_orders', request=request):
|
||||
return []
|
||||
if not request.organizer.events.filter(plugins__icontains='pretix.plugins.banktransfer'):
|
||||
return []
|
||||
|
||||
@@ -488,7 +488,7 @@ class OrganizerActionView(OrganizerBanktransferView, OrganizerPermissionRequired
|
||||
def order_qs(self):
|
||||
all = self.request.user.teams.filter(organizer=self.request.organizer, can_change_orders=True,
|
||||
can_view_orders=True, all_events=True).exists()
|
||||
if self.request.user.is_superuser or all:
|
||||
if self.request.user.has_active_staff_session(self.request.session.session_key) or all:
|
||||
return Order.objects.filter(event__organizer=self.request.organizer)
|
||||
else:
|
||||
return Order.objects.filter(
|
||||
|
||||
@@ -15,7 +15,7 @@ from pretix.control.signals import nav_event
|
||||
@receiver(nav_event, dispatch_uid="pretixdroid_nav")
|
||||
def control_nav_import(sender, request=None, **kwargs):
|
||||
url = resolve(request.path_info)
|
||||
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):
|
||||
return []
|
||||
return [
|
||||
{
|
||||
|
||||
@@ -9,7 +9,7 @@ from pretix.control.signals import nav_event
|
||||
@receiver(nav_event, dispatch_uid="sendmail_nav")
|
||||
def control_nav_import(sender, request=None, **kwargs):
|
||||
url = resolve(request.path_info)
|
||||
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):
|
||||
return []
|
||||
return [
|
||||
{
|
||||
|
||||
@@ -9,7 +9,7 @@ from pretix.control.signals import nav_event
|
||||
@receiver(nav_event, dispatch_uid="statistics_nav")
|
||||
def control_nav_import(sender, request=None, **kwargs):
|
||||
url = resolve(request.path_info)
|
||||
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):
|
||||
return []
|
||||
return [
|
||||
{
|
||||
|
||||
@@ -71,7 +71,7 @@ def _detect_event(request, require_live=True, require_plugin=None):
|
||||
url.url_name == 'event.auth'
|
||||
or (
|
||||
request.user.is_authenticated
|
||||
and request.user.has_event_permission(request.organizer, request.event)
|
||||
and request.user.has_event_permission(request.organizer, request.event, request=request)
|
||||
)
|
||||
|
||||
)
|
||||
|
||||
@@ -87,6 +87,7 @@ PRETIX_INSTANCE_NAME = config.get('pretix', 'instance_name', fallback='pretix.de
|
||||
PRETIX_REGISTRATION = config.getboolean('pretix', 'registration', fallback=True)
|
||||
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_SESSION_TIMEOUT_RELATIVE = 3600 * 3
|
||||
PRETIX_SESSION_TIMEOUT_ABSOLUTE = 3600 * 12
|
||||
|
||||
@@ -261,6 +262,8 @@ for entry_point in iter_entry_points(group='pretix.plugin', name=None):
|
||||
PLUGINS.append(entry_point.module_name)
|
||||
INSTALLED_APPS.append(entry_point.module_name)
|
||||
|
||||
HIJACK_AUTHORIZE_STAFF = True
|
||||
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_PERMISSION_CLASSES': [
|
||||
@@ -297,6 +300,7 @@ MIDDLEWARE = [
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
'pretix.control.middleware.PermissionMiddleware',
|
||||
'pretix.control.middleware.AuditLogMiddleware',
|
||||
'pretix.base.middleware.LocaleMiddleware',
|
||||
'pretix.base.middleware.SecurityMiddleware',
|
||||
'pretix.presale.middleware.EventMiddleware',
|
||||
|
||||
@@ -53,7 +53,7 @@ nav.navbar .danger, nav.navbar .danger:hover, nav.navbar .danger:active {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
#button-shop {
|
||||
#button-shop, #button-sudo {
|
||||
padding: 15px;
|
||||
min-height: 50px;
|
||||
border: none;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import pytest
|
||||
from django.test import RequestFactory
|
||||
from django.utils.timezone import now
|
||||
|
||||
from pretix.base.models import Event, Organizer, Team, User
|
||||
from pretix.multidomain.middlewares import SessionMiddleware
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -25,7 +27,19 @@ def user():
|
||||
|
||||
@pytest.fixture
|
||||
def admin():
|
||||
return User.objects.create_user('admin@dummy.dummy', 'dummy', is_superuser=True)
|
||||
u = User.objects.create_user('admin@dummy.dummy', 'dummy', is_staff=True)
|
||||
return u
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def admin_request(admin, client):
|
||||
factory = RequestFactory()
|
||||
r = factory.get('/')
|
||||
SessionMiddleware().process_request(r)
|
||||
r.session.save()
|
||||
admin.staffsession_set.create(date_start=now(), session_key=r.session.session_key)
|
||||
admin.staffsession_set.create(date_start=now(), session_key=client.session.session_key)
|
||||
return r
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@@ -193,23 +207,20 @@ def test_organizer_permissions_multiple_teams(event, user):
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_superuser(event, user):
|
||||
user.is_superuser = True
|
||||
user.save()
|
||||
def test_superuser(event, admin, admin_request):
|
||||
assert admin.has_organizer_permission(event.organizer, request=admin_request)
|
||||
assert admin.has_organizer_permission(event.organizer, 'can_create_events', request=admin_request)
|
||||
assert admin.has_event_permission(event.organizer, event, request=admin_request)
|
||||
assert admin.has_event_permission(event.organizer, event, 'can_change_event_settings', request=admin_request)
|
||||
|
||||
assert user.has_organizer_permission(event.organizer)
|
||||
assert user.has_organizer_permission(event.organizer, 'can_create_events')
|
||||
assert user.has_event_permission(event.organizer, event)
|
||||
assert user.has_event_permission(event.organizer, event, 'can_change_event_settings')
|
||||
assert 'arbitrary' not in admin.get_event_permission_set(event.organizer, event)
|
||||
assert 'arbitrary' not in admin.get_organizer_permission_set(event.organizer)
|
||||
|
||||
assert 'arbitrary' in user.get_event_permission_set(event.organizer, event)
|
||||
assert 'arbitrary' in user.get_organizer_permission_set(event.organizer)
|
||||
|
||||
assert event in user.get_events_with_any_permission()
|
||||
assert event in admin.get_events_with_any_permission(request=admin_request)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_list_of_events(event, user, admin):
|
||||
def test_list_of_events(event, user, admin, admin_request):
|
||||
orga2 = Organizer.objects.create(slug='d2', name='d2')
|
||||
event2 = Event.objects.create(
|
||||
organizer=event.organizer, name='Dummy', slug='dummy2',
|
||||
@@ -236,25 +247,25 @@ def test_list_of_events(event, user, admin):
|
||||
team2.limit_events.add(event)
|
||||
team3.limit_events.add(event3)
|
||||
|
||||
events = list(user.get_events_with_any_permission())
|
||||
events = list(user.get_events_with_any_permission(request=admin_request))
|
||||
assert event in events
|
||||
assert event2 in events
|
||||
assert event3 in events
|
||||
assert event4 not in events
|
||||
|
||||
events = list(user.get_events_with_permission('can_change_event_settings'))
|
||||
events = list(user.get_events_with_permission('can_change_event_settings', request=admin_request))
|
||||
assert event not in events
|
||||
assert event2 not in events
|
||||
assert event3 in events
|
||||
assert event4 not in events
|
||||
|
||||
assert set(event.get_users_with_any_permission()) == {user, admin}
|
||||
assert set(event2.get_users_with_any_permission()) == {user, admin}
|
||||
assert set(event3.get_users_with_any_permission()) == {user, admin}
|
||||
assert set(event4.get_users_with_any_permission()) == {admin}
|
||||
assert set(event.get_users_with_any_permission()) == {user}
|
||||
assert set(event2.get_users_with_any_permission()) == {user}
|
||||
assert set(event3.get_users_with_any_permission()) == {user}
|
||||
assert set(event4.get_users_with_any_permission()) == set()
|
||||
|
||||
assert set(event.get_users_with_permission('can_change_event_settings')) == {admin}
|
||||
assert set(event2.get_users_with_permission('can_change_event_settings')) == {admin}
|
||||
assert set(event3.get_users_with_permission('can_change_event_settings')) == {user, admin}
|
||||
assert set(event4.get_users_with_permission('can_change_event_settings')) == {admin}
|
||||
assert set(event.get_users_with_permission('can_change_orders')) == {admin, user}
|
||||
assert set(event.get_users_with_permission('can_change_event_settings')) == set()
|
||||
assert set(event2.get_users_with_permission('can_change_event_settings')) == set()
|
||||
assert set(event3.get_users_with_permission('can_change_event_settings')) == {user}
|
||||
assert set(event4.get_users_with_permission('can_change_event_settings')) == set()
|
||||
assert set(event.get_users_with_permission('can_change_orders')) == {user}
|
||||
|
||||
@@ -8,6 +8,7 @@ from django.contrib.auth.tokens import (
|
||||
)
|
||||
from django.core import mail as djmail
|
||||
from django.test import TestCase, override_settings
|
||||
from django.utils.timezone import now
|
||||
from django_otp.oath import TOTP
|
||||
from django_otp.plugins.otp_totp.models import TOTPDevice
|
||||
from u2flib_server.jsapi import JSONDict
|
||||
@@ -607,3 +608,105 @@ class SessionTimeOutTest(TestCase):
|
||||
self.client.defaults['HTTP_USER_AGENT'] = 'Mozilla/5.0 (X11; Linux x86_64) Something else'
|
||||
response = self.client.get('/control/')
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def user():
|
||||
user = User.objects.create_user('dummy@dummy.dummy', 'dummy')
|
||||
return user
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_impersonate(user, client):
|
||||
client.login(email='dummy@dummy.dummy', password='dummy')
|
||||
user.is_staff = True
|
||||
user.save()
|
||||
ss = user.staffsession_set.create(date_start=now(), session_key=client.session.session_key)
|
||||
t1 = int(time.time()) - 5
|
||||
session = client.session
|
||||
session['pretix_auth_long_session'] = False
|
||||
session['pretix_auth_login_time'] = t1
|
||||
session['pretix_auth_last_used'] = t1
|
||||
session.save()
|
||||
user2 = User.objects.create_user('dummy2@dummy.dummy', 'dummy')
|
||||
response = client.post('/control/users/{user}/impersonate'.format(user=user2.pk), follow=True)
|
||||
assert b'dummy2@' in response.content
|
||||
response = client.get('/control/global/settings/')
|
||||
assert response.status_code == 403
|
||||
response = client.get('/control/')
|
||||
response = client.post('/control/users/impersonate/stop', follow=True)
|
||||
assert b'dummy@' in response.content
|
||||
assert b'dummy2@' not in response.content
|
||||
response = client.get('/control/global/settings/')
|
||||
assert response.status_code == 200 # staff session is preserved
|
||||
assert ss.logs.filter(url='/control/', impersonating=user2).exists()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_impersonate_require_recent_auth(user, client):
|
||||
client.login(email='dummy@dummy.dummy', password='dummy')
|
||||
user.is_staff = True
|
||||
user.save()
|
||||
user.staffsession_set.create(date_start=now(), session_key=client.session.session_key)
|
||||
t1 = int(time.time()) - 5 * 3600
|
||||
session = client.session
|
||||
session['pretix_auth_long_session'] = False
|
||||
session['pretix_auth_login_time'] = t1
|
||||
session['pretix_auth_last_used'] = t1
|
||||
session.save()
|
||||
user2 = User.objects.create_user('dummy2@dummy.dummy', 'dummy')
|
||||
response = client.post('/control/users/{user}/impersonate'.format(user=user2.pk), follow=True)
|
||||
assert b'dummy2@' not in response.content
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_staff_session(user, client):
|
||||
client.login(email='dummy@dummy.dummy', password='dummy')
|
||||
user.is_staff = True
|
||||
user.save()
|
||||
t1 = int(time.time()) - 5
|
||||
session = client.session
|
||||
session['pretix_auth_long_session'] = False
|
||||
session['pretix_auth_login_time'] = t1
|
||||
session['pretix_auth_last_used'] = t1
|
||||
session.save()
|
||||
response = client.get('/control/global/settings/')
|
||||
assert response.status_code == 302
|
||||
response = client.post('/control/sudo/')
|
||||
assert response['Location'] == '/control/'
|
||||
response = client.get('/control/global/settings/')
|
||||
assert response.status_code == 200
|
||||
client.post('/control/sudo/stop', follow=True)
|
||||
response = client.get('/control/global/settings/')
|
||||
assert response.status_code == 302
|
||||
assert user.staffsession_set.last().logs.filter(url='/control/global/settings/').exists()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_staff_session_require_recent_auth(user, client):
|
||||
client.login(email='dummy@dummy.dummy', password='dummy')
|
||||
user.is_staff = True
|
||||
user.save()
|
||||
t1 = int(time.time()) - 5 * 3600
|
||||
session = client.session
|
||||
session['pretix_auth_long_session'] = False
|
||||
session['pretix_auth_login_time'] = t1
|
||||
session['pretix_auth_last_used'] = t1
|
||||
session.save()
|
||||
response = client.post('/control/sudo/')
|
||||
assert response['Location'].startswith('/control/reauth/')
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_staff_session_require_staff(user, client):
|
||||
user.is_staff = False
|
||||
user.save()
|
||||
client.login(email='dummy@dummy.dummy', password='dummy')
|
||||
t1 = int(time.time()) - 5
|
||||
session = client.session
|
||||
session['pretix_auth_long_session'] = False
|
||||
session['pretix_auth_login_time'] = t1
|
||||
session['pretix_auth_last_used'] = t1
|
||||
session.save()
|
||||
response = client.post('/control/sudo/')
|
||||
assert response.status_code == 403
|
||||
|
||||
@@ -26,13 +26,20 @@ def env():
|
||||
|
||||
superuser_urls = [
|
||||
"global/settings/",
|
||||
"global/update/",
|
||||
"users/select2",
|
||||
"users/",
|
||||
"users/add",
|
||||
"users/1/",
|
||||
"users/1/impersonate",
|
||||
"users/1/reset",
|
||||
"sudo/sessions/",
|
||||
]
|
||||
|
||||
|
||||
staff_urls = [
|
||||
"global/update/",
|
||||
"sudo/",
|
||||
"sudo/2/",
|
||||
]
|
||||
|
||||
event_urls = [
|
||||
@@ -146,10 +153,26 @@ def test_logged_out(client, env, url):
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize("url", superuser_urls)
|
||||
def test_superuser_required(perf_patch, client, env, url):
|
||||
client.login(email='dummy@dummy.dummy', password='dummy')
|
||||
env[1].is_staff = True
|
||||
env[1].save()
|
||||
response = client.get('/control/' + url)
|
||||
if response.status_code == 302:
|
||||
assert '/sudo/' in response['Location']
|
||||
else:
|
||||
assert response.status_code == 403
|
||||
env[1].staffsession_set.create(date_start=now(), session_key=client.session.session_key)
|
||||
response = client.get('/control/' + url)
|
||||
assert response.status_code in (200, 302, 404)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize("url", staff_urls)
|
||||
def test_staff_required(perf_patch, client, env, url):
|
||||
client.login(email='dummy@dummy.dummy', password='dummy')
|
||||
response = client.get('/control/' + url)
|
||||
assert response.status_code == 403
|
||||
env[1].is_superuser = True
|
||||
env[1].is_staff = True
|
||||
env[1].save()
|
||||
response = client.get('/control/' + url)
|
||||
assert response.status_code in (200, 302, 404)
|
||||
|
||||
@@ -99,8 +99,9 @@ class OrderSearchTest(SoupTest):
|
||||
assert 'FO1' not in resp
|
||||
assert 'FO2' not in resp
|
||||
|
||||
def test_suberuser(self):
|
||||
self.user.is_superuser = True
|
||||
def test_superuser(self):
|
||||
self.user.is_staff = True
|
||||
self.user.staffsession_set.create(date_start=now(), session_key=self.client.session.session_key)
|
||||
self.user.save()
|
||||
self.team.members.clear()
|
||||
resp = self.client.get('/control/search/orders/').rendered_content
|
||||
|
||||
@@ -34,7 +34,7 @@ def test_update_notice_displayed(client, user):
|
||||
r = client.get('/control/')
|
||||
assert 'pretix automatically checks for updates in the background' not in r.content.decode()
|
||||
|
||||
user.is_superuser = True
|
||||
user.is_staff = True
|
||||
user.save()
|
||||
r = client.get('/control/')
|
||||
assert 'pretix automatically checks for updates in the background' in r.content.decode()
|
||||
@@ -46,7 +46,7 @@ def test_update_notice_displayed(client, user):
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_settings(client, user):
|
||||
user.is_superuser = True
|
||||
user.is_staff = True
|
||||
user.save()
|
||||
client.login(email='dummy@dummy.dummy', password='dummy')
|
||||
|
||||
@@ -71,7 +71,7 @@ def test_trigger(client, user):
|
||||
content_type='application/json',
|
||||
)
|
||||
|
||||
user.is_superuser = True
|
||||
user.is_staff = True
|
||||
user.save()
|
||||
client.login(email='dummy@dummy.dummy', password='dummy')
|
||||
|
||||
|
||||
@@ -65,6 +65,7 @@ def logged_in_client(client, event):
|
||||
)
|
||||
t.members.add(user)
|
||||
client.force_login(user)
|
||||
user.staffsession_set.create(date_start=now(), session_key=client.session.session_key)
|
||||
return client
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user