Compare commits

...

3 Commits

Author SHA1 Message Date
Raphael Michel
5f724fa780 REmove dead class 2024-11-11 15:39:42 +01:00
Raphael Michel
118f61292b Update src/pretix/api/signals.py
Co-authored-by: robbi5 <richt@rami.io>
2024-10-31 13:29:58 +01:00
Raphael Michel
458a22f6a3 Make API security profiles pluggable 2024-10-31 13:26:19 +01:00
7 changed files with 83 additions and 89 deletions

View File

@@ -100,3 +100,7 @@ API
.. automodule:: pretix.base.signals .. automodule:: pretix.base.signals
:no-index: :no-index:
:members: validate_event_settings, api_event_settings_fields :members: validate_event_settings, api_event_settings_fields
.. automodule:: pretix.api.signals
:no-index:
:members: register_device_security_profile

View File

@@ -27,7 +27,7 @@ from rest_framework import exceptions
from rest_framework.authentication import TokenAuthentication from rest_framework.authentication import TokenAuthentication
from pretix.api.auth.devicesecurity import ( from pretix.api.auth.devicesecurity import (
DEVICE_SECURITY_PROFILES, FullAccessSecurityProfile, FullAccessSecurityProfile, get_all_security_profiles,
) )
from pretix.base.models import Device from pretix.base.models import Device
@@ -58,7 +58,8 @@ class DeviceTokenAuthentication(TokenAuthentication):
def authenticate(self, request): def authenticate(self, request):
r = super().authenticate(request) r = super().authenticate(request)
if r and isinstance(r[1], Device): if r and isinstance(r[1], Device):
profile = DEVICE_SECURITY_PROFILES.get(r[1].security_profile, FullAccessSecurityProfile) profiles = get_all_security_profiles()
profile = profiles.get(r[1].security_profile, FullAccessSecurityProfile())
if not profile.is_allowed(request): if not profile.is_allowed(request):
raise exceptions.PermissionDenied('Request denied by device security profile.') raise exceptions.PermissionDenied('Request denied by device security profile.')
return r return r

View File

