Country Access: block enrollment

Block users from enrolling in a course if the user
is blocked by country access rules.

1) Enrollment via the login/registration page.
2) Enrollment from the marketing iframe (via student.views.change_enrollment)
3) Enrollment using 100% redeem codes.
4) Enrollment via upgrade.

This does NOT cover enrollment through third party authentication,
which is sufficiently complex to deserve its own commit.
This commit is contained in:
Will Daly
2015-02-10 13:07:51 -05:00
parent 6ba6afced8
commit e609f982d7
12 changed files with 337 additions and 24 deletions

View File

@@ -11,6 +11,7 @@ from xmodule.modulestore.tests.django_utils import (
)
from util.testing import UrlResetMixin
from embargo.test_utils import restrict_course
from xmodule.modulestore.tests.factories import CourseFactory
from course_modes.tests.factories import CourseModeFactory
from student.tests.factories import CourseEnrollmentFactory, UserFactory
@@ -274,7 +275,7 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase):
# Create a verified mode
url = reverse('create_mode', args=[unicode(self.course.id)])
response = self.client.get(url, parameters)
self.client.get(url, parameters)
honor_mode = Mode(u'honor', u'Honor Code Certificate', 0, '', 'usd', None, None)
verified_mode = Mode(u'verified', u'Verified Certificate', 10, '10,20', 'usd', None, None)
@@ -282,3 +283,34 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase):
course_modes = CourseMode.modes_for_course(self.course.id)
self.assertEquals(course_modes, expected_modes)
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class TrackSelectionEmbargoTest(UrlResetMixin, ModuleStoreTestCase):
"""Test embargo restrictions on the track selection page. """
@patch.dict(settings.FEATURES, {'ENABLE_COUNTRY_ACCESS': True})
def setUp(self):
super(TrackSelectionEmbargoTest, self).setUp('embargo')
# Create a course and course modes
self.course = CourseFactory.create()
CourseModeFactory(mode_slug='honor', course_id=self.course.id)
CourseModeFactory(mode_slug='verified', course_id=self.course.id, min_price=10)
# Create a user and log in
self.user = UserFactory.create(username="Bob", email="bob@example.com", password="edx")
self.client.login(username=self.user.username, password="edx")
# Construct the URL for the track selection page
self.url = reverse('course_modes_choose', args=[unicode(self.course.id)])
@patch.dict(settings.FEATURES, {'ENABLE_COUNTRY_ACCESS': True})
def test_embargo_restrict(self):
with restrict_course(self.course.id) as redirect_url:
response = self.client.get(self.url)
self.assertRedirects(response, redirect_url)
def test_embargo_allow(self):
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)

View File

