Compare commits

...

6 Commits

Author SHA1 Message Date
Lukas Bockstaller
55f3bb3c1d include review 2026-04-15 15:20:51 +02:00
Lukas Bockstaller
5bb6a09549 styling 2026-04-15 12:19:34 +02:00
Lukas Bockstaller
23de795b3f only overwrite final representation not the serializer 2026-04-15 12:16:18 +02:00
Lukas Bockstaller
958e318b4c add permission checks in to_representation 2026-04-13 13:37:57 +02:00
Lukas Bockstaller
1ca90892b3 add failing tests 2026-04-13 13:37:38 +02:00
pajowu
3473fa738d Fix AttributeError in CheckPrivateNetworkMixin (#6076) 2026-04-10 12:47:53 +02:00
6 changed files with 143 additions and 9 deletions

View File

@@ -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 = (

View File

@@ -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',

View File

@@ -251,7 +251,7 @@ def create_connection(address, timeout=socket.getdefaulttimeout(),
for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM):
af, socktype, proto, canonname, sa = res
if not settings.get("MAIL_CUSTOM_SMTP_ALLOW_PRIVATE_NETWORKS", False):
if not getattr(settings, "MAIL_CUSTOM_SMTP_ALLOW_PRIVATE_NETWORKS", False):
ip_addr = ipaddress.ip_address(sa[0])
if ip_addr.is_multicast:
raise socket.error(f"Request to multicast address {sa[0]} blocked")

View File

@@ -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",

View File

@@ -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",

View File

@@ -610,7 +610,7 @@ PRIVATE_IPS_RES = [
@contextmanager
def test_mail_connection(res, should_connect, use_ssl):
def assert_mail_connection(res, should_connect, use_ssl):
with (
mock.patch('socket.socket') as mock_socket,
mock.patch('socket.getaddrinfo', return_value=res),
@@ -638,14 +638,14 @@ def test_mail_connection(res, should_connect, use_ssl):
def test_private_smtp_ip(res, use_ssl, settings):
settings.EMAIL_CUSTOM_SMTP_BACKEND = 'pretix.base.email.CheckPrivateNetworkSmtpBackend'
settings.MAIL_CUSTOM_SMTP_ALLOW_PRIVATE_NETWORKS = False
with test_mail_connection(res=res, should_connect=False, use_ssl=use_ssl), pytest.raises(match="Request to .* blocked"):
with assert_mail_connection(res=res, should_connect=False, use_ssl=use_ssl), pytest.raises(match="Request to .* blocked"):
connection = djmail.get_connection(backend=settings.EMAIL_CUSTOM_SMTP_BACKEND,
host="localhost",
use_ssl=use_ssl)
connection.open()
settings.MAIL_CUSTOM_SMTP_ALLOW_PRIVATE_NETWORKS = True
with test_mail_connection(res=res, should_connect=True, use_ssl=use_ssl):
with assert_mail_connection(res=res, should_connect=True, use_ssl=use_ssl):
connection = djmail.get_connection(backend=settings.EMAIL_CUSTOM_SMTP_BACKEND,
host="localhost",
use_ssl=use_ssl)
@@ -662,7 +662,7 @@ def test_public_smtp_ip(use_ssl, allow_private, settings):
settings.EMAIL_CUSTOM_SMTP_BACKEND = 'pretix.base.email.CheckPrivateNetworkSmtpBackend'
settings.MAIL_CUSTOM_SMTP_ALLOW_PRIVATE_NETWORKS = allow_private
with test_mail_connection(res=[(socket.AF_INET, socket.SOCK_STREAM, 6, '', ('8.8.8.8', 443))], should_connect=True, use_ssl=use_ssl):
with assert_mail_connection(res=[(socket.AF_INET, socket.SOCK_STREAM, 6, '', ('8.8.8.8', 443))], should_connect=True, use_ssl=use_ssl):
connection = djmail.get_connection(backend=settings.EMAIL_CUSTOM_SMTP_BACKEND,
host="localhost",
use_ssl=use_ssl)
@@ -702,7 +702,7 @@ def test_send_mail_private_ip(res, use_ssl, allow_private_networks, env):
m.refresh_from_db()
return m
with test_mail_connection(res=res, should_connect=allow_private_networks, use_ssl=use_ssl):
with assert_mail_connection(res=res, should_connect=allow_private_networks, use_ssl=use_ssl):
m = send_mail()
if allow_private_networks:
assert m.status == OutgoingMail.STATUS_SENT