From 58b4982d2da013039367418ab984c9bdadb2e6be Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Mon, 10 Nov 2014 12:08:28 -0500 Subject: [PATCH] harden down access to the shoppingcart if the ENABLE_PAID_COURSE_REGISTRATION flag is not set (either via the main Django configuration settings) or through microsite overrides --- common/djangoapps/student/tests/tests.py | 1 + lms/djangoapps/courseware/views.py | 5 ++-- lms/djangoapps/instructor/tests/test_api.py | 1 + .../shoppingcart/context_processor.py | 15 ++-------- lms/djangoapps/shoppingcart/decorators.py | 22 ++++++++++++++ .../shoppingcart/tests/test_views.py | 29 +++++++++++++++++++ lms/djangoapps/shoppingcart/urls.py | 26 +++++++---------- lms/djangoapps/shoppingcart/utils.py | 24 +++++++++++++++ lms/djangoapps/shoppingcart/views.py | 12 +++++++- 9 files changed, 105 insertions(+), 30 deletions(-) create mode 100644 lms/djangoapps/shoppingcart/decorators.py create mode 100644 lms/djangoapps/shoppingcart/utils.py diff --git a/common/djangoapps/student/tests/tests.py b/common/djangoapps/student/tests/tests.py index 1ef2defca4..3bf2fde715 100644 --- a/common/djangoapps/student/tests/tests.py +++ b/common/djangoapps/student/tests/tests.py @@ -265,6 +265,7 @@ class DashboardTest(ModuleStoreTestCase): @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') @patch('courseware.views.log.warning') + @patch.dict('django.conf.settings.FEATURES', {'ENABLE_PAID_COURSE_REGISTRATION': True}) def test_blocked_course_scenario(self, log_warning): self.client.login(username="jack", password="test") diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index 6cb05bd0a9..cf44331ec7 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -49,6 +49,7 @@ from xmodule.tabs import CourseTabList, StaffGradingTab, PeerGradingTab, OpenEnd from xmodule.x_module import STUDENT_VIEW import shoppingcart from shoppingcart.models import CourseRegistrationCode +from shoppingcart.utils import is_shopping_cart_enabled from opaque_keys import InvalidKeyError from microsite_configuration import microsite @@ -731,8 +732,8 @@ def course_about(request, course_id): registration_price = 0 in_cart = False reg_then_add_to_cart_link = "" - if (settings.FEATURES.get('ENABLE_SHOPPING_CART') and - settings.FEATURES.get('ENABLE_PAID_COURSE_REGISTRATION')): + + if (is_shopping_cart_enabled()): registration_price = CourseMode.min_course_price_for_currency(course_key, settings.PAID_COURSE_REGISTRATION_CURRENCY[0]) if request.user.is_authenticated(): diff --git a/lms/djangoapps/instructor/tests/test_api.py b/lms/djangoapps/instructor/tests/test_api.py index feda24b885..b42c1dc3ad 100644 --- a/lms/djangoapps/instructor/tests/test_api.py +++ b/lms/djangoapps/instructor/tests/test_api.py @@ -1604,6 +1604,7 @@ class TestInstructorAPILevelsAccess(ModuleStoreTestCase, LoginEnrollmentTestCase @ddt.ddt @override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE) +@patch.dict('django.conf.settings.FEATURES', {'ENABLE_PAID_COURSE_REGISTRATION': True}) class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCase): """ Test endpoints that show data without side effects. diff --git a/lms/djangoapps/shoppingcart/context_processor.py b/lms/djangoapps/shoppingcart/context_processor.py index e884bef7e3..20e096155b 100644 --- a/lms/djangoapps/shoppingcart/context_processor.py +++ b/lms/djangoapps/shoppingcart/context_processor.py @@ -5,9 +5,8 @@ navigation. We want to do this in the context_processor to 1) keep database accesses out of templates (this led to a transaction bug with user email changes) 2) because navigation.html is "called" by being included in other templates, there's no "views.py" to put this. """ -from django.conf import settings + import shoppingcart -from microsite_configuration import microsite def user_has_cart_context_processor(request): @@ -19,16 +18,8 @@ def user_has_cart_context_processor(request): display_shopping_cart = ( # user is logged in and request.user.is_authenticated() and - # settings enable paid course reg - microsite.get_value( - 'ENABLE_PAID_COURSE_REGISTRATION', - settings.FEATURES.get('ENABLE_PAID_COURSE_REGISTRATION') - ) and - # settings enable shopping cart - microsite.get_value( - 'ENABLE_SHOPPING_CART', - settings.FEATURES.get('ENABLE_SHOPPING_CART') - ) and + # do we have the feature turned on + shoppingcart.utils.is_shopping_cart_enabled() and # user's cart has PaidCourseRegistrations shoppingcart.models.Order.user_cart_has_items( request.user, diff --git a/lms/djangoapps/shoppingcart/decorators.py b/lms/djangoapps/shoppingcart/decorators.py new file mode 100644 index 0000000000..9f4366f8bb --- /dev/null +++ b/lms/djangoapps/shoppingcart/decorators.py @@ -0,0 +1,22 @@ +""" +This file defines any decorators used by the shopping cart app +""" + +from django.http import Http404 +from .utils import is_shopping_cart_enabled + + +def enforce_shopping_cart_enabled(func): + """ + Is a decorator that forces a wrapped method to be run in a runtime + which has the ENABLE_SHOPPING_CART flag set + """ + def func_wrapper(*args, **kwargs): + """ + Wrapper function that does the enforcement that + the shopping cart feature is enabled + """ + if not is_shopping_cart_enabled(): + raise Http404 + return func(*args, **kwargs) + return func_wrapper diff --git a/lms/djangoapps/shoppingcart/tests/test_views.py b/lms/djangoapps/shoppingcart/tests/test_views.py index 873960f068..aed264158f 100644 --- a/lms/djangoapps/shoppingcart/tests/test_views.py +++ b/lms/djangoapps/shoppingcart/tests/test_views.py @@ -64,6 +64,7 @@ MODULESTORE_CONFIG = mixed_store_config(settings.COMMON_TEST_DATA_ROOT, {}, incl @override_settings(MODULESTORE=MODULESTORE_CONFIG) +@patch.dict('django.conf.settings.FEATURES', {'ENABLE_PAID_COURSE_REGISTRATION': True}) class ShoppingCartViewsTests(ModuleStoreTestCase): def setUp(self): patcher = patch('student.models.tracker') @@ -963,8 +964,36 @@ class ShoppingCartViewsTests(ModuleStoreTestCase): ((template, _context), _tmp) = render_mock.call_args self.assertEqual(template, cert_item.single_item_receipt_template) + def _assert_404(self, url, use_post=False): + """ + Helper method to assert that a given url will return a 404 status code + """ + if use_post: + response = self.client.post(url) + else: + response = self.client.get(url) + self.assertEquals(response.status_code, 404) + + @patch.dict('django.conf.settings.FEATURES', {'ENABLE_PAID_COURSE_REGISTRATION': False}) + def test_disabled_paid_courses(self): + """ + Assert that the pages that require ENABLE_PAID_COURSE_REGISTRATION=True return a + HTTP 404 status code when we have this flag turned off + """ + self.login_user() + self._assert_404(reverse('shoppingcart.views.show_cart', args=[])) + self._assert_404(reverse('shoppingcart.views.clear_cart', args=[])) + self._assert_404(reverse('shoppingcart.views.remove_item', args=[]), use_post=True) + self._assert_404(reverse('shoppingcart.views.register_code_redemption', args=["testing"])) + self._assert_404(reverse('shoppingcart.views.use_code', args=[]), use_post=True) + self._assert_404(reverse('shoppingcart.views.update_user_cart', args=[])) + self._assert_404(reverse('shoppingcart.views.reset_code_redemption', args=[]), use_post=True) + self._assert_404(reverse('shoppingcart.views.billing_details', args=[])) + self._assert_404(reverse('shoppingcart.views.register_courses', args=[])) + @override_settings(MODULESTORE=MODULESTORE_CONFIG) +@patch.dict('django.conf.settings.FEATURES', {'ENABLE_PAID_COURSE_REGISTRATION': True}) class RegistrationCodeRedemptionCourseEnrollment(ModuleStoreTestCase): """ Test suite for RegistrationCodeRedemption Course Enrollments diff --git a/lms/djangoapps/shoppingcart/urls.py b/lms/djangoapps/shoppingcart/urls.py index 02776da0a0..2ae1f408a4 100644 --- a/lms/djangoapps/shoppingcart/urls.py +++ b/lms/djangoapps/shoppingcart/urls.py @@ -6,23 +6,19 @@ urlpatterns = patterns('shoppingcart.views', # nopep8 url(r'^receipt/(?P[0-9]*)/$', 'show_receipt'), url(r'^donation/$', 'donate', name='donation'), url(r'^csv_report/$', 'csv_report', name='payment_csv_report'), + # These following URLs are only valid if the ENABLE_SHOPPING_CART feature flag is set + url(r'^$', 'show_cart'), + url(r'^clear/$', 'clear_cart'), + url(r'^remove_item/$', 'remove_item'), + url(r'^add/course/{}/$'.format(settings.COURSE_ID_PATTERN), 'add_course_to_cart', name='add_course_to_cart'), + url(r'^register/redeem/(?P[0-9A-Za-z]+)/$', 'register_code_redemption', name='register_code_redemption'), + url(r'^use_code/$', 'use_code'), + url(r'^update_user_cart/$', 'update_user_cart'), + url(r'^reset_code_redemption/$', 'reset_code_redemption'), + url(r'^billing_details/$', 'billing_details', name='billing_details'), + url(r'^register_courses/$', 'register_courses'), ) -if settings.FEATURES['ENABLE_SHOPPING_CART']: - urlpatterns += patterns( - 'shoppingcart.views', - url(r'^$', 'show_cart'), - url(r'^clear/$', 'clear_cart'), - url(r'^remove_item/$', 'remove_item'), - url(r'^add/course/{}/$'.format(settings.COURSE_ID_PATTERN), 'add_course_to_cart', name='add_course_to_cart'), - url(r'^register/redeem/(?P[0-9A-Za-z]+)/$', 'register_code_redemption', name='register_code_redemption'), - url(r'^use_code/$', 'use_code'), - url(r'^update_user_cart/$', 'update_user_cart'), - url(r'^reset_code_redemption/$', 'reset_code_redemption'), - url(r'^billing_details/$', 'billing_details', name='billing_details'), - url(r'^register_courses/$', 'register_courses'), - ) - if settings.FEATURES.get('ENABLE_PAYMENT_FAKE'): from shoppingcart.tests.payment_fake import PaymentFakeView urlpatterns += patterns( diff --git a/lms/djangoapps/shoppingcart/utils.py b/lms/djangoapps/shoppingcart/utils.py new file mode 100644 index 0000000000..de469f29be --- /dev/null +++ b/lms/djangoapps/shoppingcart/utils.py @@ -0,0 +1,24 @@ +""" +Utility methods for the Shopping Cart app +""" + +from django.conf import settings +from microsite_configuration import microsite + + +def is_shopping_cart_enabled(): + """ + Utility method to check the various configuration to verify that + all of the settings have been enabled + """ + enable_paid_course_registration = microsite.get_value( + 'ENABLE_PAID_COURSE_REGISTRATION', + settings.FEATURES.get('ENABLE_PAID_COURSE_REGISTRATION') + ) + + enable_shopping_cart = microsite.get_value( + 'ENABLE_SHOPPING_CART', + settings.FEATURES.get('ENABLE_SHOPPING_CART') + ) + + return (enable_paid_course_registration and enable_shopping_cart) diff --git a/lms/djangoapps/shoppingcart/views.py b/lms/djangoapps/shoppingcart/views.py index 53c72219ab..aa1985f95f 100644 --- a/lms/djangoapps/shoppingcart/views.py +++ b/lms/djangoapps/shoppingcart/views.py @@ -33,7 +33,7 @@ from .exceptions import ( ) from .models import ( Order, OrderTypes, - PaidCourseRegistration, OrderItem, Coupon, CourseRegCodeItem, + PaidCourseRegistration, OrderItem, Coupon, CouponRedemption, CourseRegistrationCode, RegistrationCodeRedemption, Donation, DonationConfiguration ) @@ -44,6 +44,7 @@ from .processors import ( import json from xmodule_django.models import CourseKeyField +from .decorators import enforce_shopping_cart_enabled log = logging.getLogger("shoppingcart") AUDIT_LOG = logging.getLogger("audit") @@ -94,6 +95,7 @@ def add_course_to_cart(request, course_id): @login_required +@enforce_shopping_cart_enabled def update_user_cart(request): """ when user change the number-of-students from the UI then @@ -127,6 +129,7 @@ def update_user_cart(request): @login_required +@enforce_shopping_cart_enabled def show_cart(request): """ This view shows cart items. @@ -158,6 +161,7 @@ def show_cart(request): @login_required +@enforce_shopping_cart_enabled def clear_cart(request): cart = Order.get_cart_for_user(request.user) cart.clear() @@ -175,6 +179,7 @@ def clear_cart(request): @login_required +@enforce_shopping_cart_enabled def remove_item(request): """ This will remove an item from the user cart and also delete the corresponding coupon codes redemption. @@ -227,6 +232,7 @@ def remove_code_redemption(order_item_course_id, item_id, item, user): @login_required +@enforce_shopping_cart_enabled def reset_code_redemption(request): """ This method reset the code redemption from user cart items. @@ -239,6 +245,7 @@ def reset_code_redemption(request): @login_required +@enforce_shopping_cart_enabled def use_code(request): """ This method may generate the discount against valid coupon code @@ -291,6 +298,7 @@ def get_reg_code_validity(registration_code, request, limiter): @require_http_methods(["GET", "POST"]) @login_required +@enforce_shopping_cart_enabled def register_code_redemption(request, registration_code): """ This view allows the student to redeem the registration code @@ -382,6 +390,7 @@ def use_coupon_code(coupons, user): @login_required +@enforce_shopping_cart_enabled def register_courses(request): """ This method enroll the user for available course(s) @@ -518,6 +527,7 @@ def postpay_callback(request): @require_http_methods(["GET", "POST"]) @login_required +@enforce_shopping_cart_enabled def billing_details(request): """ This is the view for capturing additional billing details