@@ -3,6 +3,8 @@ Views for the course_mode module
"""
import decimal
from ipware.ip import get_ip
from django.core.urlresolvers import reverse
from django.conf import settings
from django.http import HttpResponse, HttpResponseBadRequest
@@ -22,6 +24,8 @@ from opaque_keys.edx.keys import CourseKey
from util.db import commit_on_success_with_read_committed
from xmodule.modulestore.django import modulestore
from embargo import api as embargo_api
class ChooseModeView(View):
"""View used when the user is asked to pick a mode.
@@ -52,6 +56,17 @@ class ChooseModeView(View):
"""
course_key = CourseKey.from_string(course_id)
# Check whether the user has access to this course
# based on country access rules.
embargo_redirect = embargo_api.redirect_if_blocked(
course_key,
user=request.user,
ip_address=get_ip(request),
url=request.path
)
if embargo_redirect:
return redirect(embargo_redirect)
enrollment_mode, is_active = CourseEnrollment.enrollment_mode_for_user(request.user, course_key)
modes = CourseMode.modes_for_course_dict(course_key)

View File

@@ -6,19 +6,18 @@ import json
import unittest
from mock import patch
from django.test.utils import override_settings
from django.core.urlresolvers import reverse
from rest_framework.test import APITestCase
from rest_framework import status
from django.conf import settings
from xmodule.modulestore.tests.django_utils import (
ModuleStoreTestCase, mixed_store_config
)
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from util.testing import UrlResetMixin
from enrollment import api
from enrollment.errors import CourseEnrollmentError
from student.tests.factories import UserFactory, CourseModeFactory
from student.models import CourseEnrollment
from embargo.test_utils import restrict_course
@ddt.ddt
@@ -245,3 +244,66 @@ class EnrollmentTest(ModuleStoreTestCase, APITestCase):
)
self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn("No course ", resp.content)
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class EnrollmentEmbargoTest(UrlResetMixin, ModuleStoreTestCase):
"""Test that enrollment is blocked from embargoed countries. """
USERNAME = "Bob"
EMAIL = "bob@example.com"
PASSWORD = "edx"
@patch.dict(settings.FEATURES, {'ENABLE_COUNTRY_ACCESS': True})
def setUp(self):
""" Create a course and user, then log in. """
super(EnrollmentEmbargoTest, self).setUp('embargo')
self.course = CourseFactory.create()
self.user = UserFactory.create(username=self.USERNAME, email=self.EMAIL, password=self.PASSWORD)
self.client.login(username=self.USERNAME, password=self.PASSWORD)
@patch.dict(settings.FEATURES, {'ENABLE_COUNTRY_ACCESS': True})
def test_embargo_change_enrollment_restrict(self):
url = reverse('courseenrollments')
data = json.dumps({
'course_details': {
'course_id': unicode(self.course.id)
},
'user': self.user.username
})
# Attempt to enroll from a country embargoed for this course
with restrict_course(self.course.id) as redirect_url:
response = self.client.post(url, data, content_type='application/json')
# Expect an error response
self.assertEqual(response.status_code, 403)
# Expect that the redirect URL is included in the response
resp_data = json.loads(response.content)
self.assertEqual(resp_data['user_message_url'], redirect_url)
# Verify that we were not enrolled
self.assertEqual(self._get_enrollments(), [])
@patch.dict(settings.FEATURES, {'ENABLE_COUNTRY_ACCESS': True})
def test_embargo_change_enrollment_allow(self):
url = reverse('courseenrollments')
data = json.dumps({
'course_details': {
'course_id': unicode(self.course.id)
},
'user': self.user.username
})
response = self.client.post(url, data, content_type='application/json')
self.assertEqual(response.status_code, 200)
# Verify that we were enrolled
self.assertEqual(len(self._get_enrollments()), 1)
def _get_enrollments(self):
"""Retrieve the enrollment list for the current user. """
url = reverse('courseenrollments')
resp = self.client.get(url)
return json.loads(resp.content)

View File

@@ -3,15 +3,19 @@ The Enrollment API Views should be simple, lean HTTP endpoints for API access. T
consist primarily of authentication, request validation, and serialization.
"""
from opaque_keys import InvalidKeyError
from ipware.ip import get_ip
from django.conf import settings
from rest_framework import status
from rest_framework.authentication import OAuth2Authentication
from rest_framework import permissions
from rest_framework.response import Response
from rest_framework.throttling import UserRateThrottle
from rest_framework.views import APIView
from opaque_keys.edx.keys import CourseKey
from opaque_keys import InvalidKeyError
from enrollment import api
from enrollment.errors import CourseNotFoundError, CourseEnrollmentError, CourseModeNotFoundError
from embargo import api as embargo_api
from util.authentication import SessionAuthenticationAllowInactiveUser
from util.disable_rate_limit import can_disable_rate_limit
@@ -278,7 +282,36 @@ class EnrollmentListView(APIView):
course_id = request.DATA['course_details']['course_id']
try:
return Response(api.add_enrollment(user, course_id))
course_id = CourseKey.from_string(course_id)
except InvalidKeyError:
return Response(
status=status.HTTP_400_BAD_REQUEST,
data={
"message": u"No course '{course_id}' found for enrollment".format(course_id=course_id)
}
)
# Check whether any country access rules block the user from enrollment
# We do this at the view level (rather than the Python API level)
# because this check requires information about the HTTP request.
redirect_url = embargo_api.redirect_if_blocked(
course_id, user=request.user,
ip_address=get_ip(request),
url=request.path
)
if redirect_url:
return Response(
status=status.HTTP_403_FORBIDDEN,
data={
"message": (
u"Users from this location cannot access the course '{course_id}'."
).format(course_id=course_id),
"user_message_url": redirect_url
}
)
try:
return Response(api.add_enrollment(user, unicode(course_id)))
except CourseModeNotFoundError as error:
return Response(
status=status.HTTP_400_BAD_REQUEST,
@@ -305,10 +338,3 @@ class EnrollmentListView(APIView):
).format(user=user, course_id=course_id)
}
)
except InvalidKeyError:
return Response(
status=status.HTTP_400_BAD_REQUEST,
data={
"message": u"No course '{course_id}' found for enrollment".format(course_id=course_id)
}
)

