Commit e8186a18 by Ned Batchelder

Merge pull request #11953 from edx/preview-security-fix

Restrict non-staff users to access preview content.
parents eebae966 c0e78618
...@@ -6,7 +6,7 @@ import ddt ...@@ -6,7 +6,7 @@ import ddt
import json import json
import copy import copy
import mock import mock
from mock import patch from mock import Mock, patch
import unittest import unittest
from django.conf import settings from django.conf import settings
...@@ -20,7 +20,7 @@ from models.settings.course_metadata import CourseMetadata ...@@ -20,7 +20,7 @@ from models.settings.course_metadata import CourseMetadata
from models.settings.encoder import CourseSettingsEncoder from models.settings.encoder import CourseSettingsEncoder
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
from openedx.core.djangoapps.models.course_details import CourseDetails from openedx.core.djangoapps.models.course_details import CourseDetails
from student.roles import CourseInstructorRole from student.roles import CourseInstructorRole, CourseStaffRole
from student.tests.factories import UserFactory from student.tests.factories import UserFactory
from xmodule.fields import Date from xmodule.fields import Date
from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore import ModuleStoreEnum
...@@ -29,7 +29,7 @@ from xmodule.modulestore.tests.factories import CourseFactory ...@@ -29,7 +29,7 @@ from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.tabs import InvalidTabsException from xmodule.tabs import InvalidTabsException
from util.milestones_helpers import seed_milestone_relationship_types from util.milestones_helpers import seed_milestone_relationship_types
from .utils import CourseTestCase from .utils import CourseTestCase, AjaxEnabledTestClient
def get_url(course_id, handler_name='settings_handler'): def get_url(course_id, handler_name='settings_handler'):
...@@ -955,6 +955,23 @@ class CourseMetadataEditingTest(CourseTestCase): ...@@ -955,6 +955,23 @@ class CourseMetadataEditingTest(CourseTestCase):
tab_list.append(self.notes_tab) tab_list.append(self.notes_tab)
self.assertEqual(tab_list, course.tabs) self.assertEqual(tab_list, course.tabs)
@patch.dict(settings.FEATURES, {'ENABLE_EDXNOTES': True})
@patch('xmodule.util.django.get_current_request')
def test_post_settings_with_staff_not_enrolled(self, mock_request):
"""
Tests that we can post advance settings when course staff is not enrolled.
"""
mock_request.return_value = Mock(META={'HTTP_HOST': 'localhost'})
user = UserFactory.create(is_staff=True)
CourseStaffRole(self.course.id).add_users(user)
client = AjaxEnabledTestClient()
client.login(username=user.username, password=user.password)
response = self.client.ajax_post(self.course_setting_url, {
'advanced_modules': {"value": [""]}
})
self.assertEqual(response.status_code, 200)
class CourseGraderUpdatesTest(CourseTestCase): class CourseGraderUpdatesTest(CourseTestCase):
""" """
......
...@@ -79,7 +79,7 @@ class LMSLinksTestCase(TestCase): ...@@ -79,7 +79,7 @@ class LMSLinksTestCase(TestCase):
link = utils.get_lms_link_for_item(location, True) link = utils.get_lms_link_for_item(location, True)
self.assertEquals( self.assertEquals(
link, link,
"//preview/courses/mitX/101/test/jump_to/i4x://mitX/101/vertical/contacting_us" "//preview.localhost/courses/mitX/101/test/jump_to/i4x://mitX/101/vertical/contacting_us"
) )
# now test with the course' location # now test with the course' location
......
...@@ -73,7 +73,7 @@ ...@@ -73,7 +73,7 @@
"ENABLE_S3_GRADE_DOWNLOADS": true, "ENABLE_S3_GRADE_DOWNLOADS": true,
"ENTRANCE_EXAMS": true, "ENTRANCE_EXAMS": true,
"MILESTONES_APP": true, "MILESTONES_APP": true,
"PREVIEW_LMS_BASE": "localhost:8003", "PREVIEW_LMS_BASE": "preview.localhost:8003",
"SUBDOMAIN_BRANDING": false, "SUBDOMAIN_BRANDING": false,
"SUBDOMAIN_COURSE_LISTINGS": false, "SUBDOMAIN_COURSE_LISTINGS": false,
"ALLOW_ALL_ADVANCED_COMPONENTS": true, "ALLOW_ALL_ADVANCED_COMPONENTS": true,
......
...@@ -134,7 +134,8 @@ DATABASES = { ...@@ -134,7 +134,8 @@ DATABASES = {
MIGRATION_MODULES = {app: "app.migrations_not_used_in_tests" for app in INSTALLED_APPS} MIGRATION_MODULES = {app: "app.migrations_not_used_in_tests" for app in INSTALLED_APPS}
LMS_BASE = "localhost:8000" LMS_BASE = "localhost:8000"
FEATURES['PREVIEW_LMS_BASE'] = "preview" FEATURES['PREVIEW_LMS_BASE'] = "preview.localhost"
CACHES = { CACHES = {
# This is the cache used for most things. Askbot will not work without a # This is the cache used for most things. Askbot will not work without a
......
...@@ -60,7 +60,10 @@ from courseware.access_response import ( ...@@ -60,7 +60,10 @@ from courseware.access_response import (
MobileAvailabilityError, MobileAvailabilityError,
VisibilityError, VisibilityError,
) )
from courseware.access_utils import adjust_start_date, check_start_date, debug, ACCESS_GRANTED, ACCESS_DENIED from courseware.access_utils import (
adjust_start_date, check_start_date, debug, ACCESS_GRANTED, ACCESS_DENIED,
in_preview_mode
)
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -102,6 +105,10 @@ def has_access(user, action, obj, course_key=None): ...@@ -102,6 +105,10 @@ def has_access(user, action, obj, course_key=None):
if isinstance(course_key, CCXLocator): if isinstance(course_key, CCXLocator):
course_key = course_key.to_course_locator() course_key = course_key.to_course_locator()
if in_preview_mode():
if not bool(has_staff_access_to_preview_mode(user=user, obj=obj, course_key=course_key)):
return ACCESS_DENIED
# delegate the work to type-specific functions. # delegate the work to type-specific functions.
# (start with more specific types, then get more general) # (start with more specific types, then get more general)
if isinstance(obj, CourseDescriptor): if isinstance(obj, CourseDescriptor):
...@@ -139,6 +146,52 @@ def has_access(user, action, obj, course_key=None): ...@@ -139,6 +146,52 @@ def has_access(user, action, obj, course_key=None):
# ================ Implementation helpers ================================ # ================ Implementation helpers ================================
def has_staff_access_to_preview_mode(user, obj, course_key=None):
"""
Returns whether user has staff access to specified modules or not.
Arguments:
user: a Django user object.
obj: The object to check access for.
course_key: A course_key specifying which course this access is for.
Returns an AccessResponse object.
"""
if course_key is None:
if isinstance(obj, CourseDescriptor) or isinstance(obj, CourseOverview):
course_key = obj.id
elif isinstance(obj, ErrorDescriptor):
course_key = obj.location.course_key
elif isinstance(obj, XModule):
course_key = obj.descriptor.course_key
elif isinstance(obj, XBlock):
course_key = obj.location.course_key
elif isinstance(obj, CCXLocator):
course_key = obj.to_course_locator()
elif isinstance(obj, CourseKey):
course_key = obj
elif isinstance(obj, UsageKey):
course_key = obj.course_key
if course_key is None:
if GlobalStaff().has_user(user):
return ACCESS_GRANTED
else:
return ACCESS_DENIED
return _has_access_to_course(user, 'staff', course_key=course_key)
def _can_access_descriptor_with_start_date(user, descriptor, course_key): # pylint: disable=invalid-name def _can_access_descriptor_with_start_date(user, descriptor, course_key): # pylint: disable=invalid-name
""" """
Checks if a user has access to a descriptor based on its start date. Checks if a user has access to a descriptor based on its start date.
...@@ -659,7 +712,7 @@ def _has_access_to_course(user, access_level, course_key): ...@@ -659,7 +712,7 @@ def _has_access_to_course(user, access_level, course_key):
debug("Deny: no user or anon user") debug("Deny: no user or anon user")
return ACCESS_DENIED return ACCESS_DENIED
if is_masquerading_as_student(user, course_key): if not in_preview_mode() and is_masquerading_as_student(user, course_key):
return ACCESS_DENIED return ACCESS_DENIED
if GlobalStaff().has_user(user): if GlobalStaff().has_user(user):
......
...@@ -75,4 +75,5 @@ def in_preview_mode(): ...@@ -75,4 +75,5 @@ def in_preview_mode():
Returns whether the user is in preview mode or not. Returns whether the user is in preview mode or not.
""" """
hostname = get_current_request_hostname() hostname = get_current_request_hostname()
return bool(hostname and settings.PREVIEW_DOMAIN in hostname.split('.')) preview_lms_base = settings.FEATURES.get('PREVIEW_LMS_BASE', None)
return bool(preview_lms_base and hostname and hostname.split(':')[0] == preview_lms_base.split(':')[0])
...@@ -402,6 +402,13 @@ if 'DJFS' in AUTH_TOKENS and AUTH_TOKENS['DJFS'] is not None: ...@@ -402,6 +402,13 @@ if 'DJFS' in AUTH_TOKENS and AUTH_TOKENS['DJFS'] is not None:
############### Module Store Items ########## ############### Module Store Items ##########
HOSTNAME_MODULESTORE_DEFAULT_MAPPINGS = ENV_TOKENS.get('HOSTNAME_MODULESTORE_DEFAULT_MAPPINGS', {}) HOSTNAME_MODULESTORE_DEFAULT_MAPPINGS = ENV_TOKENS.get('HOSTNAME_MODULESTORE_DEFAULT_MAPPINGS', {})
# PREVIEW DOMAIN must be present in HOSTNAME_MODULESTORE_DEFAULT_MAPPINGS for the preview to show draft changes
if 'PREVIEW_LMS_BASE' in FEATURES and FEATURES['PREVIEW_LMS_BASE'] != '':
PREVIEW_DOMAIN = FEATURES['PREVIEW_LMS_BASE'].split(':')[0]
# update dictionary with preview domain regex
HOSTNAME_MODULESTORE_DEFAULT_MAPPINGS.update({
PREVIEW_DOMAIN: 'draft-preferred'
})
############### Mixed Related(Secure/Not-Secure) Items ########## ############### Mixed Related(Secure/Not-Secure) Items ##########
LMS_SEGMENT_KEY = AUTH_TOKENS.get('SEGMENT_KEY') LMS_SEGMENT_KEY = AUTH_TOKENS.get('SEGMENT_KEY')
......
...@@ -82,7 +82,7 @@ ...@@ -82,7 +82,7 @@
"ENABLE_S3_GRADE_DOWNLOADS": true, "ENABLE_S3_GRADE_DOWNLOADS": true,
"ENABLE_THIRD_PARTY_AUTH": true, "ENABLE_THIRD_PARTY_AUTH": true,
"ENABLE_COMBINED_LOGIN_REGISTRATION": true, "ENABLE_COMBINED_LOGIN_REGISTRATION": true,
"PREVIEW_LMS_BASE": "localhost:8003", "PREVIEW_LMS_BASE": "preview.localhost:8003",
"SUBDOMAIN_BRANDING": false, "SUBDOMAIN_BRANDING": false,
"SUBDOMAIN_COURSE_LISTINGS": false, "SUBDOMAIN_COURSE_LISTINGS": false,
"ALLOW_AUTOMATED_SIGNUPS": true, "ALLOW_AUTOMATED_SIGNUPS": true,
......
...@@ -2597,9 +2597,6 @@ PROFILE_IMAGE_SECRET_KEY = 'placeholder secret key' ...@@ -2597,9 +2597,6 @@ PROFILE_IMAGE_SECRET_KEY = 'placeholder secret key'
PROFILE_IMAGE_MAX_BYTES = 1024 * 1024 PROFILE_IMAGE_MAX_BYTES = 1024 * 1024
PROFILE_IMAGE_MIN_BYTES = 100 PROFILE_IMAGE_MIN_BYTES = 100
# This is to check the domain in case of preview.
PREVIEW_DOMAIN = 'preview'
# Sets the maximum number of courses listed on the homepage # Sets the maximum number of courses listed on the homepage
# If set to None, all courses will be listed on the homepage # If set to None, all courses will be listed on the homepage
HOMEPAGE_COURSE_MAX = None HOMEPAGE_COURSE_MAX = None
......
...@@ -397,6 +397,14 @@ YOUTUBE_PORT = 8031 ...@@ -397,6 +397,14 @@ YOUTUBE_PORT = 8031
LTI_PORT = 8765 LTI_PORT = 8765
VIDEO_SOURCE_PORT = 8777 VIDEO_SOURCE_PORT = 8777
FEATURES['PREVIEW_LMS_BASE'] = "preview.localhost"
############### Module Store Items ##########
PREVIEW_DOMAIN = FEATURES['PREVIEW_LMS_BASE'].split(':')[0]
HOSTNAME_MODULESTORE_DEFAULT_MAPPINGS = {
PREVIEW_DOMAIN: 'draft-preferred'
}
################### Make tests faster ################### Make tests faster
#http://slacy.com/blog/2012/04/make-your-tests-faster-in-django-1-4/ #http://slacy.com/blog/2012/04/make-your-tests-faster-in-django-1-4/
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment