diff --git a/common/djangoapps/cors_csrf/authentication.py b/common/djangoapps/cors_csrf/authentication.py new file mode 100644 index 0000000000..723ec8eed1 --- /dev/null +++ b/common/djangoapps/cors_csrf/authentication.py @@ -0,0 +1,29 @@ +"""Django Rest Framework Authentication classes for cross-domain end-points.""" +from rest_framework import authentication +from cors_csrf.helpers import is_cross_domain_request_allowed, skip_cross_domain_referer_check + + +class SessionAuthenticationCrossDomainCsrf(authentication.SessionAuthentication): + """Session authentication that skips the referer check over secure connections. + + Django Rest Framework's `SessionAuthentication` class calls Django's + CSRF middleware implementation directly, which bypasses the middleware + stack. + + This version of `SessionAuthentication` performs the same workaround + as `CorsCSRFMiddleware` to skip the referer check for whitelisted + domains over a secure connection. See `cors_csrf.middleware` for + more information. + + Since this subclass overrides only the `enforce_csrf()` method, + it can be mixed in with other `SessionAuthentication` subclasses. + + """ + + def enforce_csrf(self, request): + """Skip the referer check if the cross-domain request is allowed. """ + if is_cross_domain_request_allowed(request): + with skip_cross_domain_referer_check(request): + return super(SessionAuthenticationCrossDomainCsrf, self).enforce_csrf(request) + else: + return super(SessionAuthenticationCrossDomainCsrf, self).enforce_csrf(request) diff --git a/common/djangoapps/cors_csrf/helpers.py b/common/djangoapps/cors_csrf/helpers.py new file mode 100644 index 0000000000..c1a059c1a9 --- /dev/null +++ b/common/djangoapps/cors_csrf/helpers.py @@ -0,0 +1,92 @@ +"""Helper methods for CORS and CSRF checks. """ +import logging +import urlparse +import contextlib + +from django.conf import settings + +log = logging.getLogger(__name__) + + +def is_cross_domain_request_allowed(request): + """Check whether we should allow the cross-domain request. + + We allow a cross-domain request only if: + + 1) The request is made securely and the referer has "https://" as the protocol. + 2) The referer domain has been whitelisted. + + Arguments: + request (HttpRequest) + + Returns: + bool + + """ + referer = request.META.get('HTTP_REFERER') + referer_parts = urlparse.urlparse(referer) if referer else None + referer_hostname = referer_parts.hostname if referer_parts is not None else None + + # Use CORS_ALLOW_INSECURE *only* for development and testing environments; + # it should never be enabled in production. + if not getattr(settings, 'CORS_ALLOW_INSECURE', False): + if not request.is_secure(): + log.debug( + u"Request is not secure, so we cannot send the CSRF token. " + u"For testing purposes, you can disable this check by setting " + u"`CORS_ALLOW_INSECURE` to True in the settings" + ) + return False + + if not referer: + log.debug(u"No referer provided over a secure connection, so we cannot check the protocol.") + return False + + if not referer_parts.scheme == 'https': + log.debug(u"Referer '%s' must have the scheme 'https'") + return False + + domain_is_whitelisted = ( + getattr(settings, 'CORS_ORIGIN_ALLOW_ALL', False) or + referer_hostname in getattr(settings, 'CORS_ORIGIN_WHITELIST', []) + ) + if not domain_is_whitelisted: + if referer_hostname is None: + # If no referer is specified, we can't check if it's a cross-domain + # request or not. + log.debug(u"Referrer hostname is `None`, so it is not on the whitelist.") + elif referer_hostname != request.get_host(): + log.info( + ( + u"Domain '%s' is not on the cross domain whitelist. " + u"Add the domain to `CORS_ORIGIN_WHITELIST` or set " + u"`CORS_ORIGIN_ALLOW_ALL` to True in the settings." + ), referer_hostname + ) + else: + log.debug( + ( + u"Domain '%s' is the same as the hostname in the request, " + u"so we are not going to treat it as a cross-domain request." + ), referer_hostname + ) + return False + + return True + + +@contextlib.contextmanager +def skip_cross_domain_referer_check(request): + """Skip the cross-domain CSRF referer check. + + Django's CSRF middleware performs the referer check + only when the request is made over a secure connection. + To skip the check, we patch `request.is_secure()` to + False. + """ + is_secure_default = request.is_secure + request.is_secure = lambda: False + try: + yield + finally: + request.is_secure = is_secure_default diff --git a/common/djangoapps/cors_csrf/middleware.py b/common/djangoapps/cors_csrf/middleware.py index 41f51b618e..dfd554d9dc 100644 --- a/common/djangoapps/cors_csrf/middleware.py +++ b/common/djangoapps/cors_csrf/middleware.py @@ -43,82 +43,16 @@ CSRF cookie. """ import logging -import urlparse from django.conf import settings from django.middleware.csrf import CsrfViewMiddleware from django.core.exceptions import MiddlewareNotUsed, ImproperlyConfigured +from cors_csrf.helpers import is_cross_domain_request_allowed, skip_cross_domain_referer_check + log = logging.getLogger(__name__) -def is_cross_domain_request_allowed(request): - """Check whether we should allow the cross-domain request. - - We allow a cross-domain request only if: - - 1) The request is made securely and the referer has "https://" as the protocol. - 2) The referer domain has been whitelisted. - - Arguments: - request (HttpRequest) - - Returns: - bool - - """ - referer = request.META.get('HTTP_REFERER') - referer_parts = urlparse.urlparse(referer) if referer else None - referer_hostname = referer_parts.hostname if referer_parts is not None else None - - # Use CORS_ALLOW_INSECURE *only* for development and testing environments; - # it should never be enabled in production. - if not getattr(settings, 'CORS_ALLOW_INSECURE', False): - if not request.is_secure(): - log.debug( - u"Request is not secure, so we cannot send the CSRF token. " - u"For testing purposes, you can disable this check by setting " - u"`CORS_ALLOW_INSECURE` to True in the settings" - ) - return False - - if not referer: - log.debug(u"No referer provided over a secure connection, so we cannot check the protocol.") - return False - - if not referer_parts.scheme == 'https': - log.debug(u"Referer '%s' must have the scheme 'https'") - return False - - domain_is_whitelisted = ( - getattr(settings, 'CORS_ORIGIN_ALLOW_ALL', False) or - referer_hostname in getattr(settings, 'CORS_ORIGIN_WHITELIST', []) - ) - if not domain_is_whitelisted: - if referer_hostname is None: - # If no referer is specified, we can't check if it's a cross-domain - # request or not. - log.debug(u"Referrer hostname is `None`, so it is not on the whitelist.") - elif referer_hostname != request.get_host(): - log.info( - ( - u"Domain '%s' is not on the cross domain whitelist. " - u"Add the domain to `CORS_ORIGIN_WHITELIST` or set " - u"`CORS_ORIGIN_ALLOW_ALL` to True in the settings." - ), referer_hostname - ) - else: - log.debug( - ( - u"Domain '%s' is the same as the hostname in the request, " - u"so we are not going to treat it as a cross-domain request." - ), referer_hostname - ) - return False - - return True - - class CorsCSRFMiddleware(CsrfViewMiddleware): """ Middleware for handling CSRF checks with CORS requests @@ -134,18 +68,8 @@ class CorsCSRFMiddleware(CsrfViewMiddleware): log.debug("Could not disable CSRF middleware referer check for cross-domain request.") return - is_secure_default = request.is_secure - - def is_secure_patched(): - """ - Avoid triggering the additional CSRF middleware checks on the referrer - """ - return False - request.is_secure = is_secure_patched - - res = super(CorsCSRFMiddleware, self).process_view(request, callback, callback_args, callback_kwargs) - request.is_secure = is_secure_default - return res + with skip_cross_domain_referer_check(request): + return super(CorsCSRFMiddleware, self).process_view(request, callback, callback_args, callback_kwargs) class CsrfCrossDomainCookieMiddleware(object): diff --git a/common/djangoapps/cors_csrf/tests/test_authentication.py b/common/djangoapps/cors_csrf/tests/test_authentication.py new file mode 100644 index 0000000000..7f2d78fd8a --- /dev/null +++ b/common/djangoapps/cors_csrf/tests/test_authentication.py @@ -0,0 +1,56 @@ +"""Tests for the CORS CSRF version of Django Rest Framework's SessionAuthentication.""" +from mock import patch + +from django.test import TestCase +from django.test.utils import override_settings +from django.test.client import RequestFactory +from django.conf import settings + +from rest_framework.exceptions import AuthenticationFailed + +from cors_csrf.authentication import SessionAuthenticationCrossDomainCsrf + + +class CrossDomainAuthTest(TestCase): + """Tests for the CORS CSRF version of Django Rest Framework's SessionAuthentication. """ + + URL = "/dummy_url" + REFERER = "https://www.edx.org" + CSRF_TOKEN = 'abcd1234' + + def setUp(self): + super(CrossDomainAuthTest, self).setUp() + self.auth = SessionAuthenticationCrossDomainCsrf() + + def test_perform_csrf_referer_check(self): + request = self._fake_request() + with self.assertRaisesRegexp(AuthenticationFailed, 'CSRF'): + self.auth.enforce_csrf(request) + + @patch.dict(settings.FEATURES, { + 'ENABLE_CORS_HEADERS': True, + 'ENABLE_CROSS_DOMAIN_CSRF_COOKIE': True + }) + @override_settings( + CORS_ORIGIN_WHITELIST=["www.edx.org"], + CROSS_DOMAIN_CSRF_COOKIE_NAME="prod-edx-csrftoken", + CROSS_DOMAIN_CSRF_COOKIE_DOMAIN=".edx.org" + ) + def test_skip_csrf_referer_check(self): + request = self._fake_request() + result = self.auth.enforce_csrf(request) + self.assertIs(result, None) + self.assertTrue(request.is_secure()) + + def _fake_request(self): + """Construct a fake request with a referer and CSRF token over a secure connection. """ + factory = RequestFactory() + factory.cookies[settings.CSRF_COOKIE_NAME] = self.CSRF_TOKEN + + request = factory.post( + self.URL, + HTTP_REFERER=self.REFERER, + HTTP_X_CSRFTOKEN=self.CSRF_TOKEN + ) + request.is_secure = lambda: True + return request diff --git a/common/djangoapps/enrollment/tests/test_views.py b/common/djangoapps/enrollment/tests/test_views.py index c399bc51b4..a3e961a69f 100644 --- a/common/djangoapps/enrollment/tests/test_views.py +++ b/common/djangoapps/enrollment/tests/test_views.py @@ -6,6 +6,8 @@ import json import unittest from mock import patch +from django.test import Client +from django.core.handlers.wsgi import WSGIRequest from django.core.urlresolvers import reverse from rest_framework.test import APITestCase from rest_framework import status @@ -504,3 +506,81 @@ class EnrollmentEmbargoTest(UrlResetMixin, ModuleStoreTestCase): url = reverse('courseenrollments') resp = self.client.get(url) return json.loads(resp.content) + + +def cross_domain_config(func): + """Decorator for configuring a cross-domain request. """ + feature_flag_decorator = patch.dict(settings.FEATURES, { + 'ENABLE_CORS_HEADERS': True, + 'ENABLE_CROSS_DOMAIN_CSRF_COOKIE': True + }) + settings_decorator = override_settings( + CORS_ORIGIN_WHITELIST=["www.edx.org"], + CROSS_DOMAIN_CSRF_COOKIE_NAME="prod-edx-csrftoken", + CROSS_DOMAIN_CSRF_COOKIE_DOMAIN=".edx.org" + ) + is_secure_decorator = patch.object(WSGIRequest, 'is_secure', return_value=True) + + return feature_flag_decorator( + settings_decorator( + is_secure_decorator(func) + ) + ) + + +@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') +class EnrollmentCrossDomainTest(ModuleStoreTestCase): + """Test cross-domain calls to the enrollment end-points. """ + + USERNAME = "Bob" + EMAIL = "bob@example.com" + PASSWORD = "edx" + REFERER = "https://www.edx.org" + + def setUp(self): + """ Create a course and user, then log in. """ + super(EnrollmentCrossDomainTest, self).setUp() + self.course = CourseFactory.create() + self.user = UserFactory.create(username=self.USERNAME, email=self.EMAIL, password=self.PASSWORD) + + self.client = Client(enforce_csrf_checks=True) + self.client.login(username=self.USERNAME, password=self.PASSWORD) + + @cross_domain_config + def test_cross_domain_change_enrollment(self, *args): # pylint: disable=unused-argument + csrf_cookie = self._get_csrf_cookie() + resp = self._cross_domain_post(csrf_cookie) + + # Expect that the request gets through successfully, + # passing the CSRF checks (including the referer check). + self.assertEqual(resp.status_code, 200) + + @cross_domain_config + def test_cross_domain_missing_csrf(self, *args): # pylint: disable=unused-argument + resp = self._cross_domain_post('invalid_csrf_token') + self.assertEqual(resp.status_code, 401) + + def _get_csrf_cookie(self): + """Retrieve the cross-domain CSRF cookie. """ + url = reverse('courseenrollment', kwargs={ + 'course_id': unicode(self.course.id) + }) + resp = self.client.get(url, HTTP_REFERER=self.REFERER) + self.assertEqual(resp.status_code, 200) + self.assertIn('prod-edx-csrftoken', resp.cookies) # pylint: disable=no-member + return resp.cookies['prod-edx-csrftoken'].value # pylint: disable=no-member + + def _cross_domain_post(self, csrf_cookie): + """Perform a cross-domain POST request. """ + url = reverse('courseenrollments') + params = json.dumps({ + 'course_details': { + 'course_id': unicode(self.course.id), + }, + 'user': self.user.username + }) + return self.client.post( + url, params, content_type='application/json', + HTTP_REFERER=self.REFERER, + HTTP_X_CSRFTOKEN=csrf_cookie + ) diff --git a/common/djangoapps/enrollment/views.py b/common/djangoapps/enrollment/views.py index 4198a4a62a..bdd867dd4d 100644 --- a/common/djangoapps/enrollment/views.py +++ b/common/djangoapps/enrollment/views.py @@ -15,6 +15,7 @@ from rest_framework.throttling import UserRateThrottle from rest_framework.views import APIView from opaque_keys.edx.keys import CourseKey from embargo import api as embargo_api +from cors_csrf.authentication import SessionAuthenticationCrossDomainCsrf from cors_csrf.decorators import ensure_csrf_cookie_cross_domain from util.authentication import SessionAuthenticationAllowInactiveUser, OAuth2AuthenticationAllowInactiveUser from util.disable_rate_limit import can_disable_rate_limit @@ -25,6 +26,11 @@ from enrollment.errors import ( ) +class EnrollmentCrossDomainSessionAuth(SessionAuthenticationAllowInactiveUser, SessionAuthenticationCrossDomainCsrf): + """Session authentication that allows inactive users and cross-domain requests. """ + pass + + class ApiKeyPermissionMixIn(object): """ This mixin is used to provide a convenience function for doing individual permission checks @@ -277,7 +283,7 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn): * user: The ID of the user. """ - authentication_classes = OAuth2AuthenticationAllowInactiveUser, SessionAuthenticationAllowInactiveUser + authentication_classes = OAuth2AuthenticationAllowInactiveUser, EnrollmentCrossDomainSessionAuth permission_classes = ApiKeyHeaderPermissionIsAuthenticated, throttle_classes = EnrollmentUserThrottle, diff --git a/common/djangoapps/util/sandboxing.py b/common/djangoapps/util/sandboxing.py index 5fea7ed25c..8fcfa6dfbc 100644 --- a/common/djangoapps/util/sandboxing.py +++ b/common/djangoapps/util/sandboxing.py @@ -25,7 +25,7 @@ def can_execute_unsafe_code(course_id): # To others using this: the code as-is is brittle and likely to be changed in the future, # as per the TODO, so please consider carefully before adding more values to COURSES_WITH_UNSAFE_CODE for regex in getattr(settings, 'COURSES_WITH_UNSAFE_CODE', []): - if re.match(regex, course_id.to_deprecated_string()): + if re.match(regex, unicode(course_id)): return True return False diff --git a/common/djangoapps/util/tests/test_sandboxing.py b/common/djangoapps/util/tests/test_sandboxing.py index 7a33fbbe95..21179b1dfa 100644 --- a/common/djangoapps/util/tests/test_sandboxing.py +++ b/common/djangoapps/util/tests/test_sandboxing.py @@ -3,6 +3,7 @@ Tests for sandboxing.py in util app """ from django.test import TestCase +from opaque_keys.edx.locator import LibraryLocator from util.sandboxing import can_execute_unsafe_code from django.test.utils import override_settings from opaque_keys.edx.locations import SlashSeparatedCourseKey @@ -12,12 +13,13 @@ class SandboxingTest(TestCase): """ Test sandbox whitelisting """ - @override_settings(COURSES_WITH_UNSAFE_CODE=['edX/full/.*']) + @override_settings(COURSES_WITH_UNSAFE_CODE=['edX/full/.*', 'library:v1-edX+.*']) def test_sandbox_exclusion(self): """ Test to make sure that a non-match returns false """ self.assertFalse(can_execute_unsafe_code(SlashSeparatedCourseKey('edX', 'notful', 'empty'))) + self.assertFalse(can_execute_unsafe_code(LibraryLocator('edY', 'test_bank'))) @override_settings(COURSES_WITH_UNSAFE_CODE=['edX/full/.*']) def test_sandbox_inclusion(self): @@ -26,10 +28,12 @@ class SandboxingTest(TestCase): """ self.assertTrue(can_execute_unsafe_code(SlashSeparatedCourseKey('edX', 'full', '2012_Fall'))) self.assertTrue(can_execute_unsafe_code(SlashSeparatedCourseKey('edX', 'full', '2013_Spring'))) + self.assertFalse(can_execute_unsafe_code(LibraryLocator('edX', 'test_bank'))) - def test_courses_with_unsafe_code_default(self): + def test_courselikes_with_unsafe_code_default(self): """ Test that the default setting for COURSES_WITH_UNSAFE_CODE is an empty setting, e.g. we don't use @override_settings in these tests """ self.assertFalse(can_execute_unsafe_code(SlashSeparatedCourseKey('edX', 'full', '2012_Fall'))) self.assertFalse(can_execute_unsafe_code(SlashSeparatedCourseKey('edX', 'full', '2013_Spring'))) + self.assertFalse(can_execute_unsafe_code(LibraryLocator('edX', 'test_bank'))) diff --git a/common/templates/mathjax_include.html b/common/templates/mathjax_include.html index d3fef03ec2..bb0c14ece9 100644 --- a/common/templates/mathjax_include.html +++ b/common/templates/mathjax_include.html @@ -6,38 +6,33 @@ ## This enables ASCIIMathJAX, and is used by js_textbox -<%def name="mathjaxConfig()"> - %if mathjax_mode is not Undefined and mathjax_mode == 'wiki': - MathJax.Hub.Config({ - tex2jax: {inlineMath: [ ['$','$'], ["\\(","\\)"]], - displayMath: [ ['$$','$$'], ["\\[","\\]"]]} - }); - %else: - MathJax.Hub.Config({ - tex2jax: { - inlineMath: [ - ["\\(","\\)"], - ['[mathjaxinline]','[/mathjaxinline]'] - ], - displayMath: [ - ["\\[","\\]"], - ['[mathjax]','[/mathjax]'] - ] - } - }); - %endif - MathJax.Hub.Configured(); - window.HUB = MathJax.Hub; - +%if mathjax_mode is not Undefined and mathjax_mode == 'wiki': + +%else: + +%endif - + diff --git a/lms/djangoapps/commerce/constants.py b/lms/djangoapps/commerce/constants.py index fbbeae0666..a59ff3fa95 100644 --- a/lms/djangoapps/commerce/constants.py +++ b/lms/djangoapps/commerce/constants.py @@ -19,3 +19,4 @@ class Messages(object): NO_SKU_ENROLLED = u'The {enrollment_mode} mode for {course_id} does not have a SKU. Enrolling {username} directly.' ORDER_COMPLETED = u'Order {order_number} was completed.' ORDER_INCOMPLETE_ENROLLED = u'Order {order_number} was created, but is not yet complete. User was enrolled.' + NO_HONOR_MODE = u'Course {course_id} does not have an honor mode.' diff --git a/lms/djangoapps/commerce/tests.py b/lms/djangoapps/commerce/tests.py index dcf8366545..88f6e594f5 100644 --- a/lms/djangoapps/commerce/tests.py +++ b/lms/djangoapps/commerce/tests.py @@ -254,7 +254,6 @@ class OrdersViewTests(EnrollmentEventTestMixin, ModuleStoreTestCase): response = self._post_to_view() # Validate the response - self._mock_ecommerce_api() self.assertEqual(response.status_code, 200) msg = Messages.NO_ECOM_API.format(username=self.user.username, course_id=self.course.id) self.assertResponseMessage(response, msg) @@ -272,3 +271,29 @@ class OrdersViewTests(EnrollmentEventTestMixin, ModuleStoreTestCase): course_mode.save() self._test_course_without_sku() + + def _test_professional_mode_only(self): + """ Verifies that the view behaves appropriately when the course only has a professional mode. """ + CourseMode.objects.filter(course_id=self.course.id).delete() + mode = 'no-id-professional' + CourseModeFactory.create(course_id=self.course.id, mode_slug=mode, mode_display_name=mode, + sku=uuid4().hex.decode('ascii')) + self._mock_ecommerce_api() + response = self._post_to_view() + self.assertEqual(response.status_code, 406) + msg = Messages.NO_HONOR_MODE.format(course_id=self.course.id) + self.assertResponseMessage(response, msg) + + @httpretty.activate + def test_course_with_professional_mode_only(self): + """ Verifies that the view behaves appropriately when the course only has a professional mode. """ + self._test_professional_mode_only() + + @httpretty.activate + @override_settings(ECOMMERCE_API_URL=None, ECOMMERCE_API_SIGNING_KEY=None) + def test_no_settings_and_professional_mode_only(self): + """ + Verifies that the view behaves appropriately when the course only has a professional mode and + the E-Commerce API is not configured. + """ + self._test_professional_mode_only() diff --git a/lms/djangoapps/commerce/views.py b/lms/djangoapps/commerce/views.py index ba4b82bdfa..e8be34f1f1 100644 --- a/lms/djangoapps/commerce/views.py +++ b/lms/djangoapps/commerce/views.py @@ -54,17 +54,16 @@ class OrdersView(APIView): return True, course_key, None - def _get_jwt(self, user): + def _get_jwt(self, user, ecommerce_api_signing_key): """ Returns a JWT object with the specified user's info. - Raises AttributeError if settings.ECOMMERCE_API_SIGNING_KEY is not set. """ data = { 'username': user.username, 'email': user.email } - return jwt.encode(data, getattr(settings, 'ECOMMERCE_API_SIGNING_KEY')) + return jwt.encode(data, ecommerce_api_signing_key) def _enroll(self, course_key, user): """ Enroll the user in the course. """ @@ -79,40 +78,44 @@ class OrdersView(APIView): if not valid: return DetailResponse(error, status=HTTP_406_NOT_ACCEPTABLE) + # Ensure that the course has an honor mode with SKU + honor_mode = CourseMode.mode_for_course(course_key, CourseMode.HONOR) + course_id = unicode(course_key) + + # If there is no honor course mode, this most likely a Prof-Ed course. Return an error so that the JS + # redirects to track selection. + if not honor_mode: + msg = Messages.NO_HONOR_MODE.format(course_id=course_id) + return DetailResponse(msg, status=HTTP_406_NOT_ACCEPTABLE) + elif not honor_mode.sku: + # If there are no course modes with SKUs, enroll the user without contacting the external API. + msg = Messages.NO_SKU_ENROLLED.format(enrollment_mode=CourseMode.HONOR, course_id=course_id, + username=user.username) + log.debug(msg) + self._enroll(course_key, user) + return DetailResponse(msg) + # Ensure that the E-Commerce API is setup properly ecommerce_api_url = getattr(settings, 'ECOMMERCE_API_URL', None) ecommerce_api_signing_key = getattr(settings, 'ECOMMERCE_API_SIGNING_KEY', None) if not (ecommerce_api_url and ecommerce_api_signing_key): self._enroll(course_key, user) - msg = Messages.NO_ECOM_API.format(username=user.username, course_id=unicode(course_key)) + msg = Messages.NO_ECOM_API.format(username=user.username, course_id=course_id) log.debug(msg) return DetailResponse(msg) - # Default to honor mode. In the future we may expand this view to support additional modes. - mode = CourseMode.DEFAULT_MODE_SLUG - course_modes = CourseMode.objects.filter(course_id=course_key, mode_slug=mode)\ - .exclude(sku__isnull=True).exclude(sku__exact='') - - # If there are no course modes with SKUs, enroll the user without contacting the external API. - if not course_modes.exists(): - msg = Messages.NO_SKU_ENROLLED.format(enrollment_mode=mode, course_id=unicode(course_key), - username=user.username) - log.debug(msg) - self._enroll(course_key, user) - return DetailResponse(msg) - # Contact external API headers = { 'Content-Type': 'application/json', - 'Authorization': 'JWT {}'.format(self._get_jwt(user)) + 'Authorization': 'JWT {}'.format(self._get_jwt(user, ecommerce_api_signing_key)) } url = '{}/orders/'.format(ecommerce_api_url.strip('/')) try: timeout = getattr(settings, 'ECOMMERCE_API_TIMEOUT', 5) - response = requests.post(url, data=json.dumps({'sku': course_modes[0].sku}), headers=headers, + response = requests.post(url, data=json.dumps({'sku': honor_mode.sku}), headers=headers, timeout=timeout) except Exception as ex: # pylint: disable=broad-except log.exception('Call to E-Commerce API failed: %s.', ex.message) @@ -144,7 +147,7 @@ class OrdersView(APIView): 'status': order_status, 'complete_status': OrderStatus.COMPLETE, 'username': user.username, - 'course_id': unicode(course_key), + 'course_id': course_id, } log.error(msg, msg_kwargs) diff --git a/lms/envs/aws.py b/lms/envs/aws.py index 0b9f61a0e0..f5cc4daf88 100644 --- a/lms/envs/aws.py +++ b/lms/envs/aws.py @@ -556,3 +556,8 @@ XBLOCK_SETTINGS = ENV_TOKENS.get('XBLOCK_SETTINGS', {}) ##### CDN EXPERIMENT/MONITORING FLAGS ##### PERFORMANCE_GRAPHITE_URL = ENV_TOKENS.get('PERFORMANCE_GRAPHITE_URL', PERFORMANCE_GRAPHITE_URL) CDN_VIDEO_URLS = ENV_TOKENS.get('CDN_VIDEO_URLS', CDN_VIDEO_URLS) + +##### ECOMMERCE API CONFIGURATION SETTINGS ##### +ECOMMERCE_API_URL = ENV_TOKENS.get('ECOMMERCE_API_URL', ECOMMERCE_API_URL) +ECOMMERCE_API_SIGNING_KEY = AUTH_TOKENS.get('ECOMMERCE_API_SIGNING_KEY', ECOMMERCE_API_SIGNING_KEY) +ECOMMERCE_API_TIMEOUT = ENV_TOKENS.get('ECOMMERCE_API_TIMEOUT', ECOMMERCE_API_TIMEOUT) diff --git a/lms/static/require-config-lms.js b/lms/static/require-config-lms.js index 50b3404f24..3530432f09 100644 --- a/lms/static/require-config-lms.js +++ b/lms/static/require-config-lms.js @@ -67,7 +67,6 @@ "ova": 'js/vendor/ova/ova', "catch": 'js/vendor/ova/catch/js/catch', "handlebars": 'js/vendor/ova/catch/js/handlebars-1.1.2', - "mathjax": 'https://cdn.mathjax.org/mathjax/2.4-latest/MathJax.js?config=TeX-MML-AM_HTMLorMML-full' // end of files needed by OVA }, shim: { diff --git a/requirements/edx/github.txt b/requirements/edx/github.txt index 0b71ae36f4..05bc28928e 100644 --- a/requirements/edx/github.txt +++ b/requirements/edx/github.txt @@ -29,7 +29,7 @@ git+https://github.com/mitocw/django-cas.git@60a5b8e5a62e63e0d5d224a87f0b489201a -e git+https://github.com/edx/bok-choy.git@d62839324cbea30dda564596f20175f9d5c28516#egg=bok_choy -e git+https://github.com/edx-solutions/django-splash.git@7579d052afcf474ece1239153cffe1c89935bc4f#egg=django-splash -e git+https://github.com/edx/acid-block.git@e46f9cda8a03e121a00c7e347084d142d22ebfb7#egg=acid-xblock --e git+https://github.com/edx/edx-ora2.git@4773573b79bc530f0fe7c8f90a10491e4224dc2d#egg=edx-ora2 +-e git+https://github.com/edx/edx-ora2.git@release-2015-03-16T17.59#egg=edx-ora2 -e git+https://github.com/edx/edx-submissions.git@8fb070d2a3087dd7656d27022e550d12e3b85ba3#egg=edx-submissions -e git+https://github.com/edx/opaque-keys.git@1254ed4d615a428591850656f39f26509b86d30a#egg=opaque-keys -e git+https://github.com/edx/ease.git@97de68448e5495385ba043d3091f570a699d5b5f#egg=ease