View File

@@ -5,18 +5,19 @@ import ddt
import unittest
from mock import patch
from django.test.utils import override_settings
from django.conf import settings
from django.core.urlresolvers import reverse
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from util.testing import UrlResetMixin
from embargo.test_utils import restrict_course
from student.tests.factories import UserFactory, CourseModeFactory
from student.models import CourseEnrollment
@ddt.ddt
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class EnrollmentTest(ModuleStoreTestCase):
class EnrollmentTest(UrlResetMixin, ModuleStoreTestCase):
"""
Test student enrollment, especially with different course modes.
"""
@@ -25,9 +26,10 @@ class EnrollmentTest(ModuleStoreTestCase):
EMAIL = "bob@example.com"
PASSWORD = "edx"
@patch.dict(settings.FEATURES, {'ENABLE_COUNTRY_ACCESS': True})
def setUp(self):
""" Create a course and user, then log in. """
super(EnrollmentTest, self).setUp()
super(EnrollmentTest, self).setUp('embargo')
self.course = CourseFactory.create()
self.user = UserFactory.create(username=self.USERNAME, email=self.EMAIL, password=self.PASSWORD)
self.client.login(username=self.USERNAME, password=self.PASSWORD)
@@ -132,6 +134,29 @@ class EnrollmentTest(ModuleStoreTestCase):
else:
self.assertFalse(mock_update_email_opt_in.called)
@patch.dict(settings.FEATURES, {'ENABLE_COUNTRY_ACCESS': True})
def test_embargo_restrict(self):
# When accessing the course from an embargoed country,
# we should be blocked.
with restrict_course(self.course.id) as redirect_url:
response = self._change_enrollment('enroll')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.content, redirect_url)
# Verify that we weren't enrolled
is_enrolled = CourseEnrollment.is_enrolled(self.user, self.course.id)
self.assertFalse(is_enrolled)
@patch.dict(settings.FEATURES, {'ENABLE_COUNTRY_ACCESS': True})
def test_embargo_allow(self):
response = self._change_enrollment('enroll')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.content, '')
# Verify that we were enrolled
is_enrolled = CourseEnrollment.is_enrolled(self.user, self.course.id)
self.assertTrue(is_enrolled)
def test_user_not_authenticated(self):
# Log out, so we're no longer authenticated
self.client.logout()

View File

@@ -9,6 +9,7 @@ import time
import json
from collections import defaultdict
from pytz import UTC
from ipware.ip import get_ip
from django.conf import settings
from django.contrib.auth import logout, authenticate, login
@@ -113,6 +114,8 @@ from xmodule.error_module import ErrorDescriptor
from shoppingcart.models import DonationConfiguration, CourseRegistrationCode
from openedx.core.djangoapps.user_api.api import profile as profile_api
from embargo import api as embargo_api
import analytics
from eventtracking import tracker
@@ -876,6 +879,17 @@ def change_enrollment(request, check_access=True):
available_modes = CourseMode.modes_for_course_dict(course_id)
# Check whether the user is blocked from enrolling in this course
# This can occur if the user's IP is on a global blacklist
# or if the user is enrolling in a country in which the course
# is not available.
redirect_url = embargo_api.redirect_if_blocked(
course_id, user=user, ip_address=get_ip(request),
url=request.path
)
if redirect_url:
return HttpResponse(redirect_url)
# Check that auto enrollment is allowed for this course
# (= the course is NOT behind a paywall)
if CourseMode.can_auto_enroll(course_id):