Add proper permission checks for crashreports and heartbeats
diff --git a/crashreports/apps.py b/crashreports/apps.py
index e8d6894..d651203 100644
--- a/crashreports/apps.py
+++ b/crashreports/apps.py
@@ -2,5 +2,6 @@
from django.apps import AppConfig
+
class crashreportsConfig(AppConfig):
name = 'crashreports'
diff --git a/crashreports/models.py b/crashreports/models.py
index ebf9c4b..8ac1222 100644
--- a/crashreports/models.py
+++ b/crashreports/models.py
@@ -1,20 +1,26 @@
- # -*- coding: utf-8 -*-
+# -*- coding: utf-8 -*-
from django.db import models
-import datetime
+
from django.contrib.auth.models import User
from taggit.managers import TaggableManager
+
import uuid
+
class Device(models.Model):
# for every device there is a django user
- uuid = models.CharField(max_length=64, unique=True, default=uuid.uuid4, editable=False)
- user = models.OneToOneField(User, related_name='Hiccup_Device', on_delete=models.CASCADE, unique=True)
- imei = models.CharField(max_length=32, null=True, blank=True)
- board_date = models.DateTimeField(null=True, blank= True)
- chipset = models.CharField(max_length=200, null=True, blank= True)
- tags = TaggableManager(blank=True)
+ uuid = models.CharField(max_length=64, unique=True,
+ default=uuid.uuid4, editable=False)
+ user = models.OneToOneField(
+ User, related_name='Hiccup_Device', on_delete=models.CASCADE,
+ unique=True)
+ imei = models.CharField(max_length=32, null=True, blank=True)
+ board_date = models.DateTimeField(null=True, blank=True)
+ chipset = models.CharField(max_length=200, null=True, blank=True)
+ tags = TaggableManager(blank=True)
last_heartbeat = models.DateTimeField(null=True, blank=True)
- token = models.CharField(max_length=200, null=True, blank=True)
+ token = models.CharField(max_length=200, null=True, blank=True)
+
def crashreport_file_name(instance, filename):
return '/'.join([
@@ -24,32 +30,40 @@
str(instance.crashreport.date),
filename])
+
class Crashreport(models.Model):
- device = models.ForeignKey(Device, on_delete=models.CASCADE)
- is_fake_report = models.BooleanField(default=False)
- app_version = models.IntegerField()
- uptime = models.CharField(max_length=200)
+ device = models.ForeignKey(Device, on_delete=models.CASCADE)
+ is_fake_report = models.BooleanField(default=False)
+ app_version = models.IntegerField()
+ uptime = models.CharField(max_length=200)
build_fingerprint = models.CharField(max_length=200)
- boot_reason = models.CharField(max_length=200)
- power_on_reason = models.CharField(max_length=200)
- power_off_reason = models.CharField(max_length=200)
- date = models.DateTimeField()
+ boot_reason = models.CharField(max_length=200)
+ power_on_reason = models.CharField(max_length=200)
+ power_off_reason = models.CharField(max_length=200)
+ date = models.DateTimeField()
tags = TaggableManager(blank=True)
+
def _get_uuid(self):
"Returns the person's full name."
return self.device.uuid
uuid = property(_get_uuid)
-
+
class LogFile(models.Model):
- logfile_type = models.TextField(max_length=36)
- device = models.ForeignKey(Device, on_delete=models.CASCADE)
- crashreport = models.ForeignKey(Crashreport, on_delete=models.CASCADE)
- crashreport_file = models.FileField(upload_to=crashreport_file_name)
+ logfile_type = models.TextField(max_length=36)
+ device = models.ForeignKey(Device, on_delete=models.CASCADE)
+ crashreport = models.ForeignKey(Crashreport, on_delete=models.CASCADE)
+ crashreport_file = models.FileField(upload_to=crashreport_file_name)
-class HeartBeat(models.Model):
+
+class HeartBeat(models.Model):
device = models.ForeignKey(Device, on_delete=models.CASCADE)
app_version = models.IntegerField()
uptime = models.CharField(max_length=200)
build_fingerprint = models.CharField(max_length=200)
date = models.DateTimeField()
+
+ def _get_uuid(self):
+ "Returns the person's full name."
+ return self.device.uuid
+ uuid = property(_get_uuid)
diff --git a/crashreports/permissions.py b/crashreports/permissions.py
new file mode 100644
index 0000000..5f48caa
--- /dev/null
+++ b/crashreports/permissions.py
@@ -0,0 +1,29 @@
+from crashreports.models import Device
+from rest_framework.permissions import BasePermission
+
+
+class HasRightsOrIsDeviceOwnerDeviceCreation(BasePermission):
+
+ def has_permission(self, request, view):
+ # if user has all permissions for crashreport return true
+ if (request.user.has_perm('crashreports.add_crashreport')
+ and request.user.has_perm('crashreports.change_crashreport')
+ and request.user.has_perm('crashreports.del_crashreport')
+ and request.user.has_perm('heartbeat.add_crashreport')
+ and request.user.has_perm('heartbeat.change_crashreport')
+ and request.user.has_perm('heartbeat.del_crashreport')):
+ return True
+ # special case:
+ # user is the owner of a device. in this case creations are allowed.
+ # we have to check if the device with the supplied uuid indeed
+ # belongs to the user
+ if request.method == 'POST':
+ try:
+ device = Device.objects.get(user=request.user)
+ except:
+ return False
+ if ('uuid' not in request.data):
+ return False
+ if (request.data['uuid'] == device.uuid):
+ return True
+ return False
diff --git a/crashreports/rest_api_crashreports.py b/crashreports/rest_api_crashreports.py
index 83b94e4..d23821c 100644
--- a/crashreports/rest_api_crashreports.py
+++ b/crashreports/rest_api_crashreports.py
@@ -1,38 +1,12 @@
-from rest_framework import filters
from rest_framework import generics
-from rest_framework.response import Response
-
-from rest_framework.decorators import api_view
-from rest_framework.decorators import permission_classes
-
-from rest_framework.permissions import AllowAny
-
-from rest_framework.permissions import IsAdminUser
-from rest_framework.permissions import IsAuthenticated
-from rest_framework.permissions import AllowAny
-
-
-from rest_framework.permissions import BasePermission
-
-from rest_framework.authtoken.models import Token
-
from crashreports.models import Crashreport
+from crashreports.permissions import HasRightsOrIsDeviceOwnerDeviceCreation
from crashreports.serializers import CrashReportSerializer
-# class IsCreationAndHasCreationRights(BasePermission):
-# def has_permission(self, request, view):
-# if not request.user.is_authenticated():
-# if view.action == 'create':
-# if request.user.has_permission(""):
-# return True
-# return False
-# else:
-# return True
-#
class ListCreateCrashReport(generics.ListCreateAPIView):
queryset = Crashreport.objects.all()
paginate_by = 20
- permission_classes = (IsAuthenticated, )
+ permission_classes = (HasRightsOrIsDeviceOwnerDeviceCreation, )
serializer_class = CrashReportSerializer
pass
diff --git a/crashreports/rest_api_heartbeats.py b/crashreports/rest_api_heartbeats.py
new file mode 100644
index 0000000..88a79f5
--- /dev/null
+++ b/crashreports/rest_api_heartbeats.py
@@ -0,0 +1,12 @@
+from rest_framework import generics
+from crashreports.models import HeartBeat
+from crashreports.serializers import HeartBeatSerializer
+from crashreports.permissions import HasRightsOrIsDeviceOwnerDeviceCreation
+
+
+class ListCreateHeartBeat(generics.ListCreateAPIView):
+ queryset = HeartBeat.objects.all()
+ paginate_by = 20
+ permission_classes = (HasRightsOrIsDeviceOwnerDeviceCreation, )
+ serializer_class = HeartBeatSerializer
+ pass
diff --git a/crashreports/serializers.py b/crashreports/serializers.py
index 5ccf3b8..1ef8dbd 100644
--- a/crashreports/serializers.py
+++ b/crashreports/serializers.py
@@ -1,16 +1,16 @@
from rest_framework import serializers
-from rest_framework.serializers import PrimaryKeyRelatedField
-from rest_framework.serializers import CharField
-from rest_framework.serializers import ObjectDoesNotExist
+
from rest_framework.exceptions import NotFound
from crashreports.models import Crashreport
from crashreports.models import Device
+from crashreports.models import HeartBeat
from rest_framework import permissions
class CrashReportSerializer(serializers.ModelSerializer):
permission_classes = (permissions.AllowAny,)
uuid = serializers.CharField(max_length=64)
+
class Meta:
model = Crashreport
exclude = ('device',)
@@ -22,12 +22,33 @@
raise NotFound(detail="uuid does not exist")
validated_data.pop('uuid', None)
report = Crashreport(**validated_data)
- report.device=device
+ report.device = device
report.save()
return report
-
+
+
+class HeartBeatSerializer(serializers.ModelSerializer):
+ permission_classes = (permissions.AllowAny,)
+ uuid = serializers.CharField(max_length=64)
+
+ class Meta:
+ model = HeartBeat
+ exclude = ('device',)
+
+ def create(self, validated_data):
+ try:
+ device = Device.objects.get(uuid=validated_data['uuid'])
+ except:
+ raise NotFound(detail="uuid does not exist")
+ validated_data.pop('uuid', None)
+ heartbeat = HeartBeat(**validated_data)
+ heartbeat.device = device
+ heartbeat.save()
+ return heartbeat
+
class DeviceSerializer(serializers.ModelSerializer):
permission_classes = (permissions.IsAdminUser,)
+
class Meta:
model = Device
diff --git a/crashreports/tests.py b/crashreports/tests.py
index 3eb6854..43acf1d 100644
--- a/crashreports/tests.py
+++ b/crashreports/tests.py
@@ -1,8 +1,7 @@
-from django.test import TestCase
+from django.contrib.auth.models import User
from rest_framework.test import APITestCase
from rest_framework.test import APIClient
-from django.contrib.auth.models import User
-from rest_framework.test import force_authenticate
+import datetime
# Create your tests here.
@@ -69,31 +68,16 @@
client = APIClient()
client.login(username='myuser', password='test')
url = "/hiccup/api/v1/devices/{}/".format(self.uuid_to_delete)
- request = client.delete(url, {})
- # self.assertEqual(request.status_code, 204)
- # request = client.delete("/hiccup/api/v1/devices/{}/".format(self.uuid_to_delete),{})
- # self.assertEqual(request.status_code, 404)
+ request = client.delete(
+ url.format(self.uuid_to_delete), {})
+ self.assertEqual(request.status_code, 204)
+ request = client.delete(
+ url.format(self.uuid_to_delete), {})
+ self.assertEqual(request.status_code, 404)
client.logout()
-import datetime
-
-def create_dummy_crash_report(uuid="not set"):
- return {
- 'uuid': uuid,
- 'is_fake_report': 0,
- 'app_version': 2,
- 'uptime': "2 Hours",
- 'build_fingerprint': "models.CharField(max_length=200)",
- 'boot_reason': "models.CharField(max_length=200)",
- 'power_on_reason': "models.CharField(max_length=200)",
- 'power_off_reason': "models.CharField(max_length=200)",
- 'date': str(datetime.datetime(year=2016, month=1, day=1))
- }
-
-from crashreports.models import Crashreport
-
-class CreateCrashreportTestCase(APITestCase):
+class HeartbeatListTestCase(APITestCase):
def setUp(self):
self.password = "test"
@@ -106,37 +90,112 @@
request = self.client.post("/hiccup/api/v1/devices/register/", {})
self.other_uuid = request.data['uuid']
self.other_token = request.data['token']
- self.crashreport = create_dummy_crash_report(self.uuid)
+ self.data = self.create_dummy_data(self.uuid)
+ self.url = "/hiccup/api/v1/heartbeats/"
- def test_create_crashreport_no_auth(self):
+ def create_dummy_data(self, uuid="not set"):
+ return {
+ 'uuid': uuid,
+ 'app_version': 2,
+ 'uptime': "2 Hours",
+ 'build_fingerprint': "models.CharField(max_length=200)",
+ 'date': str(datetime.datetime(year=2016, month=1, day=1))
+ }
+
+ def test_create_no_auth(self):
client = APIClient()
- request = client.post("/hiccup/api/v1/crashreports/", self.crashreport)
+ request = client.post(self.url, self.data)
self.assertEqual(request.status_code, 401)
- def test_create_crashreport_as_admin(self):
+ def test_create_as_admin(self):
client = APIClient()
client.login(username='myuser', password='test')
- request = client.post("/hiccup/api/v1/crashreports/", self.crashreport)
+ request = client.post(self.url, self.data)
self.assertEqual(request.status_code, 201)
client.logout()
- def test_create_crashreport_as_admin_not_existing_device(self):
+ def test_create_as_admin_not_existing_device(self):
client = APIClient()
client.login(username='myuser', password='test')
- request = client.post("/hiccup/api/v1/crashreports/", create_dummy_crash_report())
+ request = client.post(self.url,
+ self.create_dummy_data())
self.assertEqual(request.status_code, 404)
client.logout()
-
- def test_create_crashreport_as_uuid_owner(self):
+
+ def test_create_as_uuid_owner(self):
client = APIClient()
client.credentials(HTTP_AUTHORIZATION='Token ' + self.token)
- request = client.post("/hiccup/api/v1/crashreports/", create_dummy_crash_report(self.uuid))
+ request = client.post(self.url,
+ self.create_dummy_data(self.uuid))
self.assertEqual(request.status_code, 201)
client.credentials()
-
- def test_create_crashreport_as_uuid_not_owner(self):
+
+ def test_create_as_uuid_not_owner(self):
client = APIClient()
client.credentials(HTTP_AUTHORIZATION='Token ' + self.token)
- request = client.post("/hiccup/api/v1/crashreports/", create_dummy_crash_report(self.other_uuid))
- self.assertEqual(request.status_code, 403)
+ request = client.post(self.url,
+ self.create_dummy_data(self.other_uuid))
+ self.assertEqual(request.status_code, 403)
client.credentials()
+
+ def post_heartbeats(self, client, heartbeat, count=5):
+ for i in range(count):
+ client.post(self.url, heartbeat)
+
+ def test_list(self):
+ count = 5
+ client = APIClient()
+ client.login(username='myuser', password='test')
+ self.post_heartbeats(client, self.data, count)
+ request = client.get(self.url)
+ self.assertEqual(request.status_code, 200)
+ self.assertTrue(len(request.data) >= count)
+ client.logout()
+
+ def test_list_noauth(self):
+ count = 5
+ client = APIClient()
+ client.login(username='myuser', password='test')
+ self.post_heartbeats(client, self.data, count)
+ client.logout()
+ request = client.get(self.url)
+ self.assertEqual(request.status_code, 401)
+
+ def test_list_device_owner(self):
+ count = 5
+ client = APIClient()
+ client.credentials(HTTP_AUTHORIZATION='Token ' + self.token)
+ self.post_heartbeats(client, self.data, count)
+ request = client.get(self.url)
+ client.logout()
+ self.assertEqual(request.status_code, 403)
+
+
+class CrashreportListTestCase(HeartbeatListTestCase):
+
+ def setUp(self):
+ self.password = "test"
+ self.admin = User.objects.create_superuser(
+ 'myuser', 'myemail@test.com', self.password)
+ # we need a device
+ request = self.client.post("/hiccup/api/v1/devices/register/", {})
+ self.uuid = request.data['uuid']
+ self.token = request.data['token']
+ request = self.client.post("/hiccup/api/v1/devices/register/", {})
+ self.other_uuid = request.data['uuid']
+ self.other_token = request.data['token']
+ self.data = self.create_dummy_data(self.uuid)
+ self.url = "/hiccup/api/v1/crashreports/"
+
+ def create_dummy_data(self, uuid="not set"):
+ return {
+ 'uuid': uuid,
+ 'is_fake_report': 0,
+ 'app_version': 2,
+ 'uptime': "2 Hours",
+ 'build_fingerprint': "models.CharField(max_length=200)",
+ 'boot_reason': "models.CharField(max_length=200)",
+ 'power_on_reason': "models.CharField(max_length=200)",
+ 'power_off_reason': "models.CharField(max_length=200)",
+ 'date': str(datetime.datetime(year=2016, month=1, day=1))
+ }
diff --git a/crashreports/urls.py b/crashreports/urls.py
index a81f242..2120b9d 100644
--- a/crashreports/urls.py
+++ b/crashreports/urls.py
@@ -2,15 +2,16 @@
from . import views
from . import rest_api_devices
from . import rest_api_crashreports
-
-
+from . import rest_api_heartbeats
urlpatterns = [
-
url(r'^api/v1/crashreports/$', rest_api_crashreports.ListCreateCrashReport.as_view(), name='api_v1_crashreports'),
# url(r'^api/v1/crashreports/(?P<pk>[0-9]+)/$', views.index, name='api_v1_crashreports_single'),
- # url(r'^api/v1/crashreports/(?P<pk>[0-9]+)/logfiles/$', views.index, name='api_v1_crashreports_single_logfiles'),
+ #url(r'^api/v1/crashreports/(?P<pk>[0-9]+)/logfiles/$', views.index, name='api_v1_crashreports_single_logfiles'),
# url(r'^api/v1/crashreports/(?P<pk>[0-9]+)/logfiles/(?P<pk>[0-9]+)/$', views.index, name='api_v1_crashreports_single_logfiles_single'),
+
+ url(r'^api/v1/heartbeats/$', rest_api_heartbeats.ListCreateHeartBeat.as_view(), name='api_v1_heartbeat'),
+
#
# url(r'^api/v1//logfiles/$',views.index, name='api_v1_logfiles'),
# url(r'^api/v1//logfiles/(?P<pk>[0-9]+)/$',views.index, name='api_v1_logfiles_single')
diff --git a/crashreports/views.py b/crashreports/views.py
index a25b071..804bd1b 100644
--- a/crashreports/views.py
+++ b/crashreports/views.py
@@ -1,48 +1,14 @@
# -*- coding: utf-8 -*-
-import datetime
import django_filters
-import os
-import time
-
-from django.conf import settings
-from django.contrib.auth.decorators import login_required
-from django.core.urlresolvers import reverse
-from django.db.models import Count
-from django.http import Http404
-from django.http import HttpResponse
-from django.http import HttpResponseRedirect
-from django.shortcuts import render
-from django.shortcuts import render_to_response
-from django.template import loader
-from django.views.decorators.csrf import csrf_exempt
-from django.views.static import serve
-
-from ratelimit.decorators import ratelimit
-
from rest_framework import filters
-from rest_framework import generics
from rest_framework.permissions import BasePermission
from rest_framework import viewsets
-
-import rest_framework
-
from crashreports.models import Crashreport
-
from crashreports.serializers import CrashReportSerializer
-# @login_required
-# def serve_saved_crashreport (request, path):
-# if settings.DEBUG == False:
-# response = HttpResponse()
-# response["Content-Disposition"] = "attachment; filename={0}".format(
-# os.path.basename(path))
-# response['X-Accel-Redirect'] = "/hiccup/protected/{0}".format(path)
-# return response
-# else:
-# return serve(request, os.path.basename(path), os.path.dirname(settings.BASE_DIR + "/crashreport_uploads/" + path))
-
class IsCreationOrIsAuthenticated(BasePermission):
+
def has_permission(self, request, view):
if not request.user.is_authenticated():
if view.action == 'create':
@@ -53,24 +19,28 @@
return True
-
class ListFilter(django_filters.Filter):
+
def filter(self, qs, value):
value_list = value.split(u',')
- return super(ListFilter, self).filter(qs, django_filters.fields.Lookup(value_list, 'in'))
+ return super(ListFilter, self).filter(
+ qs, django_filters.fields.Lookup(value_list, 'in'))
class CrashreportFilter(filters.FilterSet):
start_date = django_filters.DateTimeFilter(name="date", lookup_expr='gte')
end_date = django_filters.DateTimeFilter(name="date", lookup_expr='lte')
boot_reason = ListFilter(name='boot_reason')
+
class Meta:
model = Crashreport
- fields = ['build_fingerprint','boot_reason', 'power_on_reason', 'power_off_reason']
+ fields = ['build_fingerprint', 'boot_reason',
+ 'power_on_reason', 'power_off_reason']
+
class CrashreportViewSet(viewsets.ModelViewSet):
queryset = Crashreport.objects.all()
- serializer_class = CrashReportSerializer
+ serializer_class = CrashReportSerializer
permission_classes = [IsCreationOrIsAuthenticated]
filter_backends = (filters.DjangoFilterBackend,)
filter_class = CrashreportFilter