Make tests pass

This commit is contained in:
Raphael Michel
2026-01-09 17:30:51 +01:00
parent 70973c8c6f
commit e566ab3405
30 changed files with 219 additions and 117 deletions

View File

@@ -341,7 +341,7 @@ class CloneEventViewSet(viewsets.ModelViewSet):
lookup_field = 'slug'
lookup_url_kwarg = 'event'
http_method_names = ['post']
write_permission = 'organizer.events:create'
write_permission = 'event.settings.general:write'
def get_serializer_context(self):
ctx = super().get_serializer_context()
@@ -350,6 +350,12 @@ class CloneEventViewSet(viewsets.ModelViewSet):
return ctx
def perform_create(self, serializer):
# Weird edge case: Requires settings permission on the event (to read) but also on the organizer (two write)
perm_holder = (self.request.auth if isinstance(self.request.auth, (Device, TeamAPIToken))
else self.request.user)
if not perm_holder.has_organizer_permission(self.request.organizer, "organizer.events:create", request=self.request):
raise PermissionDenied("No permission to create events")
serializer.save(organizer=self.request.organizer)
serializer.instance.log_action(

View File

@@ -1942,10 +1942,15 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
ordering = ('nr',)
ordering_fields = ('nr', 'date')
filterset_class = InvoiceFilter
permission = 'event.orders:read'
lookup_url_kwarg = 'number'
lookup_field = 'nr'
write_permission = 'event.orders:write'
def _get_permission_name(self, request):
if 'event' in request.resolver_match.kwargs:
if request.method not in SAFE_METHODS:
return "event.orders:write"
return "event.orders:read"
return None # org-level is handled by event__in check
def get_queryset(self):
perm = "event.orders:read" if self.request.method in SAFE_METHODS else "event.orders:write"

View File

@@ -472,7 +472,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
:return: set
"""
teams = self._get_teams_for_event(organizer, event)
sets = [t.permission_set() for t in teams]
sets = [t.event_permission_set() for t in teams]
if sets:
return set.union(*sets)
else:
@@ -486,7 +486,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
:return: set
"""
teams = self._get_teams_for_organizer(organizer)
sets = [t.permission_set() for t in teams]
sets = [t.organizer_permission_set() for t in teams]
if sets:
return set.union(*sets)
else:

View File

@@ -189,11 +189,15 @@ class Device(LoggedModel):
kwargs['update_fields'] = {'device_id'}.union(kwargs['update_fields'])
super().save(*args, **kwargs)
def permission_set(self) -> set:
def _event_permission_set(self) -> set:
return {
'event.orders:read',
'event.orders:write',
'event.vouchers:read',
}
def _organizer_permission_set(self) -> set:
return {
'organizer.giftcards:read',
'organizer.giftcards:write',
'organizer.reusablemedia:read',
@@ -211,7 +215,7 @@ class Device(LoggedModel):
has_event_access = (self.all_events and organizer == self.organizer) or (
event in self.limit_events.all()
)
return self.permission_set() if has_event_access else set()
return self._event_permission_set() if has_event_access else set()
def get_organizer_permission_set(self, organizer) -> set:
"""
@@ -220,7 +224,7 @@ class Device(LoggedModel):
:param organizer: The organizer of the event
:return: set of permissions
"""
return self.permission_set() if self.organizer == organizer else set()
return self._organizer_permission_set() if self.organizer == organizer else set()
def has_event_permission(self, organizer, event, perm_name=None, request=None) -> bool:
"""
@@ -237,8 +241,8 @@ class Device(LoggedModel):
event in self.limit_events.all()
)
if isinstance(perm_name, (tuple, list)):
return has_event_access and any(p in self.permission_set() for p in perm_name)
return has_event_access and (not perm_name or perm_name in self.permission_set())
return has_event_access and any(p in self._event_permission_set() for p in perm_name)
return has_event_access and (not perm_name or perm_name in self._event_permission_set())
def has_organizer_permission(self, organizer, perm_name=None, request=None):
"""
@@ -251,8 +255,8 @@ class Device(LoggedModel):
:return: bool
"""
if isinstance(perm_name, (tuple, list)):
return organizer == self.organizer and any(p in self.permission_set() for p in perm_name)
return organizer == self.organizer and (not perm_name or perm_name in self.permission_set())
return organizer == self.organizer and any(p in self._organizer_permission_set() for p in perm_name)
return organizer == self.organizer and (not perm_name or perm_name in self._organizer_permission_set())
def get_events_with_any_permission(self):
"""
@@ -273,8 +277,8 @@ class Device(LoggedModel):
:return: Iterable of Events
"""
if (
isinstance(permission, (list, tuple)) and any(p in self.permission_set() for p in permission)
) or (isinstance(permission, str) and permission in self.permission_set()):
isinstance(permission, (list, tuple)) and any(p in self._event_permission_set() for p in permission)
) or (isinstance(permission, str) and permission in self._event_permission_set()):
return self.get_events_with_any_permission()
else:
return self.organizer.events.none()

View File

@@ -1386,14 +1386,13 @@ class Event(EventMixin, LoggedModel):
from .auth import User
if permission:
kwargs = {permission: True}
qs = Team.objects.with_event_permission(permission)
else:
kwargs = {}
qs = Team.objects.all()
team_with_perm = Team.objects.filter(
team_with_perm = qs.filter(
members__pk=OuterRef('pk'),
organizer=self.organizer,
**kwargs
).filter(
Q(all_events=True) | Q(limit_events__pk=self.pk)
)

View File

@@ -397,26 +397,32 @@ class Team(LoggedModel):
'object': str(self.organizer),
}
def permission_set(self, include_legacy=True) -> set:
from ..permissions import (
get_all_event_permissions, get_all_organizer_permissions,
)
def event_permission_set(self, include_legacy=True) -> set:
from ..permissions import get_all_event_permissions
result = set()
for permission in get_all_event_permissions().keys():
if self.all_event_permissions or self.limit_event_permissions.get(permission):
result.add(permission)
for permission in get_all_organizer_permissions().keys():
if self.all_organizer_permissions or self.limit_organizer_permissions.get(permission):
result.add(permission)
if include_legacy:
# Add legacy permissions as well for plugin compatibility
for k, v in OLD_TO_NEW_EVENT_COMPAT.items():
if self.all_event_permissions or all(self.limit_event_permissions.get(kk) for kk in v):
result.add(k)
return result
def organizer_permission_set(self, include_legacy=True) -> set:
from ..permissions import get_all_organizer_permissions
result = set()
for permission in get_all_organizer_permissions().keys():
if self.all_organizer_permissions or self.limit_organizer_permissions.get(permission):
result.add(permission)
if include_legacy:
# Add legacy permissions as well for plugin compatibility
for k, v in OLD_TO_NEW_ORGANIZER_COMPAT.items():
if self.all_organizer_permissions or all(self.limit_organizer_permissions.get(kk) for kk in v):
result.add(k)
@@ -516,7 +522,7 @@ class TeamAPIToken(models.Model):
has_event_access = (self.team.all_events and organizer == self.team.organizer) or (
event in self.team.limit_events.all()
)
return self.team.permission_set() if has_event_access else set()
return self.team.event_permission_set() if has_event_access else set()
def get_organizer_permission_set(self, organizer) -> set:
"""
@@ -525,7 +531,7 @@ class TeamAPIToken(models.Model):
:param organizer: The organizer of the event
:return: set of permissions
"""
return self.team.permission_set() if self.team.organizer == organizer else set()
return self.team.organizer_permission_set() if self.team.organizer == organizer else set()
def has_event_permission(self, organizer, event, perm_name=None, request=None) -> bool:
"""

View File

@@ -376,7 +376,7 @@ class TeamForm(forms.ModelForm):
def clean(self):
data = super().clean()
if self.instance.pk and not data['all_organizer_permissions'] and not data.get('limit_organizer_permissions', {}).get('organizer.teams:write'):
if self.instance.pk and not data['all_organizer_permissions'] and 'organizer.teams:write' not in data.get('limit_organizer_permissions', []):
if not self.instance.organizer.teams.exclude(pk=self.instance.pk).filter(
TeamQuerySet.organizer_permission_q("organizer.teams:write"),
members__isnull=False

View File

@@ -245,7 +245,7 @@ class OrganizerDetail(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin
class OrganizerTeamView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DetailView):
model = Organizer
template_name = 'pretixcontrol/organizers/teams.html'
permission = 'organizer.teams:read'
permission = 'organizer.teams:write'
context_object_name = 'organizer'
@@ -1068,7 +1068,7 @@ class TeamMemberView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
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.has_permission() 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())
@@ -1754,7 +1754,7 @@ class GiftCardAcceptanceListView(OrganizerDetailViewMixin, OrganizerPermissionRe
class GiftCardListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
model = GiftCard
template_name = 'pretixcontrol/organizers/giftcards.html'
permission = 'organizer.giftcards:write'
permission = 'organizer.giftcards:read'
context_object_name = 'giftcards'
paginate_by = 50
@@ -1788,7 +1788,7 @@ class GiftCardListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixi
class GiftCardDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DetailView):
template_name = 'pretixcontrol/organizers/giftcard.html'
permission = 'organizer.giftcards:write'
permission = 'organizer.giftcards:read'
context_object_name = 'card'
def get_object(self, queryset=None) -> Organizer:
@@ -1799,6 +1799,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)

View File

@@ -45,7 +45,7 @@ OLD_TO_NEW_ORGANIZER_MIGRATION = {
"can_create_events": ["organizer.events:create"],
"can_change_organizer_settings": ["organizer.settings.general:write", "organizer.devices:read",
"organizer.devices:write"],
"can_change_teams": ["organizer.teams:write", "organizer.teams:read"],
"can_change_teams": ["organizer.teams:write"],
"can_manage_gift_cards": ["organizer.giftcards:read", "organizer.giftcards:write"],
"can_manage_customers": ["organizer.customers:read", "organizer.customers:write"],
"can_manage_reusable_media": ["organizer.reusablemedia:read", "organizer.reusablemedia:write"],
@@ -66,7 +66,7 @@ OLD_TO_NEW_EVENT_COMPAT = {
OLD_TO_NEW_ORGANIZER_COMPAT = {
"can_create_events": ["organizer.events:create"],
"can_change_organizer_settings": ["organizer.settings.general:write"],
"can_change_teams": ["organizer.teams:write", "organizer.teams:read"],
"can_change_teams": ["organizer.teams:write"],
"can_manage_gift_cards": ["organizer.giftcards:read", "organizer.giftcards:write"],
"can_manage_customers": ["organizer.customers:read", "organizer.customers:write"],
"can_manage_reusable_media": ["organizer.reusablemedia:read", "organizer.reusablemedia:write"],

View File

@@ -97,7 +97,6 @@ class BankImportJobViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
queryset = BankImportJob.objects.none()
filter_backends = (DjangoFilterBackend,)
filterset_class = JobFilter
permission = 'event.orders:read'
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, 'event.orders:write'):
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

@@ -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, 'event.orders:write', 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
@@ -61,9 +62,7 @@ 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
@@ -626,6 +625,11 @@ 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)
@@ -633,27 +637,26 @@ class EventImportView(EventPermissionRequiredMixin, ImportView):
permission = 'event.orders:write'
class OrganizerImportView(OrganizerBanktransferView, OrganizerPermissionRequiredMixin, OrganizerDetailViewMixin,
class OrganizerImportView(OrganizerBanktransferView, OrganizerDetailViewMixin,
ImportView):
permission = 'event.orders:write'
pass
class EventJobDetailView(EventPermissionRequiredMixin, JobDetailView):
permission = 'event.orders:write'
class OrganizerJobDetailView(OrganizerBanktransferView, OrganizerPermissionRequiredMixin, OrganizerDetailViewMixin,
class OrganizerJobDetailView(OrganizerBanktransferView, OrganizerDetailViewMixin,
JobDetailView):
permission = 'event.orders:write'
pass
class EventActionView(EventPermissionRequiredMixin, ActionView):
permission = 'event.orders:write'
class OrganizerActionView(OrganizerBanktransferView, OrganizerPermissionRequiredMixin, OrganizerDetailViewMixin,
class OrganizerActionView(OrganizerBanktransferView, OrganizerDetailViewMixin,
ActionView):
permission = 'event.orders:write'
def order_qs(self):
all = self.request.user.teams.filter(
@@ -784,8 +787,7 @@ class EventRefundExportListView(EventPermissionRequiredMixin, RefundExportListVi
)
class OrganizerRefundExportListView(OrganizerPermissionRequiredMixin, RefundExportListView):
permission = 'event.orders:write'
class OrganizerRefundExportListView(OrganizerBanktransferView, RefundExportListView):
def get_success_url(self):
return reverse('plugins:banktransfer:refunds.list', kwargs={
@@ -828,8 +830,7 @@ class EventDownloadRefundExportView(EventPermissionRequiredMixin, DownloadRefund
)
class OrganizerDownloadRefundExportView(OrganizerPermissionRequiredMixin, OrganizerDetailViewMixin, DownloadRefundExportView):
permission = 'event.orders:write'
class OrganizerDownloadRefundExportView(OrganizerBanktransferView, OrganizerDetailViewMixin, DownloadRefundExportView):
def get_object(self, *args, **kwargs):
return get_object_or_404(
@@ -857,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
@@ -891,8 +892,7 @@ class EventSepaXMLExportView(EventPermissionRequiredMixin, SepaXMLExportView):
return form
class OrganizerSepaXMLExportView(OrganizerPermissionRequiredMixin, OrganizerDetailViewMixin, SepaXMLExportView):
permission = 'event.orders:write'
class OrganizerSepaXMLExportView(OrganizerBanktransferView, OrganizerDetailViewMixin, SepaXMLExportView):
def get_object(self, *args, **kwargs):
return get_object_or_404(