Add device security profiles (#1806)

This commit is contained in:
Raphael Michel
2020-10-13 17:40:25 +02:00
committed by GitHub
parent 301849f771
commit e8f3ad633a
15 changed files with 177 additions and 10 deletions

View File

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

View File

@@ -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,
)
}

View File

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

View File

@@ -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)

View File

@@ -35,7 +35,7 @@ class DeviceSerializer(serializers.ModelSerializer):
model = Device
fields = [
'organizer', 'device_id', 'unique_serial', 'api_token',
'name'
'name',
]

View File

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

View File

@@ -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),
),
]

View File

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

View File

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

View File

@@ -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" %}
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}

View File

@@ -25,9 +25,9 @@
<span class="label label-warning">{% trans "Reserved" %}</span>
{% elif subev.best_availability_state < 20 %}
{% if subev.has_paid_item %}
<span class="fa fa-ticket"></span> {% trans "Sold out" %}
<span class="label label-danger">{% trans "Sold out" %}</span>
{% else %}
<span class="fa fa-ticket"></span> {% trans "Fully booked" %}
<span class="label label-danger">{% trans "Fully booked" %}</span>
{% endif %}
{% endif %}
{% elif subev.presale_is_running %}

View File

@@ -76,3 +76,16 @@ def test_device_auth_revoked(client, device):
resp = client.get('/api/v1/organizers/')
assert resp.status_code == 401
assert str(resp.data['detail']) == "Device access has been revoked."
@pytest.mark.django_db
def test_device_auth_security_profile(client, device):
client.credentials(HTTP_AUTHORIZATION='Device ' + device.api_token)
device.security_profile = "pretixscan"
device.save()
resp = client.get('/api/v1/organizers/dummy/giftcards/')
assert resp.status_code == 403
device.security_profile = "pretixpos"
device.save()
resp = client.get('/api/v1/organizers/dummy/giftcards/')
assert resp.status_code == 200

View File

@@ -38,7 +38,8 @@ TEST_DEV_RES = {
"hardware_brand": "Zebra",
"hardware_model": "TC25",
"software_brand": "pretixSCAN",
"software_version": "1.5.1"
"software_version": "1.5.1",
"security_profile": "full"
}

View File

@@ -50,8 +50,8 @@ def test_create_device(event, admin_user, admin_team, client):
resp = client.post('/control/organizer/dummy/device/add', {
'name': 'Foo',
'limit_events': str(event.pk),
'security_profile': 'full',
}, follow=True)
print(resp.status_code, resp.content)
with scopes_disabled():
d = Device.objects.last()
assert d.name == 'Foo'
@@ -66,6 +66,7 @@ def test_update_device(event, admin_user, admin_team, device, client):
client.post('/control/organizer/dummy/device/{}/edit'.format(device.pk), {
'name': 'Cashdesk 2',
'limit_events': str(event.pk),
'security_profile': 'full',
}, follow=True)
device.refresh_from_db()
assert device.name == 'Cashdesk 2'