diff --git a/src/pretix/api/serializers/event.py b/src/pretix/api/serializers/event.py index d1debdeacc..ca163f0d33 100644 --- a/src/pretix/api/serializers/event.py +++ b/src/pretix/api/serializers/event.py @@ -445,7 +445,7 @@ class CloneEventSerializer(EventSerializer): date_admission = validated_data.pop('date_admission', None) new_event = super().create({**validated_data, 'plugins': None}) - event = Event.objects.filter(slug=self.context['event'], organizer=self.context['organizer'].pk).first() + event = self.context['event'] new_event.copy_data_from(event, skip_meta_data='meta_data' in validated_data) if plugins is not None: diff --git a/src/pretix/api/views/event.py b/src/pretix/api/views/event.py index 55f67e315e..604300b300 100644 --- a/src/pretix/api/views/event.py +++ b/src/pretix/api/views/event.py @@ -281,6 +281,11 @@ class EventViewSet(viewsets.ModelViewSet): new_event = serializer.save(organizer=self.request.organizer) if copy_from: + perm_holder = (self.request.auth if isinstance(self.request.auth, (Device, TeamAPIToken)) + else self.request.user) + if not copy_from.allow_copy_data(self.request.organizer, perm_holder): + raise PermissionDenied("Not sufficient permission on source event to copy") + new_event.copy_data_from(copy_from, skip_meta_data='meta_data' in serializer.validated_data) if plugins is not None: @@ -345,7 +350,7 @@ class CloneEventViewSet(viewsets.ModelViewSet): def get_serializer_context(self): ctx = super().get_serializer_context() - ctx['event'] = self.kwargs['event'] + ctx['event'] = Event.objects.get(slug=self.kwargs['event'], organizer=self.request.organizer) ctx['organizer'] = self.request.organizer return ctx @@ -356,6 +361,9 @@ class CloneEventViewSet(viewsets.ModelViewSet): if not perm_holder.has_organizer_permission(self.request.organizer, "organizer.events:create", request=self.request): raise PermissionDenied("No permission to create events") + if not serializer.context['event'].allow_copy_data(self.request.organizer, perm_holder): + raise PermissionDenied("Not sufficient permission on source event to copy") + serializer.save(organizer=self.request.organizer) serializer.instance.log_action( diff --git a/src/pretix/base/models/event.py b/src/pretix/base/models/event.py index 88058714b5..e12842cd85 100644 --- a/src/pretix/base/models/event.py +++ b/src/pretix/base/models/event.py @@ -843,6 +843,33 @@ class Event(EventMixin, LoggedModel): time(hour=23, minute=59, second=59) ), tz) + def allow_copy_data(self, new_organizer, auth) -> bool: + """ + Returns whether it is allowed to copy the event to the target organizer. Auth can be TeamAPIToken or User. + """ + from ..permissions import get_all_event_permissions + from .auth import User + + if self.organizer == new_organizer: + # Copying in the same organizer is always okay with any read access, we just need to ensure it does not + # grant more permissions than I had before, but that is handled by the view logic + return auth.has_event_permission(self.organizer, self, None) + + if isinstance(auth, User): + # Cross-organizer copying requires almost full permission of source to prevent settings extraction + required_permissions = get_all_event_permissions() - { + # We do not require these, as this data is not copied + "event:orders.read", "event:orders.write", "event:vouchers.read", "event:vouchers.write", + "event:subevents.write", + } + given_permission = auth.get_event_permission_set(self.organizer, self) + return all(p in given_permission for p in required_permissions if ":" not in p) + + else: + # Tokens or devices can never copy between organizers, as they are organizer-bound. Kept for future + # compatibility and easier calling + return False + def copy_data_from(self, other, skip_meta_data=False): from ..signals import event_copy_data from . import ( diff --git a/src/pretix/control/forms/event.py b/src/pretix/control/forms/event.py index d36f776a30..100ddc1fab 100644 --- a/src/pretix/control/forms/event.py +++ b/src/pretix/control/forms/event.py @@ -101,6 +101,7 @@ class EventWizardFoundationForm(forms.Form): def __init__(self, *args, **kwargs): self.user = kwargs.pop('user') self.session = kwargs.pop('session') + self.clone_from = kwargs.pop('clone_from') super().__init__(*args, **kwargs) qs = Organizer.objects.all() if not self.user.has_active_staff_session(self.session.session_key): @@ -126,6 +127,16 @@ class EventWizardFoundationForm(forms.Form): self.fields['organizer'].initial = organizer self.fields['locales'].initial = organizer.settings.locales + def clean(self): + d = super().clean() + if d.get('organizer') and self.clone_from and not self.user.has_active_staff_session(self.session.session_key): + if not self.clone_from.allow_copy_data(d['organizer'], self.user): + raise ValidationError({ + "organizer": _("You do not have a sufficient level of access on the event you selected " + "to copy it to the desired organizer.") + }) + return d + class EventWizardBasicsForm(I18nModelForm): error_messages = { @@ -199,6 +210,7 @@ class EventWizardBasicsForm(I18nModelForm): self.has_subevents = kwargs.pop('has_subevents') self.user = kwargs.pop('user') self.session = kwargs.pop('session') + self.clone_from = kwargs.pop('clone_from') super().__init__(*args, **kwargs) if 'timezone' not in self.initial: self.initial['timezone'] = get_current_timezone_name() @@ -239,6 +251,18 @@ class EventWizardBasicsForm(I18nModelForm): 'check "{field}" above.').format(field=self.fields["no_taxes"].label) }) + if self.clone_from and not self.user.has_active_staff_session(self.session.session_key): + if data.get("team"): + source_event_perms = self.user.get_event_permission_set(self.organizer, self.clone_from) + team_perms = data["team"].event_permission_set(include_legacy=False) + print(source_event_perms) + print(team_perms) + if any(t not in source_event_perms for t in team_perms if ":" in t): + raise ValidationError({ + "team": _("You cannot choose a team that would give you more access than you have on " + "the event you are copying.") + }) + # change timezone zone = ZoneInfo(data.get('timezone')) data['date_from'] = self.reset_timezone(zone, data.get('date_from')) @@ -262,10 +286,9 @@ class EventWizardBasicsForm(I18nModelForm): @staticmethod def has_control_rights(user, organizer, session): + # It's mostly pointless to let a user create an event where they can't event change the name or create products, + # so we detect if the user has sufficient access for that on a new event. return user.teams.filter( - TeamQuerySet.event_permission_q("event.items:write"), - TeamQuerySet.event_permission_q("event.orders:write"), - TeamQuerySet.event_permission_q("event.vouchers:write"), TeamQuerySet.event_permission_q("event.settings.general:write"), organizer=organizer, all_events=True, @@ -298,23 +321,24 @@ class EventWizardCopyForm(forms.Form): if user.has_active_staff_session(session.session_key): return Event.objects.all() return Event.objects.filter( + # It is generally pointless to let users copy events when they would not even be able to change the + # date of the event they have just created. Therefore, even if it looks wrong, we're checking a write + # permission for read access. Q(organizer_id__in=user.teams.filter( - # TODO: review these! - # Restrict cross-organizer copying further than same-organizer copying? TeamQuerySet.event_permission_q("event.settings.general:write"), - TeamQuerySet.event_permission_q("event.items:write"), all_events=True, ).values_list('organizer', flat=True)) | Q(id__in=user.teams.filter( TeamQuerySet.event_permission_q("event.settings.general:write"), - TeamQuerySet.event_permission_q("event.items:write"), ).values_list('limit_events__id', flat=True)) ) def __init__(self, *args, **kwargs): - kwargs.pop('organizer') + self.organizer = kwargs.pop('organizer') kwargs.pop('locales') self.session = kwargs.pop('session') + self.team = kwargs.pop('team') kwargs.pop('has_subevents') + kwargs.pop('clone_from') self.user = kwargs.pop('user') super().__init__(*args, **kwargs) @@ -333,6 +357,24 @@ class EventWizardCopyForm(forms.Form): ) self.fields['copy_from_event'].widget.choices = self.fields['copy_from_event'].choices + def clean(self): + d = super().clean() + if d.get('copy_from_event') and not self.user.has_active_staff_session(self.session.session_key): + if not d['copy_from_event'].allow_copy_data(self.organizer, self.user): + raise ValidationError({ + "copy_from_event": _("You do not have a sufficient level of access on the event you selected " + "to copy it to the desired organizer.") + }) + if self.team: + source_event_perms = self.user.get_event_permission_set(self.organizer, d['copy_from_event']) + team_perms = self.team.event_permission_set(include_legacy=False) + if any(t not in source_event_perms for t in team_perms if ":" in t): + raise ValidationError({ + "copy_from_event": _("You cannot choose an event on which you have less access than the " + "team you selected in the previous step.") + }) + return d + class EventMetaValueForm(forms.ModelForm): diff --git a/src/pretix/control/views/main.py b/src/pretix/control/views/main.py index 18bbc6a461..349f139ad7 100644 --- a/src/pretix/control/views/main.py +++ b/src/pretix/control/views/main.py @@ -211,12 +211,7 @@ class EventWizard(SafeSessionWizardView): except Event.DoesNotExist: allow = False else: - allow = ( - request.user.has_event_permission(clone_from.organizer, clone_from, - 'event.settings.general:write', request) - and request.user.has_event_permission(clone_from.organizer, clone_from, - 'event.items:write', request) - ) + allow = request.user.has_event_permission(clone_from.organizer, clone_from, None, request) if not allow: messages.error(self.request, _('You do not have permission to clone this event.')) else: @@ -242,6 +237,7 @@ class EventWizard(SafeSessionWizardView): kwargs = { 'user': self.request.user, 'session': self.request.session, + 'clone_from': self.clone_from, } if step != 'foundation': fdata = self.get_cleaned_data_for_step('foundation') @@ -253,6 +249,13 @@ class EventWizard(SafeSessionWizardView): } # The show must go on, we catch this error in render() kwargs.update(fdata) + if step == 'copy': + bdata = self.get_cleaned_data_for_step('basics') + if bdata: + bdata = { + 'team': bdata.get('team'), + } + kwargs.update(bdata) return kwargs def get_template_names(self): @@ -277,18 +280,41 @@ class EventWizard(SafeSessionWizardView): user=self.request.user, ) - if not EventWizardBasicsForm.has_control_rights(self.request.user, event.organizer, self.request.session): + if copy_data and copy_data['copy_from_event']: + copy_from_event = copy_data['copy_from_event'] + elif self.clone_from: + copy_from_event = self.clone_from + else: + copy_from_event = None + + if not EventWizardBasicsForm.has_control_rights( + self.request.user, event.organizer, self.request.session + ): if basics_data["team"] is not None: t = basics_data["team"] t.limit_events.add(event) elif event.organizer.settings.event_team_provisioning: + # Create a new team for new events with full access, but for copied events with the same access + # as the source + limit_event_permissions = {} + if copy_from_event and copy_from_event.organizer == event.organizer: + source_teams = self.request.user._get_teams_for_event(copy_from_event.organizer, copy_from_event) + all_event_permissions = any(t.all_event_permissions for t in source_teams) + if not all_event_permissions: + for t in source_teams: + limit_event_permissions.update(t.limit_event_permissions) + else: + # The cross-organizer case is protected through allow_copy_data + all_event_permissions = True + t = Team.objects.create( organizer=event.organizer, name=_('Team {event}').format( event=str(event.name)[:100] + "…" if len(str(event.name)) > 100 else str(event.name) ), all_organizer_permissions=False, - all_event_permissions=True, + all_event_permissions=all_event_permissions, + limit_event_permissions=limit_event_permissions, ) t.members.add(self.request.user) t.limit_events.add(event) @@ -296,7 +322,8 @@ class EventWizard(SafeSessionWizardView): '_created_by_event_wizard': True, 'name': t.name, 'all_organizer_permissions': False, - 'all_event_permissions': True, + 'all_event_permissions': all_event_permissions, + 'limit_event_permissions': limit_event_permissions, 'limit_events': [event.pk], }) @@ -307,11 +334,8 @@ class EventWizard(SafeSessionWizardView): }) event.log_action('pretix.event.settings', user=self.request.user, data=logdata) - if copy_data and copy_data['copy_from_event']: - from_event = copy_data['copy_from_event'] - event.copy_data_from(from_event) - elif self.clone_from: - event.copy_data_from(self.clone_from) + if copy_from_event: + event.copy_data_from(copy_from_event) else: event.set_active_plugins(settings.PRETIX_PLUGINS_DEFAULT.split(","), allow_restricted=settings.PRETIX_PLUGINS_DEFAULT.split(",")) @@ -324,10 +348,8 @@ class EventWizard(SafeSessionWizardView): event.set_defaults() if basics_data['tax_rate'] is not None: - if self.clone_from: - default_tax_rule = self.clone_from.cached_default_tax_rule - elif copy_data and copy_data['copy_from_event']: - default_tax_rule = from_event.cached_default_tax_rule + if copy_from_event: + default_tax_rule = copy_from_event.cached_default_tax_rule else: default_tax_rule = None if not default_tax_rule or default_tax_rule.rate != basics_data['tax_rate']: @@ -341,7 +363,7 @@ class EventWizard(SafeSessionWizardView): event.settings.set('locale', basics_data['locale']) event.settings.set('locales', foundation_data['locales']) - if (copy_data and copy_data['copy_from_event']) or self.clone_from or event.has_subevents: + if copy_from_event or event.has_subevents: return redirect(reverse('control:event.settings', kwargs={ 'organizer': event.organizer.slug, 'event': event.slug, diff --git a/src/tests/api/test_events.py b/src/tests/api/test_events.py index 08d999d2eb..fd19291855 100644 --- a/src/tests/api/test_events.py +++ b/src/tests/api/test_events.py @@ -577,7 +577,9 @@ def test_event_create_with_clone_unknown_source(user, user_client, organizer, ev @pytest.mark.django_db -def test_event_create_with_clone_across_organizers(user, user_client, organizer, event, taxrule): +def test_event_create_with_clone_across_organizers(user, user_client, organizer, event, taxrule, team): + team.all_event_permissions = True + team.save() with scopes_disabled(): target_org = Organizer.objects.create(name='Dummy', slug='dummy2') team = target_org.teams.create( @@ -622,6 +624,51 @@ def test_event_create_with_clone_across_organizers(user, user_client, organizer, assert cloned_event.tax_rules.exists() +@pytest.mark.django_db +def test_event_create_with_clone_across_organizers_lack_of_permission_on_source(user, user_client, team, organizer, event, taxrule): + team.all_event_permissions = False + team.limit_event_permissions = { + "event.settings.general:write": True, + } + team.save() + with scopes_disabled(): + target_org = Organizer.objects.create(name='Dummy', slug='dummy2') + team = target_org.teams.create( + name="Test-Team", + all_event_permissions=True, + all_organizer_permissions=True, + ) + team.members.add(user) + + resp = user_client.post( + '/api/v1/organizers/{}/events/?clone_from={}/{}'.format(target_org.slug, organizer.slug, event.slug), + { + "name": { + "de": "Demo Konference 2020 Test", + "en": "Demo Conference 2020 Test" + }, + "live": False, + "testmode": True, + "currency": "EUR", + "date_from": "2018-12-27T10:00:00Z", + "date_to": "2018-12-28T10:00:00Z", + "date_admission": None, + "is_public": False, + "presale_start": None, + "presale_end": None, + "location": None, + "slug": "2030", + "plugins": [ + "pretix.plugins.ticketoutputpdf" + ], + "timezone": "Europe/Vienna" + }, + format='json' + ) + assert resp.status_code == 403 + assert resp.data["detail"] == "Not sufficient permission on source event to copy" + + @pytest.mark.django_db def test_event_put_with_clone(token_client, organizer, event, meta_prop): resp = token_client.put( diff --git a/src/tests/control/test_events.py b/src/tests/control/test_events.py index 31077daeb1..d03aa5da05 100644 --- a/src/tests/control/test_events.py +++ b/src/tests/control/test_events.py @@ -76,11 +76,16 @@ class EventsTest(SoupTest): date_from=datetime.datetime(2014, 9, 5, tzinfo=datetime.timezone.utc), ) - self.team1 = Team.objects.create(organizer=self.orga1, all_event_permissions=True, limit_organizer_permissions={"organizer.events:create": True}) + self.team1 = Team.objects.create( + organizer=self.orga1, + name="T1", + all_event_permissions=True, + limit_organizer_permissions={"organizer.events:create": True} + ) self.team1.members.add(self.user) self.team1.limit_events.add(self.event1) - self.team2 = Team.objects.create(organizer=self.orga1, all_event_permissions=True) + self.team2 = Team.objects.create(organizer=self.orga1, name="T2", all_event_permissions=True) self.team2.members.add(self.user) self.client.login(email='dummy@dummy.dummy', password='dummy') @@ -1260,6 +1265,265 @@ class EventsTest(SoupTest): }) assert doc.select(".has-error") + def test_create_event_copy_from_other_org_validates_source_permissions(self): + # To prevent leaks of e.g. settings contents, a user may only copy from one organizer to the other + # if they have basically all permissions on the old event for all data that may be copied. + self.team1.all_event_permissions = False + self.team1.limit_event_permissions = {"event.settings.general:write": True, "event.orders:read": True} + self.team1.save() + team3 = Team.objects.create(organizer=self.orga2, all_event_permissions=True, all_organizer_permissions=True) + team3.members.add(self.user) + + doc = self.post_doc('/control/events/add', { + 'event_wizard-current_step': 'foundation', + 'event_wizard-prefix': 'event_wizard', + 'foundation-organizer': self.orga2.pk, + 'foundation-locales': ('en', 'de') + }) + assert doc.select("#id_basics-name_0") + + doc = self.post_doc('/control/events/add', { + 'event_wizard-current_step': 'basics', + 'event_wizard-prefix': 'event_wizard', + 'basics-name_0': '33C3', + 'basics-name_1': '33C3', + 'basics-slug': '33c3', + 'basics-date_from_0': '2016-12-27', + 'basics-date_from_1': '10:00:00', + 'basics-date_to_0': '2016-12-30', + 'basics-date_to_1': '19:00:00', + 'basics-location_0': 'Hamburg', + 'basics-location_1': 'Hamburg', + 'basics-currency': 'EUR', + 'basics-tax_rate': '19.00', + 'basics-locale': 'en', + 'basics-timezone': 'Europe/Berlin', + 'basics-presale_start_0': '2016-11-01', + 'basics-presale_start_1': '10:00:00', + 'basics-presale_end_0': '2016-11-30', + 'basics-presale_end_1': '18:00:00', + }) + assert doc.select("#id_copy-copy_from_event") + + doc = self.post_doc('/control/events/add', { + 'event_wizard-current_step': 'copy', + 'event_wizard-prefix': 'event_wizard', + 'copy-copy_from_event': self.event1.pk + }) + assert doc.select(".alert-danger") + assert "sufficient level of access" in doc.select(".has-error")[0].text + + def test_create_event_clone_from_other_org_validates_source_permissions(self): + # To prevent leaks of e.g. settings contents, a user may only copy from one organizer to the other + # if they have basically all permissions on the old event for all data that may be copied. + self.team1.all_event_permissions = False + self.team1.limit_event_permissions = {"event.settings.general:write": True, "event.orders:read": True} + self.team1.save() + team3 = Team.objects.create(organizer=self.orga2, all_event_permissions=True, all_organizer_permissions=True) + team3.members.add(self.user) + + doc = self.post_doc(f'/control/events/add?clone={self.event1.pk}', { + 'event_wizard-current_step': 'foundation', + 'event_wizard-prefix': 'event_wizard', + 'foundation-organizer': self.orga2.pk, + 'foundation-locales': ('en', 'de') + }) + assert doc.select(".alert-danger") + assert "sufficient level of access" in doc.select(".has-error")[0].text + + def test_create_event_copy_from_same_org_creates_new_team_with_same_permissions(self): + # To prevent unwanted permission escalations, when a user copies an event and a new team is created to make + # sure they can access the new event, the new event must be created with the same level of access they have + # on the old event. + self.team1.all_event_permissions = False + self.team1.limit_event_permissions = {"event.settings.general:write": True, "event.orders:read": True} + self.team1.save() + + doc = self.post_doc('/control/events/add', { + 'event_wizard-current_step': 'foundation', + 'event_wizard-prefix': 'event_wizard', + 'foundation-organizer': self.orga1.pk, + 'foundation-locales': ('en', 'de') + }) + assert doc.select("#id_basics-name_0") + + doc = self.post_doc('/control/events/add', { + 'event_wizard-current_step': 'basics', + 'event_wizard-prefix': 'event_wizard', + 'basics-name_0': '33C3', + 'basics-name_1': '33C3', + 'basics-slug': '33c3', + 'basics-date_from_0': '2016-12-27', + 'basics-date_from_1': '10:00:00', + 'basics-date_to_0': '2016-12-30', + 'basics-date_to_1': '19:00:00', + 'basics-location_0': 'Hamburg', + 'basics-location_1': 'Hamburg', + 'basics-currency': 'EUR', + 'basics-tax_rate': '19.00', + 'basics-locale': 'en', + 'basics-timezone': 'Europe/Berlin', + 'basics-presale_start_0': '2016-11-01', + 'basics-presale_start_1': '10:00:00', + 'basics-presale_end_0': '2016-11-30', + 'basics-presale_end_1': '18:00:00', + }) + assert doc.select("#id_copy-copy_from_event") + + self.post_doc('/control/events/add', { + 'event_wizard-current_step': 'copy', + 'event_wizard-prefix': 'event_wizard', + 'copy-copy_from_event': self.event1.pk + }) + with scopes_disabled(): + ev = Event.objects.get(slug='33c3') + new_team = Team.objects.get(limit_events=ev, members=self.user) + assert new_team.pk > self.team2.pk + assert new_team.all_event_permissions is False + assert new_team.all_organizer_permissions is False + assert new_team.limit_event_permissions == {"event.settings.general:write": True, "event.orders:read": True} + assert new_team.limit_organizer_permissions == {} + assert new_team.all_events is False + assert new_team.limit_events.get() == ev + + def test_create_event_clone_from_same_org_creates_new_team_with_same_permissions(self): + # To prevent unwanted permission escalations, when a user copies an event and a new team is created to make + # sure they can access the new event, the new event must be created with the same level of access they have + # on the old event. + self.team1.all_event_permissions = False + self.team1.limit_event_permissions = {"event.settings.general:write": True, "event.orders:read": True} + self.team1.save() + + doc = self.post_doc(f'/control/events/add?clone={self.event1.pk}', { + 'event_wizard-current_step': 'foundation', + 'event_wizard-prefix': 'event_wizard', + 'foundation-organizer': self.orga1.pk, + 'foundation-locales': ('en', 'de') + }) + assert doc.select("#id_basics-name_0") + + self.post_doc(f'/control/events/add?clone={self.event1.pk}', { + 'event_wizard-current_step': 'basics', + 'event_wizard-prefix': 'event_wizard', + 'basics-name_0': '33C3', + 'basics-name_1': '33C3', + 'basics-slug': '33c3', + 'basics-date_from_0': '2016-12-27', + 'basics-date_from_1': '10:00:00', + 'basics-date_to_0': '2016-12-30', + 'basics-date_to_1': '19:00:00', + 'basics-location_0': 'Hamburg', + 'basics-location_1': 'Hamburg', + 'basics-currency': 'EUR', + 'basics-tax_rate': '19.00', + 'basics-locale': 'en', + 'basics-timezone': 'Europe/Berlin', + 'basics-presale_start_0': '2016-11-01', + 'basics-presale_start_1': '10:00:00', + 'basics-presale_end_0': '2016-11-30', + 'basics-presale_end_1': '18:00:00', + }) + with scopes_disabled(): + ev = Event.objects.get(slug='33c3') + new_team = Team.objects.get(limit_events=ev, members=self.user) + assert new_team.pk > self.team2.pk + assert new_team.all_event_permissions is False + assert new_team.all_organizer_permissions is False + assert new_team.limit_event_permissions == {"event.settings.general:write": True, "event.orders:read": True} + assert new_team.limit_organizer_permissions == {} + assert new_team.all_events is False + assert new_team.limit_events.get() == ev + + def test_create_event_copy_from_same_org_validates_selected_team_permissions(self): + # To prevent unwanted permission escalations, when a user copies an event and selects the team the new event + # should be attached to, this new team may not have higher permissions than the permissions the user holds for + # the event that is copied from. + self.team1.all_event_permissions = False + self.team1.limit_event_permissions = {"event.settings.general:write": True, "event.orders:read": True} + self.team1.save() + + doc = self.post_doc('/control/events/add', { + 'event_wizard-current_step': 'foundation', + 'event_wizard-prefix': 'event_wizard', + 'foundation-organizer': self.orga1.pk, + 'foundation-locales': ('en', 'de') + }) + assert doc.select("#id_basics-name_0") + assert doc.select("#id_basics-team") + + doc = self.post_doc('/control/events/add', { + 'event_wizard-current_step': 'basics', + 'event_wizard-prefix': 'event_wizard', + 'basics-name_0': '33C3', + 'basics-name_1': '33C3', + 'basics-slug': '33c3', + 'basics-date_from_0': '2016-12-27', + 'basics-date_from_1': '10:00:00', + 'basics-date_to_0': '2016-12-30', + 'basics-date_to_1': '19:00:00', + 'basics-location_0': 'Hamburg', + 'basics-location_1': 'Hamburg', + 'basics-currency': 'EUR', + 'basics-tax_rate': '19.00', + 'basics-locale': 'en', + 'basics-team': self.team2.pk, + 'basics-timezone': 'Europe/Berlin', + 'basics-presale_start_0': '2016-11-01', + 'basics-presale_start_1': '10:00:00', + 'basics-presale_end_0': '2016-11-30', + 'basics-presale_end_1': '18:00:00', + }) + assert doc.select("#id_copy-copy_from_event") + + doc = self.post_doc('/control/events/add', { + 'event_wizard-current_step': 'copy', + 'event_wizard-prefix': 'event_wizard', + 'copy-copy_from_event': self.event1.pk + }) + assert doc.select(".alert-danger") + assert "less access than" in doc.select(".has-error")[0].text + + def test_create_event_clone_from_same_org_validates_selected_team_permissions(self): + # To prevent unwanted permission escalations, when a user copies an event and selects the team the new event + # should be attached to, this new team may not have higher permissions than the permissions the user holds for + # the event that is copied from. + self.team1.all_event_permissions = False + self.team1.limit_event_permissions = {"event.settings.general:write": True, "event.orders:read": True} + self.team1.save() + + doc = self.post_doc(f'/control/events/add?clone={self.event1.pk}', { + 'event_wizard-current_step': 'foundation', + 'event_wizard-prefix': 'event_wizard', + 'foundation-organizer': self.orga1.pk, + 'foundation-locales': ('en', 'de') + }) + assert doc.select("#id_basics-name_0") + assert doc.select("#id_basics-team") + + doc = self.post_doc(f'/control/events/add?clone={self.event1.pk}', { + 'event_wizard-current_step': 'basics', + 'event_wizard-prefix': 'event_wizard', + 'basics-name_0': '33C3', + 'basics-name_1': '33C3', + 'basics-slug': '33c3', + 'basics-date_from_0': '2016-12-27', + 'basics-date_from_1': '10:00:00', + 'basics-date_to_0': '2016-12-30', + 'basics-date_to_1': '19:00:00', + 'basics-location_0': 'Hamburg', + 'basics-location_1': 'Hamburg', + 'basics-currency': 'EUR', + 'basics-tax_rate': '19.00', + 'basics-locale': 'en', + 'basics-team': self.team2.pk, + 'basics-timezone': 'Europe/Berlin', + 'basics-presale_start_0': '2016-11-01', + 'basics-presale_start_1': '10:00:00', + 'basics-presale_end_0': '2016-11-30', + 'basics-presale_end_1': '18:00:00', + }) + assert "would give you more access than" in doc.select(".has-error")[0].text + class EventDeletionTest(SoupTest): @scopes_disabled()