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
...@@ -39,7 +39,11 @@ from lms.djangoapps.ccx.models import CcxFieldOverride, CustomCourseForEdX ...@@ -39,7 +39,11 @@ from lms.djangoapps.ccx.models import CcxFieldOverride, CustomCourseForEdX
from lms.djangoapps.ccx.overrides import override_field_for_ccx from lms.djangoapps.ccx.overrides import override_field_for_ccx
from lms.djangoapps.ccx.tests.utils import CcxTestCase from lms.djangoapps.ccx.tests.utils import CcxTestCase
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
from student.roles import CourseCcxCoachRole from student.roles import (
CourseInstructorRole,
CourseCcxCoachRole,
CourseStaffRole,
)
from student.tests.factories import AdminFactory from student.tests.factories import AdminFactory
...@@ -62,6 +66,8 @@ class CcxRestApiTest(CcxTestCase, APITestCase): ...@@ -62,6 +66,8 @@ class CcxRestApiTest(CcxTestCase, APITestCase):
# OAUTH2 setup # OAUTH2 setup
# create a specific user for the application # create a specific user for the application
app_user = User.objects.create_user('test_app_user', 'test_app_user@openedx.org', 'test') app_user = User.objects.create_user('test_app_user', 'test_app_user@openedx.org', 'test')
# add staff role to the app user
CourseStaffRole(self.master_course_key).add_users(app_user)
# create an oauth client app entry # create an oauth client app entry
self.app_client = Client.objects.create( self.app_client = Client.objects.create(
user=app_user, user=app_user,
...@@ -135,15 +141,15 @@ class CcxListTest(CcxRestApiTest): ...@@ -135,15 +141,15 @@ class CcxListTest(CcxRestApiTest):
""" """
super(CcxListTest, self).setUp() super(CcxListTest, self).setUp()
self.list_url = reverse('ccx_api:v0:ccx:list') self.list_url = reverse('ccx_api:v0:ccx:list')
self.list_url_master_course = urlparse.urljoin(
self.list_url,
'?master_course_id={0}'.format(urllib.quote_plus(self.master_course_key_str))
)
def test_authorization(self): def test_authorization(self):
""" """
Test that only the right token is authorized Test that only the right token is authorized
""" """
url = urlparse.urljoin(
self.list_url,
'?master_course_id={0}'.format(urllib.quote_plus(self.master_course_key_str))
)
auth_list = [ auth_list = [
"Wrong token-type-obviously", "Wrong token-type-obviously",
"Bearer wrong token format", "Bearer wrong token format",
...@@ -153,15 +159,86 @@ class CcxListTest(CcxRestApiTest): ...@@ -153,15 +159,86 @@ class CcxListTest(CcxRestApiTest):
] ]
# all the auths in the list fail to authorize # all the auths in the list fail to authorize
for auth in auth_list: for auth in auth_list:
resp = self.client.get(url, {}, HTTP_AUTHORIZATION=auth) resp = self.client.get(self.list_url_master_course, {}, HTTP_AUTHORIZATION=auth)
self.assertEqual(resp.status_code, status.HTTP_401_UNAUTHORIZED) self.assertEqual(resp.status_code, status.HTTP_401_UNAUTHORIZED)
resp = self.client.get(url, {}, HTTP_AUTHORIZATION=self.auth) resp = self.client.get(self.list_url_master_course, {}, HTTP_AUTHORIZATION=self.auth)
self.assertEqual(resp.status_code, status.HTTP_200_OK)
def test_authorization_no_oauth_staff(self):
"""
Check authorization for staff users logged in without oauth
"""
# create a staff user
staff_user = User.objects.create_user('test_staff_user', 'test_staff_user@openedx.org', 'test')
# add staff role to the staff user
CourseStaffRole(self.master_course_key).add_users(staff_user)
data = {
'master_course_id': self.master_course_key_str,
'max_students_allowed': 111,
'display_name': 'CCX Test Title',
'coach_email': self.coach.email
}
# the staff user can perform the request
self.client.login(username=staff_user.username, password='test')
resp = self.client.get(self.list_url_master_course)
self.assertEqual(resp.status_code, status.HTTP_200_OK)
resp = self.client.post(self.list_url, data, format='json')
self.assertEqual(resp.status_code, status.HTTP_201_CREATED)
def test_authorization_no_oauth_instructor(self):
"""
Check authorization for instructor users logged in without oauth
"""
# create an instructor user
instructor_user = User.objects.create_user('test_instructor_user', 'test_instructor_user@openedx.org', 'test')
# add instructor role to the instructor user
CourseInstructorRole(self.master_course_key).add_users(instructor_user)
data = {
'master_course_id': self.master_course_key_str,
'max_students_allowed': 111,
'display_name': 'CCX Test Title',
'coach_email': self.coach.email
}
# the instructor user can perform the request
self.client.login(username=instructor_user.username, password='test')
resp = self.client.get(self.list_url_master_course)
self.assertEqual(resp.status_code, status.HTTP_200_OK) self.assertEqual(resp.status_code, status.HTTP_200_OK)
resp = self.client.post(self.list_url, data, format='json')
self.assertEqual(resp.status_code, status.HTTP_201_CREATED)
def test_authorization_no_oauth(self):
"""
Check authorization for coach users logged in without oauth
"""
# create an coach user
coach_user = User.objects.create_user('test_coach_user', 'test_coach_user@openedx.org', 'test')
# add coach role to the coach user
CourseCcxCoachRole(self.master_course_key).add_users(coach_user)
data = {
'master_course_id': self.master_course_key_str,
'max_students_allowed': 111,
'display_name': 'CCX Test Title',
'coach_email': self.coach.email
}
# the coach user cannot perform the request: this type of user can only get her own CCX
self.client.login(username=coach_user.username, password='test')
resp = self.client.get(self.list_url_master_course)
self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
resp = self.client.post(self.list_url, data, format='json')
self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
def test_get_list_wrong_master_course(self): def test_get_list_wrong_master_course(self):
""" """
Test for various get requests with wrong master course string Test for various get requests with wrong master course string
""" """
# mock the permission class these cases can be tested
mock_class_str = 'openedx.core.lib.api.permissions.IsMasterCourseStaffInstructor.has_permission'
with mock.patch(mock_class_str, autospec=True) as mocked_perm_class:
mocked_perm_class.return_value = True
# case with no master_course_id provided # case with no master_course_id provided
resp = self.client.get(self.list_url, {}, HTTP_AUTHORIZATION=self.auth) resp = self.client.get(self.list_url, {}, HTTP_AUTHORIZATION=self.auth)
self.expect_error(status.HTTP_400_BAD_REQUEST, 'master_course_id_not_provided', resp) self.expect_error(status.HTTP_400_BAD_REQUEST, 'master_course_id_not_provided', resp)
...@@ -182,13 +259,8 @@ class CcxListTest(CcxRestApiTest): ...@@ -182,13 +259,8 @@ class CcxListTest(CcxRestApiTest):
""" """
Tests the API to get a list of CCX Courses Tests the API to get a list of CCX Courses
""" """
# get the list of ccx
url = urlparse.urljoin(
self.list_url,
'?master_course_id={0}'.format(urllib.quote_plus(self.master_course_key_str))
)
# there are no CCX courses # there are no CCX courses
resp = self.client.get(url, {}, HTTP_AUTHORIZATION=self.auth) resp = self.client.get(self.list_url_master_course, {}, HTTP_AUTHORIZATION=self.auth)
self.assertIn('count', resp.data) # pylint: disable=no-member self.assertIn('count', resp.data) # pylint: disable=no-member
self.assertEqual(resp.data['count'], 0) # pylint: disable=no-member self.assertEqual(resp.data['count'], 0) # pylint: disable=no-member
...@@ -196,7 +268,7 @@ class CcxListTest(CcxRestApiTest): ...@@ -196,7 +268,7 @@ class CcxListTest(CcxRestApiTest):
num_ccx = 10 num_ccx = 10
for _ in xrange(num_ccx): for _ in xrange(num_ccx):
self.make_ccx() self.make_ccx()
resp = self.client.get(url, {}, HTTP_AUTHORIZATION=self.auth) resp = self.client.get(self.list_url_master_course, {}, HTTP_AUTHORIZATION=self.auth)
self.assertEqual(resp.status_code, status.HTTP_200_OK) self.assertEqual(resp.status_code, status.HTTP_200_OK)
self.assertIn('count', resp.data) # pylint: disable=no-member self.assertIn('count', resp.data) # pylint: disable=no-member
self.assertEqual(resp.data['count'], num_ccx) # pylint: disable=no-member self.assertEqual(resp.data['count'], num_ccx) # pylint: disable=no-member
...@@ -220,13 +292,8 @@ class CcxListTest(CcxRestApiTest): ...@@ -220,13 +292,8 @@ class CcxListTest(CcxRestApiTest):
ccx.display_name = title_str.format(string.ascii_lowercase[-(num + 1)]) ccx.display_name = title_str.format(string.ascii_lowercase[-(num + 1)])
ccx.save() ccx.save()
# get the list of ccx
base_url = urlparse.urljoin(
self.list_url,
'?master_course_id={0}'.format(urllib.quote_plus(self.master_course_key_str))
)
# sort by display name # sort by display name
url = '{0}&order_by=display_name'.format(base_url) url = '{0}&order_by=display_name'.format(self.list_url_master_course)
resp = self.client.get(url, {}, HTTP_AUTHORIZATION=self.auth) resp = self.client.get(url, {}, HTTP_AUTHORIZATION=self.auth)
self.assertEqual(resp.status_code, status.HTTP_200_OK) self.assertEqual(resp.status_code, status.HTTP_200_OK)
self.assertEqual(len(resp.data['results']), num_ccx) # pylint: disable=no-member self.assertEqual(len(resp.data['results']), num_ccx) # pylint: disable=no-member
...@@ -234,7 +301,7 @@ class CcxListTest(CcxRestApiTest): ...@@ -234,7 +301,7 @@ class CcxListTest(CcxRestApiTest):
for num, ccx in enumerate(resp.data['results']): # pylint: disable=no-member for num, ccx in enumerate(resp.data['results']): # pylint: disable=no-member
self.assertEqual(title_str.format(string.ascii_lowercase[-(num_ccx - num)]), ccx['display_name']) self.assertEqual(title_str.format(string.ascii_lowercase[-(num_ccx - num)]), ccx['display_name'])
# add sort order desc # add sort order desc
url = '{0}&order_by=display_name&sort_order=desc'.format(base_url) url = '{0}&order_by=display_name&sort_order=desc'.format(self.list_url_master_course)
resp = self.client.get(url, {}, HTTP_AUTHORIZATION=self.auth) resp = self.client.get(url, {}, HTTP_AUTHORIZATION=self.auth)
# the only thing I can check is that the display name is in alphabetically reversed order # the only thing I can check is that the display name is in alphabetically reversed order
# in the same way when the field has been updated above, so with the id asc # in the same way when the field has been updated above, so with the id asc
...@@ -249,15 +316,10 @@ class CcxListTest(CcxRestApiTest): ...@@ -249,15 +316,10 @@ class CcxListTest(CcxRestApiTest):
num_ccx = 357 num_ccx = 357
for _ in xrange(num_ccx): for _ in xrange(num_ccx):
self.make_ccx() self.make_ccx()
# get the list of ccx
base_url = urlparse.urljoin(
self.list_url,
'?master_course_id={0}'.format(urllib.quote_plus(self.master_course_key_str))
)
page_size = settings.REST_FRAMEWORK.get('PAGE_SIZE', 10) page_size = settings.REST_FRAMEWORK.get('PAGE_SIZE', 10)
num_pages = int(math.ceil(num_ccx / float(page_size))) num_pages = int(math.ceil(num_ccx / float(page_size)))
# get first page # get first page
resp = self.client.get(base_url, {}, HTTP_AUTHORIZATION=self.auth) resp = self.client.get(self.list_url_master_course, {}, HTTP_AUTHORIZATION=self.auth)
self.assertEqual(resp.status_code, status.HTTP_200_OK) self.assertEqual(resp.status_code, status.HTTP_200_OK)
self.assertEqual(resp.data['count'], num_ccx) # pylint: disable=no-member self.assertEqual(resp.data['count'], num_ccx) # pylint: disable=no-member
self.assertEqual(resp.data['num_pages'], num_pages) # pylint: disable=no-member self.assertEqual(resp.data['num_pages'], num_pages) # pylint: disable=no-member
...@@ -266,7 +328,7 @@ class CcxListTest(CcxRestApiTest): ...@@ -266,7 +328,7 @@ class CcxListTest(CcxRestApiTest):
self.assertIsNotNone(resp.data['next']) # pylint: disable=no-member self.assertIsNotNone(resp.data['next']) # pylint: disable=no-member
self.assertIsNone(resp.data['previous']) # pylint: disable=no-member self.assertIsNone(resp.data['previous']) # pylint: disable=no-member
# get a page in the middle # get a page in the middle
url = '{0}&page=24'.format(base_url) url = '{0}&page=24'.format(self.list_url_master_course)
resp = self.client.get(url, {}, HTTP_AUTHORIZATION=self.auth) resp = self.client.get(url, {}, HTTP_AUTHORIZATION=self.auth)
self.assertEqual(resp.status_code, status.HTTP_200_OK) self.assertEqual(resp.status_code, status.HTTP_200_OK)
self.assertEqual(resp.data['count'], num_ccx) # pylint: disable=no-member self.assertEqual(resp.data['count'], num_ccx) # pylint: disable=no-member
...@@ -276,7 +338,7 @@ class CcxListTest(CcxRestApiTest): ...@@ -276,7 +338,7 @@ class CcxListTest(CcxRestApiTest):
self.assertIsNotNone(resp.data['next']) # pylint: disable=no-member self.assertIsNotNone(resp.data['next']) # pylint: disable=no-member
self.assertIsNotNone(resp.data['previous']) # pylint: disable=no-member self.assertIsNotNone(resp.data['previous']) # pylint: disable=no-member
# get last page # get last page
url = '{0}&page={1}'.format(base_url, num_pages) url = '{0}&page={1}'.format(self.list_url_master_course, num_pages)
resp = self.client.get(url, {}, HTTP_AUTHORIZATION=self.auth) resp = self.client.get(url, {}, HTTP_AUTHORIZATION=self.auth)
self.assertEqual(resp.status_code, status.HTTP_200_OK) self.assertEqual(resp.status_code, status.HTTP_200_OK)
self.assertEqual(resp.data['count'], num_ccx) # pylint: disable=no-member self.assertEqual(resp.data['count'], num_ccx) # pylint: disable=no-member
...@@ -286,7 +348,7 @@ class CcxListTest(CcxRestApiTest): ...@@ -286,7 +348,7 @@ class CcxListTest(CcxRestApiTest):
self.assertIsNone(resp.data['next']) # pylint: disable=no-member self.assertIsNone(resp.data['next']) # pylint: disable=no-member
self.assertIsNotNone(resp.data['previous']) # pylint: disable=no-member self.assertIsNotNone(resp.data['previous']) # pylint: disable=no-member
# last page + 1 # last page + 1
url = '{0}&page={1}'.format(base_url, num_pages + 1) url = '{0}&page={1}'.format(self.list_url_master_course, num_pages + 1)
resp = self.client.get(url, {}, HTTP_AUTHORIZATION=self.auth) resp = self.client.get(url, {}, HTTP_AUTHORIZATION=self.auth)
self.assertEqual(resp.status_code, status.HTTP_404_NOT_FOUND) self.assertEqual(resp.status_code, status.HTTP_404_NOT_FOUND)
...@@ -322,6 +384,10 @@ class CcxListTest(CcxRestApiTest): ...@@ -322,6 +384,10 @@ class CcxListTest(CcxRestApiTest):
""" """
Test for various post requests with wrong master course string Test for various post requests with wrong master course string
""" """
# mock the permission class these cases can be tested
mock_class_str = 'openedx.core.lib.api.permissions.IsMasterCourseStaffInstructor.has_permission'
with mock.patch(mock_class_str, autospec=True) as mocked_perm_class:
mocked_perm_class.return_value = True
# case with no master_course_id provided # case with no master_course_id provided
resp = self.client.post(self.list_url, data, format='json', HTTP_AUTHORIZATION=self.auth) resp = self.client.post(self.list_url, data, format='json', HTTP_AUTHORIZATION=self.auth)
self.expect_error(expected_http_error, expected_error_string, resp) self.expect_error(expected_http_error, expected_error_string, resp)
...@@ -536,6 +602,69 @@ class CcxDetailTest(CcxRestApiTest): ...@@ -536,6 +602,69 @@ class CcxDetailTest(CcxRestApiTest):
resp = self.client.get(self.detail_url, {}, HTTP_AUTHORIZATION=self.auth) resp = self.client.get(self.detail_url, {}, HTTP_AUTHORIZATION=self.auth)
self.assertEqual(resp.status_code, status.HTTP_200_OK) self.assertEqual(resp.status_code, status.HTTP_200_OK)
def test_authorization_no_oauth_staff(self):
"""
Check authorization for staff users logged in without oauth
"""
# create a staff user
staff_user = User.objects.create_user('test_staff_user', 'test_staff_user@openedx.org', 'test')
# add staff role to the staff user
CourseStaffRole(self.master_course_key).add_users(staff_user)
data = {'display_name': 'CCX Title'}
# the staff user can perform the request
self.client.login(username=staff_user.username, password='test')
resp = self.client.get(self.detail_url)
self.assertEqual(resp.status_code, status.HTTP_200_OK)
resp = self.client.patch(self.detail_url, data, format='json')
self.assertEqual(resp.status_code, status.HTTP_204_NO_CONTENT)
def test_authorization_no_oauth_instructor(self):
"""
Check authorization for users logged in without oauth
"""
# create an instructor user
instructor_user = User.objects.create_user('test_instructor_user', 'test_instructor_user@openedx.org', 'test')
# add instructor role to the instructor user
CourseInstructorRole(self.master_course_key).add_users(instructor_user)
data = {'display_name': 'CCX Title'}
# the instructor user can perform the request
self.client.login(username=instructor_user.username, password='test')
resp = self.client.get(self.detail_url)
self.assertEqual(resp.status_code, status.HTTP_200_OK)
resp = self.client.patch(self.detail_url, data, format='json')
self.assertEqual(resp.status_code, status.HTTP_204_NO_CONTENT)
def test_authorization_no_oauth_other_coach(self):
"""
Check authorization for other coach users logged in without oauth
"""
# create an coach user
coach_user = User.objects.create_user('test_coach_user', 'test_coach_user@openedx.org', 'test')
# add coach role to the coach user
CourseCcxCoachRole(self.master_course_key).add_users(coach_user)
data = {'display_name': 'CCX Title'}
# the coach user cannot perform the request: this type of user can only get her own CCX
self.client.login(username=coach_user.username, password='test')
resp = self.client.get(self.detail_url)
self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
resp = self.client.patch(self.detail_url, data, format='json')
self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
def test_authorization_no_oauth_ccx_coach(self):
"""
Check authorization for ccx coach users logged in without oauth
"""
data = {'display_name': 'CCX Title'}
# the coach owner of the CCX can perform the request only if it is a get
self.client.login(username=self.coach.username, password='test')
resp = self.client.get(self.detail_url)
self.assertEqual(resp.status_code, status.HTTP_200_OK)
resp = self.client.patch(self.detail_url, data, format='json')
self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
def test_resolve_get_detail(self): def test_resolve_get_detail(self):
""" """
Test for the ccx detail view resolver. This is needed because it is assumed Test for the ccx detail view resolver. This is needed because it is assumed
...@@ -569,16 +698,36 @@ class CcxDetailTest(CcxRestApiTest): ...@@ -569,16 +698,36 @@ class CcxDetailTest(CcxRestApiTest):
""" """
client_request = getattr(self.client, http_method) client_request = getattr(self.client, http_method)
# get a detail url with a master_course id string # get a detail url with a master_course id string
mock_class_str = 'openedx.core.lib.api.permissions.IsCourseStaffInstructor.has_object_permission'
url = reverse('ccx_api:v0:ccx:detail', kwargs={'ccx_course_id': self.master_course_key_str}) url = reverse('ccx_api:v0:ccx:detail', kwargs={'ccx_course_id': self.master_course_key_str})
# the permission class will give a 403 error because will not find the CCX
resp = client_request(url, {}, HTTP_AUTHORIZATION=self.auth)
self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
# bypassing the permission class we get another kind of error
with mock.patch(mock_class_str, autospec=True) as mocked_perm_class:
mocked_perm_class.return_value = True
resp = client_request(url, {}, HTTP_AUTHORIZATION=self.auth) resp = client_request(url, {}, HTTP_AUTHORIZATION=self.auth)
self.expect_error(status.HTTP_400_BAD_REQUEST, 'course_id_not_valid_ccx_id', resp) self.expect_error(status.HTTP_400_BAD_REQUEST, 'course_id_not_valid_ccx_id', resp)
# use an non existing ccx id # use an non existing ccx id
url = reverse('ccx_api:v0:ccx:detail', kwargs={'ccx_course_id': 'ccx-v1:foo.0+course_bar_0+Run_0+ccx@1'}) url = reverse('ccx_api:v0:ccx:detail', kwargs={'ccx_course_id': 'ccx-v1:foo.0+course_bar_0+Run_0+ccx@1'})
# the permission class will give a 403 error because will not find the CCX
resp = client_request(url, {}, HTTP_AUTHORIZATION=self.auth)
self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
# bypassing the permission class we get another kind of error
with mock.patch(mock_class_str, autospec=True) as mocked_perm_class:
mocked_perm_class.return_value = True
resp = client_request(url, {}, HTTP_AUTHORIZATION=self.auth) resp = client_request(url, {}, HTTP_AUTHORIZATION=self.auth)
self.expect_error(status.HTTP_404_NOT_FOUND, 'ccx_course_id_does_not_exist', resp) self.expect_error(status.HTTP_404_NOT_FOUND, 'ccx_course_id_does_not_exist', resp)
# get a valid ccx key and add few 0s to get a non existing ccx for a valid course # get a valid ccx key and add few 0s to get a non existing ccx for a valid course
ccx_key_str = '{0}000000'.format(self.ccx_key_str) ccx_key_str = '{0}000000'.format(self.ccx_key_str)
url = reverse('ccx_api:v0:ccx:detail', kwargs={'ccx_course_id': ccx_key_str}) url = reverse('ccx_api:v0:ccx:detail', kwargs={'ccx_course_id': ccx_key_str})
# the permission class will give a 403 error because will not find the CCX
resp = client_request(url, {}, HTTP_AUTHORIZATION=self.auth)
self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
# bypassing the permission class we get another kind of error
with mock.patch(mock_class_str, autospec=True) as mocked_perm_class:
mocked_perm_class.return_value = True
resp = client_request(url, {}, HTTP_AUTHORIZATION=self.auth) resp = client_request(url, {}, HTTP_AUTHORIZATION=self.auth)
self.expect_error(status.HTTP_404_NOT_FOUND, 'ccx_course_id_does_not_exist', resp) self.expect_error(status.HTTP_404_NOT_FOUND, 'ccx_course_id_does_not_exist', resp)
......
...@@ -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