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
|
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``.
|
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
|
Locale settings
|
||||||
---------------
|
---------------
|
||||||
|
|||||||
@@ -16,4 +16,5 @@ Contents:
|
|||||||
settings
|
settings
|
||||||
background
|
background
|
||||||
email
|
email
|
||||||
|
permissions
|
||||||
logging
|
logging
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ Organizers and events
|
|||||||
.. autoclass:: pretix.base.models.Team
|
.. autoclass:: pretix.base.models.Team
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
.. autoclass:: pretix.base.models.TeamAPIToken
|
||||||
|
:members:
|
||||||
|
|
||||||
.. autoclass:: pretix.base.models.RequiredAction
|
.. autoclass:: pretix.base.models.RequiredAction
|
||||||
:members:
|
: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
|
addon
|
||||||
addons
|
addons
|
||||||
api
|
api
|
||||||
|
auditability
|
||||||
auth
|
auth
|
||||||
autobuild
|
autobuild
|
||||||
backend
|
backend
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
.. _user-teams:
|
||||||
|
|
||||||
Teams
|
Teams
|
||||||
=====
|
=====
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class OrganizerViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
if self.request.user.is_authenticated():
|
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()
|
return Organizer.objects.all()
|
||||||
else:
|
else:
|
||||||
return Organizer.objects.filter(pk__in=self.request.user.teams.values_list('organizer', flat=True))
|
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 exporters # NOQA
|
||||||
from . import invoice # NOQA
|
from . import invoice # NOQA
|
||||||
from . import notifications # 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:
|
try:
|
||||||
from .celery_app import app as celery_app # NOQA
|
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,
|
cachedcombinedticket_name, cachedticket_name, generate_position_secret,
|
||||||
generate_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 .tax import TaxRule
|
||||||
from .vouchers import Voucher
|
from .vouchers import Voucher
|
||||||
from .waitinglist import WaitingListEntry
|
from .waitinglist import WaitingListEntry
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from typing import Union
|
from datetime import timedelta
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import (
|
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 import models
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django_otp.models import Device
|
from django_otp.models import Device
|
||||||
|
|
||||||
@@ -36,7 +37,6 @@ class UserManager(BaseUserManager):
|
|||||||
raise Exception("You must provide a password")
|
raise Exception("You must provide a password")
|
||||||
user = self.model(email=email)
|
user = self.model(email=email)
|
||||||
user.is_staff = True
|
user.is_staff = True
|
||||||
user.is_superuser = True
|
|
||||||
user.set_password(password)
|
user.set_password(password)
|
||||||
user.save()
|
user.save()
|
||||||
return user
|
return user
|
||||||
@@ -46,6 +46,11 @@ def generate_notifications_token():
|
|||||||
return get_random_string(length=32)
|
return get_random_string(length=32)
|
||||||
|
|
||||||
|
|
||||||
|
class SuperuserPermissionSet:
|
||||||
|
def __contains__(self, item):
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||||
"""
|
"""
|
||||||
This is the user model used by pretix for authentication.
|
This is the user model used by pretix for authentication.
|
||||||
@@ -114,6 +119,10 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.email
|
return self.email
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_superuser(self):
|
||||||
|
return False
|
||||||
|
|
||||||
def get_short_name(self) -> str:
|
def get_short_name(self) -> str:
|
||||||
"""
|
"""
|
||||||
Returns the first of the following user properties that is found to exist:
|
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)]
|
return self._teamcache['e{}'.format(event.pk)]
|
||||||
|
|
||||||
class SuperuserPermissionSet:
|
def get_event_permission_set(self, organizer, event) -> set:
|
||||||
def __contains__(self, item):
|
|
||||||
return True
|
|
||||||
|
|
||||||
def get_event_permission_set(self, organizer, event) -> Union[set, SuperuserPermissionSet]:
|
|
||||||
"""
|
"""
|
||||||
Gets a set of permissions (as strings) that a user holds for a particular event
|
Gets a set of permissions (as strings) that a user holds for a particular event
|
||||||
|
|
||||||
:param organizer: The organizer of the event
|
:param organizer: The organizer of the event
|
||||||
:param event: The event to check
|
: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
|
:return: set
|
||||||
a in b always returns true).
|
|
||||||
"""
|
"""
|
||||||
if self.is_superuser:
|
|
||||||
return self.SuperuserPermissionSet()
|
|
||||||
|
|
||||||
teams = self._get_teams_for_event(organizer, event)
|
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
|
Gets a set of permissions (as strings) that a user holds for a particular organizer
|
||||||
|
|
||||||
:param organizer: The organizer of the event
|
: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
|
:return: set
|
||||||
a in b always returns true).
|
|
||||||
"""
|
"""
|
||||||
if self.is_superuser:
|
|
||||||
return self.SuperuserPermissionSet()
|
|
||||||
|
|
||||||
teams = self._get_teams_for_organizer(organizer)
|
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``
|
Checks if this user is part of any team that grants access of type ``perm_name``
|
||||||
to the event ``event``.
|
to the event ``event``.
|
||||||
@@ -235,9 +240,10 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
|||||||
:param organizer: The organizer of the event
|
:param organizer: The organizer of the event
|
||||||
:param event: The event to check
|
:param event: The event to check
|
||||||
:param perm_name: The permission, e.g. ``can_change_teams``
|
:param perm_name: The permission, e.g. ``can_change_teams``
|
||||||
|
:param request: The current request (optional). Required to detect staff sessions properly.
|
||||||
:return: bool
|
:return: bool
|
||||||
"""
|
"""
|
||||||
if self.is_superuser:
|
if request and self.has_active_staff_session(request.session.session_key):
|
||||||
return True
|
return True
|
||||||
teams = self._get_teams_for_event(organizer, event)
|
teams = self._get_teams_for_event(organizer, event)
|
||||||
if teams:
|
if teams:
|
||||||
@@ -246,16 +252,17 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
|||||||
return True
|
return True
|
||||||
return False
|
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``
|
Checks if this user is part of any team that grants access of type ``perm_name``
|
||||||
to the organizer ``organizer``.
|
to the organizer ``organizer``.
|
||||||
|
|
||||||
:param organizer: The organizer to check
|
:param organizer: The organizer to check
|
||||||
:param perm_name: The permission, e.g. ``can_change_teams``
|
:param perm_name: The permission, e.g. ``can_change_teams``
|
||||||
|
:param request: The current request (optional). Required to detect staff sessions properly.
|
||||||
:return: bool
|
:return: bool
|
||||||
"""
|
"""
|
||||||
if self.is_superuser:
|
if request and self.has_active_staff_session(request.session.session_key):
|
||||||
return True
|
return True
|
||||||
teams = self._get_teams_for_organizer(organizer)
|
teams = self._get_teams_for_organizer(organizer)
|
||||||
if teams:
|
if teams:
|
||||||
@@ -263,15 +270,16 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
|||||||
return True
|
return True
|
||||||
return False
|
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.
|
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
|
:return: Iterable of Events
|
||||||
"""
|
"""
|
||||||
from .event import Event
|
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.all()
|
||||||
|
|
||||||
return Event.objects.filter(
|
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))
|
| 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.
|
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
|
:return: Iterable of Events
|
||||||
"""
|
"""
|
||||||
from .event import Event
|
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.all()
|
||||||
|
|
||||||
kwargs = {permission: True}
|
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))
|
| 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):
|
class U2FDevice(Device):
|
||||||
json_data = models.TextField()
|
json_data = models.TextField()
|
||||||
|
|||||||
@@ -554,9 +554,7 @@ class Event(EventMixin, LoggedModel):
|
|||||||
Q(all_events=True) | Q(limit_events__pk=self.pk)
|
Q(all_events=True) | Q(limit_events__pk=self.pk)
|
||||||
)
|
)
|
||||||
|
|
||||||
return User.objects.annotate(twp=Exists(team_with_perm)).filter(
|
return User.objects.annotate(twp=Exists(team_with_perm)).filter(twp=True)
|
||||||
Q(is_superuser=True) | Q(twp=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
def allow_delete(self):
|
def allow_delete(self):
|
||||||
return not self.orders.exists() and not self.invoices.exists()
|
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()
|
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``
|
Checks if this token is part of a team that grants access of type ``perm_name``
|
||||||
to the event ``event``.
|
to the event ``event``.
|
||||||
@@ -272,6 +272,7 @@ class TeamAPIToken(models.Model):
|
|||||||
:param organizer: The organizer of the event
|
:param organizer: The organizer of the event
|
||||||
:param event: The event to check
|
:param event: The event to check
|
||||||
:param perm_name: The permission, e.g. ``can_change_teams``
|
: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: bool
|
||||||
"""
|
"""
|
||||||
has_event_access = (self.team.all_events and organizer == self.team.organizer) or (
|
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))
|
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``
|
Checks if this token is part of a team that grants access of type ``perm_name``
|
||||||
to the organizer ``organizer``.
|
to the organizer ``organizer``.
|
||||||
|
|
||||||
:param organizer: The organizer to check
|
:param organizer: The organizer to check
|
||||||
:param perm_name: The permission, e.g. ``can_change_teams``
|
: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: bool
|
||||||
"""
|
"""
|
||||||
return organizer == self.team.organizer and (not perm_name or self.team.has_permission(perm_name))
|
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.conf import settings
|
||||||
from django.core.urlresolvers import Resolver404, get_script_prefix, resolve
|
from django.core.urlresolvers import Resolver404, get_script_prefix, resolve
|
||||||
|
from django.db.models import Q
|
||||||
from django.utils.translation import get_language
|
from django.utils.translation import get_language
|
||||||
|
|
||||||
|
from pretix.base.models.auth import StaffSession
|
||||||
from pretix.base.settings import GlobalSettingsObject
|
from pretix.base.settings import GlobalSettingsObject
|
||||||
|
|
||||||
from ..helpers.i18n import get_javascript_format, get_moment_locale
|
from ..helpers.i18n import get_javascript_format, get_moment_locale
|
||||||
@@ -93,10 +95,19 @@ def contextprocessor(request):
|
|||||||
ctx['warning_update_check_active'] = False
|
ctx['warning_update_check_active'] = False
|
||||||
gs = GlobalSettingsObject()
|
gs = GlobalSettingsObject()
|
||||||
ctx['global_settings'] = gs.settings
|
ctx['global_settings'] = gs.settings
|
||||||
if request.user.is_superuser:
|
if request.user.is_staff:
|
||||||
if gs.settings.update_check_result_warning:
|
if gs.settings.update_check_result_warning:
|
||||||
ctx['warning_update_available'] = True
|
ctx['warning_update_available'] = True
|
||||||
if not gs.settings.update_check_ack and 'runserver' not in sys.argv:
|
if not gs.settings.update_check_ack and 'runserver' not in sys.argv:
|
||||||
ctx['warning_update_check_active'] = True
|
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
|
return ctx
|
||||||
|
|||||||
@@ -234,7 +234,7 @@ class OrderSearchFilterForm(OrderFilterForm):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
request = kwargs.pop('request')
|
request = kwargs.pop('request')
|
||||||
super().__init__(*args, **kwargs)
|
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()
|
self.fields['organizer'].queryset = Organizer.objects.all()
|
||||||
else:
|
else:
|
||||||
self.fields['organizer'].queryset = Organizer.objects.filter(
|
self.fields['organizer'].queryset = Organizer.objects.filter(
|
||||||
@@ -393,7 +393,7 @@ class EventFilterForm(FilterForm):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
request = kwargs.pop('request')
|
request = kwargs.pop('request')
|
||||||
super().__init__(*args, **kwargs)
|
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()
|
self.fields['organizer'].queryset = Organizer.objects.all()
|
||||||
else:
|
else:
|
||||||
self.fields['organizer'].queryset = Organizer.objects.filter(
|
self.fields['organizer'].queryset = Organizer.objects.filter(
|
||||||
@@ -583,9 +583,9 @@ class UserFilterForm(FilterForm):
|
|||||||
qs = qs.filter(is_active=False)
|
qs = qs.filter(is_active=False)
|
||||||
|
|
||||||
if fdata.get('superuser') == 'yes':
|
if fdata.get('superuser') == 'yes':
|
||||||
qs = qs.filter(is_superuser=True)
|
qs = qs.filter(is_staff=True)
|
||||||
elif fdata.get('superuser') == 'no':
|
elif fdata.get('superuser') == 'no':
|
||||||
qs = qs.filter(is_superuser=False)
|
qs = qs.filter(is_staff=False)
|
||||||
|
|
||||||
if fdata.get('query'):
|
if fdata.get('query'):
|
||||||
qs = qs.filter(
|
qs = qs.filter(
|
||||||
|
|||||||
@@ -8,6 +8,13 @@ from django.utils.translation import ugettext_lazy as _
|
|||||||
from pytz import common_timezones
|
from pytz import common_timezones
|
||||||
|
|
||||||
from pretix.base.models import User
|
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):
|
class UserEditForm(forms.ModelForm):
|
||||||
@@ -41,7 +48,7 @@ class UserEditForm(forms.ModelForm):
|
|||||||
'email',
|
'email',
|
||||||
'require_2fa',
|
'require_2fa',
|
||||||
'is_active',
|
'is_active',
|
||||||
'is_superuser'
|
'is_staff'
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
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.contrib.auth import REDIRECT_FIELD_NAME, logout
|
||||||
from django.core.urlresolvers import get_script_prefix, resolve, reverse
|
from django.core.urlresolvers import get_script_prefix, resolve, reverse
|
||||||
from django.http import Http404
|
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.deprecation import MiddlewareMixin
|
||||||
from django.utils.encoding import force_str
|
from django.utils.encoding import force_str
|
||||||
from django.utils.translation import ugettext as _
|
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 import Event, Organizer
|
||||||
|
from pretix.base.models.auth import SuperuserPermissionSet, User
|
||||||
from pretix.helpers.security import (
|
from pretix.helpers.security import (
|
||||||
SessionInvalid, SessionReauthRequired, assert_session_valid,
|
SessionInvalid, SessionReauthRequired, assert_session_valid,
|
||||||
)
|
)
|
||||||
@@ -81,16 +83,52 @@ class PermissionMiddleware(MiddlewareMixin):
|
|||||||
slug=url.kwargs['event'],
|
slug=url.kwargs['event'],
|
||||||
organizer__slug=url.kwargs['organizer'],
|
organizer__slug=url.kwargs['organizer'],
|
||||||
).select_related('organizer').first()
|
).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 "
|
raise Http404(_("The selected event was not found or you "
|
||||||
"have no permission to administrate it."))
|
"have no permission to administrate it."))
|
||||||
request.organizer = request.event.organizer
|
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:
|
elif 'organizer' in url.kwargs:
|
||||||
request.organizer = Organizer.objects.filter(
|
request.organizer = Organizer.objects.filter(
|
||||||
slug=url.kwargs['organizer'],
|
slug=url.kwargs['organizer'],
|
||||||
).first()
|
).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 "
|
raise Http404(_("The selected organizer was not found or you "
|
||||||
"have no permission to administrate it."))
|
"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.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 _
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
|
|
||||||
@@ -18,8 +21,7 @@ def event_permission_required(permission):
|
|||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
|
|
||||||
allowed = (
|
allowed = (
|
||||||
request.user.is_superuser
|
request.user.has_event_permission(request.organizer, request.event, permission, request=request)
|
||||||
or request.user.has_event_permission(request.organizer, request.event, permission)
|
|
||||||
)
|
)
|
||||||
if allowed:
|
if allowed:
|
||||||
return function(request, *args, **kw)
|
return function(request, *args, **kw)
|
||||||
@@ -57,10 +59,7 @@ def organizer_permission_required(permission):
|
|||||||
# just a double check, should not ever happen
|
# just a double check, should not ever happen
|
||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
|
|
||||||
allowed = (
|
allowed = request.user.has_organizer_permission(request.organizer, permission, request=request)
|
||||||
request.user.is_superuser
|
|
||||||
or request.user.has_organizer_permission(request.organizer, permission)
|
|
||||||
)
|
|
||||||
if allowed:
|
if allowed:
|
||||||
return function(request, *args, **kw)
|
return function(request, *args, **kw)
|
||||||
|
|
||||||
@@ -85,14 +84,33 @@ class OrganizerPermissionRequiredMixin:
|
|||||||
def administrator_permission_required():
|
def administrator_permission_required():
|
||||||
"""
|
"""
|
||||||
This view decorator rejects all requests with a 403 response which are not from
|
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 decorator(function):
|
||||||
def wrapper(request, *args, **kw):
|
def wrapper(request, *args, **kw):
|
||||||
if not request.user.is_authenticated: # NOQA
|
if not request.user.is_authenticated: # NOQA
|
||||||
# just a double check, should not ever happen
|
# just a double check, should not ever happen
|
||||||
raise PermissionDenied()
|
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.'))
|
raise PermissionDenied(_('You do not have permission to view this content.'))
|
||||||
return function(request, *args, **kw)
|
return function(request, *args, **kw)
|
||||||
return wrapper
|
return wrapper
|
||||||
@@ -108,3 +126,14 @@ class AdministratorPermissionRequiredMixin:
|
|||||||
def as_view(cls, **initkwargs):
|
def as_view(cls, **initkwargs):
|
||||||
view = super(AdministratorPermissionRequiredMixin, cls).as_view(**initkwargs)
|
view = super(AdministratorPermissionRequiredMixin, cls).as_view(**initkwargs)
|
||||||
return administrator_permission_required()(view)
|
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>
|
</li>
|
||||||
{% endfor %}
|
{% 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 %}
|
{% if warning_update_available %}
|
||||||
<li>
|
<li>
|
||||||
<a href="{% url 'control:global.update' %}" class="danger">
|
<a href="{% url 'control:global.update' %}" class="danger">
|
||||||
@@ -191,7 +207,7 @@
|
|||||||
{% trans "Dashboard" %}
|
{% trans "Dashboard" %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% if request.user.is_superuser %}
|
{% if staff_session %}
|
||||||
<li>
|
<li>
|
||||||
<a href="{% url 'control:global.settings' %}"
|
<a href="{% url 'control:global.settings' %}"
|
||||||
{% if "global.settings" in url_name %}class="active"{% endif %}>
|
{% if "global.settings" in url_name %}class="active"{% endif %}>
|
||||||
@@ -219,14 +235,21 @@
|
|||||||
{% trans "Order search" %}
|
{% trans "Order search" %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% if request.user.is_superuser %}
|
{% if staff_session %}
|
||||||
<li>
|
<li>
|
||||||
<a href="{% url 'control:users' %}"
|
<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>
|
<i class="fa fa-user fa-fw"></i>
|
||||||
{% trans "Users" %}
|
{% trans "Users" %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</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 %}
|
{% endif %}
|
||||||
{% for nav in nav_global %}
|
{% for nav in nav_global %}
|
||||||
<li>
|
<li>
|
||||||
@@ -260,6 +283,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</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 %}
|
{% if request|is_hijacked %}
|
||||||
<div class="impersonate-warning">
|
<div class="impersonate-warning">
|
||||||
<span class="fa fa-user-secret"></span>
|
<span class="fa fa-user-secret"></span>
|
||||||
|
|||||||
@@ -109,7 +109,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-lg-2 col-sm-6 col-xs-12">
|
<div class="col-lg-2 col-sm-6 col-xs-12">
|
||||||
{% if log.user %}
|
{% if log.user %}
|
||||||
{% if log.user.is_superuser %}
|
{% if log.user.is_staff %}
|
||||||
<span class="fa fa-id-card fa-danger fa-fw"
|
<span class="fa fa-id-card fa-danger fa-fw"
|
||||||
data-toggle="tooltip"
|
data-toggle="tooltip"
|
||||||
title="{% trans "This change was performed by a pretix administrator." %}">
|
title="{% trans "This change was performed by a pretix administrator." %}">
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-lg-2 col-sm-6 col-xs-12">
|
<div class="col-lg-2 col-sm-6 col-xs-12">
|
||||||
{% if log.user %}
|
{% if log.user %}
|
||||||
{% if log.user.is_superuser %}
|
{% if log.user.is_staff %}
|
||||||
<span class="fa fa-id-card fa-danger fa-fw"
|
<span class="fa fa-id-card fa-danger fa-fw"
|
||||||
data-toggle="tooltip"
|
data-toggle="tooltip"
|
||||||
title="{% trans "This change was performed by a pretix administrator." %}">
|
title="{% trans "This change was performed by a pretix administrator." %}">
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
<div class="col-sm-4">
|
<div class="col-sm-4">
|
||||||
{% if plugin.app.compatibility_errors %}
|
{% if plugin.app.compatibility_errors %}
|
||||||
<button class="btn disabled btn-block btn-default" disabled="disabled">{% trans "Incompatible" %}</button>
|
<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>
|
<button class="btn disabled btn-block btn-default" disabled="disabled">{% trans "Not available" %}</button>
|
||||||
{% elif plugin.module in plugins_active %}
|
{% elif plugin.module in plugins_active %}
|
||||||
<button class="btn btn-default btn-block" name="plugin:{{ plugin.module }}" value="disable">{% trans "Disable" %}</button>
|
<button class="btn btn-default btn-block" name="plugin:{{ plugin.module }}" value="disable">{% trans "Disable" %}</button>
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
{% endblocktrans %}</p>
|
{% endblocktrans %}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<p>{{ plugin.description }}</p>
|
<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">
|
<div class="alert alert-warning">
|
||||||
{% trans "This plugin needs to be enabled by a system administrator for your event." %}
|
{% trans "This plugin needs to be enabled by a system administrator for your event." %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
<div class="alert alert-danger">
|
<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." %}
|
{% trans "Every event needs to be created as part of an organizer account. Currently, you do not have access to any organizer accounts." %}
|
||||||
</div>
|
</div>
|
||||||
{% if request.user.is_superuser %}
|
{% if staff_session %}
|
||||||
<a href="{% url "control:organizers.add" %}" class="btn btn-default">
|
<a href="{% url "control:organizers.add" %}" class="btn btn-default">
|
||||||
{% trans "Create a new organizer" %}
|
{% trans "Create a new organizer" %}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<p class="meta">
|
<p class="meta">
|
||||||
<span class="fa fa-clock-o"></span> {{ log.datetime|date:"SHORT_DATETIME_FORMAT" }}
|
<span class="fa fa-clock-o"></span> {{ log.datetime|date:"SHORT_DATETIME_FORMAT" }}
|
||||||
{% if log.user %}
|
{% if log.user %}
|
||||||
{% if log.user.is_superuser %}
|
{% if log.user.is_staff %}
|
||||||
<span class="fa fa-id-card fa-danger fa-fw"
|
<span class="fa fa-id-card fa-danger fa-fw"
|
||||||
data-toggle="tooltip"
|
data-toggle="tooltip"
|
||||||
title="{% trans "This change was performed by a pretix administrator." %}">
|
title="{% trans "This change was performed by a pretix administrator." %}">
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{% if request.user.is_superuser %}
|
{% if staff_session %}
|
||||||
<p>
|
<p>
|
||||||
<a href="{% url "control:organizers.add" %}" class="btn btn-default">
|
<a href="{% url "control:organizers.add" %}" class="btn btn-default">
|
||||||
<span class="fa fa-plus"></span>
|
<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.fullname layout='control' %}
|
||||||
{% bootstrap_field form.locale layout='control' %}
|
{% bootstrap_field form.locale layout='control' %}
|
||||||
{% bootstrap_field form.timezone layout='control' %}
|
{% bootstrap_field form.timezone layout='control' %}
|
||||||
{% bootstrap_field form.is_superuser layout='control' %}
|
{% bootstrap_field form.is_staff layout='control' %}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{% trans "Log-in settings" %}</legend>
|
<legend>{% trans "Log-in settings" %}</legend>
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
</strong></td>
|
</strong></td>
|
||||||
<td>{{ u.fullname|default_if_none:"" }}</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_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">
|
<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>
|
<a href="{% url "control:users.edit" id=u.id %}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ urlpatterns = [
|
|||||||
url(r'^global/update/$', global_settings.UpdateCheckView.as_view(), name='global.update'),
|
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'^global/message/$', global_settings.MessageView.as_view(), name='global.message'),
|
||||||
url(r'^reauth/$', user.ReauthView.as_view(), name='user.reauth'),
|
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/$', users.UserListView.as_view(), name='users'),
|
||||||
url(r'^users/select2$', typeahead.users_select2, name='users.select2'),
|
url(r'^users/select2$', typeahead.users_select2, name='users.select2'),
|
||||||
url(r'^users/add$', users.UserCreateView.as_view(), name='users.add'),
|
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):
|
for r, result in event_dashboard_widgets.send(sender=request.event, subevent=subevent):
|
||||||
widgets.extend(result)
|
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 = request.event.logentry_set.all().select_related('user', 'content_type').order_by('-datetime')
|
||||||
qs = qs.exclude(action_type__in=OVERVIEW_BLACKLIST)
|
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))
|
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))
|
qs = qs.exclude(content_type=ContentType.objects.get_for_model(Voucher))
|
||||||
|
|
||||||
a_qs = request.event.requiredaction_set.filter(done=False)
|
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)
|
return render(request, 'pretixcontrol/event/index.html', ctx)
|
||||||
|
|
||||||
|
|
||||||
def annotated_event_query(user):
|
def annotated_event_query(request):
|
||||||
active_orders = Order.objects.filter(
|
active_orders = Order.objects.filter(
|
||||||
event=OuterRef('pk'),
|
event=OuterRef('pk'),
|
||||||
status__in=[Order.STATUS_PENDING, Order.STATUS_PAID]
|
status__in=[Order.STATUS_PENDING, Order.STATUS_PAID]
|
||||||
@@ -285,7 +286,7 @@ def annotated_event_query(user):
|
|||||||
event=OuterRef('pk'),
|
event=OuterRef('pk'),
|
||||||
done=False
|
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()),
|
order_count=Subquery(active_orders, output_field=IntegerField()),
|
||||||
has_ra=Exists(required_actions)
|
has_ra=Exists(required_actions)
|
||||||
).annotate(
|
).annotate(
|
||||||
@@ -299,7 +300,7 @@ def annotated_event_query(user):
|
|||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
|
||||||
def widgets_for_event_qs(qs, user, nmax):
|
def widgets_for_event_qs(request, qs, user, nmax):
|
||||||
widgets = []
|
widgets = []
|
||||||
|
|
||||||
# Get set of events where we have the permission to show the # of orders
|
# 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(
|
orders_text=ungettext('{num} order', '{num} orders', event.order_count or 0).format(
|
||||||
num=event.order_count or 0
|
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,
|
daterange=dr,
|
||||||
status=status[1],
|
status=status[1],
|
||||||
@@ -402,7 +403,8 @@ def user_index(request):
|
|||||||
ctx = {
|
ctx = {
|
||||||
'widgets': rearrange(widgets),
|
'widgets': rearrange(widgets),
|
||||||
'upcoming': widgets_for_event_qs(
|
'upcoming': widgets_for_event_qs(
|
||||||
annotated_event_query(request.user).filter(
|
request,
|
||||||
|
annotated_event_query(request).filter(
|
||||||
Q(has_subevents=False) &
|
Q(has_subevents=False) &
|
||||||
Q(
|
Q(
|
||||||
Q(Q(date_to__isnull=True) & Q(date_from__gte=now()))
|
Q(Q(date_to__isnull=True) & Q(date_from__gte=now()))
|
||||||
@@ -413,7 +415,8 @@ def user_index(request):
|
|||||||
7
|
7
|
||||||
),
|
),
|
||||||
'past': widgets_for_event_qs(
|
'past': widgets_for_event_qs(
|
||||||
annotated_event_query(request.user).filter(
|
request,
|
||||||
|
annotated_event_query(request).filter(
|
||||||
Q(has_subevents=False) &
|
Q(has_subevents=False) &
|
||||||
Q(
|
Q(
|
||||||
Q(Q(date_to__isnull=True) & Q(date_from__lt=now()))
|
Q(Q(date_to__isnull=True) & Q(date_from__lt=now()))
|
||||||
@@ -424,7 +427,8 @@ def user_index(request):
|
|||||||
8
|
8
|
||||||
),
|
),
|
||||||
'series': widgets_for_event_qs(
|
'series': widgets_for_event_qs(
|
||||||
annotated_event_query(request.user).filter(
|
request,
|
||||||
|
annotated_event_query(request).filter(
|
||||||
has_subevents=True
|
has_subevents=True
|
||||||
).order_by('-order_to'),
|
).order_by('-order_to'),
|
||||||
request.user,
|
request.user,
|
||||||
|
|||||||
@@ -212,7 +212,7 @@ class EventPlugins(EventSettingsViewMixin, EventPermissionRequiredMixin, Templat
|
|||||||
module = key.split(":")[1]
|
module = key.split(":")[1]
|
||||||
if value == "enable" and module in plugins_available:
|
if value == "enable" and module in plugins_available:
|
||||||
if getattr(plugins_available[module], 'restricted', False):
|
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
|
continue
|
||||||
|
|
||||||
if hasattr(plugins_available[module].app, 'installed'):
|
if hasattr(plugins_available[module].app, 'installed'):
|
||||||
@@ -854,9 +854,11 @@ class EventLog(EventPermissionRequiredMixin, ListView):
|
|||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
qs = self.request.event.logentry_set.all().select_related('user', 'content_type').order_by('-datetime')
|
qs = self.request.event.logentry_set.all().select_related('user', 'content_type').order_by('-datetime')
|
||||||
qs = qs.exclude(action_type__in=OVERVIEW_BLACKLIST)
|
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))
|
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))
|
qs = qs.exclude(content_type=ContentType.objects.get_for_model(Voucher))
|
||||||
|
|
||||||
if self.request.GET.get('user') == 'yes':
|
if self.request.GET.get('user') == 'yes':
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ from pretix.base.settings import GlobalSettingsObject
|
|||||||
from pretix.control.forms.global_settings import (
|
from pretix.control.forms.global_settings import (
|
||||||
GlobalSettingsForm, UpdateSettingsForm,
|
GlobalSettingsForm, UpdateSettingsForm,
|
||||||
)
|
)
|
||||||
from pretix.control.permissions import AdministratorPermissionRequiredMixin
|
from pretix.control.permissions import (
|
||||||
|
AdministratorPermissionRequiredMixin, StaffMemberRequiredMixin,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class GlobalSettingsView(AdministratorPermissionRequiredMixin, FormView):
|
class GlobalSettingsView(AdministratorPermissionRequiredMixin, FormView):
|
||||||
@@ -28,7 +30,7 @@ class GlobalSettingsView(AdministratorPermissionRequiredMixin, FormView):
|
|||||||
return reverse('control:global.settings')
|
return reverse('control:global.settings')
|
||||||
|
|
||||||
|
|
||||||
class UpdateCheckView(AdministratorPermissionRequiredMixin, FormView):
|
class UpdateCheckView(StaffMemberRequiredMixin, FormView):
|
||||||
template_name = 'pretixcontrol/global_update.html'
|
template_name = 'pretixcontrol/global_update.html'
|
||||||
form_class = UpdateSettingsForm
|
form_class = UpdateSettingsForm
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class EventList(PaginationMixin, ListView):
|
|||||||
template_name = 'pretixcontrol/events/index.html'
|
template_name = 'pretixcontrol/events/index.html'
|
||||||
|
|
||||||
def get_queryset(self):
|
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'
|
'_settings_objects', 'organizer___settings_objects'
|
||||||
).order_by('-date_from')
|
).order_by('-date_from')
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class OrganizerList(PaginationMixin, ListView):
|
|||||||
qs = Organizer.objects.all()
|
qs = Organizer.objects.all()
|
||||||
if self.filter_form.is_valid():
|
if self.filter_form.is_valid():
|
||||||
qs = self.filter_form.filter_qs(qs)
|
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
|
return qs
|
||||||
else:
|
else:
|
||||||
return qs.filter(pk__in=self.request.user.teams.values_list('organizer', flat=True))
|
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):
|
def get_form_kwargs(self):
|
||||||
kwargs = super().get_form_kwargs()
|
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
|
kwargs['domain'] = True
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
@@ -271,7 +271,7 @@ class OrganizerCreate(CreateView):
|
|||||||
context_object_name = 'organizer'
|
context_object_name = 'organizer'
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
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
|
raise PermissionDenied() # TODO
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class OrderSearch(PaginationMixin, ListView):
|
|||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
qs = Order.objects.select_related('invoice_address')
|
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(
|
qs = qs.filter(
|
||||||
Q(event__organizer_id__in=self.request.user.teams.filter(
|
Q(event__organizer_id__in=self.request.user.teams.filter(
|
||||||
all_events=True, can_view_orders=True).values_list('organizer', flat=True))
|
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'))
|
page = int(request.GET.get('page', '1'))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
page = 1
|
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(name__icontains=i18ncomp(query)) | Q(slug__icontains=query) |
|
||||||
Q(organizer__name__icontains=i18ncomp(query)) | Q(organizer__slug__icontains=query)
|
Q(organizer__name__icontains=i18ncomp(query)) | Q(organizer__slug__icontains=query)
|
||||||
).annotate(
|
).annotate(
|
||||||
@@ -107,7 +107,7 @@ def organizer_select2(request):
|
|||||||
qs = Organizer.objects.all()
|
qs = Organizer.objects.all()
|
||||||
if term:
|
if term:
|
||||||
qs = qs.filter(Q(name__icontains=term) | Q(slug__icontains=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))
|
qs = qs.filter(pk__in=request.user.teams.values_list('organizer', flat=True))
|
||||||
|
|
||||||
total = qs.count()
|
total = qs.count()
|
||||||
@@ -130,7 +130,7 @@ def organizer_select2(request):
|
|||||||
|
|
||||||
|
|
||||||
def users_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()
|
raise PermissionDenied()
|
||||||
|
|
||||||
term = request.GET.get('query', '')
|
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.crypto import get_random_string
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.http import is_safe_url
|
from django.utils.http import is_safe_url
|
||||||
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import ugettext_lazy as _
|
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_static.models import StaticDevice
|
||||||
from django_otp.plugins.otp_totp.models import TOTPDevice
|
from django_otp.plugins.otp_totp.models import TOTPDevice
|
||||||
from u2flib_server import u2f
|
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.forms.user import User2FADeviceAddForm, UserSettingsForm
|
||||||
from pretix.base.models import Event, NotificationSetting, U2FDevice, User
|
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.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
|
from pretix.control.views.auth import get_u2f_appid
|
||||||
|
|
||||||
REAL_DEVICE_TYPES = (TOTPDevice, U2FDevice)
|
REAL_DEVICE_TYPES = (TOTPDevice, U2FDevice)
|
||||||
@@ -472,3 +479,67 @@ class UserNotificationsEditView(TemplateView):
|
|||||||
if self.event:
|
if self.event:
|
||||||
ctx['permset'] = self.request.user.get_event_permission_set(self.event.organizer, self.event)
|
ctx['permset'] = self.request.user.get_event_permission_set(self.event.organizer, self.event)
|
||||||
return ctx
|
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': self.kwargs.get("id"),
|
||||||
'other_email': self.object.email
|
'other_email': self.object.email
|
||||||
})
|
})
|
||||||
|
oldkey = request.session.session_key
|
||||||
login_user(request, self.object)
|
login_user(request, self.object)
|
||||||
|
request.session['hijacker_session'] = oldkey
|
||||||
return redirect(reverse('control:index'))
|
return redirect(reverse('control:index'))
|
||||||
|
|
||||||
|
|
||||||
@@ -120,7 +122,14 @@ class UserImpersonateStopView(LoginRequiredMixin, View):
|
|||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
impersonated = request.user
|
impersonated = request.user
|
||||||
|
hijs = request.session['hijacker_session']
|
||||||
release_hijack(request)
|
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',
|
request.user.log_action('pretix.control.auth.user.impersonate_stopped',
|
||||||
user=request.user,
|
user=request.user,
|
||||||
data={
|
data={
|
||||||
|
|||||||
@@ -48,7 +48,8 @@ class WaitingListView(EventPermissionRequiredMixin, PaginationMixin, ListView):
|
|||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
if 'assign' in request.POST:
|
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'))
|
messages.error(request, _('You do not have permission to do this'))
|
||||||
return redirect(reverse('control:event.orders.waitinglist', kwargs={
|
return redirect(reverse('control:event.orders.waitinglist', kwargs={
|
||||||
'event': request.event.slug,
|
'event': request.event.slug,
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ def control_nav_import(sender, request=None, **kwargs):
|
|||||||
@receiver(nav_organizer, dispatch_uid="payment_banktransfer_organav")
|
@receiver(nav_organizer, dispatch_uid="payment_banktransfer_organav")
|
||||||
def control_nav_orga_import(sender, request=None, **kwargs):
|
def control_nav_orga_import(sender, request=None, **kwargs):
|
||||||
url = resolve(request.path_info)
|
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 []
|
return []
|
||||||
if not request.organizer.events.filter(plugins__icontains='pretix.plugins.banktransfer'):
|
if not request.organizer.events.filter(plugins__icontains='pretix.plugins.banktransfer'):
|
||||||
return []
|
return []
|
||||||
|
|||||||
@@ -488,7 +488,7 @@ class OrganizerActionView(OrganizerBanktransferView, OrganizerPermissionRequired
|
|||||||
def order_qs(self):
|
def order_qs(self):
|
||||||
all = self.request.user.teams.filter(organizer=self.request.organizer, can_change_orders=True,
|
all = self.request.user.teams.filter(organizer=self.request.organizer, can_change_orders=True,
|
||||||
can_view_orders=True, all_events=True).exists()
|
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)
|
return Order.objects.filter(event__organizer=self.request.organizer)
|
||||||
else:
|
else:
|
||||||
return Order.objects.filter(
|
return Order.objects.filter(
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from pretix.control.signals import nav_event
|
|||||||
@receiver(nav_event, dispatch_uid="pretixdroid_nav")
|
@receiver(nav_event, dispatch_uid="pretixdroid_nav")
|
||||||
def control_nav_import(sender, request=None, **kwargs):
|
def control_nav_import(sender, request=None, **kwargs):
|
||||||
url = resolve(request.path_info)
|
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 []
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from pretix.control.signals import nav_event
|
|||||||
@receiver(nav_event, dispatch_uid="sendmail_nav")
|
@receiver(nav_event, dispatch_uid="sendmail_nav")
|
||||||
def control_nav_import(sender, request=None, **kwargs):
|
def control_nav_import(sender, request=None, **kwargs):
|
||||||
url = resolve(request.path_info)
|
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 []
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from pretix.control.signals import nav_event
|
|||||||
@receiver(nav_event, dispatch_uid="statistics_nav")
|
@receiver(nav_event, dispatch_uid="statistics_nav")
|
||||||
def control_nav_import(sender, request=None, **kwargs):
|
def control_nav_import(sender, request=None, **kwargs):
|
||||||
url = resolve(request.path_info)
|
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 []
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ def _detect_event(request, require_live=True, require_plugin=None):
|
|||||||
url.url_name == 'event.auth'
|
url.url_name == 'event.auth'
|
||||||
or (
|
or (
|
||||||
request.user.is_authenticated
|
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_REGISTRATION = config.getboolean('pretix', 'registration', fallback=True)
|
||||||
PRETIX_PASSWORD_RESET = config.getboolean('pretix', 'password_reset', fallback=True)
|
PRETIX_PASSWORD_RESET = config.getboolean('pretix', 'password_reset', fallback=True)
|
||||||
PRETIX_LONG_SESSIONS = config.getboolean('pretix', 'long_sessions', 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_RELATIVE = 3600 * 3
|
||||||
PRETIX_SESSION_TIMEOUT_ABSOLUTE = 3600 * 12
|
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)
|
PLUGINS.append(entry_point.module_name)
|
||||||
INSTALLED_APPS.append(entry_point.module_name)
|
INSTALLED_APPS.append(entry_point.module_name)
|
||||||
|
|
||||||
|
HIJACK_AUTHORIZE_STAFF = True
|
||||||
|
|
||||||
|
|
||||||
REST_FRAMEWORK = {
|
REST_FRAMEWORK = {
|
||||||
'DEFAULT_PERMISSION_CLASSES': [
|
'DEFAULT_PERMISSION_CLASSES': [
|
||||||
@@ -297,6 +300,7 @@ MIDDLEWARE = [
|
|||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
'pretix.control.middleware.PermissionMiddleware',
|
'pretix.control.middleware.PermissionMiddleware',
|
||||||
|
'pretix.control.middleware.AuditLogMiddleware',
|
||||||
'pretix.base.middleware.LocaleMiddleware',
|
'pretix.base.middleware.LocaleMiddleware',
|
||||||
'pretix.base.middleware.SecurityMiddleware',
|
'pretix.base.middleware.SecurityMiddleware',
|
||||||
'pretix.presale.middleware.EventMiddleware',
|
'pretix.presale.middleware.EventMiddleware',
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ nav.navbar .danger, nav.navbar .danger:hover, nav.navbar .danger:active {
|
|||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#button-shop {
|
#button-shop, #button-sudo {
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
min-height: 50px;
|
min-height: 50px;
|
||||||
border: none;
|
border: none;
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
from django.test import RequestFactory
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
|
|
||||||
from pretix.base.models import Event, Organizer, Team, User
|
from pretix.base.models import Event, Organizer, Team, User
|
||||||
|
from pretix.multidomain.middlewares import SessionMiddleware
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -25,7 +27,19 @@ def user():
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def admin():
|
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
|
@pytest.mark.django_db
|
||||||
@@ -193,23 +207,20 @@ def test_organizer_permissions_multiple_teams(event, user):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_superuser(event, user):
|
def test_superuser(event, admin, admin_request):
|
||||||
user.is_superuser = True
|
assert admin.has_organizer_permission(event.organizer, request=admin_request)
|
||||||
user.save()
|
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 'arbitrary' not in admin.get_event_permission_set(event.organizer, event)
|
||||||
assert user.has_organizer_permission(event.organizer, 'can_create_events')
|
assert 'arbitrary' not in admin.get_organizer_permission_set(event.organizer)
|
||||||
assert user.has_event_permission(event.organizer, event)
|
|
||||||
assert user.has_event_permission(event.organizer, event, 'can_change_event_settings')
|
|
||||||
|
|
||||||
assert 'arbitrary' in user.get_event_permission_set(event.organizer, event)
|
assert event in admin.get_events_with_any_permission(request=admin_request)
|
||||||
assert 'arbitrary' in user.get_organizer_permission_set(event.organizer)
|
|
||||||
|
|
||||||
assert event in user.get_events_with_any_permission()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@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')
|
orga2 = Organizer.objects.create(slug='d2', name='d2')
|
||||||
event2 = Event.objects.create(
|
event2 = Event.objects.create(
|
||||||
organizer=event.organizer, name='Dummy', slug='dummy2',
|
organizer=event.organizer, name='Dummy', slug='dummy2',
|
||||||
@@ -236,25 +247,25 @@ def test_list_of_events(event, user, admin):
|
|||||||
team2.limit_events.add(event)
|
team2.limit_events.add(event)
|
||||||
team3.limit_events.add(event3)
|
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 event in events
|
||||||
assert event2 in events
|
assert event2 in events
|
||||||
assert event3 in events
|
assert event3 in events
|
||||||
assert event4 not 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 event not in events
|
||||||
assert event2 not in events
|
assert event2 not in events
|
||||||
assert event3 in events
|
assert event3 in events
|
||||||
assert event4 not in events
|
assert event4 not in events
|
||||||
|
|
||||||
assert set(event.get_users_with_any_permission()) == {user, admin}
|
assert set(event.get_users_with_any_permission()) == {user}
|
||||||
assert set(event2.get_users_with_any_permission()) == {user, admin}
|
assert set(event2.get_users_with_any_permission()) == {user}
|
||||||
assert set(event3.get_users_with_any_permission()) == {user, admin}
|
assert set(event3.get_users_with_any_permission()) == {user}
|
||||||
assert set(event4.get_users_with_any_permission()) == {admin}
|
assert set(event4.get_users_with_any_permission()) == set()
|
||||||
|
|
||||||
assert set(event.get_users_with_permission('can_change_event_settings')) == {admin}
|
assert set(event.get_users_with_permission('can_change_event_settings')) == set()
|
||||||
assert set(event2.get_users_with_permission('can_change_event_settings')) == {admin}
|
assert set(event2.get_users_with_permission('can_change_event_settings')) == set()
|
||||||
assert set(event3.get_users_with_permission('can_change_event_settings')) == {user, admin}
|
assert set(event3.get_users_with_permission('can_change_event_settings')) == {user}
|
||||||
assert set(event4.get_users_with_permission('can_change_event_settings')) == {admin}
|
assert set(event4.get_users_with_permission('can_change_event_settings')) == set()
|
||||||
assert set(event.get_users_with_permission('can_change_orders')) == {admin, user}
|
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.core import mail as djmail
|
||||||
from django.test import TestCase, override_settings
|
from django.test import TestCase, override_settings
|
||||||
|
from django.utils.timezone import now
|
||||||
from django_otp.oath import TOTP
|
from django_otp.oath import TOTP
|
||||||
from django_otp.plugins.otp_totp.models import TOTPDevice
|
from django_otp.plugins.otp_totp.models import TOTPDevice
|
||||||
from u2flib_server.jsapi import JSONDict
|
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'
|
self.client.defaults['HTTP_USER_AGENT'] = 'Mozilla/5.0 (X11; Linux x86_64) Something else'
|
||||||
response = self.client.get('/control/')
|
response = self.client.get('/control/')
|
||||||
self.assertEqual(response.status_code, 302)
|
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 = [
|
superuser_urls = [
|
||||||
"global/settings/",
|
"global/settings/",
|
||||||
"global/update/",
|
|
||||||
"users/select2",
|
"users/select2",
|
||||||
"users/",
|
"users/",
|
||||||
"users/add",
|
"users/add",
|
||||||
"users/1/",
|
"users/1/",
|
||||||
"users/1/impersonate",
|
"users/1/impersonate",
|
||||||
"users/1/reset",
|
"users/1/reset",
|
||||||
|
"sudo/sessions/",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
staff_urls = [
|
||||||
|
"global/update/",
|
||||||
|
"sudo/",
|
||||||
|
"sudo/2/",
|
||||||
]
|
]
|
||||||
|
|
||||||
event_urls = [
|
event_urls = [
|
||||||
@@ -146,10 +153,26 @@ def test_logged_out(client, env, url):
|
|||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize("url", superuser_urls)
|
@pytest.mark.parametrize("url", superuser_urls)
|
||||||
def test_superuser_required(perf_patch, client, env, url):
|
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')
|
client.login(email='dummy@dummy.dummy', password='dummy')
|
||||||
response = client.get('/control/' + url)
|
response = client.get('/control/' + url)
|
||||||
assert response.status_code == 403
|
assert response.status_code == 403
|
||||||
env[1].is_superuser = True
|
env[1].is_staff = True
|
||||||
env[1].save()
|
env[1].save()
|
||||||
response = client.get('/control/' + url)
|
response = client.get('/control/' + url)
|
||||||
assert response.status_code in (200, 302, 404)
|
assert response.status_code in (200, 302, 404)
|
||||||
|
|||||||
@@ -99,8 +99,9 @@ class OrderSearchTest(SoupTest):
|
|||||||
assert 'FO1' not in resp
|
assert 'FO1' not in resp
|
||||||
assert 'FO2' not in resp
|
assert 'FO2' not in resp
|
||||||
|
|
||||||
def test_suberuser(self):
|
def test_superuser(self):
|
||||||
self.user.is_superuser = True
|
self.user.is_staff = True
|
||||||
|
self.user.staffsession_set.create(date_start=now(), session_key=self.client.session.session_key)
|
||||||
self.user.save()
|
self.user.save()
|
||||||
self.team.members.clear()
|
self.team.members.clear()
|
||||||
resp = self.client.get('/control/search/orders/').rendered_content
|
resp = self.client.get('/control/search/orders/').rendered_content
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ def test_update_notice_displayed(client, user):
|
|||||||
r = client.get('/control/')
|
r = client.get('/control/')
|
||||||
assert 'pretix automatically checks for updates in the background' not in r.content.decode()
|
assert 'pretix automatically checks for updates in the background' not in r.content.decode()
|
||||||
|
|
||||||
user.is_superuser = True
|
user.is_staff = True
|
||||||
user.save()
|
user.save()
|
||||||
r = client.get('/control/')
|
r = client.get('/control/')
|
||||||
assert 'pretix automatically checks for updates in the background' in r.content.decode()
|
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
|
@pytest.mark.django_db
|
||||||
def test_settings(client, user):
|
def test_settings(client, user):
|
||||||
user.is_superuser = True
|
user.is_staff = True
|
||||||
user.save()
|
user.save()
|
||||||
client.login(email='dummy@dummy.dummy', password='dummy')
|
client.login(email='dummy@dummy.dummy', password='dummy')
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ def test_trigger(client, user):
|
|||||||
content_type='application/json',
|
content_type='application/json',
|
||||||
)
|
)
|
||||||
|
|
||||||
user.is_superuser = True
|
user.is_staff = True
|
||||||
user.save()
|
user.save()
|
||||||
client.login(email='dummy@dummy.dummy', password='dummy')
|
client.login(email='dummy@dummy.dummy', password='dummy')
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ def logged_in_client(client, event):
|
|||||||
)
|
)
|
||||||
t.members.add(user)
|
t.members.add(user)
|
||||||
client.force_login(user)
|
client.force_login(user)
|
||||||
|
user.staffsession_set.create(date_start=now(), session_key=client.session.session_key)
|
||||||
return client
|
return client
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user