@@ -20,13 +20,40 @@
# <https://www.gnu.org/licenses/>. # <https://www.gnu.org/licenses/>.
# #
import logging import logging
from collections import OrderedDict
from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from pretix.api.signals import register_device_security_profile
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_ALL_PROFILES = None
class FullAccessSecurityProfile: class BaseSecurityProfile:
@property
def identifier(self) -> str:
"""
Unique identifier for this profile.
"""
raise NotImplementedError()
@property
def verbose_name(self) -> str:
"""
Human-readable name (can be a ``gettext_lazy`` object).
"""
raise NotImplementedError()
def is_allowed(self, request) -> bool:
"""
Return whether a given request should be allowed.
"""
raise NotImplementedError()
class FullAccessSecurityProfile(BaseSecurityProfile):
identifier = 'full' identifier = 'full'
verbose_name = _('Full device access (reading and changing orders and gift cards, reading of products and settings)') verbose_name = _('Full device access (reading and changing orders and gift cards, reading of products and settings)')
@@ -34,7 +61,7 @@ class FullAccessSecurityProfile:
return True return True
class AllowListSecurityProfile: class AllowListSecurityProfile(BaseSecurityProfile):
allowlist = () allowlist = ()
def is_allowed(self, request): def is_allowed(self, request):
@@ -157,88 +184,28 @@ class PretixScanNoSyncSecurityProfile(AllowListSecurityProfile):
) )
class PretixPosSecurityProfile(AllowListSecurityProfile): def get_all_security_profiles():
identifier = 'pretixpos' global _ALL_PROFILES
verbose_name = _('pretixPOS')
allowlist = ( if _ALL_PROFILES:
('GET', 'api-v1:version'), return _ALL_PROFILES
('GET', 'api-v1:device.eventselection'),
('GET', 'api-v1:idempotency.query'), types = OrderedDict()
('GET', 'api-v1:device.info'), for recv, ret in register_device_security_profile.send(None):
('POST', 'api-v1:device.update'), if isinstance(ret, (list, tuple)):
('POST', 'api-v1:device.revoke'), for r in ret:
('POST', 'api-v1:device.roll'), types[r.identifier] = r
('GET', 'api-v1:event-list'), else:
('GET', 'api-v1:event-detail'), types[ret.identifier] = ret
('GET', 'api-v1:subevent-list'), _ALL_PROFILES = types
('GET', 'api-v1:subevent-detail'), return types
('GET', 'api-v1:itemcategory-list'),
('GET', 'api-v1:item-list'),
('GET', 'api-v1:question-list'),
('GET', 'api-v1:quota-list'),
('GET', 'api-v1:taxrule-list'),
('GET', 'api-v1:ticketlayout-list'),
('GET', 'api-v1:ticketlayoutitem-list'),
('GET', 'api-v1:badgelayout-list'),
('GET', 'api-v1:badgeitem-list'),
('GET', 'api-v1:voucher-list'),
('GET', 'api-v1:voucher-detail'),
('GET', 'api-v1:order-list'),
('POST', 'api-v1:order-list'),
('GET', 'api-v1:order-detail'),
('DELETE', 'api-v1:orderposition-detail'),
('PATCH', 'api-v1:orderposition-detail'),
('GET', 'api-v1:orderposition-list'),
('GET', 'api-v1:orderposition-answer'),
('GET', 'api-v1:orderposition-pdf_image'),
('POST', 'api-v1:orderposition-printlog'),
('POST', 'api-v1:order-mark-canceled'),
('POST', 'api-v1:orderpayment-list'),
('POST', 'api-v1:orderrefund-list'),
('POST', 'api-v1:orderrefund-done'),
('POST', 'api-v1:cartposition-list'),
('POST', 'api-v1:cartposition-bulk-create'),
('GET', 'api-v1:checkinlist-list'),
('POST', 'api-v1:checkinlistpos-redeem'),
('POST', 'plugins:pretix_posbackend:order.posprintlog'),
('POST', 'plugins:pretix_posbackend:order.poslock'),
('DELETE', 'plugins:pretix_posbackend:order.poslock'),
('DELETE', 'api-v1:cartposition-detail'),
('GET', 'api-v1:giftcard-list'),
('POST', 'api-v1:giftcard-transact'),
('PATCH', 'api-v1:giftcard-detail'),
('GET', 'plugins:pretix_posbackend:posclosing-list'),
('POST', 'plugins:pretix_posbackend:posreceipt-list'),
('POST', 'plugins:pretix_posbackend:posclosing-list'),
('POST', 'plugins:pretix_posbackend:posdebugdump-list'),
('POST', 'plugins:pretix_posbackend:posdebuglogentry-list'),
('POST', 'plugins:pretix_posbackend:posdebuglogentry-bulk-create'),
('GET', 'plugins:pretix_posbackend:poscashier-list'),
('POST', 'plugins:pretix_posbackend:stripeterminal.token'),
('POST', 'plugins:pretix_posbackend:stripeterminal.paymentintent'),
('PUT', 'plugins:pretix_posbackend:file.upload'),
('GET', 'api-v1:revokedsecrets-list'),
('GET', 'api-v1:blockedsecrets-list'),
('GET', 'api-v1:event.settings'),
('GET', 'plugins:pretix_seating:event.event'),
('GET', 'plugins:pretix_seating:event.event.subevent'),
('GET', 'plugins:pretix_seating:event.plan'),
('GET', 'plugins:pretix_seating:selection.simple'),
('POST', 'api-v1:upload'),
('POST', 'api-v1:checkinrpc.redeem'),
('GET', 'api-v1:checkinrpc.search'),
('POST', 'api-v1:reusablemedium-lookup'),
('GET', 'api-v1:reusablemedium-list'),
('POST', 'api-v1:reusablemedium-list'),
)
DEVICE_SECURITY_PROFILES = { @receiver(register_device_security_profile, dispatch_uid="base_register_default_security_profiles")
k.identifier: k() for k in ( def register_default_webhook_events(sender, **kwargs):
FullAccessSecurityProfile, return (
PretixScanSecurityProfile, FullAccessSecurityProfile(),
PretixScanNoSyncSecurityProfile, PretixScanSecurityProfile(),
PretixScanNoSyncNoSearchSecurityProfile, PretixScanNoSyncSecurityProfile(),
PretixPosSecurityProfile, PretixScanNoSyncNoSearchSecurityProfile(),
) )
}

View File

