Commit 8735e39e by David Ormsbee

Merge pull request #24 from edx/ormsbee/authz

Authorization changes to tighten permissions and use OAuth2.
parents ffb1dfb8 f2862412
...@@ -7,21 +7,21 @@ from rest_framework.test import APITestCase ...@@ -7,21 +7,21 @@ from rest_framework.test import APITestCase
class APIAuthTestCase(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): def setUp(self):
self.username = self.password = 'readwrite' self.username = self.password = 'readwrite'
self.readwrite_user = User.objects.create_user(self.username, password=self.password) 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.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() self._login()
def _logout(self): def _logout(self):
self.client.logout() self.client.logout()
def _login(self, readonly=False): def _login(self, unauthorized=False):
if readonly: if unauthorized:
username = password = 'readonly' username = password = 'unauthorized'
else: else:
username, password = self.username, self.password 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): ...@@ -26,21 +26,21 @@ class VideoDetail(APIAuthTestCase):
def test_anonymous_denied(self): def test_anonymous_denied(self):
""" """
Tests that writing checks model permissions. Tests that reading/writing is not allowed for anonymous users.
""" """
self._logout() self._logout()
url = reverse('video-list') url = reverse('video-list')
response = self.client.post(url, constants.VIDEO_DICT_ANIMAL, format='json') 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) 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): 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._logout()
self._login(readonly=True) self._login(unauthorized=True)
url = reverse('video-list') url = reverse('video-list')
response = self.client.post(url, constants.VIDEO_DICT_ANIMAL, format='json') 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_403_FORBIDDEN)
...@@ -563,15 +563,15 @@ class VideoDetailTest(APIAuthTestCase): ...@@ -563,15 +563,15 @@ class VideoDetailTest(APIAuthTestCase):
self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.status_code, status.HTTP_201_CREATED)
response = self.client.post(url, constants.VIDEO_DICT_ZEBRA, format='json') response = self.client.post(url, constants.VIDEO_DICT_ZEBRA, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.status_code, status.HTTP_201_CREATED)
with self.assertNumQueries(7): with self.assertNumQueries(9):
self.client.get("/edxval/videos/").data self.client.get("/edxval/videos/").data
response = self.client.post(url, constants.COMPLETE_SET_FISH, format='json') response = self.client.post(url, constants.COMPLETE_SET_FISH, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.status_code, status.HTTP_201_CREATED)
with self.assertNumQueries(12): with self.assertNumQueries(14):
self.client.get("/edxval/videos/").data self.client.get("/edxval/videos/").data
response = self.client.post(url, constants.COMPLETE_SET_STAR, format='json') response = self.client.post(url, constants.COMPLETE_SET_STAR, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.status_code, status.HTTP_201_CREATED)
with self.assertNumQueries(15): with self.assertNumQueries(17):
self.client.get("/edxval/videos/").data self.client.get("/edxval/videos/").data
......
""" """
Views file for django app edxval. Views file for django app edxval.
""" """
from rest_framework import generics from rest_framework import generics
from rest_framework.authentication import OAuth2Authentication, SessionAuthentication
from rest_framework.permissions import DjangoModelPermissions from rest_framework.permissions import DjangoModelPermissions
from django.http import HttpResponse from django.http import HttpResponse
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
...@@ -15,6 +15,25 @@ from edxval.serializers import ( ...@@ -15,6 +15,25 @@ from edxval.serializers import (
SubtitleSerializer 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): class MultipleFieldLookupMixin(object):
""" """
Apply this mixin to any view or viewset to get multiple field filtering Apply this mixin to any view or viewset to get multiple field filtering
...@@ -33,14 +52,16 @@ class VideoList(generics.ListCreateAPIView): ...@@ -33,14 +52,16 @@ class VideoList(generics.ListCreateAPIView):
""" """
GETs or POST video objects GETs or POST video objects
""" """
permission_classes = (DjangoModelPermissions,) authentication_classes = (OAuth2Authentication, SessionAuthentication)
permission_classes = (ReadRestrictedDjangoModelPermissions,)
queryset = Video.objects.all().prefetch_related("encoded_videos", "courses") queryset = Video.objects.all().prefetch_related("encoded_videos", "courses")
lookup_field = "edx_video_id" lookup_field = "edx_video_id"
serializer_class = VideoSerializer serializer_class = VideoSerializer
class CourseVideoList(generics.ListAPIView): class CourseVideoList(generics.ListAPIView):
permission_classes = (DjangoModelPermissions,) authentication_classes = (OAuth2Authentication, SessionAuthentication)
permission_classes = (ReadRestrictedDjangoModelPermissions,)
queryset = Video.objects.all().prefetch_related("encoded_videos") queryset = Video.objects.all().prefetch_related("encoded_videos")
lookup_field = "course_id" lookup_field = "course_id"
serializer_class = VideoSerializer serializer_class = VideoSerializer
...@@ -53,7 +74,8 @@ class ProfileList(generics.ListCreateAPIView): ...@@ -53,7 +74,8 @@ class ProfileList(generics.ListCreateAPIView):
""" """
GETs or POST video objects GETs or POST video objects
""" """
permission_classes = (DjangoModelPermissions,) authentication_classes = (OAuth2Authentication, SessionAuthentication)
permission_classes = (ReadRestrictedDjangoModelPermissions,)
queryset = Profile.objects.all() queryset = Profile.objects.all()
lookup_field = "profile_name" lookup_field = "profile_name"
serializer_class = ProfileSerializer serializer_class = ProfileSerializer
...@@ -63,7 +85,8 @@ class VideoDetail(generics.RetrieveUpdateDestroyAPIView): ...@@ -63,7 +85,8 @@ class VideoDetail(generics.RetrieveUpdateDestroyAPIView):
""" """
Gets a video instance given its edx_video_id Gets a video instance given its edx_video_id
""" """
permission_classes = (DjangoModelPermissions,) authentication_classes = (OAuth2Authentication, SessionAuthentication)
permission_classes = (ReadRestrictedDjangoModelPermissions,)
lookup_field = "edx_video_id" lookup_field = "edx_video_id"
queryset = Video.objects.all() queryset = Video.objects.all()
serializer_class = VideoSerializer serializer_class = VideoSerializer
...@@ -73,7 +96,8 @@ class SubtitleDetail(MultipleFieldLookupMixin, generics.RetrieveUpdateDestroyAPI ...@@ -73,7 +96,8 @@ class SubtitleDetail(MultipleFieldLookupMixin, generics.RetrieveUpdateDestroyAPI
""" """
Gets a subtitle instance given its id Gets a subtitle instance given its id
""" """
permission_classes = (DjangoModelPermissions,) authentication_classes = (OAuth2Authentication, SessionAuthentication)
permission_classes = (ReadRestrictedDjangoModelPermissions,)
lookup_fields = ("video__edx_video_id", "language") lookup_fields = ("video__edx_video_id", "language")
queryset = Subtitle.objects.all() queryset = Subtitle.objects.all()
serializer_class = SubtitleSerializer serializer_class = SubtitleSerializer
......
django>=1.4,<1.5 django>=1.4,<1.5
djangorestframework<2.4 djangorestframework<2.4
South==0.7.6 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( ...@@ -9,6 +9,9 @@ urlpatterns = patterns(
# Django Admin # Django Admin
url(r'^admin/', include(admin.site.urls)), 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 # edx-val
url(r'^edxval/', include('edxval.urls')) 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