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

@@ -97,7 +97,6 @@ class BankImportJobViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
queryset = BankImportJob.objects.none()
filter_backends = (DjangoFilterBackend,)
filterset_class = JobFilter
permission = 'can_view_orders'
def get_queryset(self):
return BankImportJob.objects.filter(organizer=self.request.organizer)
@@ -105,9 +104,30 @@ class BankImportJobViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
def perform_create(self, serializer):
return serializer.save()
def retrieve(self, request, *args, **kwargs):
perm_holder = (request.auth if isinstance(request.auth, (Device, TeamAPIToken)) else request.user)
has_any_event_perm = perm_holder.get_events_with_permission(
"event.orders:read", request=request
).filter(organizer=request.organizer).exists()
if not has_any_event_perm:
raise PermissionDenied('Invalid set of permissions')
return super().retrieve(request, *args, **kwargs)
def list(self, request, *args, **kwargs):
perm_holder = (request.auth if isinstance(request.auth, (Device, TeamAPIToken)) else request.user)
has_any_event_perm = perm_holder.get_events_with_permission(
"event.orders:read", request=request
).filter(organizer=request.organizer).exists()
if not has_any_event_perm:
raise PermissionDenied('Invalid set of permissions')
return super().list(request, *args, **kwargs)
def create(self, request, *args, **kwargs):
perm_holder = (request.auth if isinstance(request.auth, (Device, TeamAPIToken)) else request.user)
if not perm_holder.has_organizer_permission(request.organizer, 'can_change_orders'):
has_any_event_perm = perm_holder.get_events_with_permission(
"event.orders:write", request=request
).filter(organizer=request.organizer).exists()
if not has_any_event_perm:
raise PermissionDenied('Invalid set of permissions')
if BankImportJob.objects.filter(Q(organizer=request.organizer)).filter(

View File

@@ -41,7 +41,7 @@ def register_payment_provider(sender, **kwargs):
@receiver(nav_event, dispatch_uid="payment_banktransfer_nav")
def control_nav_import(sender, request=None, **kwargs):
url = resolve(request.path_info)
if not request.user.has_event_permission(request.organizer, request.event, 'can_change_orders', request=request):
if not request.user.has_event_permission(request.organizer, request.event, 'event.orders:write', request=request):
return []
return [
{
@@ -76,7 +76,10 @@ def control_nav_import(sender, request=None, **kwargs):
@receiver(nav_organizer, dispatch_uid="payment_banktransfer_organav")
def control_nav_orga_import(sender, request=None, **kwargs):
url = resolve(request.path_info)
if not request.user.has_organizer_permission(request.organizer, 'can_change_orders', request=request):
has_any_event_perm = request.user.get_events_with_permission(
"event.orders:write", request=request
).filter(organizer=request.organizer).exists()
if not has_any_event_perm:
return []
return [
{

View File

@@ -44,6 +44,7 @@ from typing import Set
from django import forms
from django.contrib import messages
from django.core.exceptions import PermissionDenied
from django.db import transaction
from django.db.models import Count, Q, QuerySet
from django.http import FileResponse, JsonResponse
@@ -58,11 +59,10 @@ from localflavor.generic.forms import BICFormField, IBANFormField
from pretix.base.forms.widgets import DatePickerWidget
from pretix.base.models import Event, Order, OrderPayment, OrderRefund, Quota
from pretix.base.models.organizer import TeamQuerySet
from pretix.base.settings import SettingsSandbox
from pretix.base.templatetags.money import money_filter
from pretix.control.permissions import (
EventPermissionRequiredMixin, OrganizerPermissionRequiredMixin,
)
from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.control.views.organizer import OrganizerDetailViewMixin
from pretix.helpers.json import CustomJSONEncoder
from pretix.plugins.banktransfer import camtimport, csvimport, mt940import
@@ -79,7 +79,7 @@ logger = logging.getLogger('pretix.plugins.banktransfer')
class ActionView(View):
permission = 'can_change_orders'
permission = 'event.orders:write'
def _discard(self, trans):
trans.state = BankTransaction.STATE_DISCARDED
@@ -279,7 +279,7 @@ class ActionView(View):
class JobDetailView(DetailView):
template_name = 'pretixplugins/banktransfer/job_detail.html'
permission = 'can_change_orders'
permission = 'event.orders:write'
context_objectname = 'job'
def redirect_form(self):
@@ -368,7 +368,7 @@ class BankTransactionFilterForm(forms.Form):
class ImportView(ListView):
template_name = 'pretixplugins/banktransfer/import_form.html'
permission = 'can_change_orders'
permission = 'event.orders:write'
context_object_name = 'transactions_unhandled'
paginate_by = 30
@@ -625,44 +625,54 @@ class ImportView(ListView):
class OrganizerBanktransferView:
def dispatch(self, request, *args, **kwargs):
has_any_event_perm = request.user.get_events_with_permission(
"event.orders:write", request=request
).filter(organizer=request.organizer).exists()
if not has_any_event_perm:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
class EventImportView(EventPermissionRequiredMixin, ImportView):
permission = 'can_change_orders'
permission = 'event.orders:write'
class OrganizerImportView(OrganizerBanktransferView, OrganizerPermissionRequiredMixin, OrganizerDetailViewMixin,
class OrganizerImportView(OrganizerBanktransferView, OrganizerDetailViewMixin,
ImportView):
permission = 'can_change_orders'
pass
class EventJobDetailView(EventPermissionRequiredMixin, JobDetailView):
permission = 'can_change_orders'
permission = 'event.orders:write'
class OrganizerJobDetailView(OrganizerBanktransferView, OrganizerPermissionRequiredMixin, OrganizerDetailViewMixin,
class OrganizerJobDetailView(OrganizerBanktransferView, OrganizerDetailViewMixin,
JobDetailView):
permission = 'can_change_orders'
pass
class EventActionView(EventPermissionRequiredMixin, ActionView):
permission = 'can_change_orders'
permission = 'event.orders:write'
class OrganizerActionView(OrganizerBanktransferView, OrganizerPermissionRequiredMixin, OrganizerDetailViewMixin,
class OrganizerActionView(OrganizerBanktransferView, OrganizerDetailViewMixin,
ActionView):
permission = 'can_change_orders'
def order_qs(self):
all = self.request.user.teams.filter(organizer=self.request.organizer, can_change_orders=True,
can_view_orders=True, all_events=True).exists()
all = self.request.user.teams.filter(
TeamQuerySet.event_permission_q("event.orders:read"),
TeamQuerySet.event_permission_q("event.orders:write"),
all_events=True,
organizer=self.request.organizer,
).exists()
if self.request.user.has_active_staff_session(self.request.session.session_key) or all:
return Order.objects.filter(event__organizer=self.request.organizer)
else:
return Order.objects.filter(
event_id__in=self.request.user.teams.filter(
organizer=self.request.organizer, can_change_orders=True, can_view_orders=True
TeamQuerySet.event_permission_q("event.orders:read"),
TeamQuerySet.event_permission_q("event.orders:write"),
organizer=self.request.organizer,
).values_list('limit_events__id', flat=True)
)
@@ -755,7 +765,7 @@ class RefundExportListView(ListView):
class EventRefundExportListView(EventPermissionRequiredMixin, RefundExportListView):
permission = 'can_change_orders'
permission = 'event.orders:write'
def get_success_url(self):
return reverse('plugins:banktransfer:refunds.list', kwargs={
@@ -777,8 +787,7 @@ class EventRefundExportListView(EventPermissionRequiredMixin, RefundExportListVi
)
class OrganizerRefundExportListView(OrganizerPermissionRequiredMixin, RefundExportListView):
permission = 'can_change_orders'
class OrganizerRefundExportListView(OrganizerBanktransferView, RefundExportListView):
def get_success_url(self):
return reverse('plugins:banktransfer:refunds.list', kwargs={
@@ -811,7 +820,7 @@ class DownloadRefundExportView(DetailView):
class EventDownloadRefundExportView(EventPermissionRequiredMixin, DownloadRefundExportView):
permission = 'can_change_orders'
permission = 'event.orders:write'
def get_object(self, *args, **kwargs):
return get_object_or_404(
@@ -821,8 +830,7 @@ class EventDownloadRefundExportView(EventPermissionRequiredMixin, DownloadRefund
)
class OrganizerDownloadRefundExportView(OrganizerPermissionRequiredMixin, OrganizerDetailViewMixin, DownloadRefundExportView):
permission = 'can_change_orders'
class OrganizerDownloadRefundExportView(OrganizerBanktransferView, OrganizerDetailViewMixin, DownloadRefundExportView):
def get_object(self, *args, **kwargs):
return get_object_or_404(
@@ -850,9 +858,9 @@ class SepaXMLExportView(SingleObjectMixin, FormView):
template_name = 'pretixplugins/banktransfer/sepa_export.html'
context_object_name = "export"
def setup(self, request, *args, **kwargs):
super().setup(request, *args, **kwargs)
def dispatch(self, request, *args, **kwargs):
self.object: RefundExport = self.get_object()
return super().dispatch(request, *args, **kwargs)
def form_valid(self, form):
self.object.downloaded = True
@@ -869,7 +877,7 @@ class SepaXMLExportView(SingleObjectMixin, FormView):
class EventSepaXMLExportView(EventPermissionRequiredMixin, SepaXMLExportView):
permission = 'can_change_orders'
permission = 'event.orders:write'
def get_object(self, *args, **kwargs):
return get_object_or_404(
@@ -884,8 +892,7 @@ class EventSepaXMLExportView(EventPermissionRequiredMixin, SepaXMLExportView):
return form
class OrganizerSepaXMLExportView(OrganizerPermissionRequiredMixin, OrganizerDetailViewMixin, SepaXMLExportView):
permission = 'can_change_orders'
class OrganizerSepaXMLExportView(OrganizerBanktransferView, OrganizerDetailViewMixin, SepaXMLExportView):
def get_object(self, *args, **kwargs):
return get_object_or_404(