diff --git a/src/pretix/api/auth/device.py b/src/pretix/api/auth/device.py new file mode 100644 index 0000000000..3700383082 --- /dev/null +++ b/src/pretix/api/auth/device.py @@ -0,0 +1,25 @@ +from django.contrib.auth.models import AnonymousUser +from rest_framework import exceptions +from rest_framework.authentication import TokenAuthentication + +from pretix.base.models import Device + + +class DeviceTokenAuthentication(TokenAuthentication): + model = Device + keyword = 'Device' + + def authenticate_credentials(self, key): + model = self.get_model() + try: + device = model.objects.select_related('organizer').get(api_token=key) + except model.DoesNotExist: + raise exceptions.AuthenticationFailed('Invalid token.') + + if not device.initialized: + raise exceptions.AuthenticationFailed('Device has not been initialized.') + + if not device.api_token: + raise exceptions.AuthenticationFailed('Device access has been revoked.') + + return AnonymousUser(), device diff --git a/src/pretix/api/auth/permission.py b/src/pretix/api/auth/permission.py index 0412d2004c..c8bb2529a4 100644 --- a/src/pretix/api/auth/permission.py +++ b/src/pretix/api/auth/permission.py @@ -1,7 +1,7 @@ from rest_framework.permissions import SAFE_METHODS, BasePermission from pretix.api.models import OAuthAccessToken -from pretix.base.models import Event +from pretix.base.models import Device, Event from pretix.base.models.organizer import Organizer, TeamAPIToken from pretix.helpers.security import ( SessionInvalid, SessionReauthRequired, assert_session_valid, @@ -9,10 +9,9 @@ from pretix.helpers.security import ( class EventPermission(BasePermission): - model = TeamAPIToken def has_permission(self, request, view): - if not request.user.is_authenticated and not isinstance(request.auth, TeamAPIToken): + if not request.user.is_authenticated and not isinstance(request.auth, (Device, TeamAPIToken)): return False if request.method not in SAFE_METHODS and hasattr(view, 'write_permission'): @@ -31,7 +30,7 @@ class EventPermission(BasePermission): except SessionReauthRequired: return False - perm_holder = (request.auth if isinstance(request.auth, TeamAPIToken) + perm_holder = (request.auth if isinstance(request.auth, (Device, TeamAPIToken)) else request.user) if 'event' in request.resolver_match.kwargs and 'organizer' in request.resolver_match.kwargs: request.event = Event.objects.filter( diff --git a/src/pretix/api/urls.py b/src/pretix/api/urls.py index a59bf9a68f..0a8fce1e2b 100644 --- a/src/pretix/api/urls.py +++ b/src/pretix/api/urls.py @@ -7,7 +7,8 @@ from rest_framework import routers from pretix.api.views import cart from .views import ( - checkin, event, item, oauth, order, organizer, voucher, waitinglist, + checkin, device, event, item, oauth, order, organizer, voucher, + waitinglist, ) router = routers.DefaultRouter() @@ -66,4 +67,5 @@ urlpatterns = [ url(r"^oauth/authorize$", oauth.AuthorizationView.as_view(), name="authorize"), url(r"^oauth/token$", oauth.TokenView.as_view(), name="token"), url(r"^oauth/revoke_token$", oauth.RevokeTokenView.as_view(), name="revoke-token"), + url(r"^device/initialize", device.InitializeView.as_view(), name="device.initialize"), ] diff --git a/src/pretix/api/views/device.py b/src/pretix/api/views/device.py new file mode 100644 index 0000000000..3a51ae3e70 --- /dev/null +++ b/src/pretix/api/views/device.py @@ -0,0 +1,59 @@ +import logging + +from django.utils.timezone import now +from rest_framework import serializers +from rest_framework.exceptions import ValidationError +from rest_framework.response import Response +from rest_framework.views import APIView + +from pretix.base.models import Device +from pretix.base.models.devices import generate_api_token + +logger = logging.getLogger(__name__) + + +class InitializationRequestSerializer(serializers.Serializer): + token = serializers.CharField(max_length=190) + hardware_brand = serializers.CharField(max_length=190) + hardware_model = serializers.CharField(max_length=190) + software_brand = serializers.CharField(max_length=190) + software_version = serializers.CharField(max_length=190) + + +class DeviceSerializer(serializers.ModelSerializer): + organizer = serializers.SlugRelatedField(slug_field='slug', read_only=True) + + class Meta: + model = Device + fields = [ + 'organizer', 'device_id', 'unique_serial', 'api_token', + 'name' + ] + + +class InitializeView(APIView): + authentication_classes = tuple() + permission_classes = tuple() + + def post(self, request, format=None): + serializer = InitializationRequestSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + + try: + device = Device.objects.get(initialization_token=serializer.validated_data.get('token')) + except Device.DoesNotExist: + raise ValidationError({'token': ['Unknown initialization token.']}) + + if device.initialized: + raise ValidationError({'token': ['This initialization token has already been used.']}) + + device.initialized = now() + device.hardware_brand = serializer.validated_data.get('hardware_brand') + device.hardware_model = serializer.validated_data.get('hardware_model') + device.software_brand = serializer.validated_data.get('software_brand') + device.software_version = serializer.validated_data.get('software_version') + device.api_token = generate_api_token() + device.save() + + serializer = DeviceSerializer(device) + return Response(serializer.data) diff --git a/src/pretix/base/models/devices.py b/src/pretix/base/models/devices.py index f92fdbe91b..7a8b8d4859 100644 --- a/src/pretix/base/models/devices.py +++ b/src/pretix/base/models/devices.py @@ -5,6 +5,8 @@ from django.db.models import Max from django.utils.crypto import get_random_string from django.utils.translation import ugettext_lazy as _ +from pretix.base.models import LoggedModel + def generate_serial(): serial = get_random_string(allowed_chars='ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', length=16) @@ -22,12 +24,12 @@ def generate_initialization_token(): def generate_api_token(): token = get_random_string(length=64, allowed_chars=string.ascii_lowercase + string.digits) - while Device.objects.filter(initialization_token=token).exists(): + while Device.objects.filter(api_token=token).exists(): token = get_random_string(length=64, allowed_chars=string.ascii_lowercase + string.digits) return token -class Device(models.Model): +class Device(LoggedModel): organizer = models.ForeignKey( 'pretixbase.Organizer', on_delete=models.PROTECT, @@ -80,3 +82,75 @@ class Device(models.Model): if not self.device_id: self.device_id = (self.organizer.devices.aggregate(m=Max('device_id'))['m'] or 0) + 1 super().save(*args, **kwargs) + + def permission_set(self) -> set: + return { + 'can_view_orders', + 'can_change_orders', + 'can_view_products' + } + + def get_event_permission_set(self, organizer, event) -> set: + """ + Gets a set of permissions (as strings) that a token holds for a particular event + + :param organizer: The organizer of the event + :param event: The event to check + :return: set of permissions + """ + has_event_access = (self.all_events and organizer == self.organizer) or ( + event in self.limit_events.all() + ) + return self.permission_set() if has_event_access else set() + + def get_organizer_permission_set(self, organizer) -> set: + """ + Gets a set of permissions (as strings) that a token holds for a particular organizer + + :param organizer: The organizer of the event + :return: set of permissions + """ + return self.permission_set() if self.organizer == organizer else set() + + def has_event_permission(self, organizer, event, perm_name=None, request=None) -> bool: + """ + Checks if this token is part of a team that grants access of type ``perm_name`` + to the event ``event``. + + :param organizer: The organizer of the event + :param event: The event to check + :param perm_name: The permission, e.g. ``can_change_teams`` + :param request: This parameter is ignored and only defined for compatibility reasons. + :return: bool + """ + has_event_access = (self.all_events and organizer == self.organizer) or ( + event in self.limit_events.all() + ) + if isinstance(perm_name, (tuple, list)): + return has_event_access and any(p in self.permission_set() for p in perm_name) + return has_event_access and (not perm_name or perm_name in self.permission_set()) + + def has_organizer_permission(self, organizer, perm_name=None, request=None): + """ + Checks if this token is part of a team that grants access of type ``perm_name`` + to the organizer ``organizer``. + + :param organizer: The organizer to check + :param perm_name: The permission, e.g. ``can_change_teams`` + :param request: This parameter is ignored and only defined for compatibility reasons. + :return: bool + """ + if isinstance(perm_name, (tuple, list)): + return organizer == self.organizer and any(p in self.permission_set() for p in perm_name) + return organizer == self.organizer and (not perm_name or perm_name in self.permission_set()) + + def get_events_with_any_permission(self): + """ + Returns a queryset of events the token has any permissions to. + + :return: Iterable of Events + """ + if self.all_events: + return self.organizer.events.all() + else: + return self.limit_events.all() diff --git a/src/pretix/base/models/event.py b/src/pretix/base/models/event.py index 54b0018cfd..3498f13f22 100644 --- a/src/pretix/base/models/event.py +++ b/src/pretix/base/models/event.py @@ -276,6 +276,18 @@ class Event(EventMixin, LoggedModel): else: return super().presale_has_ended + def delete_all_orders(self, really=False): + from .orders import OrderRefund, OrderPayment, OrderPosition, OrderFee + + if not really: + raise TypeError("Pass really=True as a parameter.") + + OrderPosition.objects.all().delete(order__event=self) + OrderFee.objects.all().delete(order__event=self) + OrderPayment.objects.all().delete(order__event=self) + OrderRefund.objects.all().delete(order__event=self) + self.orders.all().delete() + def save(self, *args, **kwargs): obj = super().save(*args, **kwargs) self.cache.clear() diff --git a/src/pretix/control/templates/pretixcontrol/organizers/device_connect.html b/src/pretix/control/templates/pretixcontrol/organizers/device_connect.html index e7c9ef7744..55da884e8d 100644 --- a/src/pretix/control/templates/pretixcontrol/organizers/device_connect.html +++ b/src/pretix/control/templates/pretixcontrol/organizers/device_connect.html @@ -1,7 +1,25 @@ {% extends "pretixcontrol/organizers/base.html" %} {% load i18n %} +{% load staticfiles %} {% load bootstrap3 %} {% block inner %} +