diff --git a/doc/development/api/general.rst b/doc/development/api/general.rst index ce0bcf0dc6..ac02745b76 100644 --- a/doc/development/api/general.rst +++ b/doc/development/api/general.rst @@ -14,7 +14,8 @@ Core :members: periodic_task, event_live_issues, event_copy_data, email_filter, register_notification_types, notification, item_copy_data, register_sales_channel_types, register_global_settings, quota_availability, global_email_filter, register_ticket_secret_generators, gift_card_transaction_display, - register_text_placeholders, register_mail_placeholders, device_info_updated + register_text_placeholders, register_mail_placeholders, device_info_updated, + register_event_permissions, register_organizer_permissions Order events """""""""""" diff --git a/src/pretix/api/views/event.py b/src/pretix/api/views/event.py index 77b5661734..6ec51095e7 100644 --- a/src/pretix/api/views/event.py +++ b/src/pretix/api/views/event.py @@ -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( diff --git a/src/pretix/api/views/order.py b/src/pretix/api/views/order.py index 9d3d2cf2f8..2b83b510e1 100644 --- a/src/pretix/api/views/order.py +++ b/src/pretix/api/views/order.py @@ -1915,10 +1915,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" diff --git a/src/pretix/base/models/auth.py b/src/pretix/base/models/auth.py index bce3e31b65..ab2161e9ec 100644 --- a/src/pretix/base/models/auth.py +++ b/src/pretix/base/models/auth.py @@ -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: diff --git a/src/pretix/base/models/devices.py b/src/pretix/base/models/devices.py index 183945b57c..1a9350401e 100644 --- a/src/pretix/base/models/devices.py +++ b/src/pretix/base/models/devices.py @@ -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() diff --git a/src/pretix/base/models/event.py b/src/pretix/base/models/event.py index dfe1616cb6..88058714b5 100644 --- a/src/pretix/base/models/event.py +++ b/src/pretix/base/models/event.py @@ -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) ) diff --git a/src/pretix/base/models/organizer.py b/src/pretix/base/models/organizer.py index 875cebaaff..449ce38ba1 100644 --- a/src/pretix/base/models/organizer.py +++ b/src/pretix/base/models/organizer.py @@ -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: """ diff --git a/src/pretix/control/forms/organizer.py b/src/pretix/control/forms/organizer.py index eacd008305..1fd1f93dba 100644 --- a/src/pretix/control/forms/organizer.py +++ b/src/pretix/control/forms/organizer.py @@ -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 diff --git a/src/pretix/control/views/organizer.py b/src/pretix/control/views/organizer.py index a7bbf47cfb..03f2de12ce 100644 --- a/src/pretix/control/views/organizer.py +++ b/src/pretix/control/views/organizer.py @@ -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' @@ -1070,7 +1070,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()) @@ -1744,7 +1744,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 @@ -1778,7 +1778,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: @@ -1789,6 +1789,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) diff --git a/src/pretix/helpers/permission_migration.py b/src/pretix/helpers/permission_migration.py index f05ef17e87..f421df0052 100644 --- a/src/pretix/helpers/permission_migration.py +++ b/src/pretix/helpers/permission_migration.py @@ -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"], diff --git a/src/pretix/plugins/banktransfer/api.py b/src/pretix/plugins/banktransfer/api.py index 3e3575dc18..573ae6d843 100644 --- a/src/pretix/plugins/banktransfer/api.py +++ b/src/pretix/plugins/banktransfer/api.py @@ -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( diff --git a/src/pretix/plugins/banktransfer/signals.py b/src/pretix/plugins/banktransfer/signals.py index d681ed9122..a6500b5c45 100644 --- a/src/pretix/plugins/banktransfer/signals.py +++ b/src/pretix/plugins/banktransfer/signals.py @@ -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 [ { diff --git a/src/pretix/plugins/banktransfer/views.py b/src/pretix/plugins/banktransfer/views.py index b04cf46a1e..9a7cb80689 100644 --- a/src/pretix/plugins/banktransfer/views.py +++ b/src/pretix/plugins/banktransfer/views.py @@ -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 @@ -62,9 +63,7 @@ from pretix.base.models.organizer import TeamQuerySet from pretix.base.services.mail import SendMailException 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 @@ -632,6 +631,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) @@ -639,27 +643,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( @@ -790,8 +793,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={ @@ -834,8 +836,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( @@ -863,9 +864,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 @@ -897,8 +898,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( diff --git a/src/tests/api/test_exporters.py b/src/tests/api/test_exporters.py index 805588456e..d752c056ff 100644 --- a/src/tests/api/test_exporters.py +++ b/src/tests/api/test_exporters.py @@ -364,8 +364,8 @@ def test_event_scheduled_export_list_user(user_client, organizer, event, user, t resp = user_client.get('/api/v1/organizers/{}/events/{}/scheduled_exports/'.format(organizer.slug, event.slug)) assert [res] == resp.data['results'] - team.limit_organizer_permissions = {"organizer.events:create": True} team.all_organizer_permissions = False + team.limit_event_permissions = {"event.orders:read": True} team.all_event_permissions = False team.save() diff --git a/src/tests/api/test_invoices.py b/src/tests/api/test_invoices.py index 3c2ccd706e..7f3bcf1915 100644 --- a/src/tests/api/test_invoices.py +++ b/src/tests/api/test_invoices.py @@ -340,6 +340,8 @@ def test_invoice_list_multi_filter(token_client, organizer, event, order, order2 @pytest.mark.django_db def test_organizer_level(token_client, organizer, team, event, event2, invoice, invoice2): + team.all_events = True + team.save() resp = token_client.get('/api/v1/organizers/{}/invoices/'.format(organizer.slug)) assert resp.status_code == 200 assert len(resp.data['results']) == 2 diff --git a/src/tests/api/test_permissions.py b/src/tests/api/test_permissions.py index dcf3a19457..9c73ce0881 100644 --- a/src/tests/api/test_permissions.py +++ b/src/tests/api/test_permissions.py @@ -185,7 +185,7 @@ event_permission_sub_urls = [ ('delete', 'event.settings.general:write', 'checkinlists/1/', 404), ('get', 'event.orders:read', 'checkinlists/1/positions/', 404), ('post', 'event.orders:write', 'checkinlists/1/positions/3/redeem/', 404), - ('post', 'organizer.events:create', 'clone/', 400), + ('post', ('organizer.events:create', 'event.settings.general:write'), 'clone/', 400), ('get', 'event.orders:read', 'cartpositions/', 200), ('get', 'event.orders:read', 'cartpositions/1/', 404), ('post', 'event.orders:write', 'cartpositions/', 400), @@ -328,7 +328,7 @@ def test_event_allowed_all_events(token_client, team, organizer, event, url): @pytest.mark.parametrize("url", event_urls) def test_event_allowed_all_events_device(device_client, device, organizer, event, url): resp = device_client.get('/api/v1/organizers/{}/events/{}/{}'.format(organizer.slug, event.slug, url[1])) - if url[0] is None or url[0] in device.permission_set(): + if url[0] is None or url[0] in device._event_permission_set(): assert resp.status_code == 200 else: assert resp.status_code == 403 @@ -351,7 +351,7 @@ def test_event_allowed_limit_events_device(device_client, organizer, device, eve device.save() device.limit_events.add(event) resp = device_client.get('/api/v1/organizers/{}/events/{}/{}'.format(organizer.slug, event.slug, url[1])) - if url[0] is None or url[0] in device.permission_set(): + if url[0] is None or url[0] in device._event_permission_set(): assert resp.status_code == 200 else: assert resp.status_code == 403 @@ -386,8 +386,14 @@ def test_event_not_existing(token_client, organizer, url, event): @pytest.mark.parametrize("urlset", event_permission_sub_urls) def test_token_event_subresources_permission_allowed(token_client, team, organizer, event, urlset): team.all_events = True - if urlset[1]: - setattr(team, urlset[1], True) + if urlset[1] is not None: + for t in ((urlset[1],) if isinstance(urlset[1], str) else urlset[1]): + if "organizer" in urlset[1]: + team.all_organizer_permissions = False + team.limit_organizer_permissions[t] = True + else: + team.all_event_permissions = False + team.limit_event_permissions[t] = True team.save() resp = getattr(token_client, urlset[0])('/api/v1/organizers/{}/events/{}/{}'.format( organizer.slug, event.slug, urlset[2])) @@ -401,7 +407,10 @@ def test_token_event_subresources_permission_not_allowed(token_client, team, org team.all_events = False else: team.all_events = True - setattr(team, urlset[1], False) + team.all_event_permissions = False + team.limit_event_permissions.pop(urlset[1], None) + team.all_organizer_permissions = False + team.limit_organizer_permissions.pop(urlset[1], None) team.save() resp = getattr(token_client, urlset[0])('/api/v1/organizers/{}/events/{}/{}'.format( organizer.slug, event.slug, urlset[2])) @@ -415,7 +424,14 @@ def test_token_event_subresources_permission_not_allowed(token_client, team, org @pytest.mark.parametrize("urlset", event_permission_root_urls) def test_token_event_permission_allowed(token_client, team, organizer, event, urlset): team.all_events = True - setattr(team, urlset[1], True) + if urlset[1] is not None: + for t in ((urlset[1],) if isinstance(urlset[1], str) else urlset[1]): + if "organizer" in urlset[1]: + team.all_organizer_permissions = False + team.limit_organizer_permissions[t] = True + else: + team.all_event_permissions = False + team.limit_event_permissions[t] = True team.save() if urlset[0] == 'post': resp = getattr(token_client, urlset[0])('/api/v1/organizers/{}/events/'.format(organizer.slug)) @@ -428,7 +444,9 @@ def test_token_event_permission_allowed(token_client, team, organizer, event, ur @pytest.mark.parametrize("urlset", event_permission_root_urls) def test_token_event_permission_not_allowed(token_client, team, organizer, event, urlset): team.all_events = True - setattr(team, urlset[1], False) + team.all_event_permissions = False + team.limit_event_permissions.pop(urlset[1], None) + team.all_organizer_permissions = False team.save() if urlset[0] == 'post': resp = getattr(token_client, urlset[0])('/api/v1/organizers/{}/events/'.format(organizer.slug)) @@ -540,7 +558,7 @@ def test_device_subresource_permission_check(device_client, device, organizer, e return resp = getattr(device_client, urlset[0])('/api/v1/organizers/{}/events/{}/{}'.format( organizer.slug, event.slug, urlset[2])) - if urlset[1] is None or urlset[1] in device.permission_set(): + if urlset[1] is None or urlset[1] in device._event_permission_set(): assert resp.status_code == urlset[3] else: if urlset[3] == 404: @@ -554,7 +572,8 @@ def test_device_subresource_permission_check(device_client, device, organizer, e def test_token_org_subresources_permission_allowed(token_client, team, organizer, event, urlset): team.all_events = True if urlset[1]: - setattr(team, urlset[1], True) + team.all_organizer_permissions = False + team.limit_organizer_permissions[urlset[1]] = True team.save() resp = getattr(token_client, urlset[0])('/api/v1/organizers/{}/{}'.format( organizer.slug, urlset[2].format(team_id=team.pk))) @@ -567,8 +586,8 @@ def test_token_org_subresources_permission_not_allowed(token_client, team, organ if urlset[1] is None: team.all_events = False else: - team.all_events = True - setattr(team, urlset[1], False) + team.all_organizer_permissions = False + team.limit_organizer_permissions.pop(urlset[1], None) team.save() resp = getattr(token_client, urlset[0])('/api/v1/organizers/{}/{}'.format( organizer.slug, urlset[2].format(team_id=team.pk))) diff --git a/src/tests/api/test_transactions.py b/src/tests/api/test_transactions.py index df7d4b2f8c..8bec030d09 100644 --- a/src/tests/api/test_transactions.py +++ b/src/tests/api/test_transactions.py @@ -242,8 +242,8 @@ def test_organizer_list(token_client, team, organizer, event, order, item, taxru assert resp.data["count"] == 0 team.all_events = True - team.limit_organizer_permissions = {"event.vouchers:read": True} - team.all_organizer_permissions = False + team.limit_event_permissions = {"event.vouchers:read": True} + team.all_event_permissions = False team.save() resp = token_client.get( diff --git a/src/tests/base/test_export.py b/src/tests/base/test_export.py index 73f0bd39f4..63e664359d 100644 --- a/src/tests/base/test_export.py +++ b/src/tests/base/test_export.py @@ -143,7 +143,8 @@ def test_event_fail_user_no_permission(event, user, team): s.error_counter = 0 s.save() - team.limit_event_permissions["event.orders:read"] = False + team.all_event_permissions = False + team.limit_event_permissions = {"event.vouchers:read": True} team.save() run_scheduled_exports(None) diff --git a/src/tests/base/test_notifications.py b/src/tests/base/test_notifications.py index cc30b70cf5..31e4386e93 100644 --- a/src/tests/base/test_notifications.py +++ b/src/tests/base/test_notifications.py @@ -142,7 +142,8 @@ def test_notification_ignore_same_user(event, order, user, monkeypatch_on_commit @pytest.mark.django_db def test_notification_ignore_insufficient_permissions(event, order, user, team, monkeypatch_on_commit): djmail.outbox = [] - team.limit_event_permissions["event.orders:read"] = False + team.all_event_permissions = False + team.limit_event_permissions = {"event.vouchers:read": True} team.save() user.notification_settings.create( method='mail', event=event, action_type='pretix.event.order.paid', enabled=True diff --git a/src/tests/base/test_permissions.py b/src/tests/base/test_permissions.py index 604d4efa14..7346908f51 100644 --- a/src/tests/base/test_permissions.py +++ b/src/tests/base/test_permissions.py @@ -66,13 +66,6 @@ def admin_request(admin, client): return r -@pytest.mark.django_db -def test_invalid_permission(event, user): - team = Team.objects.create(organizer=event.organizer) - with pytest.raises(ValueError): - team.has_permission('FOOOOOOBAR') - - @pytest.mark.django_db def test_any_event_permission_limited(event, user): user._teamcache = {} @@ -183,9 +176,14 @@ def test_event_permissions_multiple_teams(event, user): assert user.has_event_permission(event.organizer, event, 'event.orders:write') assert user.has_event_permission(event.organizer, event, 'event.vouchers:write') assert not user.has_event_permission(event.organizer, event, 'event.settings.general:write') - assert user.get_event_permission_set(event.organizer, event) == {'event.orders:write', 'event.vouchers:write'} - assert user.get_event_permission_set(event.organizer, event2) == {'event.orders:write', 'event.settings.general:write', - 'event.settings.general:write'} + assert user.get_event_permission_set(event.organizer, event) == { + 'event.orders:write', 'event.vouchers:write', + 'can_change_orders', 'can_change_vouchers', + } + assert user.get_event_permission_set(event.organizer, event2) == { + 'event.orders:write', 'event.settings.general:write', 'event.settings.general:write', + 'can_change_orders', 'can_change_event_settings', + } @pytest.mark.django_db @@ -230,8 +228,14 @@ def test_organizer_permissions_multiple_teams(event, user): assert user.has_organizer_permission(event.organizer, 'organizer.events:create') assert user.has_organizer_permission(event.organizer, 'organizer.settings.general:write') assert not user.has_organizer_permission(event.organizer, 'organizer.teams:write') - assert user.get_organizer_permission_set(event.organizer) == {'organizer.events:create', 'organizer.settings.general:write'} - assert user.get_organizer_permission_set(orga2) == {'organizer.teams:write'} + assert user.get_organizer_permission_set(event.organizer) == { + 'organizer.events:create', 'organizer.settings.general:write', + 'can_create_events', 'can_change_organizer_settings', + } + assert user.get_organizer_permission_set(orga2) == { + 'organizer.teams:write', + 'can_change_teams', + } @pytest.mark.django_db diff --git a/src/tests/control/test_events.py b/src/tests/control/test_events.py index 373237fb5d..31077daeb1 100644 --- a/src/tests/control/test_events.py +++ b/src/tests/control/test_events.py @@ -76,7 +76,7 @@ class EventsTest(SoupTest): date_from=datetime.datetime(2014, 9, 5, tzinfo=datetime.timezone.utc), ) - self.team1 = Team.objects.create(organizer=self.orga1, all_organizer_permissions=True, all_event_permissions=True) + self.team1 = Team.objects.create(organizer=self.orga1, all_event_permissions=True, limit_organizer_permissions={"organizer.events:create": True}) self.team1.members.add(self.user) self.team1.limit_events.add(self.event1) diff --git a/src/tests/control/test_export.py b/src/tests/control/test_export.py index b5b02ff455..29edab5cd1 100644 --- a/src/tests/control/test_export.py +++ b/src/tests/control/test_export.py @@ -162,7 +162,8 @@ def test_event_export_schedule(client, env): @pytest.mark.django_db(transaction=True) def test_event_limited_permission(client, env): - env[2].limit_event_permissions = [] + env[2].all_event_permissions = False + env[2].limit_event_permissions = {"event.orders:read": True} env[2].save() user2 = User.objects.create_user("dummy2@dummy.dummy", "dummy") @@ -198,7 +199,7 @@ def test_event_limited_permission(client, env): response = client.get(f"/control/event/dummy/dummy/orders/export/{s2.pk}/delete") assert response.status_code == 404 - env[2].limit_event_permissions = {"event:settings.general:write": True} + env[2].limit_event_permissions = {"event.settings.general:write": True, "event.orders:read": True} env[2].save() response = client.get("/control/event/dummy/dummy/orders/export/") assert b"RULE1" in response.content @@ -330,7 +331,7 @@ def test_organizer_export_schedule(client, env): @pytest.mark.django_db(transaction=True) def test_organizer_limited_permission(client, env): env[2].all_organizer_permissions = False - env[2].all_event_permissions = False + env[2].all_event_permissions = True env[2].save() user2 = User.objects.create_user("dummy2@dummy.dummy", "dummy") @@ -366,7 +367,7 @@ def test_organizer_limited_permission(client, env): response = client.post(f"/control/organizer/dummy/export/{s2.pk}/run") assert response.status_code == 404 - env[2].limit_event_permissions = {"event:settings.general:write": True} + env[2].limit_organizer_permissions = {"organizer.settings.general:write": True} env[2].save() response = client.get("/control/organizer/dummy/export/") assert b"RULE1" in response.content diff --git a/src/tests/control/test_orders.py b/src/tests/control/test_orders.py index b9e50e812c..c6cae66bc4 100644 --- a/src/tests/control/test_orders.py +++ b/src/tests/control/test_orders.py @@ -67,7 +67,7 @@ def env(): ) event.settings.set('ticketoutput_testdummy__enabled', True) user = User.objects.create_user('dummy@dummy.dummy', 'dummy') - t = Team.objects.create(organizer=o, all_event_permissions=True) + t = Team.objects.create(organizer=o, all_event_permissions=True, all_organizer_permissions=True) t.members.add(user) t.limit_events.add(event) o = Order.objects.create( diff --git a/src/tests/control/test_permissions.py b/src/tests/control/test_permissions.py index 94b6fd9e22..70c42be9d2 100644 --- a/src/tests/control/test_permissions.py +++ b/src/tests/control/test_permissions.py @@ -422,7 +422,8 @@ def test_wrong_event_permission(perf_patch, client, env, perm, url, code, http_m t = Team( pk=2, organizer=env[2], all_events=True ) - setattr(t, perm, False) + t.all_event_permissions = False + t.limit_event_permissions.pop(perm, None) t.save() t.members.add(env[1]) client.login(email='dummy@dummy.dummy', password='dummy') @@ -440,7 +441,7 @@ def test_limited_event_permission_for_other_event(perf_patch, client, env, perm, organizer=env[2], name='Dummy', slug='dummy2', date_from=now(), plugins='pretix.plugins.banktransfer' ) - t = Team.objects.create(pk=2, organizer=env[2], can_change_event_settings=True) + t = Team.objects.create(pk=2, organizer=env[2], all_event_permissions=True) t.members.add(env[1]) t.limit_events.add(event2) @@ -458,13 +459,15 @@ def test_current_permission(client, env): pk=2, organizer=env[2], all_events=True ) setattr(t, 'event.settings.general:write', True) + t.all_event_permissions = False + t.limit_event_permissions['event.settings.general:write'] = True t.save() t.members.add(env[1]) client.login(email='dummy@dummy.dummy', password='dummy') response = client.get('/control/event/dummy/dummy/settings/') assert response.status_code == 200 - setattr(t, 'event.settings.general:write', False) + t.limit_event_permissions.pop('event.settings.general:write', None) t.save() response = client.get('/control/event/dummy/dummy/settings/') assert response.status_code == 403 @@ -474,7 +477,8 @@ def test_current_permission(client, env): @pytest.mark.parametrize("perm,url,code,http_method", event_permission_urls) def test_correct_event_permission_all_events(perf_patch, client, env, perm, url, code, http_method): t = Team(pk=2, organizer=env[2], all_events=True) - setattr(t, perm, True) + t.all_event_permissions = False + t.limit_event_permissions[perm] = True t.save() t.members.add(env[1]) client.login(email='dummy@dummy.dummy', password='dummy') @@ -492,7 +496,8 @@ def test_correct_event_permission_all_events(perf_patch, client, env, perm, url, @pytest.mark.parametrize("perm,url,code,http_method", event_permission_urls) def test_correct_event_permission_limited(perf_patch, client, env, perm, url, code, http_method): t = Team(pk=2, organizer=env[2]) - setattr(t, perm, True) + t.all_event_permissions = False + t.limit_event_permissions[perm] = True t.save() t.members.add(env[1]) t.limit_events.add(env[0]) @@ -584,8 +589,11 @@ organizer_permission_urls = [ @pytest.mark.django_db @pytest.mark.parametrize("perm,url,code", organizer_permission_urls) def test_wrong_organizer_permission(perf_patch, client, env, perm, url, code): - t = Team(pk=2, organizer=env[2]) - setattr(t, perm, False) + t = Team(pk=2, organizer=env[2], all_events=True) + t.all_organizer_permissions = False + t.limit_organizer_permissions.pop(perm, None) + t.all_event_permissions = False + t.limit_event_permissions.pop(perm, None) t.save() t.members.add(env[1]) client.login(email='dummy@dummy.dummy', password='dummy') @@ -596,8 +604,14 @@ def test_wrong_organizer_permission(perf_patch, client, env, perm, url, code): @pytest.mark.django_db @pytest.mark.parametrize("perm,url,code", organizer_permission_urls) def test_correct_organizer_permission(perf_patch, client, env, perm, url, code): - t = Team(pk=2, organizer=env[2]) - setattr(t, perm, True) + t = Team(pk=2, organizer=env[2], all_events=True) + if perm.startswith("event."): + t.all_organizer_permissions = False + t.all_event_permissions = False + t.limit_event_permissions[perm] = True + else: + t.all_organizer_permissions = False + t.limit_organizer_permissions[perm] = True t.save() t.members.add(env[1]) client.login(email='dummy@dummy.dummy', password='dummy') diff --git a/src/tests/control/test_reusable_media.py b/src/tests/control/test_reusable_media.py index 9be545504a..d9b070e079 100644 --- a/src/tests/control/test_reusable_media.py +++ b/src/tests/control/test_reusable_media.py @@ -140,7 +140,8 @@ def test_typeahead(organizer, admin_user, client, gift_card): # Unprivileged user can only do exact match team.all_events = True - team.limit_event_permissions["event.orders:read"] = False + team.all_event_permissions = False + team.limit_event_permissions = {"event.vouchers:read": True} team.save() r = client.get('/control/organizer/dummy/ticket_select2?query=' + op.secret[0:3]) diff --git a/src/tests/control/test_search.py b/src/tests/control/test_search.py index 282bb3bd9e..066e0cc9e8 100644 --- a/src/tests/control/test_search.py +++ b/src/tests/control/test_search.py @@ -98,7 +98,8 @@ class OrderSearchTest(SoupTest): assert 'DEFFO2' not in resp def test_team_limit_event_wrong_permission(self): - self.team.limit_event_permissions["event.orders:read"] = False + self.team.all_event_permissions = False + self.team.limit_event_permissions = {"event.vouchers:read": True} self.team.save() resp = self.client.get('/control/search/orders/').content.decode() assert 'ABCFO1' not in resp @@ -113,7 +114,8 @@ class OrderSearchTest(SoupTest): def test_team_all_events_wrong_permission(self): self.team.all_events = True - self.team.limit_event_permissions["event.orders:read"] = False + self.team.all_event_permissions = False + self.team.limit_event_permissions = {"event.vouchers:read": True} self.team.save() resp = self.client.get('/control/search/orders/').content.decode() assert 'ABCFO1' not in resp @@ -283,7 +285,8 @@ class PaymentSearchTest(SoupTest): assert 'DEFFO2' not in resp def test_team_limit_event_wrong_permission(self): - self.team.limit_event_permissions["event.orders:read"] = False + self.team.all_event_permissions = False + self.team.limit_event_permissions = {"event.vouchers:read": True} self.team.save() resp = self.client.get('/control/search/payments/').content.decode() assert 'ABCFO1' not in resp @@ -298,7 +301,8 @@ class PaymentSearchTest(SoupTest): def test_team_all_events_wrong_permission(self): self.team.all_events = True - self.team.limit_event_permissions["event.orders:read"] = False + self.team.all_event_permissions = False + self.team.limit_event_permissions = {"event.vouchers:read": True} self.team.save() resp = self.client.get('/control/search/payments/').content.decode() assert 'ABCFO1' not in resp diff --git a/src/tests/control/test_teams.py b/src/tests/control/test_teams.py index 1e1da50d2a..ca00f79820 100644 --- a/src/tests/control/test_teams.py +++ b/src/tests/control/test_teams.py @@ -231,15 +231,16 @@ def test_create_team(event, admin_user, admin_team, client): client.login(email='dummy@dummy.dummy', password='dummy') client.post('/control/organizer/dummy/team/add', { 'name': 'Foo', - 'organizer.events:create': 'on', + 'limit_organizer_permissions': ['organizer.events:create'], 'limit_events': str(event.pk), - 'event.settings.general:write': 'on' + 'limit_event_permissions': ['event.settings.general:write'] }, follow=True) with scopes_disabled(): t = Team.objects.last() - assert t.can_change_event_settings - assert t.can_create_events - assert not t.can_change_organizer_settings + assert not t.all_event_permissions + assert t.limit_event_permissions == {"event.settings.general:write": True} + assert not t.all_organizer_permissions + assert t.limit_organizer_permissions == {"organizer.events:create": True} assert list(t.limit_events.all()) == [event] assert list(t.members.all()) == [admin_user] @@ -249,13 +250,16 @@ def test_update_team(event, admin_user, admin_team, client): client.login(email='dummy@dummy.dummy', password='dummy') client.post('/control/organizer/dummy/team/{}/edit'.format(admin_team.pk), { 'name': 'Admin', - 'organizer.teams:write': 'on', + 'limit_organizer_permissions': ['organizer.teams:write'], 'limit_events': str(event.pk), - 'event.settings.general:write': 'on' + 'all_event_permissions': 'on', + 'all_organizer_permissions': '', }, follow=True) admin_team.refresh_from_db() - assert admin_team.can_change_event_settings - assert not admin_team.can_change_organizer_settings + assert admin_team.all_event_permissions + assert admin_team.limit_event_permissions == {} + assert not admin_team.all_organizer_permissions + assert admin_team.limit_organizer_permissions == {"organizer.teams:write": True} with scopes_disabled(): assert list(admin_team.limit_events.all()) == [event] diff --git a/src/tests/plugins/banktransfer/test_actions.py b/src/tests/plugins/banktransfer/test_actions.py index 2e8f9d975c..3c79aed70f 100644 --- a/src/tests/plugins/banktransfer/test_actions.py +++ b/src/tests/plugins/banktransfer/test_actions.py @@ -291,7 +291,12 @@ def test_assign_order_organizer_no_permission_for_event(env, client): state=BankTransaction.STATE_NOMATCH, amount=23, date='unknown') team = env[1].teams.first() - team.limit_events.clear() + event2 = Event.objects.create( + organizer=env[0].organizer, name='Dummy2', slug='dummy2', + date_from=now(), plugins='pretix.plugins.banktransfer' + ) + with scopes_disabled(): + team.limit_events.set([event2]) client.login(email='dummy@dummy.dummy', password='dummy') r = json.loads(client.post('/control/organizer/{}/banktransfer/action/'.format(env[0].organizer.slug), { 'action_{}'.format(trans.pk): 'assign:{}-{}'.format(env[0].slug.upper(), env[2].code), diff --git a/src/tests/plugins/ticketoutputpdf/test_control.py b/src/tests/plugins/ticketoutputpdf/test_control.py index 6c17ec11fd..a0d63cbcb3 100644 --- a/src/tests/plugins/ticketoutputpdf/test_control.py +++ b/src/tests/plugins/ticketoutputpdf/test_control.py @@ -54,7 +54,7 @@ class TicketLayoutFormTest(SoupTest): date_from=datetime.datetime(2013, 12, 26, tzinfo=datetime.timezone.utc), ) self.item1 = Item.objects.create(event=self.event1, name="Standard", default_price=0, position=1) - t = Team.objects.create(organizer=self.orga1, all_event_permissions=True) + t = Team.objects.create(organizer=self.orga1, all_event_permissions=True, all_organizer_permissions=True) t.members.add(self.user) t.limit_events.add(self.event1) self.client.login(email='dummy@dummy.dummy', password='dummy') diff --git a/src/tests/plugins/ticketoutputpdf/test_defaults_and_copy.py b/src/tests/plugins/ticketoutputpdf/test_defaults_and_copy.py index 3cbe6e1a6f..b8167fa778 100644 --- a/src/tests/plugins/ticketoutputpdf/test_defaults_and_copy.py +++ b/src/tests/plugins/ticketoutputpdf/test_defaults_and_copy.py @@ -36,7 +36,7 @@ def env(): date_from=now(), plugins='pretix.plugins.ticketoutputpdf' ) user = User.objects.create_user('dummy@dummy.dummy', 'dummy') - t = Team.objects.create(organizer=event.organizer, all_event_permissions=True) + t = Team.objects.create(organizer=event.organizer, all_event_permissions=True, all_organizer_permissions=True) t.members.add(user) t.limit_events.add(event) item1 = Item.objects.create(event=event, name="Ticket", default_price=23)