@@ -29,6 +29,7 @@ from django.utils.translation import gettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
from pretix.api.auth.devicesecurity import get_all_security_profiles
from pretix.api.serializers import AsymmetricField from pretix.api.serializers import AsymmetricField
from pretix.api.serializers.i18n import I18nAwareModelSerializer from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.api.serializers.order import CompatibleJSONField from pretix.api.serializers.order import CompatibleJSONField
@@ -297,6 +298,7 @@ class DeviceSerializer(serializers.ModelSerializer):
revoked = serializers.BooleanField(read_only=True) revoked = serializers.BooleanField(read_only=True)
initialized = serializers.DateTimeField(read_only=True) initialized = serializers.DateTimeField(read_only=True)
initialization_token = serializers.DateTimeField(read_only=True) initialization_token = serializers.DateTimeField(read_only=True)
security_profile = serializers.ChoiceField(choices=[], required=False, default="full")
class Meta: class Meta:
model = Device model = Device
@@ -306,6 +308,10 @@ class DeviceSerializer(serializers.ModelSerializer):
'os_name', 'os_version', 'software_brand', 'software_version', 'security_profile' 'os_name', 'os_version', 'software_brand', 'software_version', 'security_profile'
) )
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['security_profile'].choices = [(k, v.verbose_name) for k, v in get_all_security_profiles().items()]
class TeamInviteSerializer(serializers.ModelSerializer): class TeamInviteSerializer(serializers.ModelSerializer):
class Meta: class Meta:

View File

@@ -32,10 +32,17 @@ from pretix.helpers.periodic import minimum_interval
register_webhook_events = Signal() register_webhook_events = Signal()
""" """
This signal is sent out to get all known webhook events. Receivers should return an This signal is sent out to get all known webhook events. Receivers should return an
instance of a subclass of pretix.api.webhooks.WebhookEvent or a list of such instance of a subclass of ``pretix.api.webhooks.WebhookEvent`` or a list of such
instances. instances.
""" """
register_device_security_profile = Signal()
"""
This signal is sent out to get all known device security_profiles. Receivers should
return an instance of a subclass of ``pretix.api.auth.devicesecurity.BaseSecurityProfile``
or a list of such instances.
"""
@receiver(periodic_task) @receiver(periodic_task)
@scopes_disabled() @scopes_disabled()

View File

@@ -28,7 +28,6 @@ from django.utils.crypto import get_random_string
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django_scopes import ScopedManager, scopes_disabled from django_scopes import ScopedManager, scopes_disabled
from pretix.api.auth.devicesecurity import DEVICE_SECURITY_PROFILES
from pretix.base.models import LoggedModel from pretix.base.models import LoggedModel
@@ -161,7 +160,6 @@ class Device(LoggedModel):
) )
security_profile = models.CharField( security_profile = models.CharField(
max_length=190, max_length=190,
choices=[(k, v.verbose_name) for k, v in DEVICE_SECURITY_PROFILES.items()],
default='full', default='full',
null=True, null=True,
blank=False blank=False

View File

@@ -54,6 +54,7 @@ from i18nfield.strings import LazyI18nString
from phonenumber_field.formfields import PhoneNumberField from phonenumber_field.formfields import PhoneNumberField
from pytz import common_timezones from pytz import common_timezones
from pretix.api.auth.devicesecurity import get_all_security_profiles
from pretix.api.models import WebHook from pretix.api.models import WebHook
from pretix.api.webhooks import get_all_webhook_events from pretix.api.webhooks import get_all_webhook_events
from pretix.base.customersso.oidc import oidc_validate_and_complete_config from pretix.base.customersso.oidc import oidc_validate_and_complete_config
@@ -311,6 +312,11 @@ class DeviceForm(forms.ModelForm):
'-has_subevents', '-date_from' '-has_subevents', '-date_from'
) )
self.fields['gate'].queryset = organizer.gates.all() self.fields['gate'].queryset = organizer.gates.all()
self.fields['security_profile'] = forms.ChoiceField(
label=self.fields['security_profile'].label,
help_text=self.fields['security_profile'].help_text,
choices=[(k, v.verbose_name) for k, v in get_all_security_profiles().items()],
)
def clean(self): def clean(self):
d = super().clean() d = super().clean()
@@ -344,6 +350,11 @@ class DeviceBulkEditForm(forms.ModelForm):
'-has_subevents', '-date_from' '-has_subevents', '-date_from'
) )
self.fields['gate'].queryset = organizer.gates.all() self.fields['gate'].queryset = organizer.gates.all()
self.fields['security_profile'] = forms.ChoiceField(
label=self.fields['security_profile'].label,
help_text=self.fields['security_profile'].help_text,
choices=[(k, v.verbose_name) for k, v in get_all_security_profiles().items()],
)
def clean(self): def clean(self):
d = super().clean() d = super().clean()