diff --git a/common/djangoapps/enrollment/tests/test_views.py b/common/djangoapps/enrollment/tests/test_views.py index a0f302e542..83a48e0b65 100644 --- a/common/djangoapps/enrollment/tests/test_views.py +++ b/common/djangoapps/enrollment/tests/test_views.py @@ -1133,7 +1133,7 @@ class EnrollmentEmbargoTest(EnrollmentTestMixin, UrlResetMixin, ModuleStoreTestC self.user.profile.country = restricted_country.country self.user.profile.save() - path = reverse('embargo_blocked_message', kwargs={'access_point': 'enrollment', 'message_key': 'default'}) + path = reverse('embargo:blocked_message', kwargs={'access_point': 'enrollment', 'message_key': 'default'}) self.assert_access_denied(path) @override_settings(EDX_API_KEY=EnrollmentTestMixin.API_KEY) diff --git a/lms/urls.py b/lms/urls.py index ba5e524b53..d98d716220 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -813,7 +813,8 @@ urlpatterns += ( # Embargo if settings.FEATURES.get('EMBARGO'): urlpatterns += ( - url(r'^embargo/', include('openedx.core.djangoapps.embargo.urls')), + url(r'^embargo/', include('openedx.core.djangoapps.embargo.urls', namespace='embargo')), + url(r'^api/embargo/', include('openedx.core.djangoapps.embargo.urls', namespace='api_embargo')), ) # Survey Djangoapp diff --git a/openedx/core/djangoapps/embargo/middleware.py b/openedx/core/djangoapps/embargo/middleware.py index d657644fd7..c1f4fb4970 100644 --- a/openedx/core/djangoapps/embargo/middleware.py +++ b/openedx/core/djangoapps/embargo/middleware.py @@ -98,7 +98,7 @@ class EmbargoMiddleware(object): # If the IP is blacklisted, reject. # This applies to any request, not just courseware URLs. ip_blacklist_url = reverse( - 'embargo_blocked_message', + 'embargo:blocked_message', kwargs={ 'access_point': 'courseware', 'message_key': 'embargo' diff --git a/openedx/core/djangoapps/embargo/models.py b/openedx/core/djangoapps/embargo/models.py index e10ee76a11..c99f01b0b5 100644 --- a/openedx/core/djangoapps/embargo/models.py +++ b/openedx/core/djangoapps/embargo/models.py @@ -317,7 +317,7 @@ class RestrictedCourse(models.Model): # We use generic messaging unless we find something more specific, # but *always* return a valid URL path. default_path = reverse( - 'embargo_blocked_message', + 'embargo:blocked_message', kwargs={ 'access_point': 'courseware', 'message_key': 'default' @@ -336,7 +336,7 @@ class RestrictedCourse(models.Model): course = cls.objects.get(course_key=course_key) msg_key = course.message_key_for_access_point(access_point) return reverse( - 'embargo_blocked_message', + 'embargo:blocked_message', kwargs={ 'access_point': access_point, 'message_key': msg_key diff --git a/openedx/core/djangoapps/embargo/test_utils.py b/openedx/core/djangoapps/embargo/test_utils.py index 5108c9bf49..5f5e825c21 100644 --- a/openedx/core/djangoapps/embargo/test_utils.py +++ b/openedx/core/djangoapps/embargo/test_utils.py @@ -75,7 +75,7 @@ def restrict_course(course_key, access_point="enrollment", disable_access_check= # Yield the redirect url so the tests don't need to know # the embargo messaging URL structure. redirect_url = reverse( - 'embargo_blocked_message', + 'embargo:blocked_message', kwargs={ 'access_point': access_point, 'message_key': 'default' diff --git a/openedx/core/djangoapps/embargo/tests/factories.py b/openedx/core/djangoapps/embargo/tests/factories.py new file mode 100644 index 0000000000..5936226172 --- /dev/null +++ b/openedx/core/djangoapps/embargo/tests/factories.py @@ -0,0 +1,30 @@ +import factory +from factory.django import DjangoModelFactory +from xmodule.modulestore.tests.factories import CourseFactory + +from ..models import Country, CountryAccessRule, RestrictedCourse + + +class CountryFactory(DjangoModelFactory): + class Meta(object): + model = Country + + country = 'US' + + +class RestrictedCourseFactory(DjangoModelFactory): + class Meta(object): + model = RestrictedCourse + + @factory.lazy_attribute + def course_key(self): + return CourseFactory().id + + +class CountryAccessRuleFactory(DjangoModelFactory): + class Meta(object): + model = CountryAccessRule + + country = factory.SubFactory(CountryFactory) + restricted_course = factory.SubFactory(RestrictedCourseFactory) + rule_type = CountryAccessRule.BLACKLIST_RULE diff --git a/openedx/core/djangoapps/embargo/tests/test_middleware.py b/openedx/core/djangoapps/embargo/tests/test_middleware.py index bf1d7a35ed..9d1c7ee01b 100644 --- a/openedx/core/djangoapps/embargo/tests/test_middleware.py +++ b/openedx/core/djangoapps/embargo/tests/test_middleware.py @@ -115,7 +115,7 @@ class EmbargoMiddlewareAccessTests(UrlResetMixin, ModuleStoreTestCase): self.assertEqual(response.status_code, 200) else: redirect_url = reverse( - 'embargo_blocked_message', + 'embargo:blocked_message', kwargs={ 'access_point': 'courseware', 'message_key': 'embargo' @@ -139,7 +139,7 @@ class EmbargoMiddlewareAccessTests(UrlResetMixin, ModuleStoreTestCase): ) url = reverse( - 'embargo_blocked_message', + 'embargo:blocked_message', kwargs={ 'access_point': access_point, 'message_key': msg_key diff --git a/openedx/core/djangoapps/embargo/tests/test_views.py b/openedx/core/djangoapps/embargo/tests/test_views.py index f9b00c566e..1c93fba33e 100644 --- a/openedx/core/djangoapps/embargo/tests/test_views.py +++ b/openedx/core/djangoapps/embargo/tests/test_views.py @@ -1,14 +1,23 @@ """Tests for embargo app views. """ -from mock import patch +import ddt +import json +import mock +import pygeoip + from django.core.urlresolvers import reverse from django.conf import settings -import ddt +from mock import patch -from util.testing import UrlResetMixin +from .factories import CountryFactory, CountryAccessRuleFactory, RestrictedCourseFactory from .. import messages +from lms.djangoapps.course_api.tests.mixins import CourseApiFactoryMixin from openedx.core.djangolib.testing.utils import CacheIsolationTestCase, skip_unless_lms from openedx.core.djangoapps.theming.tests.test_util import with_comprehensive_theme +from student.tests.factories import UserFactory +from util.testing import UrlResetMixin +from xmodule.modulestore.tests.factories import CourseFactory +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase @skip_unless_lms @@ -57,7 +66,7 @@ class CourseAccessMessageViewTest(CacheIsolationTestCase, UrlResetMixin): # Custom override specified for the "embargo" message # for backwards compatibility with previous versions # of the embargo app. - url = reverse('embargo_blocked_message', kwargs={ + url = reverse('embargo:blocked_message', kwargs={ 'access_point': access_point, 'message_key': "embargo" }) @@ -69,7 +78,7 @@ class CourseAccessMessageViewTest(CacheIsolationTestCase, UrlResetMixin): def _load_page(self, access_point, message_key, expected_status=200): """Load the message page and check the status code. """ - url = reverse('embargo_blocked_message', kwargs={ + url = reverse('embargo:blocked_message', kwargs={ 'access_point': access_point, 'message_key': message_key }) @@ -85,3 +94,56 @@ class CourseAccessMessageViewTest(CacheIsolationTestCase, UrlResetMixin): actual=response.status_code ) ) + + +@skip_unless_lms +class CheckCourseAccessViewTest(CourseApiFactoryMixin, ModuleStoreTestCase): + """ Tests the course access check endpoint. """ + + @patch.dict(settings.FEATURES, {'EMBARGO': True}) + def setUp(self): + super(CheckCourseAccessViewTest, self).setUp() + self.url = reverse('api_embargo:v1_course_access') + user = UserFactory(is_staff=True) + self.client.login(username=user.username, password=UserFactory._DEFAULT_PASSWORD) + self.course_id = str(CourseFactory().id) + self.request_data = { + 'course_ids': [self.course_id], + 'ip_address': '0.0.0.0', + 'user': self.user, + } + + def test_course_access_endpoint_with_unrestricted_course(self): + response = self.client.get(self.url, data=self.request_data) + expected_response = {'access': True} + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data, expected_response) + + def test_course_access_endpoint_with_restricted_course(self): + CountryAccessRuleFactory(restricted_course=RestrictedCourseFactory(course_key=self.course_id)) + + self.user.is_staff = False + self.user.save() + # Appear to make a request from an IP in the blocked country + with mock.patch.object(pygeoip.GeoIP, 'country_code_by_addr') as mock_ip: + mock_ip.return_value = 'US' + response = self.client.get(self.url, data=self.request_data) + expected_response = {'access': False} + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data, expected_response) + + def test_course_access_endpoint_with_logged_out_user(self): + self.client.logout() + response = self.client.get(self.url, data=self.request_data) + self.assertEqual(response.status_code, 403) + + def test_course_access_endpoint_with_non_staff_user(self): + user = UserFactory(is_staff=False) + self.client.login(username=user.username, password=UserFactory._DEFAULT_PASSWORD) + + response = self.client.get(self.url, data=self.request_data) + self.assertEqual(response.status_code, 403) + + def test_course_access_endpoint_with_invalid_data(self): + response = self.client.get(self.url, data=None) + self.assertEqual(response.status_code, 400) diff --git a/openedx/core/djangoapps/embargo/urls.py b/openedx/core/djangoapps/embargo/urls.py index abad7a5f9e..0206072088 100644 --- a/openedx/core/djangoapps/embargo/urls.py +++ b/openedx/core/djangoapps/embargo/urls.py @@ -2,13 +2,14 @@ from django.conf.urls import patterns, url -from .views import CourseAccessMessageView +from .views import CheckCourseAccessView, CourseAccessMessageView urlpatterns = patterns( 'openedx.core.djangoapps.embargo.views', url( r'^blocked-message/(?Penrollment|courseware)/(?P.+)/$', CourseAccessMessageView.as_view(), - name='embargo_blocked_message', + name='blocked_message', ), + url(r'^v1/course_access/$', CheckCourseAccessView.as_view(), name='v1_course_access'), ) diff --git a/openedx/core/djangoapps/embargo/views.py b/openedx/core/djangoapps/embargo/views.py index 7e19220972..93a368561a 100644 --- a/openedx/core/djangoapps/embargo/views.py +++ b/openedx/core/djangoapps/embargo/views.py @@ -1,11 +1,54 @@ """Views served by the embargo app. """ +from django.contrib.auth.models import User from django.http import Http404 from django.views.generic.base import View +from edx_rest_framework_extensions.authentication import JwtAuthentication +from opaque_keys.edx.keys import CourseKey +from rest_framework import permissions, status +from rest_framework.response import Response +from rest_framework.views import APIView from edxmako.shortcuts import render_to_response from . import messages +from .api import check_course_access + + +class CheckCourseAccessView(APIView): + permission_classes = (permissions.IsAuthenticated, permissions.IsAdminUser) + + def get(self, request): + """ + GET /api/embargo/v1/course_access/ + + Arguments: + request (HttpRequest) + + Return: + Response: True or False depending on the check. + + """ + course_ids = request.GET.getlist('course_ids', []) + username = request.GET.get('user') + user_ip_address = request.GET.get('ip_address') + + try: + user = User.objects.get(username=username) + except User.DoesNotExist: + user = None + + response = {'access': True} + + if course_ids and user and user_ip_address: + for course_id in course_ids: + if not check_course_access(CourseKey.from_string(course_id), user, user_ip_address): + response['access'] = False + break + else: + return Response(data=None, status=status.HTTP_400_BAD_REQUEST) + + return Response(response) class CourseAccessMessageView(View):