mirror of
https://github.com/pretix/pretix.git
synced 2026-05-03 14:54:04 +00:00
Add proper permission handling for exports
This commit is contained in:
@@ -55,11 +55,10 @@ class ExporterSerializer(serializers.Serializer):
|
||||
class JobRunSerializer(serializers.Serializer):
|
||||
def __init__(self, *args, **kwargs):
|
||||
ex = kwargs.pop('exporter')
|
||||
events = kwargs.pop('events', None)
|
||||
super().__init__(*args, **kwargs)
|
||||
if events is not None and not isinstance(ex, OrganizerLevelExportMixin):
|
||||
if ex.is_multievent and not isinstance(ex, OrganizerLevelExportMixin):
|
||||
self.fields["events"] = serializers.SlugRelatedField(
|
||||
queryset=events,
|
||||
queryset=ex.events,
|
||||
required=False,
|
||||
allow_empty=False,
|
||||
slug_field='slug',
|
||||
|
||||
@@ -38,14 +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, Event, ScheduledEventExport, ScheduledOrganizerExport,
|
||||
CachedFile, Device, ScheduledEventExport, ScheduledOrganizerExport,
|
||||
TeamAPIToken,
|
||||
)
|
||||
from pretix.base.services.export import export, multiexport
|
||||
from pretix.base.signals import (
|
||||
register_data_exporters, register_multievent_data_exporters,
|
||||
from pretix.base.services.export import (
|
||||
export, init_event_exporters, init_organizer_exporters, multiexport,
|
||||
)
|
||||
from pretix.helpers.http import ChunkBasedFileResponse
|
||||
|
||||
@@ -111,7 +109,7 @@ class ExportersMixin:
|
||||
@action(detail=True, methods=['POST'])
|
||||
def run(self, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
serializer = JobRunSerializer(exporter=instance, data=self.request.data, **self.get_serializer_kwargs())
|
||||
serializer = JobRunSerializer(exporter=instance, data=self.request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
cf = CachedFile(web_download=True)
|
||||
@@ -136,27 +134,34 @@ class ExportersMixin:
|
||||
|
||||
|
||||
class EventExportersViewSet(ExportersMixin, viewsets.ViewSet):
|
||||
permission = 'event.orders:read'
|
||||
|
||||
def get_serializer_kwargs(self):
|
||||
return {}
|
||||
permission = None
|
||||
|
||||
@cached_property
|
||||
def exporters(self):
|
||||
raw_exporters = list(init_event_exporters(
|
||||
event=self.request.event,
|
||||
user=self.request.user if self.request.user and self.request.user.is_authenticated else None,
|
||||
token=self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None,
|
||||
device=self.request.auth if isinstance(self.request.auth, Device) else None,
|
||||
request=self.request,
|
||||
))
|
||||
exporters = []
|
||||
responses = register_data_exporters.send(self.request.event)
|
||||
raw_exporters = [response(self.request.event, self.request.organizer) for r, response in responses if response]
|
||||
raw_exporters = [
|
||||
ex for ex in raw_exporters
|
||||
if ex.available_for_user(self.request.user if self.request.user and self.request.user.is_authenticated else None)
|
||||
]
|
||||
for ex in sorted(raw_exporters, key=lambda ex: str(ex.verbose_name)):
|
||||
ex._serializer = JobRunSerializer(exporter=ex)
|
||||
exporters.append(ex)
|
||||
return exporters
|
||||
|
||||
def do_export(self, cf, instance, data):
|
||||
return export.apply_async(args=(self.request.event.id, str(cf.id), instance.identifier, data))
|
||||
return export.apply_async(args=(
|
||||
self.request.event.id,
|
||||
), kwargs={
|
||||
'user': self.request.user.pk if self.request.user and self.request.user.is_authenticated else None,
|
||||
'token': self.request.auth.pk if isinstance(self.request.auth, TeamAPIToken) else None,
|
||||
'device': self.request.auth.pk if isinstance(self.request.auth, Device) else None,
|
||||
'fileid': str(cf.id),
|
||||
'provider': instance.identifier,
|
||||
'form_data': data,
|
||||
})
|
||||
|
||||
|
||||
class OrganizerExportersViewSet(ExportersMixin, viewsets.ViewSet):
|
||||
@@ -164,47 +169,23 @@ class OrganizerExportersViewSet(ExportersMixin, viewsets.ViewSet):
|
||||
|
||||
@cached_property
|
||||
def exporters(self):
|
||||
raw_exporters = list(init_organizer_exporters(
|
||||
organizer=self.request.organizer,
|
||||
user=self.request.user if self.request.user and self.request.user.is_authenticated else None,
|
||||
token=self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None,
|
||||
device=self.request.auth if isinstance(self.request.auth, Device) else None,
|
||||
request=self.request,
|
||||
))
|
||||
exporters = []
|
||||
if isinstance(self.request.auth, (Device, TeamAPIToken)):
|
||||
perm_holder = self.request.auth
|
||||
else:
|
||||
perm_holder = self.request.user
|
||||
events = perm_holder.get_events_with_permission('event.orders:read', request=self.request).filter(
|
||||
organizer=self.request.organizer
|
||||
)
|
||||
responses = register_multievent_data_exporters.send(self.request.organizer)
|
||||
raw_exporters = [
|
||||
response(Event.objects.none() if issubclass(response, OrganizerLevelExportMixin) else events, self.request.organizer)
|
||||
for r, response in responses
|
||||
if response
|
||||
]
|
||||
raw_exporters = [
|
||||
ex for ex in raw_exporters
|
||||
if (
|
||||
not isinstance(ex, OrganizerLevelExportMixin) or
|
||||
perm_holder.has_organizer_permission(self.request.organizer, ex.organizer_required_permission, self.request)
|
||||
) and ex.available_for_user(self.request.user if self.request.user and self.request.user.is_authenticated else None)
|
||||
]
|
||||
for ex in sorted(raw_exporters, key=lambda ex: str(ex.verbose_name)):
|
||||
ex._serializer = JobRunSerializer(exporter=ex, events=events)
|
||||
ex._serializer = JobRunSerializer(exporter=ex)
|
||||
exporters.append(ex)
|
||||
return exporters
|
||||
|
||||
def get_serializer_kwargs(self):
|
||||
if isinstance(self.request.auth, (Device, TeamAPIToken)):
|
||||
perm_holder = self.request.auth
|
||||
else:
|
||||
perm_holder = self.request.user
|
||||
return {
|
||||
'events': perm_holder.get_events_with_permission('event.orders:read', request=self.request).filter(
|
||||
organizer=self.request.organizer
|
||||
)
|
||||
}
|
||||
|
||||
def do_export(self, cf, instance, data):
|
||||
return multiexport.apply_async(kwargs={
|
||||
'organizer': self.request.organizer.id,
|
||||
'user': self.request.user.id if self.request.user.is_authenticated else None,
|
||||
'user': self.request.user.id if self.request.user and self.request.user.is_authenticated else None,
|
||||
'token': self.request.auth.pk if isinstance(self.request.auth, TeamAPIToken) else None,
|
||||
'device': self.request.auth.pk if isinstance(self.request.auth, Device) else None,
|
||||
'fileid': str(cf.id),
|
||||
@@ -258,8 +239,13 @@ class ScheduledEventExportViewSet(ScheduledExportersViewSet):
|
||||
|
||||
@cached_property
|
||||
def exporters(self):
|
||||
responses = register_data_exporters.send(self.request.event)
|
||||
exporters = [response(self.request.event, self.request.organizer) for r, response in responses if response]
|
||||
exporters = list(init_event_exporters(
|
||||
event=self.request.event,
|
||||
user=self.request.user if self.request.user and self.request.user.is_authenticated else None,
|
||||
token=self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None,
|
||||
device=self.request.auth if isinstance(self.request.auth, Device) else None,
|
||||
request=self.request,
|
||||
))
|
||||
return {e.identifier: e for e in exporters}
|
||||
|
||||
def perform_update(self, serializer):
|
||||
@@ -321,23 +307,15 @@ class ScheduledOrganizerExportViewSet(ScheduledExportersViewSet):
|
||||
ctx['exporters'] = self.exporters
|
||||
return ctx
|
||||
|
||||
@cached_property
|
||||
def events(self):
|
||||
if isinstance(self.request.auth, (TeamAPIToken, Device)):
|
||||
return self.request.auth.get_events_with_permission('event.orders:read')
|
||||
elif self.request.user.is_authenticated:
|
||||
return self.request.user.get_events_with_permission('event.orders:read', self.request).filter(
|
||||
organizer=self.request.organizer
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def exporters(self):
|
||||
responses = register_multievent_data_exporters.send(self.request.organizer)
|
||||
exporters = [
|
||||
response(Event.objects.none() if issubclass(response, OrganizerLevelExportMixin) else self.events,
|
||||
self.request.organizer)
|
||||
for r, response in responses if response
|
||||
]
|
||||
exporters = list(init_organizer_exporters(
|
||||
organizer=self.request.organizer,
|
||||
user=self.request.user if self.request.user and self.request.user.is_authenticated else None,
|
||||
token=self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None,
|
||||
device=self.request.auth if isinstance(self.request.auth, Device) else None,
|
||||
request=self.request,
|
||||
))
|
||||
return {e.identifier: e for e in exporters}
|
||||
|
||||
def perform_update(self, serializer):
|
||||
|
||||
@@ -73,6 +73,9 @@ class BaseExporter:
|
||||
self.events = Event.objects.filter(pk=event.pk)
|
||||
self.timezone = event.timezone
|
||||
|
||||
if hasattr(self, 'organizer_required_permission'):
|
||||
raise TypeError("Deprecated attribute organizer_required_permission no longer supported.")
|
||||
|
||||
def __str__(self):
|
||||
return self.identifier
|
||||
|
||||
@@ -176,17 +179,29 @@ class BaseExporter:
|
||||
"""
|
||||
return True
|
||||
|
||||
|
||||
class OrganizerLevelExportMixin:
|
||||
@property
|
||||
def organizer_required_permission(self) -> str:
|
||||
@classmethod
|
||||
def get_required_event_permission(cls) -> str:
|
||||
"""
|
||||
The permission level required to use this exporter. Only useful for organizer-level exports,
|
||||
not for event-level exports.
|
||||
The permission level required to use this exporter for events. For multi-event-exports, this will be used
|
||||
to limit the selection of events. Will be ignored if the `OrganizerLevelExportMixin` mixin is used.
|
||||
"""
|
||||
return 'event.orders:read'
|
||||
|
||||
|
||||
class OrganizerLevelExportMixin:
|
||||
@classmethod
|
||||
def required_event_permission(cls):
|
||||
raise TypeError("required_event_permission may not be called on OrganizerLevelExportMixin")
|
||||
|
||||
@classmethod
|
||||
def get_required_organizer_permission(cls) -> str:
|
||||
"""
|
||||
The permission level required to use this exporter. Must be set for organizer-level exports. Set to `None` to
|
||||
allow everyone with any access to the organizer.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class ListExporter(BaseExporter):
|
||||
ProgressSetTotal = namedtuple('ProgressSetTotal', 'total')
|
||||
|
||||
|
||||
@@ -47,10 +47,13 @@ from ..signals import register_multievent_data_exporters
|
||||
class CustomerListExporter(OrganizerLevelExportMixin, ListExporter):
|
||||
identifier = 'customerlist'
|
||||
verbose_name = gettext_lazy('Customer accounts')
|
||||
organizer_required_permission = 'organizer.customers:write'
|
||||
category = pgettext_lazy('export_category', 'Customer accounts')
|
||||
description = gettext_lazy('Download a spreadsheet of all currently registered customer accounts.')
|
||||
|
||||
@classmethod
|
||||
def get_required_organizer_permission(cls) -> str:
|
||||
return 'organizer.customers:write'
|
||||
|
||||
@property
|
||||
def additional_form_fields(self):
|
||||
return OrderedDict(
|
||||
|
||||
@@ -1235,11 +1235,14 @@ class QuotaListExporter(ListExporter):
|
||||
class GiftcardTransactionListExporter(OrganizerLevelExportMixin, ListExporter):
|
||||
identifier = 'giftcardtransactionlist'
|
||||
verbose_name = gettext_lazy('Gift card transactions')
|
||||
organizer_required_permission = 'organizer.giftcards:write'
|
||||
category = pgettext_lazy('export_category', 'Gift cards')
|
||||
description = gettext_lazy('Download a spreadsheet of all gift card transactions.')
|
||||
repeatable_read = False
|
||||
|
||||
@classmethod
|
||||
def get_required_organizer_permission(cls) -> str:
|
||||
return 'organizer.giftcards:read'
|
||||
|
||||
@property
|
||||
def additional_form_fields(self):
|
||||
d = [
|
||||
@@ -1342,10 +1345,13 @@ class GiftcardRedemptionListExporter(ListExporter):
|
||||
class GiftcardListExporter(OrganizerLevelExportMixin, ListExporter):
|
||||
identifier = 'giftcardlist'
|
||||
verbose_name = gettext_lazy('Gift cards')
|
||||
organizer_required_permission = 'organizer.giftcards:write'
|
||||
category = pgettext_lazy('export_category', 'Gift cards')
|
||||
description = gettext_lazy('Download a spreadsheet of all gift cards including their current value.')
|
||||
|
||||
@classmethod
|
||||
def get_required_organizer_permission(cls) -> str:
|
||||
return 'organizer.giftcards:read'
|
||||
|
||||
@property
|
||||
def additional_form_fields(self):
|
||||
return OrderedDict(
|
||||
|
||||
@@ -36,6 +36,10 @@ class ReusableMediaExporter(OrganizerLevelExportMixin, ListExporter):
|
||||
description = _('Download a spread sheet with the data of all reusable medias on your account.')
|
||||
repeatable_read = False
|
||||
|
||||
@classmethod
|
||||
def get_required_organizer_permission(cls) -> str:
|
||||
return "organizer.reusablemedia:read"
|
||||
|
||||
def iterate_list(self, form_data):
|
||||
media = ReusableMedium.objects.filter(
|
||||
organizer=self.organizer,
|
||||
|
||||
@@ -34,7 +34,7 @@ from django_scopes import scopes_disabled
|
||||
from i18nfield.strings import LazyI18nString
|
||||
|
||||
from pretix.base.email import get_email_context
|
||||
from pretix.base.exporter import OrganizerLevelExportMixin
|
||||
from pretix.base.exporter import BaseExporter, OrganizerLevelExportMixin
|
||||
from pretix.base.i18n import LazyLocaleException, language
|
||||
from pretix.base.models import (
|
||||
CachedFile, Device, Event, Organizer, ScheduledEventExport, TeamAPIToken,
|
||||
@@ -64,7 +64,15 @@ class ExportEmptyError(ExportError):
|
||||
|
||||
|
||||
@app.task(base=ProfiledEventTask, throws=(ExportError, ExportEmptyError), bind=True)
|
||||
def export(self, event: Event, fileid: str, provider: str, form_data: Dict[str, Any]) -> None:
|
||||
def export(self, event: Event, user: User, device: int, token: int, fileid: str, provider: str,
|
||||
form_data: Dict[str, Any], staff_session=False) -> None:
|
||||
if user:
|
||||
user = User.objects.get(pk=user)
|
||||
if device:
|
||||
device = Device.objects.get(pk=device)
|
||||
if token:
|
||||
device = TeamAPIToken.objects.get(pk=token)
|
||||
|
||||
def set_progress(val):
|
||||
if not self.request.called_directly:
|
||||
self.update_state(
|
||||
@@ -72,30 +80,38 @@ def export(self, event: Event, fileid: str, provider: str, form_data: Dict[str,
|
||||
meta={'value': val}
|
||||
)
|
||||
|
||||
ex = init_event_exporter(
|
||||
identifier=provider,
|
||||
event=event,
|
||||
user=user,
|
||||
token=token,
|
||||
device=device,
|
||||
staff_session=staff_session,
|
||||
progress_callback=set_progress,
|
||||
)
|
||||
if not ex:
|
||||
raise ExportError(
|
||||
gettext('Export not found or you do not have sufficient permission to perform this export.')
|
||||
)
|
||||
|
||||
file = CachedFile.objects.get(id=fileid)
|
||||
with language(event.settings.locale, event.settings.region), override(event.settings.timezone):
|
||||
responses = register_data_exporters.send(event)
|
||||
for recv, response in responses:
|
||||
if not response:
|
||||
continue
|
||||
ex = response(event, event.organizer, set_progress)
|
||||
if ex.identifier == provider:
|
||||
if ex.repeatable_read:
|
||||
with repeatable_reads_transaction():
|
||||
d = ex.render(form_data)
|
||||
else:
|
||||
d = ex.render(form_data)
|
||||
if ex.repeatable_read:
|
||||
with repeatable_reads_transaction():
|
||||
d = ex.render(form_data)
|
||||
else:
|
||||
d = ex.render(form_data)
|
||||
|
||||
if d is None:
|
||||
raise ExportError(
|
||||
gettext('Your export did not contain any data.')
|
||||
)
|
||||
file.filename, file.type, data = d
|
||||
if d is None:
|
||||
raise ExportError(
|
||||
gettext('Your export did not contain any data.')
|
||||
)
|
||||
file.filename, file.type, data = d
|
||||
|
||||
close_old_connections() # This task can run very long, we might need a new DB connection
|
||||
close_old_connections() # This task can run very long, we might need a new DB connection
|
||||
|
||||
f = ContentFile(data)
|
||||
file.file.save(cachedfile_name(file, file.filename), f)
|
||||
f = ContentFile(data)
|
||||
file.file.save(cachedfile_name(file, file.filename), f)
|
||||
return str(file.pk)
|
||||
|
||||
|
||||
@@ -105,10 +121,7 @@ def multiexport(self, organizer: Organizer, user: User, device: int, token: int,
|
||||
if device:
|
||||
device = Device.objects.get(pk=device)
|
||||
if token:
|
||||
device = TeamAPIToken.objects.get(pk=token)
|
||||
allowed_events = (device or token or user).get_events_with_permission('event.orders:read')
|
||||
if user and staff_session:
|
||||
allowed_events = organizer.events.all()
|
||||
token = TeamAPIToken.objects.get(pk=token)
|
||||
|
||||
def set_progress(val):
|
||||
if not self.request.called_directly:
|
||||
@@ -118,12 +131,35 @@ def multiexport(self, organizer: Organizer, user: User, device: int, token: int,
|
||||
)
|
||||
|
||||
file = CachedFile.objects.get(id=fileid)
|
||||
|
||||
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):
|
||||
event_qs = event_qs.filter(slug__in=form_data.get('events'))
|
||||
else:
|
||||
event_qs = event_qs.filter(pk__in=form_data.get('events'))
|
||||
|
||||
ex = init_organizer_exporter(
|
||||
identifier=provider,
|
||||
organizer=organizer,
|
||||
user=user,
|
||||
token=token,
|
||||
device=device,
|
||||
staff_session=staff_session,
|
||||
progress_callback=set_progress,
|
||||
event_qs=event_qs,
|
||||
)
|
||||
if not ex:
|
||||
raise ExportError(
|
||||
gettext('Export not found or you do not have sufficient permission to perform this export.')
|
||||
)
|
||||
|
||||
if user:
|
||||
locale = user.locale
|
||||
timezone = user.timezone
|
||||
region = None # todo: add to user?
|
||||
else:
|
||||
e = allowed_events.first()
|
||||
e = ex.events.first()
|
||||
if e:
|
||||
locale = e.settings.locale
|
||||
timezone = e.settings.timezone
|
||||
@@ -133,47 +169,140 @@ def multiexport(self, organizer: Organizer, user: User, device: int, token: int,
|
||||
timezone = organizer.settings.timezone or settings.TIME_ZONE
|
||||
region = organizer.settings.region
|
||||
with language(locale, region), override(timezone):
|
||||
if form_data.get('events') is not None and not form_data.get('all_events'):
|
||||
if isinstance(form_data['events'][0], str):
|
||||
events = allowed_events.filter(slug__in=form_data.get('events'), organizer=organizer)
|
||||
else:
|
||||
events = allowed_events.filter(pk__in=form_data.get('events'), organizer=organizer)
|
||||
if ex.repeatable_read:
|
||||
with repeatable_reads_transaction():
|
||||
d = ex.render(form_data)
|
||||
else:
|
||||
events = allowed_events.filter(organizer=organizer)
|
||||
responses = register_multievent_data_exporters.send(organizer)
|
||||
d = ex.render(form_data)
|
||||
if d is None:
|
||||
raise ExportError(
|
||||
gettext('Your export did not contain any data.')
|
||||
)
|
||||
file.filename, file.type, data = d
|
||||
|
||||
for recv, response in responses:
|
||||
if not response:
|
||||
continue
|
||||
ex = response(events, organizer, set_progress)
|
||||
if ex.identifier == provider:
|
||||
if (
|
||||
isinstance(ex, OrganizerLevelExportMixin) and
|
||||
not staff_session and
|
||||
not (device or token or user).has_organizer_permission(organizer, ex.organizer_required_permission)
|
||||
):
|
||||
raise ExportError(
|
||||
gettext('You do not have sufficient permission to perform this export.')
|
||||
)
|
||||
close_old_connections() # This task can run very long, we might need a new DB connection
|
||||
|
||||
if ex.repeatable_read:
|
||||
with repeatable_reads_transaction():
|
||||
d = ex.render(form_data)
|
||||
else:
|
||||
d = ex.render(form_data)
|
||||
if d is None:
|
||||
raise ExportError(
|
||||
gettext('Your export did not contain any data.')
|
||||
)
|
||||
file.filename, file.type, data = d
|
||||
|
||||
close_old_connections() # This task can run very long, we might need a new DB connection
|
||||
|
||||
f = ContentFile(data)
|
||||
file.file.save(cachedfile_name(file, file.filename), f)
|
||||
f = ContentFile(data)
|
||||
file.file.save(cachedfile_name(file, file.filename), f)
|
||||
return str(file.pk)
|
||||
|
||||
|
||||
def init_event_exporter(identifier, **kwargs):
|
||||
for ex in init_event_exporters(**kwargs):
|
||||
if ex.identifier == identifier:
|
||||
return ex
|
||||
return None
|
||||
|
||||
|
||||
def init_event_exporters(event, user=None, token=None, device=None, request=None, progress_callback=None, staff_session=False):
|
||||
if not user and not token and not device:
|
||||
raise ValueError("No auth source given.")
|
||||
perm_holder = device or token or user
|
||||
|
||||
responses = register_data_exporters.send(event)
|
||||
for r, response in responses:
|
||||
if not response:
|
||||
continue
|
||||
|
||||
if issubclass(response, OrganizerLevelExportMixin):
|
||||
raise TypeError("Cannot user organizer-level exporter on event level")
|
||||
|
||||
permission_name = response.get_required_event_permission()
|
||||
if not perm_holder.has_event_permission(event.organizer, event, permission_name, request) and not staff_session:
|
||||
continue
|
||||
|
||||
exporter: BaseExporter = response(event=event, organizer=event.organizer, progress_callback=progress_callback)
|
||||
|
||||
if not exporter.available_for_user(user if user and user.is_authenticated else None):
|
||||
continue
|
||||
|
||||
yield exporter
|
||||
|
||||
|
||||
def init_organizer_exporter(identifier, **kwargs):
|
||||
for ex in init_organizer_exporters(**kwargs):
|
||||
if ex.identifier == identifier:
|
||||
return ex
|
||||
return None
|
||||
|
||||
|
||||
def init_organizer_exporters(
|
||||
organizer, user=None, token=None, device=None, request=None, progress_callback=None, staff_session=False, event_qs=None
|
||||
):
|
||||
if not user and not token and not device:
|
||||
raise ValueError("No auth source given.")
|
||||
perm_holder = device or token or user
|
||||
|
||||
_event_list_cache = {}
|
||||
_has_permission_on_any_team_cache = {}
|
||||
_team_cache = None
|
||||
|
||||
responses = register_multievent_data_exporters.send(organizer)
|
||||
for r, response in responses:
|
||||
if not response:
|
||||
continue
|
||||
|
||||
if issubclass(response, OrganizerLevelExportMixin):
|
||||
exporter: BaseExporter = response(event=Event.objects.none(), organizer=organizer, progress_callback=progress_callback)
|
||||
|
||||
try:
|
||||
if not perm_holder.has_organizer_permission(organizer, response.get_required_organizer_permission(), request) and not staff_session:
|
||||
continue
|
||||
except NotImplementedError:
|
||||
logger.error(f"Not showing export {response} because get_required_organizer_permission() is not implemented.")
|
||||
continue
|
||||
|
||||
else:
|
||||
permission_name = response.get_required_event_permission()
|
||||
|
||||
if permission_name not in _event_list_cache:
|
||||
if staff_session:
|
||||
events = event_qs.all()
|
||||
elif event_qs is not None:
|
||||
events = event_qs.filter(
|
||||
pk__in=perm_holder.get_events_with_permission(
|
||||
permission_name, request=request
|
||||
).filter(
|
||||
organizer=organizer
|
||||
).values("id")
|
||||
)
|
||||
else:
|
||||
events = perm_holder.get_events_with_permission(
|
||||
permission_name, request=request
|
||||
).filter(
|
||||
organizer=organizer
|
||||
)
|
||||
|
||||
_event_list_cache[permission_name] = events
|
||||
|
||||
if permission_name not in _has_permission_on_any_team_cache:
|
||||
# Check if the user has this event permission on any teams they are part of to decide whether to show
|
||||
# the export at all.
|
||||
# This is different from _event_list_cache[permission_name].exists() for the case of an organizer with
|
||||
# zero events in total, or a team with zero events. In these cases, we still want people to be able
|
||||
# to see waht exports they'll get once they have events.
|
||||
if user:
|
||||
if _team_cache is None:
|
||||
_team_cache = list(user.teams.filter(organizer=organizer))
|
||||
_has_permission_on_any_team_cache[permission_name] = staff_session or any(
|
||||
t.has_event_permission(permission_name) for t in _team_cache
|
||||
)
|
||||
elif token:
|
||||
_has_permission_on_any_team_cache[permission_name] = token.team.has_event_permission(permission_name)
|
||||
elif device:
|
||||
_has_permission_on_any_team_cache[permission_name] = device.has_event_permission(permission_name)
|
||||
|
||||
if not _has_permission_on_any_team_cache[permission_name]:
|
||||
continue
|
||||
|
||||
exporter: BaseExporter = response(event=_event_list_cache[permission_name], organizer=organizer, progress_callback=progress_callback)
|
||||
|
||||
if not exporter.available_for_user(user if user and user.is_authenticated else None):
|
||||
continue
|
||||
|
||||
yield exporter
|
||||
|
||||
|
||||
def _run_scheduled_export(schedule, context: Union[Event, Organizer], exporter, config_url, retry_func, has_permission):
|
||||
with language(schedule.locale, context.settings.region), override(schedule.tz):
|
||||
file = CachedFile(web_download=False)
|
||||
@@ -217,7 +346,7 @@ def _run_scheduled_export(schedule, context: Union[Event, Organizer], exporter,
|
||||
|
||||
try:
|
||||
if not exporter:
|
||||
raise ExportError("Export type not found.")
|
||||
raise ExportError("Export type not found or permission denied.")
|
||||
if exporter.repeatable_read:
|
||||
with repeatable_reads_transaction():
|
||||
d = exporter.render(schedule.export_form_data)
|
||||
@@ -291,31 +420,20 @@ def _run_scheduled_export(schedule, context: Union[Event, Organizer], exporter,
|
||||
def scheduled_organizer_export(self, organizer: Organizer, schedule: int) -> None:
|
||||
schedule = organizer.scheduled_exports.get(pk=schedule)
|
||||
|
||||
allowed_events = schedule.owner.get_events_with_permission('event.orders:read')
|
||||
event_qs = organizer.events.all()
|
||||
if schedule.export_form_data.get('events') is not None and not schedule.export_form_data.get('all_events'):
|
||||
if isinstance(schedule.export_form_data['events'][0], str):
|
||||
events = allowed_events.filter(slug__in=schedule.export_form_data.get('events'), organizer=organizer)
|
||||
event_qs = event_qs.filter(slug__in=schedule.export_form_data.get('events'))
|
||||
else:
|
||||
events = allowed_events.filter(pk__in=schedule.export_form_data.get('events'), organizer=organizer)
|
||||
else:
|
||||
events = allowed_events.filter(organizer=organizer)
|
||||
|
||||
responses = register_multievent_data_exporters.send(organizer)
|
||||
exporter = None
|
||||
for recv, response in responses:
|
||||
if not response:
|
||||
continue
|
||||
ex = response(events, organizer)
|
||||
if ex.identifier == schedule.export_identifier:
|
||||
exporter = ex
|
||||
break
|
||||
event_qs = event_qs.filter(pk__in=schedule.export_form_data.get('events'))
|
||||
|
||||
exporter = init_organizer_exporter(
|
||||
identifier=schedule.export_identifier,
|
||||
organizer=organizer,
|
||||
user=schedule.owner,
|
||||
event_qs=event_qs,
|
||||
)
|
||||
has_permission = schedule.owner.is_active
|
||||
if isinstance(exporter, OrganizerLevelExportMixin):
|
||||
if not schedule.owner.has_organizer_permission(organizer, exporter.organizer_required_permission):
|
||||
has_permission = False
|
||||
if exporter and not exporter.available_for_user(schedule.owner):
|
||||
has_permission = False
|
||||
|
||||
_run_scheduled_export(
|
||||
schedule,
|
||||
@@ -336,17 +454,12 @@ def scheduled_organizer_export(self, organizer: Organizer, schedule: int) -> Non
|
||||
def scheduled_event_export(self, event: Event, schedule: int) -> None:
|
||||
schedule = event.scheduled_exports.get(pk=schedule)
|
||||
|
||||
responses = register_data_exporters.send(event)
|
||||
exporter = None
|
||||
for recv, response in responses:
|
||||
if not response:
|
||||
continue
|
||||
ex = response(event, event.organizer)
|
||||
if ex.identifier == schedule.export_identifier:
|
||||
exporter = ex
|
||||
break
|
||||
|
||||
has_permission = schedule.owner.is_active and schedule.owner.has_event_permission(event.organizer, event, 'event.orders:read')
|
||||
exporter = init_event_exporter(
|
||||
identifier=schedule.export_identifier,
|
||||
event=event,
|
||||
user=schedule.owner,
|
||||
)
|
||||
has_permission = schedule.owner.is_active
|
||||
|
||||
_run_scheduled_export(
|
||||
schedule,
|
||||
|
||||
@@ -92,7 +92,9 @@ from pretix.base.payment import PaymentException
|
||||
from pretix.base.secrets import assign_ticket_secret
|
||||
from pretix.base.services import tickets
|
||||
from pretix.base.services.cancelevent import cancel_event
|
||||
from pretix.base.services.export import export, scheduled_event_export
|
||||
from pretix.base.services.export import (
|
||||
export, init_event_exporters, scheduled_event_export,
|
||||
)
|
||||
from pretix.base.services.invoices import (
|
||||
generate_cancellation, generate_invoice, invoice_pdf, invoice_pdf_task,
|
||||
invoice_qualified, regenerate_invoice, transmit_invoice,
|
||||
@@ -111,9 +113,7 @@ from pretix.base.services.tax import (
|
||||
VATIDFinalError, VATIDTemporaryError, validate_vat_id,
|
||||
)
|
||||
from pretix.base.services.tickets import generate
|
||||
from pretix.base.signals import (
|
||||
order_modified, register_data_exporters, register_ticket_outputs,
|
||||
)
|
||||
from pretix.base.signals import order_modified, register_ticket_outputs
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
from pretix.base.templatetags.rich_text import markdown_compile_email
|
||||
from pretix.base.views.mixins import OrderQuestionsViewMixin
|
||||
@@ -2660,12 +2660,7 @@ class OrderGo(EventPermissionRequiredMixin, View):
|
||||
class ExportMixin:
|
||||
@cached_property
|
||||
def exporters(self):
|
||||
responses = register_data_exporters.send(self.request.event)
|
||||
raw_exporters = [response(self.request.event, self.request.organizer) for r, response in responses if response]
|
||||
raw_exporters = [
|
||||
ex for ex in raw_exporters
|
||||
if ex.available_for_user(self.request.user if self.request.user and self.request.user.is_authenticated else None)
|
||||
]
|
||||
raw_exporters = list(init_event_exporters(self.request.event, user=self.request.user, request=self.request))
|
||||
return sorted(
|
||||
raw_exporters,
|
||||
key=lambda ex: (0 if ex.category else 1, ex.category or "", 0 if ex.featured else 1, str(ex.verbose_name).lower())
|
||||
@@ -2737,7 +2732,7 @@ class ExportMixin:
|
||||
|
||||
|
||||
class ExportDoView(EventPermissionRequiredMixin, ExportMixin, AsyncAction, TemplateView):
|
||||
permission = 'event.orders:read'
|
||||
permission = None
|
||||
known_errortypes = ['ExportError', 'ExportEmptyError']
|
||||
task = export
|
||||
template_name = 'pretixcontrol/orders/export_form.html'
|
||||
@@ -2782,11 +2777,20 @@ class ExportDoView(EventPermissionRequiredMixin, ExportMixin, AsyncAction, Templ
|
||||
cf.date = now()
|
||||
cf.expires = now() + timedelta(hours=24)
|
||||
cf.save()
|
||||
return self.do(self.request.event.id, str(cf.id), self.exporter.identifier, data)
|
||||
return self.do(
|
||||
self.request.event.id,
|
||||
user=self.request.user.id,
|
||||
fileid=str(cf.id),
|
||||
provider=self.exporter.identifier,
|
||||
device=None,
|
||||
token=None,
|
||||
form_data=data,
|
||||
staff_session=self.request.user.has_active_staff_session(self.request.session.session_key)
|
||||
)
|
||||
|
||||
|
||||
class ExportView(EventPermissionRequiredMixin, ExportMixin, ListView):
|
||||
permission = 'event.orders:read'
|
||||
permission = None
|
||||
paginate_by = 25
|
||||
context_object_name = 'scheduled'
|
||||
|
||||
@@ -2906,7 +2910,7 @@ class ExportView(EventPermissionRequiredMixin, ExportMixin, ListView):
|
||||
|
||||
|
||||
class DeleteScheduledExportView(EventPermissionRequiredMixin, ExportMixin, CompatDeleteView):
|
||||
permission = 'event.orders:read'
|
||||
permission = None
|
||||
template_name = 'pretixcontrol/orders/export_delete.html'
|
||||
context_object_name = 'export'
|
||||
|
||||
|
||||
@@ -104,9 +104,10 @@ from pretix.base.plugins import (
|
||||
PLUGIN_LEVEL_EVENT, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID,
|
||||
PLUGIN_LEVEL_ORGANIZER,
|
||||
)
|
||||
from pretix.base.services.export import multiexport, scheduled_organizer_export
|
||||
from pretix.base.services.export import (
|
||||
init_organizer_exporters, multiexport, scheduled_organizer_export,
|
||||
)
|
||||
from pretix.base.services.mail import SendMailException, mail, prefix_subject
|
||||
from pretix.base.signals import register_multievent_data_exporters
|
||||
from pretix.base.templatetags.rich_text import markdown_compile_email
|
||||
from pretix.base.views.tasks import AsyncAction
|
||||
from pretix.control.forms.exports import ScheduledOrganizerExportForm
|
||||
@@ -1982,7 +1983,7 @@ class ExportMixin:
|
||||
)),
|
||||
('events',
|
||||
forms.ModelMultipleChoiceField(
|
||||
queryset=self.events,
|
||||
queryset=ex.events,
|
||||
widget=forms.CheckboxSelectMultiple(
|
||||
attrs={
|
||||
'class': 'scrolling-multiple-choice',
|
||||
@@ -1995,29 +1996,9 @@ class ExportMixin:
|
||||
])
|
||||
return ex
|
||||
|
||||
@cached_property
|
||||
def events(self):
|
||||
return self.request.user.get_events_with_permission('event.orders:read', request=self.request).filter(
|
||||
organizer=self.request.organizer
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def exporters(self):
|
||||
responses = register_multievent_data_exporters.send(self.request.organizer)
|
||||
raw_exporters = [
|
||||
response(Event.objects.none() if issubclass(response, OrganizerLevelExportMixin) else self.events,
|
||||
self.request.organizer)
|
||||
for r, response in responses
|
||||
if response
|
||||
]
|
||||
raw_exporters = [
|
||||
ex for ex in raw_exporters
|
||||
if (
|
||||
not isinstance(ex, OrganizerLevelExportMixin) or
|
||||
self.request.user.has_organizer_permission(self.request.organizer, ex.organizer_required_permission,
|
||||
self.request)
|
||||
) and ex.available_for_user(self.request.user if self.request.user and self.request.user.is_authenticated else None)
|
||||
]
|
||||
raw_exporters = list(init_organizer_exporters(self.request.organizer, user=self.request.user, request=self.request))
|
||||
return sorted(
|
||||
raw_exporters,
|
||||
key=lambda ex: (
|
||||
@@ -2208,11 +2189,17 @@ class ExportView(OrganizerPermissionRequiredMixin, ExportMixin, ListView):
|
||||
return self.get_scheduled_queryset()
|
||||
|
||||
def has_permission(self):
|
||||
if isinstance(self.exporter, OrganizerLevelExportMixin):
|
||||
if not self.request.user.has_organizer_permission(self.request.organizer, self.exporter.organizer_required_permission):
|
||||
# Check if permission exists even without staff session
|
||||
if self.exporter:
|
||||
if isinstance(self.exporter, OrganizerLevelExportMixin):
|
||||
if not self.request.user.has_organizer_permission(self.request.organizer, self.exporter.get_required_organizer_permission()):
|
||||
return False
|
||||
else:
|
||||
permission_name = self.exporter.get_required_event_permission()
|
||||
if not any(t.has_event_permission(permission_name) for t in self.request.user.teams.filter(organizer=self.request.organizer)):
|
||||
return False
|
||||
if not self.exporter.available_for_user(self.request.user):
|
||||
return False
|
||||
if self.exporter and not self.exporter.available_for_user(self.request.user):
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
||||
@@ -194,7 +194,6 @@ event_permission_sub_urls = [
|
||||
('post', 'event.orders:write', 'cartpositions/', 400),
|
||||
('delete', 'event.orders:write', 'cartpositions/1/', 404),
|
||||
('post', 'event.orders:read', 'exporters/invoicedata/run/', 400),
|
||||
('get', 'event.orders:read', 'exporters/invoicedata/download/bc3f9884-26ee-425b-8636-80613f84b6fa/3cb49ae6-eda3-4605-814e-099e23777b36/', 404),
|
||||
('get', None, 'item_meta_properties/', 200),
|
||||
('get', None, 'item_meta_properties/0/', 404),
|
||||
('post', 'event.settings.general:write', 'item_meta_properties/', 400),
|
||||
|
||||
@@ -105,7 +105,7 @@ def test_event_fail_invalid_config(event, user):
|
||||
assert s.error_counter == 1
|
||||
assert len(djmail.outbox) == 1
|
||||
assert djmail.outbox[0].subject == "Export failed"
|
||||
assert "Reason: Export type not found." in djmail.outbox[0].body
|
||||
assert "Reason: Export type not found" in djmail.outbox[0].body
|
||||
assert djmail.outbox[0].to == [user.email]
|
||||
|
||||
|
||||
@@ -153,7 +153,7 @@ def test_event_fail_user_no_permission(event, user, team):
|
||||
assert s.error_counter == 1
|
||||
assert len(djmail.outbox) == 1
|
||||
assert djmail.outbox[0].subject == "Export failed"
|
||||
assert "Reason: Permission denied." in djmail.outbox[0].body
|
||||
assert "Reason: Export type not found or permission denied." in djmail.outbox[0].body
|
||||
assert djmail.outbox[0].to == [user.email]
|
||||
|
||||
|
||||
@@ -236,7 +236,7 @@ def test_organizer_fail_invalid_config(event, user):
|
||||
assert s.error_counter == 1
|
||||
assert len(djmail.outbox) == 1
|
||||
assert djmail.outbox[0].subject == "Export failed"
|
||||
assert "Reason: Export type not found." in djmail.outbox[0].body
|
||||
assert "Reason: Export type not found" in djmail.outbox[0].body
|
||||
assert djmail.outbox[0].to == [user.email]
|
||||
|
||||
|
||||
@@ -284,7 +284,7 @@ def test_organizer_fail_user_does_not_have_specific_permission(event, user, team
|
||||
assert s.error_counter == 1
|
||||
assert len(djmail.outbox) == 1
|
||||
assert djmail.outbox[0].subject == "Export failed"
|
||||
assert "Reason: Permission denied." in djmail.outbox[0].body
|
||||
assert "Reason: Export type not found or permission denied." in djmail.outbox[0].body
|
||||
assert djmail.outbox[0].to == [user.email]
|
||||
|
||||
|
||||
|
||||
@@ -365,8 +365,6 @@ event_permission_urls = [
|
||||
("event.subevents:write", "subevents/bulk_action", 302, HTTP_POST),
|
||||
("event.subevents:write", "subevents/bulk_edit", 404, HTTP_POST),
|
||||
("event.orders:read", "orders/overview/", 200, HTTP_GET),
|
||||
("event.orders:read", "orders/export/", 200, HTTP_GET),
|
||||
("event.orders:read", "orders/export/do", 302, HTTP_POST),
|
||||
("event.orders:read", "orders/", 200, HTTP_GET),
|
||||
("event.orders:read", "orders/FOO/", 200, HTTP_GET),
|
||||
("event.orders:write", "orders/FOO/extend", 200, HTTP_GET),
|
||||
|
||||
Reference in New Issue
Block a user