diff --git a/src/pretix/api/serializers/media.py b/src/pretix/api/serializers/media.py index 894c1bcf0..d759f1468 100644 --- a/src/pretix/api/serializers/media.py +++ b/src/pretix/api/serializers/media.py @@ -31,7 +31,9 @@ from pretix.api.serializers.order import OrderPositionSerializer from pretix.api.serializers.organizer import ( CustomerSerializer, GiftCardSerializer, ) -from pretix.base.models import Order, OrderPosition, ReusableMedium +from pretix.base.models import ( + Device, Order, OrderPosition, ReusableMedium, TeamAPIToken, +) logger = logging.getLogger(__name__) @@ -80,8 +82,7 @@ class ReusableMediaSerializer(I18nAwareModelSerializer): ) if 'linked_orderposition' in self.context['request'].query_params.getlist('expand'): - # No additional permission check performed, documented limitation of the permission system - # Would get to complex/unusable otherwise since the permission depends on the event + # Permission Check performed in to_representation self.fields['linked_orderposition'] = NestedOrderPositionSerializer(read_only=True) else: self.fields['linked_orderposition'] = serializers.PrimaryKeyRelatedField( @@ -117,6 +118,27 @@ class ReusableMediaSerializer(I18nAwareModelSerializer): ) return data + def to_representation(self, instance): + r = super().to_representation(instance) + request = self.context.get('request') + # late permission evaluations for checks that depend on the actual linked events + expand_nested = self.context['request'].query_params.getlist('expand') + perm_holder = request.auth if isinstance(request.auth, (Device, TeamAPIToken)) else request.user + if 'linked_orderposition' in expand_nested: + if instance.linked_orderposition is not None: + event = instance.linked_orderposition.order.event + if not perm_holder.has_event_permission(event.organizer, event, 'event.orders:read', request): + r['linked_orderposition'] = {'id': instance.linked_orderposition.id} + + if 'linked_giftcard.owner_ticket' in expand_nested: + gc = instance.linked_giftcard + if gc is not None and gc.owner_ticket is not None: + event = gc.owner_ticket.order.event + if not perm_holder.has_event_permission(event.organizer, event, 'event.orders:read', request): + r['linked_giftcard']['owner_ticket'] = {'id': instance.linked_giftcard.owner_ticket.id} + + return r + class Meta: model = ReusableMedium fields = ( diff --git a/src/pretix/api/serializers/organizer.py b/src/pretix/api/serializers/organizer.py index 05f25976a..47c141aee 100644 --- a/src/pretix/api/serializers/organizer.py +++ b/src/pretix/api/serializers/organizer.py @@ -286,6 +286,19 @@ class GiftCardSerializer(I18nAwareModelSerializer): ) return data + def to_representation(self, instance): + r = super().to_representation(instance) + request = self.context.get('request') + # late permission evaluations for checks that depend on the actual linked events + if 'owner_ticket' in self.context['request'].query_params.getlist('expand'): + owner_ticket = instance.owner_ticket + if owner_ticket: + event = owner_ticket.order.event + perm_holder = request.auth if isinstance(request.auth, (Device, TeamAPIToken)) else request.user + if not perm_holder.has_event_permission(event.organizer, event, 'event.orders:read', request): + r['owner_ticket'] = {'id': instance.owner_ticket.id} + return r + class Meta: model = GiftCard fields = ('id', 'secret', 'issuance', 'value', 'currency', 'testmode', 'expires', 'conditions', 'owner_ticket', diff --git a/src/tests/api/test_giftcards.py b/src/tests/api/test_giftcards.py index 4fdb89cfe..ef3db84b4 100644 --- a/src/tests/api/test_giftcards.py +++ b/src/tests/api/test_giftcards.py @@ -171,6 +171,35 @@ def test_giftcard_detail_expand(token_client, organizer, event, giftcard): } +@pytest.mark.django_db +def test_giftcard_detail_expand_without_permissions(team, token_client, organizer, event, giftcard): + with scopes_disabled(): + o = Order.objects.create( + code='FOO', event=event, email='dummy@dummy.test', + status=Order.STATUS_PENDING, datetime=now(), expires=now() + timedelta(days=10), + sales_channel=event.organizer.sales_channels.get(identifier="web"), + total=14, locale='en' + ) + ticket = event.items.create(name='Early-bird ticket', category=None, default_price=23, admission=True, + personalized=True) + op = o.positions.create(item=ticket, price=Decimal("14")) + giftcard.owner_ticket = op + giftcard.save() + + team.all_event_permissions = False + team.save() + + res = dict(TEST_GC_RES) + res["id"] = giftcard.pk + res["issuance"] = giftcard.issuance.isoformat().replace('+00:00', 'Z') + resp = token_client.get('/api/v1/organizers/{}/giftcards/{}/?expand=owner_ticket'.format(organizer.slug, giftcard.pk)) + assert resp.status_code == 200 + + assert resp.data["owner_ticket"] == { + "id": op.pk, + } + + TEST_GIFTCARD_CREATE_PAYLOAD = { "secret": "DEFABC", "value": "12.00", diff --git a/src/tests/api/test_reusable_media.py b/src/tests/api/test_reusable_media.py index 30614e67b..18ee325a5 100644 --- a/src/tests/api/test_reusable_media.py +++ b/src/tests/api/test_reusable_media.py @@ -252,6 +252,76 @@ def test_medium_detail(token_client, organizer, event, medium, giftcard, custome } +@pytest.mark.django_db +def test_medium_detail_event_permission_missing(token_client, organizer, event, medium, giftcard, customer, team): + team.all_organizer_permissions = False + team.limit_organizer_permissions = { + "organizer.reusablemedia:read": True, + "organizer.customers:read": True, + "organizer.giftcards:read": True, + } + team.all_event_permissions = False + team.save() + + with scopes_disabled(): + o = Order.objects.create( + code='FOO', event=event, email='dummy@dummy.test', + status=Order.STATUS_PENDING, datetime=now(), expires=now() + timedelta(days=10), + sales_channel=event.organizer.sales_channels.get(identifier="web"), + total=14, locale='en' + ) + ticket = event.items.create(name='Early-bird ticket', category=None, default_price=23, admission=True, + personalized=True) + op = o.positions.create(item=ticket, price=Decimal("14")) + medium.linked_orderposition = op + medium.linked_giftcard = giftcard + medium.customer = customer + medium.save() + giftcard.owner_ticket = op + giftcard.save() + + resp = token_client.get( + '/api/v1/organizers/{}/reusablemedia/{}/?expand=linked_giftcard&expand=' + 'linked_giftcard.owner_ticket&expand=linked_orderposition&expand=customer'.format( + organizer.slug, medium.pk + ) + ) + assert resp.status_code == 200 + + assert resp.data["linked_orderposition"] == { + "id": op.pk, + } + + assert resp.data["linked_giftcard"] == { + "id": giftcard.pk, + "secret": "ABCDEF", + "issuance": giftcard.issuance.isoformat().replace("+00:00", "Z"), + "value": "23.00", + "currency": "EUR", + "testmode": False, + "expires": None, + "conditions": None, + "owner_ticket": {"id": op.pk}, + "issuer": "dummy", + } + + assert resp.data["customer"] == { + "identifier": customer.identifier, + "external_identifier": None, + "email": "foo@example.org", + "phone": None, + "name": "Foo", + "name_parts": {"_legacy": "Foo"}, + "is_active": True, + "is_verified": False, + "last_login": None, + "date_joined": customer.date_joined.isoformat().replace("+00:00", "Z"), + "locale": "en", + "last_modified": customer.last_modified.isoformat().replace("+00:00", "Z"), + "notes": None + } + + TEST_MEDIUM_CREATE_PAYLOAD = { "type": "barcode", "identifier": "FOOBAR",