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

@@ -67,6 +67,7 @@ from pretix.base.models import (
Question, ReusableMedium, RevokedTicketSecret, TeamAPIToken,
)
from pretix.base.models.orders import PrintLog
from pretix.base.permissions import AnyPermissionOf
from pretix.base.services.checkin import (
CheckInError, RequiredQuestionsError, SQLLogic, perform_checkin,
)
@@ -118,11 +119,11 @@ class CheckinListViewSet(viewsets.ModelViewSet):
def _get_permission_name(self, request):
if request.path.endswith('/failed_checkins/'):
return 'can_checkin_orders', 'can_change_orders'
return 'event.orders:checkin', 'event.orders:write'
elif request.method in SAFE_METHODS:
return 'can_view_orders', 'can_checkin_orders',
return 'event.orders:read', 'event.orders:checkin',
else:
return 'can_change_event_settings'
return 'event.settings.general:write'
def get_queryset(self):
qs = self.request.event.checkin_lists.prefetch_related(
@@ -474,7 +475,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
'event': op.order.event,
'pdf_data': pdf_data and (
user if user and user.is_authenticated else auth
).has_event_permission(request.organizer, event, 'can_view_orders', request),
).has_event_permission(request.organizer, event, 'event.orders:read', request),
}
common_checkin_args = dict(
@@ -839,8 +840,8 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
}
filterset_class = CheckinOrderPositionFilter
permission = ('can_view_orders', 'can_checkin_orders')
write_permission = ('can_change_orders', 'can_checkin_orders')
permission = AnyPermissionOf('event.orders:read', 'event.orders:checkin')
write_permission = AnyPermissionOf('event.orders:write', 'event.orders:checkin')
def get_serializer_context(self):
ctx = super().get_serializer_context()
@@ -871,7 +872,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
expand=self.request.query_params.getlist('expand'),
)
if 'pk' not in self.request.resolver_match.kwargs and 'can_view_orders' not in self.request.eventpermset \
if 'pk' not in self.request.resolver_match.kwargs and 'event.orders:read' not in self.request.eventpermset \
and len(self.request.query_params.get('search', '')) < 3:
qs = qs.none()
@@ -920,9 +921,9 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
class CheckinRPCRedeemView(views.APIView):
def post(self, request, *args, **kwargs):
if isinstance(self.request.auth, (TeamAPIToken, Device)):
events = self.request.auth.get_events_with_permission(('can_change_orders', 'can_checkin_orders'))
events = self.request.auth.get_events_with_permission(('event.orders:write', 'event.orders:checkin'))
elif self.request.user.is_authenticated:
events = self.request.user.get_events_with_permission(('can_change_orders', 'can_checkin_orders'), self.request).filter(
events = self.request.user.get_events_with_permission(('event.orders:write', 'event.orders:checkin'), self.request).filter(
organizer=self.request.organizer
)
else:
@@ -990,9 +991,9 @@ class CheckinRPCSearchView(ListAPIView):
@cached_property
def lists(self):
if isinstance(self.request.auth, (TeamAPIToken, Device)):
events = self.request.auth.get_events_with_permission(('can_view_orders', 'can_checkin_orders'))
events = self.request.auth.get_events_with_permission(('event.orders:read', 'event.orders:checkin'))
elif self.request.user.is_authenticated:
events = self.request.user.get_events_with_permission(('can_view_orders', 'can_checkin_orders'), self.request).filter(
events = self.request.user.get_events_with_permission(('event.orders:read', 'event.orders:checkin'), self.request).filter(
organizer=self.request.organizer
)
else:
@@ -1009,9 +1010,9 @@ class CheckinRPCSearchView(ListAPIView):
@cached_property
def has_full_access_permission(self):
if isinstance(self.request.auth, (TeamAPIToken, Device)):
events = self.request.auth.get_events_with_permission('can_view_orders')
events = self.request.auth.get_events_with_permission('event.orders:read')
elif self.request.user.is_authenticated:
events = self.request.user.get_events_with_permission('can_view_orders', self.request).filter(
events = self.request.user.get_events_with_permission('event.orders:read', self.request).filter(
organizer=self.request.organizer
)
else:
@@ -1038,9 +1039,9 @@ class CheckinRPCSearchView(ListAPIView):
class CheckinRPCAnnulView(views.APIView):
def post(self, request, *args, **kwargs):
if isinstance(self.request.auth, (TeamAPIToken, Device)):
events = self.request.auth.get_events_with_permission(('can_change_orders', 'can_checkin_orders'))
events = self.request.auth.get_events_with_permission(('event.orders:write', 'event.orders:checkin'))
elif self.request.user.is_authenticated:
events = self.request.user.get_events_with_permission(('can_change_orders', 'can_checkin_orders'), self.request).filter(
events = self.request.user.get_events_with_permission(('event.orders:write', 'event.orders:checkin'), self.request).filter(
organizer=self.request.organizer
)
else:
@@ -1118,7 +1119,7 @@ class CheckinViewSet(viewsets.ReadOnlyModelViewSet):
filterset_class = CheckinFilter
ordering = ('created', 'id')
ordering_fields = ('created', 'datetime', 'id',)
permission = 'can_view_orders'
permission = 'event.orders:read'
def get_queryset(self):
qs = Checkin.all.filter().select_related(