Commit 7683eadc by Giovanni Di Milia

Modified permission classes for CCX REST APIs

Modified how the per object permissions are enforced in the CCX REST APIs
parent d114be73
...@@ -23,7 +23,7 @@ from instructor.enrollment import ( ...@@ -23,7 +23,7 @@ from instructor.enrollment import (
from opaque_keys import InvalidKeyError from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.lib.api.permissions import IsCourseInstructor from openedx.core.lib.api import permissions
from student.models import CourseEnrollment from student.models import CourseEnrollment
from student.roles import CourseCcxCoachRole from student.roles import CourseCcxCoachRole
...@@ -301,7 +301,7 @@ class CCXListView(GenericAPIView): ...@@ -301,7 +301,7 @@ class CCXListView(GenericAPIView):
} }
""" """
authentication_classes = (OAuth2Authentication, SessionAuthentication,) authentication_classes = (OAuth2Authentication, SessionAuthentication,)
permission_classes = (IsAuthenticated, IsCourseInstructor) permission_classes = (IsAuthenticated, permissions.IsMasterCourseStaffInstructor)
serializer_class = CCXCourseSerializer serializer_class = CCXCourseSerializer
pagination_class = CCXAPIPagination pagination_class = CCXAPIPagination
...@@ -510,9 +510,17 @@ class CCXDetailView(GenericAPIView): ...@@ -510,9 +510,17 @@ class CCXDetailView(GenericAPIView):
""" """
authentication_classes = (OAuth2Authentication, SessionAuthentication,) authentication_classes = (OAuth2Authentication, SessionAuthentication,)
permission_classes = (IsAuthenticated, IsCourseInstructor) permission_classes = (IsAuthenticated, permissions.IsCourseStaffInstructor)
serializer_class = CCXCourseSerializer serializer_class = CCXCourseSerializer
def get_object(self, course_id, is_ccx=False): # pylint: disable=arguments-differ
"""
Override the default get_object to allow a custom getter for the CCX
"""
course_object, course_key, error_code, http_status = get_valid_course(course_id, is_ccx)
self.check_object_permissions(self.request, course_object)
return course_object, course_key, error_code, http_status
def get(self, request, ccx_course_id=None): def get(self, request, ccx_course_id=None):
""" """
Gets a CCX Course information. Gets a CCX Course information.
...@@ -524,7 +532,7 @@ class CCXDetailView(GenericAPIView): ...@@ -524,7 +532,7 @@ class CCXDetailView(GenericAPIView):
Return: Return:
A JSON serialized representation of the CCX course. A JSON serialized representation of the CCX course.
""" """
ccx_course_object, _, error_code, http_status = get_valid_course(ccx_course_id, is_ccx=True) ccx_course_object, _, error_code, http_status = self.get_object(ccx_course_id, is_ccx=True)
if ccx_course_object is None: if ccx_course_object is None:
return Response( return Response(
status=http_status, status=http_status,
...@@ -543,7 +551,7 @@ class CCXDetailView(GenericAPIView): ...@@ -543,7 +551,7 @@ class CCXDetailView(GenericAPIView):
request (Request): Django request object. request (Request): Django request object.
ccx_course_id (string): URI element specifying the CCX course location. ccx_course_id (string): URI element specifying the CCX course location.
""" """
ccx_course_object, ccx_course_key, error_code, http_status = get_valid_course(ccx_course_id, is_ccx=True) ccx_course_object, ccx_course_key, error_code, http_status = self.get_object(ccx_course_id, is_ccx=True)
if ccx_course_object is None: if ccx_course_object is None:
return Response( return Response(
status=http_status, status=http_status,
...@@ -571,7 +579,7 @@ class CCXDetailView(GenericAPIView): ...@@ -571,7 +579,7 @@ class CCXDetailView(GenericAPIView):
request (Request): Django request object. request (Request): Django request object.
ccx_course_id (string): URI element specifying the CCX course location. ccx_course_id (string): URI element specifying the CCX course location.
""" """
ccx_course_object, ccx_course_key, error_code, http_status = get_valid_course(ccx_course_id, is_ccx=True) ccx_course_object, ccx_course_key, error_code, http_status = self.get_object(ccx_course_id, is_ccx=True)
if ccx_course_object is None: if ccx_course_object is None:
return Response( return Response(
status=http_status, status=http_status,
......
...@@ -6,6 +6,8 @@ from django.conf import settings ...@@ -6,6 +6,8 @@ from django.conf import settings
from django.http import Http404 from django.http import Http404
from rest_framework import permissions from rest_framework import permissions
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
from student.roles import CourseStaffRole, CourseInstructorRole from student.roles import CourseStaffRole, CourseInstructorRole
...@@ -64,13 +66,49 @@ class IsUserInUrl(permissions.BasePermission): ...@@ -64,13 +66,49 @@ class IsUserInUrl(permissions.BasePermission):
return True return True
class IsCourseInstructor(permissions.BasePermission): class IsCourseStaffInstructor(permissions.BasePermission):
""" """
Permission to check that user is a course instructor. Permission to check that user is a course instructor or staff of
a master course given a course object or the user is a coach of
the course itself.
""" """
def has_object_permission(self, request, view, obj): def has_object_permission(self, request, view, obj):
return hasattr(request, 'user') and CourseInstructorRole(obj.course_id).has_user(request.user) return (hasattr(request, 'user') and
# either the user is a staff or instructor of the master course
(hasattr(obj, 'course_id') and
(CourseInstructorRole(obj.course_id).has_user(request.user) or
CourseStaffRole(obj.course_id).has_user(request.user))) or
# or it is a safe method and the user is a coach on the course object
(request.method in permissions.SAFE_METHODS
and hasattr(obj, 'coach') and obj.coach == request.user))
class IsMasterCourseStaffInstructor(permissions.BasePermission):
"""
Permission to check that user is instructor or staff of the master course.
"""
def has_permission(self, request, view):
"""
This method is assuming that a `master_course_id` parameter
is available in the request as a GET parameter, a POST parameter
or it is in the JSON payload included in the request.
The reason is because this permission class is going
to check if the user making the request is an instructor
for the specified course.
"""
master_course_id = (request.GET.get('master_course_id')
or request.POST.get('master_course_id')
or request.data.get('master_course_id'))
if master_course_id is not None:
try:
course_key = CourseKey.from_string(master_course_id)
except InvalidKeyError:
raise Http404()
return (hasattr(request, 'user') and
(CourseInstructorRole(course_key).has_user(request.user) or
CourseStaffRole(course_key).has_user(request.user)))
return False
class IsUserInUrlOrStaff(IsUserInUrl): class IsUserInUrlOrStaff(IsUserInUrl):
......
""" Tests for API permissions classes. """ """ Tests for API permissions classes. """
import ddt import ddt
from django.contrib.auth.models import AnonymousUser
from django.http import Http404
from django.test import TestCase, RequestFactory from django.test import TestCase, RequestFactory
from student.roles import CourseStaffRole, CourseInstructorRole from student.roles import CourseStaffRole, CourseInstructorRole
from openedx.core.lib.api.permissions import IsStaffOrOwner, IsCourseInstructor from openedx.core.lib.api.permissions import (
IsStaffOrOwner,
IsCourseStaffInstructor,
IsMasterCourseStaffInstructor,
)
from student.tests.factories import UserFactory from student.tests.factories import UserFactory
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
...@@ -16,35 +22,88 @@ class TestObject(object): ...@@ -16,35 +22,88 @@ class TestObject(object):
self.course_id = course_id self.course_id = course_id
class IsCourseInstructorTests(TestCase): class TestCcxObject(TestObject):
""" Test for IsCourseInstructor permission class. """ """ Fake class for object permission for CCX Courses """
def __init__(self, user=None, course_id=None):
super(TestCcxObject, self).__init__(user, course_id)
self.coach = user
class IsCourseStaffInstructorTests(TestCase):
""" Test for IsCourseStaffInstructor permission class. """
def setUp(self): def setUp(self):
super(IsCourseInstructorTests, self).setUp() super(IsCourseStaffInstructorTests, self).setUp()
self.permission = IsCourseInstructor() self.permission = IsCourseStaffInstructor()
self.coach = UserFactory.create()
self.user = UserFactory.create()
self.request = RequestFactory().get('/') self.request = RequestFactory().get('/')
self.request.user = self.user
self.course_key = CourseKey.from_string('edx/test123/run') self.course_key = CourseKey.from_string('edx/test123/run')
self.obj = TestObject(course_id=self.course_key) self.obj = TestCcxObject(user=self.coach, course_id=self.course_key)
def test_course_staff_has_no_access(self): def test_course_staff_has_access(self):
user = UserFactory.create() CourseStaffRole(course_key=self.course_key).add_users(self.user)
self.request.user = user self.assertTrue(self.permission.has_object_permission(self.request, None, self.obj))
CourseStaffRole(course_key=self.course_key).add_users(user)
def test_course_instructor_has_access(self):
CourseInstructorRole(course_key=self.course_key).add_users(self.user)
self.assertTrue(self.permission.has_object_permission(self.request, None, self.obj))
def test_course_coach_has_access(self):
self.request.user = self.coach
self.assertTrue(self.permission.has_object_permission(self.request, None, self.obj))
self.assertFalse( def test_any_user_has_no_access(self):
self.permission.has_object_permission(self.request, None, self.obj)) self.assertFalse(self.permission.has_object_permission(self.request, None, self.obj))
def test_anonymous_has_no_access(self):
self.request.user = AnonymousUser()
self.assertFalse(self.permission.has_object_permission(self.request, None, self.obj))
class IsMasterCourseStaffInstructorTests(TestCase):
""" Test for IsMasterCourseStaffInstructorTests permission class. """
def setUp(self):
super(IsMasterCourseStaffInstructorTests, self).setUp()
self.permission = IsMasterCourseStaffInstructor()
master_course_id = 'edx/test123/run'
self.user = UserFactory.create()
self.get_request = RequestFactory().get('/?master_course_id={}'.format(master_course_id))
self.get_request.user = self.user
self.post_request = RequestFactory().post('/', data={'master_course_id': master_course_id})
self.post_request.user = self.user
self.course_key = CourseKey.from_string(master_course_id)
def test_course_staff_has_access(self):
CourseStaffRole(course_key=self.course_key).add_users(self.user)
self.assertTrue(self.permission.has_permission(self.get_request, None))
self.assertTrue(self.permission.has_permission(self.post_request, None))
def test_course_instructor_has_access(self): def test_course_instructor_has_access(self):
user = UserFactory.create() CourseInstructorRole(course_key=self.course_key).add_users(self.user)
self.request.user = user self.assertTrue(self.permission.has_permission(self.get_request, None))
CourseInstructorRole(course_key=self.course_key).add_users(user) self.assertTrue(self.permission.has_permission(self.post_request, None))
self.assertTrue( def test_any_user_has_partial_access(self):
self.permission.has_object_permission(self.request, None, self.obj)) self.assertFalse(self.permission.has_permission(self.get_request, None))
self.assertFalse(self.permission.has_permission(self.post_request, None))
def test_anonymous_has_no_access(self): def test_anonymous_has_no_access(self):
self.assertFalse( user = AnonymousUser()
self.permission.has_object_permission(self.request, None, self.obj)) self.get_request.user = user
self.post_request.user = user
self.assertFalse(self.permission.has_permission(self.get_request, None))
self.assertFalse(self.permission.has_permission(self.post_request, None))
def test_wrong_course_id_raises(self):
get_request = RequestFactory().get('/?master_course_id=this_is_invalid')
with self.assertRaises(Http404):
self.permission.has_permission(get_request, None)
post_request = RequestFactory().post('/', data={'master_course_id': 'this_is_invalid'})
with self.assertRaises(Http404):
self.permission.has_permission(post_request, None)
@ddt.ddt @ddt.ddt
......
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