diff --git a/lms/urls.py b/lms/urls.py index 88c7581e2f..bb433b0c1d 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -117,6 +117,9 @@ urlpatterns = [ # Enrollment API RESTful endpoints url(r'^api/enrollment/v1/', include('openedx.core.djangoapps.enrollments.urls')), + # Agreements API RESTful endpoints + url(r'^api/agreements/v1/', include('openedx.core.djangoapps.agreements.urls')), + # Entitlement API RESTful endpoints url( r'^api/entitlements/', diff --git a/openedx/core/djangoapps/agreements/tests/test_views.py b/openedx/core/djangoapps/agreements/tests/test_views.py new file mode 100644 index 0000000000..86507a8478 --- /dev/null +++ b/openedx/core/djangoapps/agreements/tests/test_views.py @@ -0,0 +1,163 @@ +""" +Tests for agreements views +""" +from rest_framework.test import APITestCase +from rest_framework import status +from django.urls import reverse +from edx_toggles.toggles.testutils import override_waffle_flag + +from common.djangoapps.student.tests.factories import UserFactory, AdminFactory +from common.djangoapps.student.roles import CourseStaffRole +from openedx.core.djangolib.testing.utils import skip_unless_lms +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase +from xmodule.modulestore.tests.factories import CourseFactory + +from ..api import create_integrity_signature +from ..toggles import ENABLE_INTEGRITY_SIGNATURE + + +@skip_unless_lms +@override_waffle_flag(ENABLE_INTEGRITY_SIGNATURE, active=True) +class IntegritySignatureViewTests(APITestCase, ModuleStoreTestCase): + """ + Tests for the Integrity Signature View + """ + USERNAME = "Bob" + PASSWORD = "edx" + + OTHER_USERNAME = "Jane" + + STAFF_USERNAME = "Alice" + + def setUp(self): + super().setUp() + + self.course = CourseFactory.create() + + self.user = UserFactory.create( + username=self.USERNAME, + password=self.PASSWORD, + ) + self.other_user = UserFactory.create( + username=self.OTHER_USERNAME, + password=self.PASSWORD, + ) + + self.instructor = AdminFactory.create( + username=self.STAFF_USERNAME, + password=self.PASSWORD, + ) + self.client.login(username=self.USERNAME, password=self.PASSWORD) + self.course_id = str(self.course.id) + + def _create_signature(self, username, course_id): + """ + Create integrity signature for a given username and course id + """ + create_integrity_signature(username, course_id) + + def _assert_response(self, response, expected_response, user=None, course_id=None): + """ + Assert response is correct for the given information + """ + assert response.status_code == expected_response + if user and course_id: + data = response.data + assert data['username'] == user.username + assert data['course_id'] == course_id + + def test_200_get_for_user_request(self): + self._create_signature(self.user.username, self.course_id) + response = self.client.get( + reverse( + 'integrity_signature', + kwargs={'course_id': self.course_id}, + ) + ) + self._assert_response(response, status.HTTP_200_OK, self.user, self.course_id) + + def test_404_get_if_no_signature(self): + response = self.client.get( + reverse( + 'integrity_signature', + kwargs={'course_id': self.course_id}, + ) + ) + self._assert_response(response, status.HTTP_404_NOT_FOUND) + + def test_403_get_if_non_staff(self): + self._create_signature(self.other_user.username, self.course_id) + response = self.client.get( + reverse( + 'integrity_signature', + kwargs={'course_id': self.course_id}, + ) + + '?username={}'.format(self.other_user.username) + ) + self._assert_response(response, status.HTTP_403_FORBIDDEN) + + def test_200_get_for_course_staff_request(self): + self._create_signature(self.user.username, self.course_id) + + self.instructor.is_staff = False + self.instructor.save() + + CourseStaffRole(self.course.id).add_users(self.instructor) + self.client.login(username=self.STAFF_USERNAME, password=self.PASSWORD) + + response = self.client.get( + reverse( + 'integrity_signature', + kwargs={'course_id': self.course_id}, + ) + + '?username={}'.format(self.user.username) + ) + self._assert_response(response, status.HTTP_200_OK, self.user, self.course_id) + + def test_403_get_for_other_course_instructor(self): + self._create_signature(self.user.username, self.course_id) + + self.instructor.is_staff = False + self.instructor.save() + + # create another course and add instructor to that course + second_course = CourseFactory.create() + CourseStaffRole(second_course.id).add_users(self.instructor) + self.client.login(username=self.STAFF_USERNAME, password=self.PASSWORD) + + response = self.client.get( + reverse( + 'integrity_signature', + kwargs={'course_id': self.course_id}, + ) + + '?username={}'.format(self.user.username) + ) + self._assert_response(response, status.HTTP_403_FORBIDDEN) + + def test_200_get_for_admin(self): + self._create_signature(self.user.username, self.course_id) + + self.instructor.is_staff = True + self.instructor.save() + + self.client.login(username=self.STAFF_USERNAME, password=self.PASSWORD) + + response = self.client.get( + reverse( + 'integrity_signature', + kwargs={'course_id': self.course_id}, + ) + + '?username={}'.format(self.user.username) + ) + self._assert_response(response, status.HTTP_200_OK, self.user, self.course_id) + + @override_waffle_flag(ENABLE_INTEGRITY_SIGNATURE, active=False) + def test_404_for_no_waffle_flag(self): + self._create_signature(self.user.username, self.course_id) + response = self.client.get( + reverse( + 'integrity_signature', + kwargs={'course_id': self.course_id}, + ) + ) + self._assert_response(response, status.HTTP_404_NOT_FOUND) diff --git a/openedx/core/djangoapps/agreements/urls.py b/openedx/core/djangoapps/agreements/urls.py new file mode 100644 index 0000000000..75f649b97c --- /dev/null +++ b/openedx/core/djangoapps/agreements/urls.py @@ -0,0 +1,14 @@ +""" +URLs for the Agreements API +""" + +from django.conf import settings +from django.conf.urls import url + +from .views import IntegritySignatureView + +urlpatterns = [ + url(r'^integrity_signature/{course_id}$'.format( + course_id=settings.COURSE_ID_PATTERN + ), IntegritySignatureView.as_view(), name='integrity_signature'), +] diff --git a/openedx/core/djangoapps/agreements/views.py b/openedx/core/djangoapps/agreements/views.py new file mode 100644 index 0000000000..afb5057883 --- /dev/null +++ b/openedx/core/djangoapps/agreements/views.py @@ -0,0 +1,96 @@ +"""Views served by the agreements app. """ + +from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication +from rest_framework import status +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework.authentication import SessionAuthentication +from rest_framework.permissions import IsAuthenticated +from opaque_keys.edx.keys import CourseKey + +from common.djangoapps.student import auth +from common.djangoapps.student.roles import CourseStaffRole + +from .api import get_integrity_signature +from .toggles import is_integrity_signature_enabled + + +def is_user_course_or_global_staff(user, course_id): + """ + Return whether a user is course staff for a given course, described by the course_id, + or is global staff. + """ + + return user.is_staff or auth.user_has_role(user, CourseStaffRole(CourseKey.from_string(course_id))) + + +class AuthenticatedAPIView(APIView): + """ + Authenticated API View. + """ + authentication_classes = (SessionAuthentication, JwtAuthentication) + permission_classes = (IsAuthenticated,) + + +class IntegritySignatureView(AuthenticatedAPIView): + """ + Endpoint for an Integrity Signature + /integrity_signature/{course_id} + + Supports: + HTTP GET: Returns an existing signed integrity agreement (by course id and user) + + HTTP GET + ** Scenarios ** + ?username=xyz + returns an existing signed integrity agreement for the given user and course + """ + + def get(self, request, course_id): + """ + In order to check whether the user has signed the integrity agreement for a given course. + + Should return the following: + username (str) + course_id (str) + created_at (str) + + If a username is not given, it should default to the requesting user (or masqueraded user). + Only staff should be able to access this endpoint for other users. + """ + # check that waffle flag is enabled + if not is_integrity_signature_enabled(): + return Response( + status=status.HTTP_404_NOT_FOUND, + ) + + # check that user can make request + user = request.user.username + requested_user = request.GET.get('username') + is_staff = is_user_course_or_global_staff(request.user, course_id) + + if not is_staff and requested_user and (user != requested_user): + return Response( + status=status.HTTP_403_FORBIDDEN, + data={ + "message": "User does not have permission to view integrity agreement." + } + ) + + username = requested_user if requested_user else user + signature = get_integrity_signature(username, course_id) + + if signature is None: + return Response( + status=status.HTTP_404_NOT_FOUND, + ) + + created_at = str(signature.created) + + data = { + 'username': username, + 'course_id': course_id, + 'created_at': created_at, + } + + return Response(data)