diff --git a/exodus/restful_api/permissions.py b/exodus/restful_api/permissions.py index a1bdeaa2..7fd006a7 100644 --- a/exodus/restful_api/permissions.py +++ b/exodus/restful_api/permissions.py @@ -1,6 +1,20 @@ from rest_framework import permissions +class IsAuthenticatedOrOptions(permissions.IsAuthenticated): + """ + Allow unauthenticated OPTIONS requests through. + + CORS preflight requests always omit credentials, so they must be + answered without requiring authentication (see the Fetch spec). + """ + + def has_permission(self, request, view): + if request.method == 'OPTIONS': + return True + return super().has_permission(request, view) + + class IsOwnerOrReadOnly(permissions.BasePermission): def has_object_permission(self, request, view, obj): # Read permissions are allowed to any request, diff --git a/exodus/restful_api/tests.py b/exodus/restful_api/tests.py index 5117a3cc..d6fe7975 100644 --- a/exodus/restful_api/tests.py +++ b/exodus/restful_api/tests.py @@ -27,6 +27,11 @@ def test_returns_unauthorized_when_no_auth(self): response = self.client.get(self.PATH) self.assertEqual(response.status_code, 401) + def test_options_preflight_does_not_require_auth(self): + response = self.client.options(self.PATH) + + self.assertEqual(response.status_code, 200) + def test_returns_empty_json_when_no_applications(self): self._force_authentication() response = self.client.get(self.PATH) diff --git a/exodus/restful_api/views.py b/exodus/restful_api/views.py index fa502841..a1b12497 100644 --- a/exodus/restful_api/views.py +++ b/exodus/restful_api/views.py @@ -11,10 +11,11 @@ from rest_framework.authentication import TokenAuthentication from rest_framework.decorators import api_view, permission_classes, authentication_classes from rest_framework.parsers import JSONParser -from rest_framework.permissions import IsAuthenticated, IsAdminUser +from rest_framework.permissions import IsAdminUser from reports.models import Application, Report, Certificate from trackers.models import Tracker +from restful_api.permissions import IsAuthenticatedOrOptions from restful_api.serializers import ApplicationSerializer, TrackerSerializer, \ ReportInfosSerializer, ReportSerializer, SearchQuerySerializer, \ SearchApplicationSerializer, ApplicationShortSerializer @@ -23,7 +24,7 @@ @csrf_exempt @api_view(['GET']) @authentication_classes((TokenAuthentication,)) -@permission_classes((IsAuthenticated,)) +@permission_classes((IsAuthenticatedOrOptions,)) def get_report_infos(request, r_id): try: report = Report.objects.get(pk=r_id) @@ -52,7 +53,7 @@ def get_report_infos(request, r_id): @csrf_exempt @api_view(['GET']) @authentication_classes((TokenAuthentication,)) -@permission_classes((IsAuthenticated, IsAdminUser)) +@permission_classes((IsAuthenticatedOrOptions, IsAdminUser)) def get_apk(request, r_id): try: report = Report.objects.get(pk=r_id) @@ -123,7 +124,7 @@ def _get_tracker_list(): @csrf_exempt @api_view(['GET']) @authentication_classes((TokenAuthentication,)) -@permission_classes((IsAuthenticated,)) +@permission_classes((IsAuthenticatedOrOptions,)) def get_all_reports(request): report_list = Report.objects.order_by('-creation_date')[:500] applications = _get_reports_list(report_list) @@ -148,7 +149,7 @@ def get_all_trackers(request): @csrf_exempt @api_view(['GET']) @authentication_classes((TokenAuthentication,)) -@permission_classes((IsAuthenticated,)) +@permission_classes((IsAuthenticatedOrOptions,)) def get_all_applications(request): try: if request.GET.get('tracker'): @@ -168,7 +169,7 @@ def get_all_applications(request): @csrf_exempt @api_view(['GET']) @authentication_classes((TokenAuthentication,)) -@permission_classes((IsAuthenticated,)) +@permission_classes((IsAuthenticatedOrOptions,)) def search_strict_handle(request, handle): try: reports = Report.objects.filter(application__handle=handle).order_by('-creation_date') @@ -199,7 +200,7 @@ def search_latest_report(request, handle): @csrf_exempt @api_view(['GET']) @authentication_classes((TokenAuthentication,)) -@permission_classes((IsAuthenticated,)) +@permission_classes((IsAuthenticatedOrOptions,)) def get_report_details(request, r_id): try: report = Report.objects.get(pk=r_id) @@ -258,7 +259,7 @@ def search(request): @csrf_exempt @api_view(['GET']) @authentication_classes((TokenAuthentication,)) -@permission_classes((IsAuthenticated,)) +@permission_classes((IsAuthenticatedOrOptions,)) def search_strict_handle_details(request, handle): try: reports = Report.objects.filter(application__handle=handle) @@ -290,7 +291,7 @@ def search_strict_handle_details(request, handle): @csrf_exempt @api_view(['GET']) @authentication_classes((TokenAuthentication,)) -@permission_classes((IsAuthenticated,)) +@permission_classes((IsAuthenticatedOrOptions,)) def get_trackers_count(request): return JsonResponse({'count': Tracker.objects.count()}) @@ -298,7 +299,7 @@ def get_trackers_count(request): @csrf_exempt @api_view(['GET']) @authentication_classes((TokenAuthentication,)) -@permission_classes((IsAuthenticated,)) +@permission_classes((IsAuthenticatedOrOptions,)) def get_reports_count(request): return JsonResponse({'count': Report.objects.count()}) @@ -306,6 +307,6 @@ def get_reports_count(request): @csrf_exempt @api_view(['GET']) @authentication_classes((TokenAuthentication,)) -@permission_classes((IsAuthenticated,)) +@permission_classes((IsAuthenticatedOrOptions,)) def get_applications_count(request): return JsonResponse({'count': Application.objects.distinct('handle').count()})