diff --git a/doc/api/resources/devices.rst b/doc/api/resources/devices.rst index 9b72fc56b9..8058028ffd 100644 --- a/doc/api/resources/devices.rst +++ b/doc/api/resources/devices.rst @@ -30,6 +30,7 @@ created datetime Creation time initialized datetime Time of initialization (or ``null``) initialization_token string Token for initialization revoked boolean Whether this device no longer has access +security_profile string The name of a supported security profile restricting API access ===================================== ========================== ======================================================= Device endpoints @@ -72,6 +73,7 @@ Device endpoints "name": "Scanner", "created": "2020-09-18T14:17:40.971519Z", "initialized": "2020-09-18T14:17:44.190021Z", + "security_profile": "full", "hardware_brand": "Zebra", "hardware_model": "TC25", "software_brand": "pretixSCAN", @@ -118,6 +120,7 @@ Device endpoints "name": "Scanner", "created": "2020-09-18T14:17:40.971519Z", "initialized": "2020-09-18T14:17:44.190021Z", + "security_profile": "full", "hardware_brand": "Zebra", "hardware_model": "TC25", "software_brand": "pretixSCAN", @@ -166,6 +169,7 @@ Device endpoints "revoked": false, "name": "Scanner", "created": "2020-09-18T14:17:40.971519Z", + "security_profile": "full", "initialized": null "hardware_brand": null, "hardware_model": null, diff --git a/src/pretix/api/auth/device.py b/src/pretix/api/auth/device.py index 9e813dce83..1eda9f8033 100644 --- a/src/pretix/api/auth/device.py +++ b/src/pretix/api/auth/device.py @@ -3,6 +3,9 @@ from django_scopes import scopes_disabled from rest_framework import exceptions from rest_framework.authentication import TokenAuthentication +from pretix.api.auth.devicesecurity import ( + DEVICE_SECURITY_PROFILES, FullAccessSecurityProfile, +) from pretix.base.models import Device @@ -25,3 +28,11 @@ class DeviceTokenAuthentication(TokenAuthentication): raise exceptions.AuthenticationFailed('Device access has been revoked.') return AnonymousUser(), device + + def authenticate(self, request): + r = super().authenticate(request) + if r and isinstance(r[1], Device): + profile = DEVICE_SECURITY_PROFILES.get(r[1].security_profile, FullAccessSecurityProfile) + if not profile.is_allowed(request): + raise exceptions.PermissionDenied('Request denied by device security profile.') + return r diff --git a/src/pretix/api/auth/devicesecurity.py b/src/pretix/api/auth/devicesecurity.py new file mode 100644 index 0000000000..6e03f2b6e9 --- /dev/null +++ b/src/pretix/api/auth/devicesecurity.py @@ -0,0 +1,110 @@ +from django.utils.translation import ugettext_lazy as _ + + +class FullAccessSecurityProfile: + identifier = 'full' + verbose_name = _('Full access') + + def is_allowed(self, request): + return True + + +class WhiteListSecurityProfile: + whitelist = tuple() + + def is_allowed(self, request): + key = (request.method, f"{request.resolver_match.namespace}:{request.resolver_match.url_name}") + return key in self.whitelist + + +class PretixScanSecurityProfile(WhiteListSecurityProfile): + identifier = 'pretixscan' + verbose_name = _('pretixSCAN') + whitelist = ( + ('GET', 'api-v1:version'), + ('GET', 'api-v1:device.update'), + ('GET', 'api-v1:device.revoke'), + ('GET', 'api-v1:event-list'), + ('GET', 'api-v1:event-detail'), + ('GET', 'api-v1:subevent-list'), + ('GET', 'api-v1:subevent-detail'), + ('GET', 'api-v1:itemcategory-list'), + ('GET', 'api-v1:item-list'), + ('GET', 'api-v1:question-list'), + ('GET', 'api-v1:badgelayout-list'), + ('GET', 'api-v1:badgeitem-list'), + ('GET', 'api-v1:checkinlist-list'), + ('GET', 'api-v1:checkinlist-status'), + ('GET', 'api-v1:checkinlistpos-list'), + ('POST', 'api-v1:checkinlistpos-redeem'), + ('GET', 'api-v1:order-list'), + ('GET', 'api-v1:event.settings'), + ) + + +class PretixScanNoSyncSecurityProfile(WhiteListSecurityProfile): + identifier = 'pretixscan_online_kiosk' + verbose_name = _('pretixSCAN (kiosk mode, online only)') + whitelist = ( + ('GET', 'api-v1:version'), + ('GET', 'api-v1:device.update'), + ('GET', 'api-v1:device.revoke'), + ('GET', 'api-v1:event-list'), + ('GET', 'api-v1:event-detail'), + ('GET', 'api-v1:subevent-list'), + ('GET', 'api-v1:subevent-detail'), + ('GET', 'api-v1:itemcategory-list'), + ('GET', 'api-v1:item-list'), + ('GET', 'api-v1:question-list'), + ('GET', 'api-v1:badgelayout-list'), + ('GET', 'api-v1:badgeitem-list'), + ('POST', 'api-v1:checkinlistpos-redeem'), + ('GET', 'api-v1:event.settings'), + ) + + +class PretixPosSecurityProfile(WhiteListSecurityProfile): + identifier = 'pretixpos' + verbose_name = _('pretixPOS') + whitelist = ( + ('GET', 'api-v1:version'), + ('GET', 'api-v1:device.update'), + ('GET', 'api-v1:device.revoke'), + ('GET', 'api-v1:event-list'), + ('GET', 'api-v1:event-detail'), + ('GET', 'api-v1:subevent-list'), + ('GET', 'api-v1:subevent-detail'), + ('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:order-list'), + ('POST', 'api-v1:order-list'), + ('GET', 'api-v1:order-detail'), + ('DELETE', 'api-v1:orderposition-detail'), + ('POST', 'api-v1:order-mark_canceled'), + ('POST', 'api-v1:orderrefund-list'), + ('POST', 'api-v1:orderrefund-done'), + ('POST', 'api-v1:cartposition-list'), + ('DELETE', 'api-v1:cartposition-detail'), + ('GET', 'api-v1:giftcard-list'), + ('POST', 'api-v1:giftcard-transact'), + ('POST', 'plugins:pretix_posbackend:posreceipt-list'), + ('POST', 'plugins:pretix_posbackend:posclosing-list'), + ('POST', 'plugins:pretix_posbackend:posdebugdump-list'), + ('POST', 'plugins:pretix_posbackend:stripeterminal.token'), + ('GET', 'api-v1:event.settings'), + ) + + +DEVICE_SECURITY_PROFILES = { + k.identifier: k() for k in ( + FullAccessSecurityProfile, + PretixScanSecurityProfile, + PretixScanNoSyncSecurityProfile, + PretixPosSecurityProfile, + ) +} diff --git a/src/pretix/api/serializers/organizer.py b/src/pretix/api/serializers/organizer.py index 869b7863ed..60d3d094be 100644 --- a/src/pretix/api/serializers/organizer.py +++ b/src/pretix/api/serializers/organizer.py @@ -102,7 +102,7 @@ class DeviceSerializer(serializers.ModelSerializer): fields = ( 'device_id', 'unique_serial', 'initialization_token', 'all_events', 'limit_events', 'revoked', 'name', 'created', 'initialized', 'hardware_brand', 'hardware_model', - 'software_brand', 'software_version' + 'software_brand', 'software_version', 'security_profile' ) diff --git a/src/pretix/api/urls.py b/src/pretix/api/urls.py index a8a3d8b64f..6c510b4928 100644 --- a/src/pretix/api/urls.py +++ b/src/pretix/api/urls.py @@ -45,7 +45,7 @@ event_router.register(r'checkinlists', checkin.CheckinListViewSet) event_router.register(r'cartpositions', cart.CartPositionViewSet) checkinlist_router = routers.DefaultRouter() -checkinlist_router.register(r'positions', checkin.CheckinListPositionViewSet) +checkinlist_router.register(r'positions', checkin.CheckinListPositionViewSet, basename='checkinlistpos') question_router = routers.DefaultRouter() question_router.register(r'options', item.QuestionOptionViewSet) diff --git a/src/pretix/api/views/device.py b/src/pretix/api/views/device.py index 9f5766a348..59f8468aec 100644 --- a/src/pretix/api/views/device.py +++ b/src/pretix/api/views/device.py @@ -35,7 +35,7 @@ class DeviceSerializer(serializers.ModelSerializer): model = Device fields = [ 'organizer', 'device_id', 'unique_serial', 'api_token', - 'name' + 'name', ] diff --git a/src/pretix/api/views/event.py b/src/pretix/api/views/event.py index 114fdf38c4..e40b8535dd 100644 --- a/src/pretix/api/views/event.py +++ b/src/pretix/api/views/event.py @@ -15,7 +15,7 @@ from pretix.api.serializers.event import ( ) from pretix.api.views import ConditionalListView from pretix.base.models import ( - CartPosition, Device, Event, ItemCategory, TaxRule, TeamAPIToken, + CartPosition, Device, Event, TaxRule, TeamAPIToken, ) from pretix.base.models.event import SubEvent from pretix.helpers.dicts import merge_dicts @@ -229,7 +229,7 @@ with scopes_disabled(): class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet): serializer_class = SubEventSerializer - queryset = ItemCategory.objects.none() + queryset = SubEvent.objects.none() write_permission = 'can_change_event_settings' filter_backends = (DjangoFilterBackend, filters.OrderingFilter) filterset_class = SubEventFilter diff --git a/src/pretix/base/migrations/0163_device_security_profile.py b/src/pretix/base/migrations/0163_device_security_profile.py new file mode 100644 index 0000000000..8ec808113a --- /dev/null +++ b/src/pretix/base/migrations/0163_device_security_profile.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.9 on 2020-10-13 08:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pretixbase', '0162_remove_seat_name'), + ] + + operations = [ + migrations.AddField( + model_name='device', + name='security_profile', + field=models.CharField(default='full', max_length=190, null=True), + ), + ] diff --git a/src/pretix/base/models/devices.py b/src/pretix/base/models/devices.py index aa286dc6e7..5347e98acc 100644 --- a/src/pretix/base/models/devices.py +++ b/src/pretix/base/models/devices.py @@ -6,6 +6,7 @@ from django.utils.crypto import get_random_string from django.utils.translation import gettext_lazy as _ from django_scopes import ScopedManager, scopes_disabled +from pretix.api.auth.devicesecurity import DEVICE_SECURITY_PROFILES from pretix.base.models import LoggedModel @@ -74,6 +75,13 @@ class Device(LoggedModel): max_length=190, null=True, blank=True ) + security_profile = models.CharField( + max_length=190, + choices=[(k, v.verbose_name) for k, v in DEVICE_SECURITY_PROFILES.items()], + default='full', + null=True, + blank=False + ) objects = ScopedManager(organizer='organizer') diff --git a/src/pretix/control/forms/organizer.py b/src/pretix/control/forms/organizer.py index f2176b2562..38bc312b62 100644 --- a/src/pretix/control/forms/organizer.py +++ b/src/pretix/control/forms/organizer.py @@ -193,7 +193,7 @@ class DeviceForm(forms.ModelForm): class Meta: model = Device - fields = ['name', 'all_events', 'limit_events'] + fields = ['name', 'all_events', 'limit_events', 'security_profile'] widgets = { 'limit_events': forms.CheckboxSelectMultiple(attrs={ 'data-inverse-dependency': '#id_all_events', diff --git a/src/pretix/control/templates/pretixcontrol/organizers/device_edit.html b/src/pretix/control/templates/pretixcontrol/organizers/device_edit.html index a7ac4c702f..56075a7d56 100644 --- a/src/pretix/control/templates/pretixcontrol/organizers/device_edit.html +++ b/src/pretix/control/templates/pretixcontrol/organizers/device_edit.html @@ -17,6 +17,7 @@ {% bootstrap_field form.name layout="control" %} {% bootstrap_field form.all_events layout="control" %} {% bootstrap_field form.limit_events layout="control" %} + {% bootstrap_field form.security_profile layout="control" %}