diff --git a/src/pretix/api/views/organizer.py b/src/pretix/api/views/organizer.py index b23f03f7b8..9e323386e1 100644 --- a/src/pretix/api/views/organizer.py +++ b/src/pretix/api/views/organizer.py @@ -23,5 +23,7 @@ class OrganizerViewSet(viewsets.ReadOnlyModelViewSet): ) else: return Organizer.objects.filter(pk__in=self.request.user.teams.values_list('organizer', flat=True)) + elif hasattr(self.request.auth, 'organizer_id'): + return Organizer.objects.filter(pk=self.request.auth.organizer_id) else: return Organizer.objects.filter(pk=self.request.auth.team.organizer_id) diff --git a/src/pretix/base/models/devices.py b/src/pretix/base/models/devices.py index 7a8b8d4859..068b484ab8 100644 --- a/src/pretix/base/models/devices.py +++ b/src/pretix/base/models/devices.py @@ -85,9 +85,10 @@ class Device(LoggedModel): def permission_set(self) -> set: return { + 'can_change_items', # TODO: Remove, after read operations are allowed without 'can_view_orders', 'can_change_orders', - 'can_view_products' + 'can_view_vouchers', # TODO: Really required } def get_event_permission_set(self, organizer, event) -> set: diff --git a/src/pretix/base/services/orders.py b/src/pretix/base/services/orders.py index bfa15dc2ee..effa207267 100644 --- a/src/pretix/base/services/orders.py +++ b/src/pretix/base/services/orders.py @@ -1301,10 +1301,11 @@ def perform_order(self, event: str, payment_provider: str, positions: List[str], @app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,)) -def cancel_order(self, order: int, user: int=None, send_mail: bool=True, api_token=None, oauth_application=None): +def cancel_order(self, order: int, user: int=None, send_mail: bool=True, api_token=None, oauth_application=None, + device=None): try: try: - return _cancel_order(order, user, send_mail, api_token, oauth_application) + return _cancel_order(order, user, send_mail, api_token, device, oauth_application) except LockTimeoutException: self.retry() except (MaxRetriesExceededError, LockTimeoutException): diff --git a/src/tests/api/conftest.py b/src/tests/api/conftest.py index 8d043d4e86..50077fb466 100644 --- a/src/tests/api/conftest.py +++ b/src/tests/api/conftest.py @@ -1,10 +1,12 @@ from datetime import datetime import pytest +from django.utils.timezone import now from pytz import UTC from rest_framework.test import APIClient -from pretix.base.models import Event, Organizer, Team, User +from pretix.base.models import Device, Event, Organizer, Team, User +from pretix.base.models.devices import generate_api_token @pytest.fixture @@ -69,6 +71,17 @@ def team(organizer): ) +@pytest.fixture +def device(organizer): + return Device.objects.create( + organizer=organizer, + all_events=True, + name='Foo', + initialized=now(), + api_token=generate_api_token() + ) + + @pytest.fixture def user(): return User.objects.create_user('dummy@dummy.dummy', 'dummy') @@ -96,6 +109,12 @@ def token_client(client, team): return client +@pytest.fixture +def device_client(client, device): + client.credentials(HTTP_AUTHORIZATION='Device ' + device.api_token) + return client + + @pytest.fixture def subevent(event, meta_prop): event.has_subevents = True diff --git a/src/tests/api/test_auth.py b/src/tests/api/test_auth.py index 19e796250b..c3b8f1daf3 100644 --- a/src/tests/api/test_auth.py +++ b/src/tests/api/test_auth.py @@ -52,3 +52,27 @@ def test_token_auth_inactive(client, team): client.credentials(HTTP_AUTHORIZATION='Token ' + t.token) resp = client.get('/api/v1/organizers/') assert resp.status_code == 401 + + +@pytest.mark.django_db +def test_device_invalid(client): + client.credentials(HTTP_AUTHORIZATION='Device ABCDE') + resp = client.get('/api/v1/organizers/') + assert resp.status_code == 401 + + +@pytest.mark.django_db +def test_device_auth_valid(client, device): + client.credentials(HTTP_AUTHORIZATION='Device ' + device.api_token) + resp = client.get('/api/v1/organizers/') + assert resp.status_code == 200 + assert len(resp.data['results']) == 1 + + +@pytest.mark.django_db +def test_device_auth_revoked(client, device): + client.credentials(HTTP_AUTHORIZATION='Device ' + device.api_token) + device.api_token = None + device.save() + resp = client.get('/api/v1/organizers/') + assert resp.status_code == 401 diff --git a/src/tests/api/test_deviceauth.py b/src/tests/api/test_deviceauth.py new file mode 100644 index 0000000000..1f2bba40b7 --- /dev/null +++ b/src/tests/api/test_deviceauth.py @@ -0,0 +1,148 @@ +import pytest + +from pretix.base.models import Device + + +@pytest.fixture +def new_device(organizer): + return Device.objects.create( + name="Foo", + all_events=True, + organizer=organizer + ) + + +@pytest.mark.django_db +def test_initialize_required_fields(client, new_device: Device): + resp = client.post('/api/v1/device/initialize') + assert resp.status_code == 400 + assert resp.data == { + 'token': ['This field is required.'], + 'hardware_brand': ['This field is required.'], + 'hardware_model': ['This field is required.'], + 'software_brand': ['This field is required.'], + 'software_version': ['This field is required.'], + } + + +@pytest.mark.django_db +def test_initialize_unknown_token(client, new_device: Device): + resp = client.post('/api/v1/device/initialize', { + 'token': 'aaa', + 'hardware_brand': 'Samsung', + 'hardware_model': 'Galaxy S', + 'software_brand': 'pretixdroid', + 'software_version': '4.0.0' + }) + assert resp.status_code == 400 + assert resp.data == {'token': ['Unknown initialization token.']} + + +@pytest.mark.django_db +def test_initialize_used_token(client, device: Device): + resp = client.post('/api/v1/device/initialize', { + 'token': device.initialization_token, + 'hardware_brand': 'Samsung', + 'hardware_model': 'Galaxy S', + 'software_brand': 'pretixdroid', + 'software_version': '4.0.0' + }) + assert resp.status_code == 400 + assert resp.data == {'token': ['This initialization token has already been used.']} + + +@pytest.mark.django_db +def test_initialize_valid_token(client, new_device: Device): + resp = client.post('/api/v1/device/initialize', { + 'token': new_device.initialization_token, + 'hardware_brand': 'Samsung', + 'hardware_model': 'Galaxy S', + 'software_brand': 'pretixdroid', + 'software_version': '4.0.0' + }) + assert resp.status_code == 200 + assert resp.data['organizer'] == 'dummy' + assert resp.data['name'] == 'Foo' + assert 'device_id' in resp.data + assert 'unique_serial' in resp.data + assert 'api_token' in resp.data + new_device.refresh_from_db() + assert new_device.api_token + assert new_device.initialized + + +@pytest.mark.django_db +def test_update_required_fields(device_client, device: Device): + resp = device_client.post('/api/v1/device/update') + assert resp.status_code == 400 + assert resp.data == { + 'hardware_brand': ['This field is required.'], + 'hardware_model': ['This field is required.'], + 'software_brand': ['This field is required.'], + 'software_version': ['This field is required.'], + } + + +@pytest.mark.django_db +def test_update_required_auth(client, token_client, device: Device): + resp = client.post('/api/v1/device/update', { + 'hardware_brand': 'Samsung', + 'hardware_model': 'Galaxy S', + 'software_brand': 'pretixdroid', + 'software_version': '5.0.0' + }) + assert resp.status_code == 401 + resp = token_client.post('/api/v1/device/update', { + 'hardware_brand': 'Samsung', + 'hardware_model': 'Galaxy S', + 'software_brand': 'pretixdroid', + 'software_version': '5.0.0' + }) + assert resp.status_code == 401 + + +@pytest.mark.django_db +def test_update_valid_fields(device_client, device: Device): + resp = device_client.post('/api/v1/device/update', { + 'hardware_brand': 'Samsung', + 'hardware_model': 'Galaxy S', + 'software_brand': 'pretixdroid', + 'software_version': '5.0.0' + }) + assert resp.status_code == 200 + device.refresh_from_db() + assert device.software_version == '5.0.0' + + +@pytest.mark.django_db +def test_keyroll_required_auth(client, token_client, device: Device): + resp = client.post('/api/v1/device/roll', {}) + assert resp.status_code == 401 + resp = token_client.post('/api/v1/device/roll', {}) + assert resp.status_code == 401 + + +@pytest.mark.django_db +def test_keyroll_valid(device_client, device: Device): + token = device.api_token + resp = device_client.post('/api/v1/device/roll') + assert resp.status_code == 200 + device.refresh_from_db() + assert device.api_token + assert device.api_token != token + + +@pytest.mark.django_db +def test_revoke_required_auth(client, token_client, device: Device): + resp = client.post('/api/v1/device/revoke', {}) + assert resp.status_code == 401 + resp = token_client.post('/api/v1/device/revoke', {}) + assert resp.status_code == 401 + + +@pytest.mark.django_db +def test_revoke_valid(device_client, device: Device): + resp = device_client.post('/api/v1/device/revoke') + assert resp.status_code == 200 + device.refresh_from_db() + assert not device.api_token diff --git a/src/tests/api/test_permissions.py b/src/tests/api/test_permissions.py index ba6ca2d5cb..3a0125ab2c 100644 --- a/src/tests/api/test_permissions.py +++ b/src/tests/api/test_permissions.py @@ -127,6 +127,13 @@ def test_organizer_not_allowed(token_client, organizer): assert resp.status_code == 403 +@pytest.mark.django_db +def test_organizer_not_allowed_device(device_client, organizer): + o2 = Organizer.objects.create(slug='o2', name='Organizer 2') + resp = device_client.get('/api/v1/organizers/{}/events/'.format(o2.slug)) + assert resp.status_code == 403 + + @pytest.mark.django_db def test_organizer_not_existing(token_client, organizer): resp = token_client.get('/api/v1/organizers/{}/events/'.format('o2')) @@ -142,6 +149,13 @@ def test_event_allowed_all_events(token_client, team, organizer, event, url): assert resp.status_code == 200 +@pytest.mark.django_db +@pytest.mark.parametrize("url", event_urls) +def test_event_allowed_all_events_device(device_client, device, organizer, event, url): + resp = device_client.get('/api/v1/organizers/{}/events/{}/{}'.format(organizer.slug, event.slug, url)) + assert resp.status_code == 200 + + @pytest.mark.django_db @pytest.mark.parametrize("url", event_urls) def test_event_allowed_limit_events(token_client, organizer, team, event, url): @@ -152,6 +166,16 @@ def test_event_allowed_limit_events(token_client, organizer, team, event, url): assert resp.status_code == 200 +@pytest.mark.django_db +@pytest.mark.parametrize("url", event_urls) +def test_event_allowed_limit_events_device(device_client, organizer, device, event, url): + device.all_events = False + device.save() + device.limit_events.add(event) + resp = device_client.get('/api/v1/organizers/{}/events/{}/{}'.format(organizer.slug, event.slug, url)) + assert resp.status_code == 200 + + @pytest.mark.django_db @pytest.mark.parametrize("url", event_urls) def test_event_not_allowed(token_client, organizer, team, event, url): @@ -161,6 +185,15 @@ def test_event_not_allowed(token_client, organizer, team, event, url): assert resp.status_code == 403 +@pytest.mark.django_db +@pytest.mark.parametrize("url", event_urls) +def test_event_not_allowed_device(device_client, organizer, device, event, url): + device.all_events = False + device.save() + resp = device_client.get('/api/v1/organizers/{}/events/{}/{}'.format(organizer.slug, event.slug, url)) + assert resp.status_code == 403 + + @pytest.mark.django_db @pytest.mark.parametrize("url", event_urls) def test_event_not_existing(token_client, organizer, url, event):