Commit f2862412 by David Ormsbee

Authorization changes to tighten permissions and use OAuth2.

1. Explicitly added OAuth2 and Session as the authentication types
allowed. Django Rest Framework allows the use of defaults via
settings, but this may have unwanted side-effects since there are
others using DRF with different APIs that have different requirements.

2. Created a ReadRestrictedDjangoModelPermissions class that is a
subclass of DjangoModelPermissions. DjangoModelPermissions only cares
about edit/update/delete permissions and makes everything world
readable by default, which is not desirable for this API.

3. Added the DRF login URLs for running this project locally.

4. Add edX fork of django-oauth2-provider to requirements.
parent ffb1dfb8
......@@ -7,21 +7,21 @@ from rest_framework.test import APITestCase
class APIAuthTestCase(APITestCase):
"""
TestCase that creates a readwrite and readonly user in setUp
TestCase that creates a readwrite and an unauthorized user in setUp
"""
def setUp(self):
self.username = self.password = 'readwrite'
self.readwrite_user = User.objects.create_user(self.username, password=self.password)
self.readwrite_user.user_permissions = Permission.objects.filter(content_type__app_label='edxval')
self.readonly_user = User.objects.create_user('readonly', 'readonly')
self.readonly_user = User.objects.create_user('unauthorized', password='unauthorized')
self._login()
def _logout(self):
self.client.logout()
def _login(self, readonly=False):
if readonly:
username = password = 'readonly'
def _login(self, unauthorized=False):
if unauthorized:
username = password = 'unauthorized'
else:
username, password = self.username, self.password
self.client.login(username=username, password=password)
print self.client.login(username=username, password=password)
......@@ -26,21 +26,21 @@ class VideoDetail(APIAuthTestCase):
def test_anonymous_denied(self):
"""
Tests that writing checks model permissions.
Tests that reading/writing is not allowed for anonymous users.
"""
self._logout()
url = reverse('video-list')
response = self.client.post(url, constants.VIDEO_DICT_ANIMAL, format='json')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
def test_no_perms(self):
"""
Tests that writing checks model permissions, even for logged in users.
Tests that reading/writing checks model permissions for logged in users.
"""
self._logout()
self._login(readonly=True)
self._login(unauthorized=True)
url = reverse('video-list')
response = self.client.post(url, constants.VIDEO_DICT_ANIMAL, format='json')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
......@@ -563,15 +563,15 @@ class VideoDetailTest(APIAuthTestCase):
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
response = self.client.post(url, constants.VIDEO_DICT_ZEBRA, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
with self.assertNumQueries(7):
with self.assertNumQueries(9):
self.client.get("/edxval/videos/").data
response = self.client.post(url, constants.COMPLETE_SET_FISH, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
with self.assertNumQueries(12):
with self.assertNumQueries(14):
self.client.get("/edxval/videos/").data
response = self.client.post(url, constants.COMPLETE_SET_STAR, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
with self.assertNumQueries(15):
with self.assertNumQueries(17):
self.client.get("/edxval/videos/").data
......
"""
Views file for django app edxval.
"""
from rest_framework import generics
from rest_framework.authentication import OAuth2Authentication, SessionAuthentication
from rest_framework.permissions import DjangoModelPermissions
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
......@@ -15,6 +15,25 @@ from edxval.serializers import (
SubtitleSerializer
)
class ReadRestrictedDjangoModelPermissions(DjangoModelPermissions):
"""Extending DjangoModelPermissions to allow us to restrict read access.
Django permissions typically only have add/change/delete. This class assumes
that if you don't have permission to change it, you don't have permission to
see it either. The only users of this REST API for the moment are those
authorized to upload assets from video production.
"""
perms_map = {
'GET': ['%(app_label)s.change_%(model_name)s'],
'OPTIONS': ['%(app_label)s.change_%(model_name)s'],
'HEAD': ['%(app_label)s.change_%(model_name)s'],
'POST': ['%(app_label)s.add_%(model_name)s'],
'PUT': ['%(app_label)s.change_%(model_name)s'],
'PATCH': ['%(app_label)s.change_%(model_name)s'],
'DELETE': ['%(app_label)s.delete_%(model_name)s'],
}
class MultipleFieldLookupMixin(object):
"""
Apply this mixin to any view or viewset to get multiple field filtering
......@@ -33,14 +52,16 @@ class VideoList(generics.ListCreateAPIView):
"""
GETs or POST video objects
"""
permission_classes = (DjangoModelPermissions,)
authentication_classes = (OAuth2Authentication, SessionAuthentication)
permission_classes = (ReadRestrictedDjangoModelPermissions,)
queryset = Video.objects.all().prefetch_related("encoded_videos", "courses")
lookup_field = "edx_video_id"
serializer_class = VideoSerializer
class CourseVideoList(generics.ListAPIView):
permission_classes = (DjangoModelPermissions,)
authentication_classes = (OAuth2Authentication, SessionAuthentication)
permission_classes = (ReadRestrictedDjangoModelPermissions,)
queryset = Video.objects.all().prefetch_related("encoded_videos")
lookup_field = "course_id"
serializer_class = VideoSerializer
......@@ -53,7 +74,8 @@ class ProfileList(generics.ListCreateAPIView):
"""
GETs or POST video objects
"""
permission_classes = (DjangoModelPermissions,)
authentication_classes = (OAuth2Authentication, SessionAuthentication)
permission_classes = (ReadRestrictedDjangoModelPermissions,)
queryset = Profile.objects.all()
lookup_field = "profile_name"
serializer_class = ProfileSerializer
......@@ -63,7 +85,8 @@ class VideoDetail(generics.RetrieveUpdateDestroyAPIView):
"""
Gets a video instance given its edx_video_id
"""
permission_classes = (DjangoModelPermissions,)
authentication_classes = (OAuth2Authentication, SessionAuthentication)
permission_classes = (ReadRestrictedDjangoModelPermissions,)
lookup_field = "edx_video_id"
queryset = Video.objects.all()
serializer_class = VideoSerializer
......@@ -73,7 +96,8 @@ class SubtitleDetail(MultipleFieldLookupMixin, generics.RetrieveUpdateDestroyAPI
"""
Gets a subtitle instance given its id
"""
permission_classes = (DjangoModelPermissions,)
authentication_classes = (OAuth2Authentication, SessionAuthentication)
permission_classes = (ReadRestrictedDjangoModelPermissions,)
lookup_fields = ("video__edx_video_id", "language")
queryset = Subtitle.objects.all()
serializer_class = SubtitleSerializer
......
django>=1.4,<1.5
djangorestframework<2.4
South==0.7.6
-e git+https://github.com/edx/django-oauth2-provider.git@0.2.7-fork-edx-1#egg=django-oauth2-provider
......@@ -9,6 +9,9 @@ urlpatterns = patterns(
# Django Admin
url(r'^admin/', include(admin.site.urls)),
# Allow Django Rest Framework Auth login
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
# edx-val
url(r'^edxval/', include('edxval.urls'))
)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment