Pluggable permissions (#5728)

* Data model draft

* Refactor query and assignment usages of old permissions

* Backend UI

* API serializer

* Big string replace

* Docs, tests and fixes for teams api

* Update docs for device auth

* Eliminate old names

* Make tests pass

* Use new permissions, remove inconsistencies

* Add test for translations

* Show plugin permissions

* Add permission for seating plans

* Fix plugin activation

* Fix failing test

* Refactor to permission groups

* Update doc/api/resources/devices.rst

Co-authored-by: luelista <weller@rami.io>

* Update doc/api/resources/events.rst

Co-authored-by: luelista <weller@rami.io>

* Update src/pretix/api/serializers/organizer.py

Co-authored-by: luelista <weller@rami.io>

* Fix typo

* Fix python version compat

* Replacement after rebase

* Add proper permission handling for exports

* Docs for exporters

* Runtime linting of permission names

* Fix typos

* Show export page even without orders permission

* More legacy compat

* Do not strongly validate before plugins are loaded

* Rebase migration

* Add permission for outgoing mails

* Review notes

* Update doc/api/resources/teams.rst

Co-authored-by: Richard Schreiber <schreiber@pretix.eu>

* Clean up logic around exporters

* Review and failures

* Fix migration leading to forbidden combination

* Handle permissions on event copying

* Remove print-statements

* Make test clearer

* Review feedback

* Add AnyPermissionOf

* migration safety

---------

Co-authored-by: luelista <weller@rami.io>
Co-authored-by: Richard Schreiber <schreiber@pretix.eu>
This commit is contained in:
Raphael Michel
2026-03-17 14:43:56 +01:00
committed by GitHub
parent eddde2b6c0
commit df0b580dd6
203 changed files with 5374 additions and 2331 deletions

View File

@@ -96,15 +96,18 @@ from pretix.base.models.giftcards import (
GiftCardAcceptance, GiftCardTransaction, gen_giftcard_secret,
)
from pretix.base.models.orders import CancellationRequest
from pretix.base.models.organizer import SalesChannel, TeamAPIToken
from pretix.base.models.organizer import (
SalesChannel, TeamAPIToken, TeamQuerySet,
)
from pretix.base.payment import PaymentException
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 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
@@ -245,13 +248,13 @@ class OrganizerDetail(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin
class OrganizerTeamView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DetailView):
model = Organizer
template_name = 'pretixcontrol/organizers/teams.html'
permission = 'can_change_permissions'
permission = 'organizer.teams:write'
context_object_name = 'organizer'
class OrganizerSettingsFormView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, FormView):
model = Organizer
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
@@ -282,7 +285,7 @@ class OrganizerSettingsFormView(OrganizerDetailViewMixin, OrganizerPermissionReq
class OrganizerMailSettings(OrganizerSettingsFormView):
form_class = MailSettingsForm
template_name = 'pretixcontrol/organizers/mail.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
def get_success_url(self):
return reverse('control:organizer.settings.mail', kwargs={
@@ -308,7 +311,7 @@ class OrganizerMailSettings(OrganizerSettingsFormView):
class MailSettingsSetup(OrganizerPermissionRequiredMixin, MailSettingsSetupView):
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
basetpl = 'pretixcontrol/base.html'
def get_success_url(self):
@@ -323,7 +326,7 @@ class MailSettingsSetup(OrganizerPermissionRequiredMixin, MailSettingsSetupView)
class MailSettingsPreview(OrganizerPermissionRequiredMixin, View):
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
# return the origin text if key is missing in dict
class SafeDict(dict):
@@ -455,7 +458,7 @@ class OrganizerUpdate(OrganizerPermissionRequiredMixin, UpdateView):
model = Organizer
form_class = OrganizerUpdateForm
template_name = 'pretixcontrol/organizers/edit.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
context_object_name = 'organizer'
@cached_property
@@ -583,10 +586,7 @@ class OrganizerCreate(CreateView):
ret = super().form_valid(form)
t = Team.objects.create(
organizer=form.instance, name=_('Administrators'),
all_events=True, can_create_events=True, can_change_teams=True, can_manage_gift_cards=True,
can_change_organizer_settings=True, can_change_event_settings=True, can_change_items=True,
can_manage_customers=True, can_manage_reusable_media=True,
can_view_orders=True, can_change_orders=True, can_view_vouchers=True, can_change_vouchers=True
all_events=True, all_event_permissions=True, all_organizer_permissions=True,
)
t.members.add(self.request.user)
return ret
@@ -600,7 +600,7 @@ class OrganizerCreate(CreateView):
class OrganizerPlugins(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, TemplateView, SingleObjectMixin):
model = Organizer
context_object_name = 'organizer'
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
template_name = 'pretixcontrol/organizers/plugins.html'
def get_object(self, queryset=None) -> Organizer:
@@ -772,14 +772,14 @@ class OrganizerPlugins(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixi
class OrganizerPluginEvents(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, FormView):
model = Organizer
context_object_name = 'organizer'
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
template_name = 'pretixcontrol/organizers/plugin_events.html'
form_class = OrganizerPluginEventsForm
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["events"] = self.request.user.get_events_with_permission(
"can_change_event_settings", request=self.request
"event.settings.general:write", request=self.request
).filter(organizer=self.request.organizer)
kwargs["initial"] = {
"events": self.request.organizer.events.filter(plugins__regex='(^|,)' + self.plugin.module + '(,|$)')
@@ -857,7 +857,7 @@ class OrganizerPluginEvents(OrganizerDetailViewMixin, OrganizerPermissionRequire
class TeamListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, PaginationMixin, ListView):
model = Team
template_name = 'pretixcontrol/organizers/teams.html'
permission = 'can_change_teams'
permission = 'organizer.teams:write'
context_object_name = 'teams'
def get_queryset(self):
@@ -883,7 +883,7 @@ class TeamListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, P
class TeamCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
model = Team
template_name = 'pretixcontrol/organizers/team_edit.html'
permission = 'can_change_teams'
permission = 'organizer.teams:write'
form_class = TeamForm
def get_form_kwargs(self):
@@ -906,10 +906,7 @@ class TeamCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
form.instance.organizer = self.request.organizer
ret = super().form_valid(form)
form.instance.members.add(self.request.user)
form.instance.log_action('pretix.team.created', user=self.request.user, data={
k: getattr(self.object, k) if k != 'limit_events' else [e.id for e in getattr(self.object, k).all()]
for k in form.changed_data
})
form.instance.log_action('pretix.team.created', user=self.request.user, data=form.changed_data_for_log)
return ret
def form_invalid(self, form):
@@ -920,7 +917,7 @@ class TeamCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
class TeamUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, UpdateView):
model = Team
template_name = 'pretixcontrol/organizers/team_edit.html'
permission = 'can_change_teams'
permission = 'organizer.teams:write'
context_object_name = 'team'
form_class = TeamForm
@@ -941,10 +938,7 @@ class TeamUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
@transaction.atomic
def form_valid(self, form):
if form.has_changed():
self.object.log_action('pretix.team.changed', user=self.request.user, data={
k: getattr(self.object, k) if k != 'limit_events' else [e.id for e in getattr(self.object, k).all()]
for k in form.changed_data
})
self.object.log_action('pretix.team.changed', user=self.request.user, data=form.changed_data_for_log)
messages.success(self.request, _('Your changes have been saved.'))
return super().form_valid(form)
@@ -956,7 +950,7 @@ class TeamUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
class TeamDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CompatDeleteView):
model = Team
template_name = 'pretixcontrol/organizers/team_delete.html'
permission = 'can_change_teams'
permission = 'organizer.teams:write'
context_object_name = 'team'
def get_object(self, queryset=None):
@@ -974,7 +968,8 @@ class TeamDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
def is_allowed(self) -> bool:
return self.request.organizer.teams.exclude(pk=self.kwargs.get('team')).filter(
can_change_teams=True, members__isnull=False
TeamQuerySet.organizer_permission_q("organizer.teams:write"),
members__isnull=False
).exists() or self.request.user.has_active_staff_session(self.request.session.session_key)
@transaction.atomic
@@ -1015,7 +1010,7 @@ class TeamDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
class TeamMemberView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DetailView):
template_name = 'pretixcontrol/organizers/team_members.html'
context_object_name = 'team'
permission = 'can_change_teams'
permission = 'organizer.teams:write'
model = Team
def get_object(self, queryset=None):
@@ -1067,9 +1062,10 @@ class TeamMemberView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
pass
else:
other_admin_teams = self.request.organizer.teams.exclude(pk=self.object.pk).filter(
can_change_teams=True, members__isnull=False
TeamQuerySet.organizer_permission_q("organizer.teams:write"),
members__isnull=False
).exists() or self.request.user.has_active_staff_session(self.request.session.session_key)
if not other_admin_teams and self.object.can_change_teams and self.object.members.count() == 1:
if not other_admin_teams and self.object.has_organizer_permission("organizer.teams:write") and self.object.members.count() == 1:
messages.error(self.request, _('You cannot remove the last member from this team as no one would '
'be left with the permission to change teams.'))
return redirect(self.get_success_url())
@@ -1233,7 +1229,7 @@ class DeviceQueryMixin:
class DeviceListView(DeviceQueryMixin, OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
model = Device
template_name = 'pretixcontrol/organizers/devices.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.devices:read'
context_object_name = 'devices'
paginate_by = 100
@@ -1246,7 +1242,7 @@ class DeviceListView(DeviceQueryMixin, OrganizerDetailViewMixin, OrganizerPermis
class DeviceCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
model = Device
template_name = 'pretixcontrol/organizers/device_edit.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.devices:write'
form_class = DeviceForm
def get_form_kwargs(self):
@@ -1277,7 +1273,7 @@ class DeviceCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixi
class DeviceLogView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
template_name = 'pretixcontrol/organizers/device_logs.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.devices:read'
model = LogEntry
context_object_name = 'logs'
paginate_by = 20
@@ -1305,7 +1301,7 @@ class DeviceLogView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
class DeviceUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, UpdateView):
model = Device
template_name = 'pretixcontrol/organizers/device_edit.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.devices:write'
context_object_name = 'device'
form_class = DeviceForm
@@ -1348,7 +1344,7 @@ class DeviceUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixi
class DeviceBulkUpdateView(DeviceQueryMixin, OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, FormView):
template_name = 'pretixcontrol/organizers/device_bulk_edit.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.devices:write'
context_object_name = 'device'
form_class = DeviceBulkEditForm
@@ -1462,7 +1458,7 @@ class DeviceBulkUpdateView(DeviceQueryMixin, OrganizerDetailViewMixin, Organizer
class DeviceConnectView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DetailView):
model = Device
template_name = 'pretixcontrol/organizers/device_connect.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.devices:write'
context_object_name = 'device'
def get_object(self, queryset=None):
@@ -1494,7 +1490,7 @@ class DeviceConnectView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMix
class DeviceRevokeView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DetailView):
model = Device
template_name = 'pretixcontrol/organizers/device_revoke.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.devices:write'
context_object_name = 'device'
def get_object(self, queryset=None):
@@ -1524,7 +1520,7 @@ class DeviceRevokeView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixi
class WebHookListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
model = WebHook
template_name = 'pretixcontrol/organizers/webhooks.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
context_object_name = 'webhooks'
def get_queryset(self):
@@ -1534,7 +1530,7 @@ class WebHookListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin
class WebHookCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
model = WebHook
template_name = 'pretixcontrol/organizers/webhook_edit.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
form_class = WebHookForm
def get_form_kwargs(self):
@@ -1568,7 +1564,7 @@ class WebHookCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMix
class WebHookUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, UpdateView):
model = WebHook
template_name = 'pretixcontrol/organizers/webhook_edit.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
context_object_name = 'webhook'
form_class = WebHookForm
@@ -1611,7 +1607,7 @@ class WebHookUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMix
class WebHookLogsView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
model = WebHook
template_name = 'pretixcontrol/organizers/webhook_logs.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
context_object_name = 'calls'
paginate_by = 50
@@ -1653,7 +1649,7 @@ class WebHookLogsView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin
class GiftCardAcceptanceInviteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, FormView):
model = GiftCardAcceptance
template_name = 'pretixcontrol/organizers/giftcard_acceptance_invite.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
form_class = GiftCardAcceptanceInviteForm
def get_form_kwargs(self):
@@ -1686,7 +1682,7 @@ class GiftCardAcceptanceInviteView(OrganizerDetailViewMixin, OrganizerPermission
class GiftCardAcceptanceListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
model = GiftCardAcceptance
template_name = 'pretixcontrol/organizers/giftcard_acceptance_list.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
context_object_name = 'acceptor_acceptance'
paginate_by = 50
@@ -1755,7 +1751,7 @@ class GiftCardAcceptanceListView(OrganizerDetailViewMixin, OrganizerPermissionRe
class GiftCardListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
model = GiftCard
template_name = 'pretixcontrol/organizers/giftcards.html'
permission = 'can_manage_gift_cards'
permission = 'organizer.giftcards:read'
context_object_name = 'giftcards'
paginate_by = 50
@@ -1778,7 +1774,7 @@ class GiftCardListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixi
ctx = super().get_context_data(**kwargs)
ctx['filter_form'] = self.filter_form
ctx['other_organizers'] = self.request.user.get_organizers_with_permission(
'can_manage_gift_cards', self.request
'organizer.giftcards:write', self.request
).exclude(pk=self.request.organizer.pk)
return ctx
@@ -1789,7 +1785,7 @@ class GiftCardListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixi
class GiftCardDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DetailView):
template_name = 'pretixcontrol/organizers/giftcard.html'
permission = 'can_manage_gift_cards'
permission = 'organizer.giftcards:read'
context_object_name = 'card'
def get_object(self, queryset=None) -> Organizer:
@@ -1800,6 +1796,8 @@ class GiftCardDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
@transaction.atomic()
def post(self, request, *args, **kwargs):
if not request.user.has_organizer_permission(request.organizer, "organizer.giftcards:write", request=request):
raise PermissionDenied()
self.object = GiftCard.objects.select_for_update(of=OF_SELF).get(pk=self.get_object().pk)
if 'revert' in request.POST:
t = get_object_or_404(self.object.transactions.all(), pk=request.POST.get('revert'), order__isnull=False)
@@ -1881,7 +1879,7 @@ class GiftCardDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
class GiftCardCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
template_name = 'pretixcontrol/organizers/giftcard_create.html'
permission = 'can_manage_gift_cards'
permission = 'organizer.giftcards:write'
form_class = GiftCardCreateForm
success_url = 'invalid'
@@ -1932,7 +1930,7 @@ class GiftCardCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
class GiftCardUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, UpdateView):
template_name = 'pretixcontrol/organizers/giftcard_edit.html'
permission = 'can_manage_gift_cards'
permission = 'organizer.giftcards:write'
form_class = GiftCardUpdateForm
success_url = 'invalid'
context_object_name = 'card'
@@ -2012,7 +2010,7 @@ class ExportMixin:
)),
('events',
forms.ModelMultipleChoiceField(
queryset=self.events,
queryset=ex.events,
widget=forms.CheckboxSelectMultiple(
attrs={
'class': 'scrolling-multiple-choice',
@@ -2025,29 +2023,9 @@ class ExportMixin:
])
return ex
@cached_property
def events(self):
return self.request.user.get_events_with_permission('can_view_orders', 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: (
@@ -2061,7 +2039,7 @@ class ExportMixin:
return ctx
def get_scheduled_queryset(self):
if not self.request.user.has_organizer_permission(self.request.organizer, 'can_change_organizer_settings',
if not self.request.user.has_organizer_permission(self.request.organizer, 'organizer.settings.general:write',
request=self.request):
qs = self.request.organizer.scheduled_exports.filter(owner=self.request.user)
else:
@@ -2146,7 +2124,16 @@ class ExportView(OrganizerPermissionRequiredMixin, ExportMixin, ListView):
@transaction.atomic()
def post(self, request, *args, **kwargs):
if request.POST.get("schedule") == "save":
if not self.has_permission():
if self.scheduled and self.scheduled.pk and not self.has_permission_to_edit_scheduled():
messages.error(
self.request,
_(
"Your user account does not have sufficient permission to run this report, therefore "
"you cannot change it."
)
)
return super().get(request, *args, **kwargs)
elif (not self.scheduled or not self.scheduled.pk) and not self.has_permission_to_create_scheduled():
messages.error(
self.request,
_(
@@ -2237,12 +2224,58 @@ class ExportView(OrganizerPermissionRequiredMixin, ExportMixin, ListView):
def get_queryset(self):
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):
return False
if self.exporter and not self.exporter.available_for_user(self.request.user):
def has_permission_to_edit_scheduled(self):
# Exports can be edited by
# - their owner
# - any staff session user
# - any user with permission for organizer settings *and* the permissions required to run the report
# 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.
if not self.exporter:
# Triggered in scenario 5 in test_organizer_edit_restrictions
return False
if self.scheduled.owner == self.request.user:
return True
if self.request.user.has_active_staff_session(self.request.session.session_key):
return True
if not self.exporter.available_for_user(self.request.user):
return False
if self.request.user.has_organizer_permission(self.request.organizer, "organizer.settings.general:write", request=self.request):
if isinstance(self.exporter, OrganizerLevelExportMixin):
# Test scenario 5/6 in test_organizer_edit_restrictions
return self.request.user.has_organizer_permission(
self.request.organizer, self.exporter.get_required_organizer_permission(), request=self.request
)
else:
if self.scheduled.export_form_data.get("all_events", False):
# Test scenario 1/2 in test_organizer_edit_restrictions
return self.request.user.teams.filter(
TeamQuerySet.event_permission_q(self.exporter.get_required_event_permission()),
all_events=True,
).exists()
else:
# Test scenario 3/4 in test_organizer_edit_restrictions
events_selected = self.scheduled.export_form_data.get("events", [])
events_permission = set(self.request.user.get_events_with_permission(
self.exporter.get_required_event_permission(), request=self.request,
).values_list("pk", flat=True))
return all(e in events_permission for e in events_selected)
def has_permission_to_create_scheduled(self):
# Exports can only be created if the user has the correct permissions. We *ignore* staff sessions, because
# the export is not *run* during a staff session and then would fail at the scheduled time.
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
return True
def get_context_data(self, **kwargs):
@@ -2251,6 +2284,17 @@ class ExportView(OrganizerPermissionRequiredMixin, ExportMixin, ListView):
ctx['schedule_form'] = self.schedule_form
ctx['rrule_form'] = self.rrule_form
ctx['scheduled_copy_from'] = self.scheduled_copy_from
if self.scheduled and self.scheduled.pk and not self.has_permission_to_edit_scheduled() and self.exporter:
ctx['no_save'] = True
for f in self.exporter.form.fields.values():
f.disabled = True
f.widget.attrs.pop("data-inverse-dependency", None)
for f in self.rrule_form.fields.values():
f.disabled = True
for f in self.schedule_form.fields.values():
f.disabled = True
elif not self.exporter:
for s in ctx['scheduled']:
try:
@@ -2307,7 +2351,7 @@ class RunScheduledExportView(OrganizerPermissionRequiredMixin, ExportMixin, View
class GateListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
model = Gate
template_name = 'pretixcontrol/organizers/gates.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.devices:read'
context_object_name = 'gates'
def get_queryset(self):
@@ -2317,7 +2361,7 @@ class GateListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, L
class GateCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
model = Gate
template_name = 'pretixcontrol/organizers/gate_edit.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.devices:write'
form_class = GateForm
def get_form_kwargs(self):
@@ -2351,7 +2395,7 @@ class GateCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
class GateUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, UpdateView):
model = Gate
template_name = 'pretixcontrol/organizers/gate_edit.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.devices:write'
context_object_name = 'gate'
form_class = GateForm
@@ -2386,7 +2430,7 @@ class GateUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
class GateDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CompatDeleteView):
model = Gate
template_name = 'pretixcontrol/organizers/gate_delete.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.devices:write'
context_object_name = 'gate'
def get_object(self, queryset=None):
@@ -2410,7 +2454,7 @@ class GateDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
class EventMetaPropertyListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
model = EventMetaProperty
template_name = 'pretixcontrol/organizers/properties.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
context_object_name = 'properties'
def get_queryset(self):
@@ -2461,7 +2505,7 @@ class EventMetaPropertyEditorMixin:
class EventMetaPropertyCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, EventMetaPropertyEditorMixin, CreateView):
model = EventMetaProperty
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
def get_object(self, queryset=None):
return EventMetaProperty()
@@ -2491,7 +2535,7 @@ class EventMetaPropertyCreateView(OrganizerDetailViewMixin, OrganizerPermissionR
class EventMetaPropertyUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, EventMetaPropertyEditorMixin, UpdateView):
model = EventMetaProperty
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
context_object_name = 'property'
def get_object(self, queryset=None):
@@ -2523,7 +2567,7 @@ class EventMetaPropertyUpdateView(OrganizerDetailViewMixin, OrganizerPermissionR
class EventMetaPropertyDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CompatDeleteView):
model = EventMetaProperty
template_name = 'pretixcontrol/organizers/property_delete.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
context_object_name = 'property'
def get_object(self, queryset=None):
@@ -2567,7 +2611,7 @@ def meta_property_move(request, property, up=True):
messages.success(request, _('The order of properties has been updated.'))
@organizer_permission_required("can_change_organizer_settings")
@organizer_permission_required("organizer.settings.general:write")
@require_http_methods(["POST"])
def meta_property_move_up(request, organizer, property):
meta_property_move(request, property, up=True)
@@ -2575,7 +2619,7 @@ def meta_property_move_up(request, organizer, property):
organizer=request.organizer.slug)
@organizer_permission_required("can_change_organizer_settings")
@organizer_permission_required("organizer.settings.general:write")
@require_http_methods(["POST"])
def meta_property_move_down(request, organizer, property):
meta_property_move(request, property, up=False)
@@ -2584,7 +2628,7 @@ def meta_property_move_down(request, organizer, property):
@transaction.atomic
@organizer_permission_required("can_change_organizer_settings")
@organizer_permission_required("organizer.settings.general:write")
@require_http_methods(["POST"])
def reorder_meta_properties(request, organizer):
try:
@@ -2616,7 +2660,7 @@ def reorder_meta_properties(request, organizer):
class LogView(OrganizerPermissionRequiredMixin, PaginationMixin, ListView):
template_name = 'pretixcontrol/organizers/logs.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
model = LogEntry
context_object_name = 'logs'
@@ -2641,7 +2685,7 @@ class LogView(OrganizerPermissionRequiredMixin, PaginationMixin, ListView):
class MembershipTypeListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
model = MembershipType
template_name = 'pretixcontrol/organizers/membershiptypes.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
context_object_name = 'types'
def get_queryset(self):
@@ -2651,7 +2695,7 @@ class MembershipTypeListView(OrganizerDetailViewMixin, OrganizerPermissionRequir
class MembershipTypeCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
model = MembershipType
template_name = 'pretixcontrol/organizers/membershiptype_edit.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
form_class = MembershipTypeForm
def get_object(self, queryset=None):
@@ -2685,7 +2729,7 @@ class MembershipTypeCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequ
class MembershipTypeUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, UpdateView):
model = MembershipType
template_name = 'pretixcontrol/organizers/membershiptype_edit.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
context_object_name = 'type'
form_class = MembershipTypeForm
@@ -2720,7 +2764,7 @@ class MembershipTypeUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequ
class MembershipTypeDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CompatDeleteView):
model = MembershipType
template_name = 'pretixcontrol/organizers/membershiptype_delete.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
context_object_name = 'type'
def get_object(self, queryset=None):
@@ -2750,7 +2794,7 @@ class MembershipTypeDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequ
class SSOProviderListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
model = CustomerSSOProvider
template_name = 'pretixcontrol/organizers/ssoproviders.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
context_object_name = 'providers'
def get_queryset(self):
@@ -2760,7 +2804,7 @@ class SSOProviderListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredM
class SSOProviderCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
model = CustomerSSOProvider
template_name = 'pretixcontrol/organizers/ssoprovider_edit.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
form_class = SSOProviderForm
def get_object(self, queryset=None):
@@ -2794,7 +2838,7 @@ class SSOProviderCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequire
class SSOProviderUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, UpdateView):
model = CustomerSSOProvider
template_name = 'pretixcontrol/organizers/ssoprovider_edit.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
context_object_name = 'provider'
form_class = SSOProviderForm
@@ -2836,7 +2880,7 @@ class SSOProviderUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequire
class SSOProviderDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CompatDeleteView):
model = CustomerSSOProvider
template_name = 'pretixcontrol/organizers/ssoprovider_delete.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
context_object_name = 'provider'
def get_object(self, queryset=None):
@@ -2866,7 +2910,7 @@ class SSOProviderDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequire
class SSOClientListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
model = CustomerSSOClient
template_name = 'pretixcontrol/organizers/ssoclients.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
context_object_name = 'clients'
def get_queryset(self):
@@ -2876,7 +2920,7 @@ class SSOClientListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMix
class SSOClientCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
model = CustomerSSOClient
template_name = 'pretixcontrol/organizers/ssoclient_edit.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
form_class = SSOClientForm
def get_object(self, queryset=None):
@@ -2916,7 +2960,7 @@ class SSOClientCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredM
class SSOClientUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, UpdateView):
model = CustomerSSOClient
template_name = 'pretixcontrol/organizers/ssoclient_edit.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
context_object_name = 'client'
form_class = SSOClientForm
@@ -2966,7 +3010,7 @@ class SSOClientUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredM
class SSOClientDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CompatDeleteView):
model = CustomerSSOClient
template_name = 'pretixcontrol/organizers/ssoclient_delete.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
context_object_name = 'client'
def get_object(self, queryset=None):
@@ -2996,7 +3040,7 @@ class SSOClientDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredM
class CustomerListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, PaginationMixin, ListView):
model = Customer
template_name = 'pretixcontrol/organizers/customers.html'
permission = 'can_manage_customers'
permission = 'organizer.customers:read'
context_object_name = 'customers'
def get_queryset(self):
@@ -3017,7 +3061,7 @@ class CustomerListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixi
class CustomerDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, PaginationMixin, ListView):
template_name = 'pretixcontrol/organizers/customer.html'
permission = 'can_manage_customers'
permission = 'organizer.customers:read'
context_object_name = 'orders'
def get_queryset(self):
@@ -3133,7 +3177,7 @@ class CustomerDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
class CustomerCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
template_name = 'pretixcontrol/organizers/customer_edit.html'
permission = 'can_manage_customers'
permission = 'organizer.customers:write'
context_object_name = 'customer'
form_class = CustomerCreateForm
@@ -3163,7 +3207,7 @@ class CustomerCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
class CustomerUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, UpdateView):
template_name = 'pretixcontrol/organizers/customer_edit.html'
permission = 'can_manage_customers'
permission = 'organizer.customers:write'
context_object_name = 'customer'
form_class = CustomerUpdateForm
@@ -3192,7 +3236,7 @@ class CustomerUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
class MembershipUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, UpdateView):
template_name = 'pretixcontrol/organizers/customer_membership.html'
permission = 'can_manage_customers'
permission = 'organizer.customers:write'
context_object_name = 'membership'
form_class = MembershipUpdateForm
@@ -3232,7 +3276,7 @@ class MembershipUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequired
class MembershipDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CompatDeleteView):
template_name = 'pretixcontrol/organizers/customer_membership_delete.html'
permission = 'can_manage_customers'
permission = 'organizer.customers:write'
context_object_name = 'membership'
def get_object(self, queryset=None):
@@ -3270,7 +3314,7 @@ class MembershipDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequired
class MembershipCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
template_name = 'pretixcontrol/organizers/customer_membership.html'
permission = 'can_manage_customers'
permission = 'organizer.customers:write'
context_object_name = 'membership'
form_class = MembershipUpdateForm
@@ -3309,7 +3353,7 @@ class MembershipCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequired
class CustomerAnonymizeView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DetailView):
template_name = 'pretixcontrol/organizers/customer_anonymize.html'
permission = 'can_manage_customers'
permission = 'organizer.customers:write'
context_object_name = 'customer'
def get_object(self, queryset=None):
@@ -3336,7 +3380,7 @@ class CustomerAnonymizeView(OrganizerDetailViewMixin, OrganizerPermissionRequire
class ReusableMediaListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, PaginationMixin, ListView):
model = ReusableMedium
template_name = 'pretixcontrol/organizers/reusable_media.html'
permission = 'can_manage_reusable_media'
permission = 'organizer.reusablemedia:read'
context_object_name = 'media'
def get_queryset(self):
@@ -3360,7 +3404,7 @@ class ReusableMediaListView(OrganizerDetailViewMixin, OrganizerPermissionRequire
class ReusableMediumDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, TemplateView):
template_name = 'pretixcontrol/organizers/reusable_medium.html'
permission = 'can_manage_reusable_media'
permission = 'organizer.reusablemedia:read'
@cached_property
def medium(self):
@@ -3377,7 +3421,7 @@ class ReusableMediumDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequ
class ReusableMediumCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
template_name = 'pretixcontrol/organizers/reusable_medium_edit.html'
permission = 'can_manage_reusable_media'
permission = 'organizer.reusablemedia:write'
context_object_name = 'medium'
form_class = ReusableMediumCreateForm
@@ -3406,7 +3450,7 @@ class ReusableMediumCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequ
class ReusableMediumUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, UpdateView):
template_name = 'pretixcontrol/organizers/reusable_medium_edit.html'
permission = 'can_manage_reusable_media'
permission = 'organizer.reusablemedia:write'
context_object_name = 'medium'
form_class = ReusableMediumUpdateForm
@@ -3436,7 +3480,7 @@ class ReusableMediumUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequ
class ChannelListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
model = SalesChannel
template_name = 'pretixcontrol/organizers/channels.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
context_object_name = 'channels'
def get_queryset(self):
@@ -3455,7 +3499,7 @@ class ChannelEditorMixin:
class ChannelCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ChannelEditorMixin, CreateView):
model = SalesChannel
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
template_name = 'pretixcontrol/organizers/channel_add.html'
def get_object(self, queryset=None):
@@ -3527,7 +3571,7 @@ class ChannelCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMix
class ChannelUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ChannelEditorMixin, UpdateView):
model = SalesChannel
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
context_object_name = 'channel'
template_name = 'pretixcontrol/organizers/channel_edit.html'
@@ -3572,7 +3616,7 @@ class ChannelUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMix
class ChannelDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CompatDeleteView):
model = SalesChannel
template_name = 'pretixcontrol/organizers/channel_delete.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
context_object_name = 'channel'
def get_object(self, queryset=None):
@@ -3628,7 +3672,7 @@ def channel_move(request, channel, up=True):
messages.success(request, _('The order of sales channels has been updated.'))
@organizer_permission_required("can_change_organizer_settings")
@organizer_permission_required("organizer.settings.general:write")
@require_http_methods(["POST"])
def channel_move_up(request, organizer, channel):
channel_move(request, channel, up=True)
@@ -3636,7 +3680,7 @@ def channel_move_up(request, organizer, channel):
organizer=request.organizer.slug)
@organizer_permission_required("can_change_organizer_settings")
@organizer_permission_required("organizer.settings.general:write")
@require_http_methods(["POST"])
def channel_move_down(request, organizer, channel):
channel_move(request, channel, up=False)
@@ -3645,7 +3689,7 @@ def channel_move_down(request, organizer, channel):
@transaction.atomic
@organizer_permission_required("can_change_organizer_settings")
@organizer_permission_required("organizer.settings.general:write")
@require_http_methods(["POST"])
def reorder_channels(request, organizer):
try: