diff --git a/src/pretix/api/serializers/exporters.py b/src/pretix/api/serializers/exporters.py
index 40ce3a96bb..1a612b1023 100644
--- a/src/pretix/api/serializers/exporters.py
+++ b/src/pretix/api/serializers/exporters.py
@@ -27,7 +27,9 @@ from rest_framework.exceptions import ValidationError
from pretix.api.serializers.forms import form_field_to_serializer_field
from pretix.base.exporter import OrganizerLevelExportMixin
-from pretix.base.models import ScheduledEventExport, ScheduledOrganizerExport
+from pretix.base.models import (
+ Event, ScheduledEventExport, ScheduledOrganizerExport,
+)
from pretix.base.timeframes import SerializerDateFrameField
@@ -54,19 +56,28 @@ class ExporterSerializer(serializers.Serializer):
class JobRunSerializer(serializers.Serializer):
def __init__(self, *args, **kwargs):
- ex = kwargs.pop('exporter')
+ ex = self.ex = kwargs.pop('exporter')
super().__init__(*args, **kwargs)
if ex.is_multievent and not isinstance(ex, OrganizerLevelExportMixin):
+ self.fields["all_events"] = serializers.BooleanField(
+ required=False,
+ )
self.fields["events"] = serializers.SlugRelatedField(
queryset=ex.events,
required=False,
- allow_empty=False,
+ allow_empty=True,
slug_field='slug',
many=True
)
for k, v in ex.export_form_fields.items():
self.fields[k] = form_field_to_serializer_field(v)
+ def to_representation(self, instance):
+ # Translate between events as a list of slugs (API) and list of ints (database)
+ if self.ex.is_multievent and not isinstance(self.ex, OrganizerLevelExportMixin) and "events" in instance and isinstance(instance["events"], list):
+ instance["events"] = [e.slug for e in self.ex.events.filter(pk__in=instance["events"]).only("slug")]
+ return instance
+
def to_internal_value(self, data):
if isinstance(data, QueryDict):
data = data.copy()
@@ -94,6 +105,14 @@ class JobRunSerializer(serializers.Serializer):
data[fk] = f'{d_from.isoformat() if d_from else ""}/{d_to.isoformat() if d_to else ""}'
data = super().to_internal_value(data)
+
+ # Translate between events as a list of slugs (API) and list of ints (database)
+ if self.ex.is_multievent and not isinstance(self.ex, OrganizerLevelExportMixin) and "events" in data and isinstance(data["events"], list):
+ if data["events"] and isinstance(data["events"][0], Event):
+ data["events"] = [e.pk for e in data["events"]]
+ elif data["events"] and isinstance(data["events"][0], str):
+ data["events"] = [e.pk for e in self.ex.events.filter(slug__in=data["events"]).only("pk")]
+
return data
def is_valid(self, raise_exception=False):
@@ -130,13 +149,20 @@ class ScheduledExportSerializer(serializers.ModelSerializer):
exporter = self.context['exporters'].get(identifier)
if exporter:
try:
- JobRunSerializer(exporter=exporter).to_internal_value(attrs["export_form_data"])
+ attrs["export_form_data"] = JobRunSerializer(exporter=exporter).to_internal_value(attrs["export_form_data"])
except ValidationError as e:
raise ValidationError({"export_form_data": e.detail})
else:
raise ValidationError({"export_identifier": ["Unknown exporter."]})
return attrs
+ def to_representation(self, instance):
+ repr = super().to_representation(instance)
+ exporter = self.context['exporters'].get(instance.export_identifier)
+ if exporter:
+ repr["export_form_data"] = JobRunSerializer(exporter=exporter).to_representation(repr["export_form_data"])
+ return repr
+
def validate_mail_additional_recipients(self, value):
d = value.replace(' ', '')
if len(d.split(',')) > 25:
diff --git a/src/pretix/api/views/exporters.py b/src/pretix/api/views/exporters.py
index c39571693e..66b5f10108 100644
--- a/src/pretix/api/views/exporters.py
+++ b/src/pretix/api/views/exporters.py
@@ -38,10 +38,12 @@ from pretix.api.serializers.exporters import (
ExporterSerializer, JobRunSerializer, ScheduledEventExportSerializer,
ScheduledOrganizerExportSerializer,
)
+from pretix.base.exporter import OrganizerLevelExportMixin
from pretix.base.models import (
CachedFile, Device, ScheduledEventExport, ScheduledOrganizerExport,
TeamAPIToken,
)
+from pretix.base.models.organizer import TeamQuerySet
from pretix.base.services.export import (
export, init_event_exporters, init_organizer_exporters, multiexport,
)
@@ -203,7 +205,7 @@ class ScheduledExportersViewSet(viewsets.ModelViewSet):
class ScheduledEventExportViewSet(ScheduledExportersViewSet):
serializer_class = ScheduledEventExportSerializer
queryset = ScheduledEventExport.objects.none()
- permission = 'event.orders:read'
+ permission = None
def get_queryset(self):
perm_holder = self.request.auth if isinstance(self.request.auth, (TeamAPIToken, Device)) else self.request.user
@@ -249,6 +251,18 @@ class ScheduledEventExportViewSet(ScheduledExportersViewSet):
return {e.identifier: e for e in exporters}
def perform_update(self, serializer):
+ if not self.request.user.is_authenticated or self.request.user != serializer.instance.owner:
+ # This is to prevent a possible privilege escalation where user A creates a scheduled export and
+ # user B has settings permission (= they can see the export configuration), but not enough permission
+ # to run the export themselves. Without this check, user B could modify the export and add themselves
+ # as a recipient. Thereby, user B would gain access to data they can't have.
+ exporter = self.exporters.get(serializer.instance.export_identifier)
+ if not exporter:
+ raise PermissionDenied("No access to exporter.")
+ perm_holder = self.request.auth if isinstance(self.request.auth, (TeamAPIToken, Device)) else self.request.user
+ if not perm_holder.has_event_permission(self.request.organizer, self.request.event, exporter.get_required_event_permission()):
+ raise PermissionDenied("No permission to edit exports you could not run.")
+
serializer.save(event=self.request.event)
serializer.instance.compute_next_run()
serializer.instance.error_counter = 0
@@ -319,6 +333,43 @@ class ScheduledOrganizerExportViewSet(ScheduledExportersViewSet):
return {e.identifier: e for e in exporters}
def perform_update(self, serializer):
+ if not self.request.user.is_authenticated or self.request.user != serializer.instance.owner:
+ # This is to prevent a possible privilege escalation where user A creates a scheduled export and
+ # user B has settings permission (= they can see the export configuration), but not enough permission
+ # to run the export themselves. Without this check, user B could modify the export and add themselves
+ # as a recipient. Thereby, user B would gain access to data they can't have.
+ exporter = self.exporters.get(serializer.instance.export_identifier)
+ if not exporter:
+ raise PermissionDenied("No access to exporter.")
+ perm_holder = (self.request.auth if isinstance(self.request.auth, (Device, TeamAPIToken))
+ else self.request.user)
+ if isinstance(exporter, OrganizerLevelExportMixin):
+ if not perm_holder.has_organizer_permission(
+ self.request.organizer, exporter.get_required_organizer_permission(), request=self.request,
+ ):
+ raise PermissionDenied("No permission to edit exports you could not run.")
+ else:
+ if serializer.instance.export_form_data.get("all_events", False):
+ if isinstance(self.request.auth, Device):
+ if not self.request.auth.all_events:
+ raise PermissionDenied("No permission to edit exports you could not run.")
+ elif isinstance(self.request.auth, TeamAPIToken):
+ if not self.request.auth.team.all_events:
+ raise PermissionDenied("No permission to edit exports you could not run.")
+ elif self.request.user.is_authenticated:
+ if not self.request.user.teams.filter(
+ TeamQuerySet.event_permission_q(exporter.get_required_event_permission()),
+ all_events=True,
+ ).exists():
+ raise PermissionDenied("No permission to edit exports you could not run.")
+ else:
+ events_selected = serializer.instance.export_form_data.get("events", [])
+ events_permission = set(perm_holder.get_events_with_permission(
+ exporter.get_required_event_permission(), request=self.request
+ ).values_list("pk", flat=True))
+ if not all(e in events_permission for e in events_selected):
+ raise PermissionDenied("No permission to edit exports you could not run.")
+
serializer.save(organizer=self.request.organizer)
serializer.instance.compute_next_run()
serializer.instance.error_counter = 0
diff --git a/src/pretix/base/exporters/orderlist.py b/src/pretix/base/exporters/orderlist.py
index 54b7485e42..fcf32abf7f 100644
--- a/src/pretix/base/exporters/orderlist.py
+++ b/src/pretix/base/exporters/orderlist.py
@@ -271,7 +271,7 @@ class OrderListExporter(MultiSheetListExporter):
qs = self._date_filter(qs, form_data, rel='')
- if form_data['paid_only']:
+ if form_data.get('paid_only'):
qs = qs.filter(status=Order.STATUS_PAID)
return qs
@@ -457,7 +457,7 @@ class OrderListExporter(MultiSheetListExporter):
).annotate(
payment_providers=Subquery(p_providers, output_field=CharField()),
).select_related('order', 'order__invoice_address', 'order__customer', 'tax_rule')
- if form_data['paid_only']:
+ if form_data.get('paid_only'):
qs = qs.filter(order__status=Order.STATUS_PAID, canceled=False)
if form_data.get('items'):
@@ -560,7 +560,7 @@ class OrderListExporter(MultiSheetListExporter):
qs = OrderPosition.all.filter(
order__event__in=self.events,
)
- if form_data['paid_only']:
+ if form_data.get('paid_only'):
qs = qs.filter(order__status=Order.STATUS_PAID, canceled=False)
if form_data.get('items'):
diff --git a/src/pretix/base/services/export.py b/src/pretix/base/services/export.py
index 5cd31fd3ff..2b4be717dd 100644
--- a/src/pretix/base/services/export.py
+++ b/src/pretix/base/services/export.py
@@ -134,7 +134,7 @@ def multiexport(self, organizer: Organizer, user: User, device: int, token: int,
event_qs = organizer.events.all()
if form_data.get('events') is not None and not form_data.get('all_events'):
- if isinstance(form_data['events'][0], str):
+ if form_data['events'] and isinstance(form_data['events'][0], str): # legacy API-created schedules
event_qs = event_qs.filter(slug__in=form_data.get('events'))
else:
event_qs = event_qs.filter(pk__in=form_data.get('events'))
diff --git a/src/pretix/control/templates/pretixcontrol/orders/export.html b/src/pretix/control/templates/pretixcontrol/orders/export.html
index df8fd791a4..8c45f4fac9 100644
--- a/src/pretix/control/templates/pretixcontrol/orders/export.html
+++ b/src/pretix/control/templates/pretixcontrol/orders/export.html
@@ -34,7 +34,7 @@
{% if s.export_verbose_name == "?" %}
- {% trans "Exporter not found" %}
+ {% trans "Exporter not found or no permission" %}
{% elif s.error_counter >= 5 %}
diff --git a/src/pretix/control/templates/pretixcontrol/orders/export_form.html b/src/pretix/control/templates/pretixcontrol/orders/export_form.html
index e2ed55aedb..38975624a3 100644
--- a/src/pretix/control/templates/pretixcontrol/orders/export_form.html
+++ b/src/pretix/control/templates/pretixcontrol/orders/export_form.html
@@ -39,16 +39,18 @@
{% if schedule_form %}
{% include "pretixcontrol/orders/fragment_export_schedule_form.html" %}
-