diff --git a/common/lib/xmodule/xmodule/abtest_module.py b/common/lib/xmodule/xmodule/abtest_module.py index 035413a402..ca00db4c9a 100644 --- a/common/lib/xmodule/xmodule/abtest_module.py +++ b/common/lib/xmodule/xmodule/abtest_module.py @@ -49,9 +49,9 @@ class ABTestModule(XModule): return json.dumps({'group': self.group}) def displayable_items(self): - return [self.system.get_module(child) - for child - in self.definition['data']['group_content'][self.group]] + return filter(None, [self.system.get_module(child) + for child + in self.definition['data']['group_content'][self.group]]) # TODO (cpennington): Use Groups should be a first class object, rather than being diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index 2c440cba2f..11d4e090f9 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -6,7 +6,7 @@ from xmodule.util.decorators import lazyproperty from xmodule.graders import load_grading_policy from xmodule.modulestore import Location from xmodule.seq_module import SequenceDescriptor, SequenceModule -from xmodule.timeparse import parse_time +from xmodule.timeparse import parse_time, stringify_time log = logging.getLogger(__name__) @@ -18,16 +18,10 @@ class CourseDescriptor(SequenceDescriptor): super(CourseDescriptor, self).__init__(system, definition, **kwargs) msg = None - try: - self.start = parse_time(self.metadata["start"]) - except KeyError: - msg = "Course loaded without a start date. id = %s" % self.id - except ValueError as e: - msg = "Course loaded with a bad start date. %s '%s'" % (self.id, e) - - # Don't call the tracker from the exception handler. - if msg is not None: - self.start = time.gmtime(0) # The epoch + if self.start is None: + msg = "Course loaded without a valid start date. id = %s" % self.id + # hack it -- start in 1970 + self.metadata['start'] = stringify_time(time.gmtime(0)) log.critical(msg) system.error_tracker(msg) diff --git a/common/lib/xmodule/xmodule/error_module.py b/common/lib/xmodule/xmodule/error_module.py index 20301ee460..bdd7179a0a 100644 --- a/common/lib/xmodule/xmodule/error_module.py +++ b/common/lib/xmodule/xmodule/error_module.py @@ -24,16 +24,8 @@ class ErrorModule(XModule): return self.system.render_template('module-error.html', { 'data' : self.definition['data']['contents'], 'error' : self.definition['data']['error_msg'], - 'is_staff' : self.system.is_staff, }) - def displayable_items(self): - """Hide errors in the profile and table of contents for non-staff - users. - """ - if self.system.is_staff: - return [self] - return [] class ErrorDescriptor(EditingDescriptor): """ diff --git a/common/lib/xmodule/xmodule/timeparse.py b/common/lib/xmodule/xmodule/timeparse.py index 28a437bd5f..117105d085 100644 --- a/common/lib/xmodule/xmodule/timeparse.py +++ b/common/lib/xmodule/xmodule/timeparse.py @@ -3,9 +3,17 @@ Helper functions for handling time in the format we like. """ import time +TIME_FORMAT = "%Y-%m-%dT%H:%M" + def parse_time(time_str): """ - Takes a time string in our format ("%Y-%m-%dT%H:%M"), and returns + Takes a time string in TIME_FORMAT, returns it as a time_struct. Raises ValueError if the string is not in the right format. """ - return time.strptime(time_str, "%Y-%m-%dT%H:%M") + return time.strptime(time_str, TIME_FORMAT) + +def stringify_time(time_struct): + """ + Convert a time struct to a string + """ + return time.strftime(TIME_FORMAT, time_struct) diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py index 94f8010e10..dd2f879113 100644 --- a/common/lib/xmodule/xmodule/x_module.py +++ b/common/lib/xmodule/xmodule/x_module.py @@ -219,9 +219,11 @@ class XModule(HTMLSnippet): Return module instances for all the children of this module. ''' if self._loaded_children is None: - self._loaded_children = [ - self.system.get_module(child) - for child in self.definition.get('children', [])] + # get_module returns None if the current user doesn't have access + # to the location. + self._loaded_children = filter(None, + [self.system.get_module(child) + for child in self.definition.get('children', [])]) return self._loaded_children @@ -385,9 +387,6 @@ class XModuleDescriptor(Plugin, HTMLSnippet): self.category = self.location.category self.shared_state_key = kwargs.get('shared_state_key') - # look for a start time, setting to None if not present - self.start = self._try_parse_time('start') - self._child_instances = None self._inherited_metadata = set() @@ -400,6 +399,15 @@ class XModuleDescriptor(Plugin, HTMLSnippet): return self.metadata.get('display_name', self.url_name.replace('_', ' ')) + @property + def start(self): + """ + If self.metadata contains start, return it. Else return None. + """ + if 'start' not in self.metadata: + return None + return self._try_parse_time('start') + @property def own_metadata(self): """ @@ -609,7 +617,7 @@ class XModuleDescriptor(Plugin, HTMLSnippet): """ if key in self.metadata: try: - parse_time(self.metadata[key]) + return parse_time(self.metadata[key]) except ValueError as e: msg = "Descriptor {} loaded with a bad metadata key '{}': '{}'".format( self.location.url(), self.metadata[key], e) @@ -709,7 +717,8 @@ class ModuleSystem(object): files. Update or remove. get_module - function that takes (location) and returns a corresponding - module instance object. + module instance object. If the current user does not have + access to that location, returns None. render_template - a function that takes (template_file, context), and returns rendered html. diff --git a/lms/djangoapps/courseware/access.py b/lms/djangoapps/courseware/access.py index a8145aa1ce..8039d16896 100644 --- a/lms/djangoapps/courseware/access.py +++ b/lms/djangoapps/courseware/access.py @@ -8,6 +8,7 @@ import time from django.conf import settings from xmodule.course_module import CourseDescriptor +from xmodule.error_module import ErrorDescriptor from xmodule.modulestore import Location from xmodule.timeparse import parse_time from xmodule.x_module import XModule, XModuleDescriptor @@ -49,6 +50,10 @@ def has_access(user, obj, action): if isinstance(obj, CourseDescriptor): return _has_access_course_desc(user, obj, action) + if isinstance(obj, ErrorDescriptor): + return _has_access_error_desc(user, obj, action) + + # NOTE: any descriptor access checkers need to go above this if isinstance(obj, XModuleDescriptor): return _has_access_descriptor(user, obj, action) @@ -94,6 +99,7 @@ def _has_access_course_desc(user, course, action): if (start is None or now > start) and (end is None or now < end): # in enrollment period, so any user is allowed to enroll. + debug("Allow: in enrollment period") return True # otherwise, need staff access @@ -115,6 +121,7 @@ def _has_access_course_desc(user, course, action): # if this feature is on, only allow courses that have ispublic set to be # seen by non-staff if course.metadata.get('ispublic'): + debug("Allow: ACCESS_REQUIRE_STAFF_FOR_COURSE and ispublic") return True return _has_staff_access_to_descriptor(user, course) @@ -130,6 +137,25 @@ def _has_access_course_desc(user, course, action): return _dispatch(checkers, action, user, course) +def _has_access_error_desc(user, descriptor, action): + """ + Only staff should see error descriptors. + + Valid actions: + 'load' -- load this descriptor, showing it to the user. + 'staff' -- staff access to descriptor. + """ + def check_for_staff(): + return _has_staff_access_to_descriptor(user, descriptor) + + checkers = { + 'load': check_for_staff, + 'staff': check_for_staff + } + + return _dispatch(checkers, action, user, descriptor) + + def _has_access_descriptor(user, descriptor, action): """ Check if user has access to this descriptor. @@ -145,6 +171,7 @@ def _has_access_descriptor(user, descriptor, action): def can_load(): # If start dates are off, can always load if settings.MITX_FEATURES['DISABLE_START_DATES']: + debug("Allow: DISABLE_START_DATES") return True # Check start date @@ -152,11 +179,13 @@ def _has_access_descriptor(user, descriptor, action): now = time.gmtime() if now > descriptor.start: # after start date, everyone can see it + debug("Allow: now > start date") return True # otherwise, need staff access return _has_staff_access_to_descriptor(user, descriptor) # No start date, so can always load. + debug("Allow: no start date") return True checkers = { @@ -212,7 +241,9 @@ def _dispatch(table, action, user, obj): result = table[action]() debug("%s user %s, object %s, action %s", 'ALLOWED' if result else 'DENIED', - user, obj, action) + user, + obj.location.url() if isinstance(obj, XModuleDescriptor) else str(obj)[:60], + action) return result raise ValueError("Unknown action for object type '{}': '{}'".format( @@ -239,15 +270,19 @@ def _has_staff_access_to_location(user, location): course is a string: the course field of the location being accessed. ''' if user is None or (not user.is_authenticated()): + debug("Deny: no user or anon user") return False if user.is_staff: + debug("Allow: user.is_staff") return True # If not global staff, is the user in the Auth group for this class? user_groups = [x[1] for x in user.groups.values_list()] staff_group = _course_staff_group_name(location) if staff_group in user_groups: + debug("Allow: user in group %s", staff_group) return True + debug("Deny: user not in group %s", staff_group) return False def _has_staff_access_to_course_id(user, course_id): diff --git a/lms/djangoapps/courseware/grades.py b/lms/djangoapps/courseware/grades.py index aa160ee22a..8417bf9a48 100644 --- a/lms/djangoapps/courseware/grades.py +++ b/lms/djangoapps/courseware/grades.py @@ -63,7 +63,12 @@ def grade(student, request, course, student_module_cache=None): scores = [] # TODO: We need the request to pass into here. If we could forgo that, our arguments # would be simpler - section_module = get_module(student, request, section_descriptor.location, student_module_cache) + section_module = get_module(student, request, + section_descriptor.location, student_module_cache) + if section_module is None: + # student doesn't have access to this module, or something else + # went wrong. + continue # TODO: We may be able to speed this up by only getting a list of children IDs from section_module # Then, we may not need to instatiate any problems if they are already in the database diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 768139c2f0..86ccba7345 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -65,6 +65,9 @@ def toc_for_course(user, request, course, active_chapter, active_section): Everything else comes from the xml, or defaults to "". chapters with name 'hidden' are skipped. + + NOTE: assumes that if we got this far, user has access to course. Returns + None if this is not the case. ''' student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(user, course, depth=2) @@ -144,6 +147,11 @@ def get_module(user, request, location, student_module_cache, position=None): location = Location(location) descriptor = modulestore().get_item(location) + # Short circuit--if the user shouldn't have access, bail without doing any work + if not has_access(user, descriptor, 'load'): + return None + + #TODO Only check the cache if this module can possibly have state instance_module = None shared_module = None @@ -192,6 +200,9 @@ def get_module(user, request, location, student_module_cache, position=None): 'default_queuename': xqueue_default_queuename.replace(' ', '_')} def _get_module(location): + """ + Delegate to get_module. It does an access check, so may return None + """ return get_module(user, request, location, student_module_cache, position) @@ -308,10 +319,14 @@ def xqueue_callback(request, course_id, userid, id, dispatch): student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(user, modulestore().get_item(id)) instance = get_module(user, request, id, student_module_cache) + if instance is None: + log.debug("No module {} for user {}--access denied?".format(id, user)) + raise Http404 + instance_module = get_instance_module(user, instance, student_module_cache) if instance_module is None: - log.debug("Couldn't find module '%s' for user '%s'", id, user) + log.debug("Couldn't find instance of module '%s' for user '%s'", id, user) raise Http404 oldgrade = instance_module.grade @@ -365,6 +380,11 @@ def modx_dispatch(request, dispatch=None, id=None, course_id=None): student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(request.user, modulestore().get_item(id)) instance = get_module(request.user, request, id, student_module_cache) + if instance is None: + # Either permissions just changed, or someone is trying to be clever + # and load something they shouldn't have access to. + log.debug("No module {} for user {}--access denied?".format(id, user)) + raise Http404 instance_module = get_instance_module(request.user, instance, student_module_cache) shared_module = get_shared_instance_module(request.user, instance, student_module_cache) diff --git a/lms/djangoapps/courseware/tests/tests.py b/lms/djangoapps/courseware/tests/tests.py index 8eb9c22584..92ddb2767e 100644 --- a/lms/djangoapps/courseware/tests/tests.py +++ b/lms/djangoapps/courseware/tests/tests.py @@ -25,7 +25,7 @@ from student.models import Registration from xmodule.modulestore.django import modulestore from xmodule.modulestore import Location from xmodule.modulestore.xml_importer import import_from_xml - +from xmodule.timeparse import stringify_time def parse_json(response): """Parse response, which is assumed to be json""" @@ -371,8 +371,8 @@ class TestViewAuth(PageLoader): # Make courses start in the future tomorrow = time.time() + 24*3600 - self.toy.start = self.toy.metadata['start'] = time.gmtime(tomorrow) - self.full.start = self.full.metadata['start'] = time.gmtime(tomorrow) + self.toy.metadata['start'] = stringify_time(time.gmtime(tomorrow)) + self.full.metadata['start'] = stringify_time(time.gmtime(tomorrow)) self.assertFalse(self.toy.has_started()) self.assertFalse(self.full.has_started()) diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index f54a76da1f..97bafb3e0f 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -136,6 +136,10 @@ def index(request, course_id, chapter=None, section=None, module = get_module(request.user, request, section_descriptor.location, student_module_cache) + if module is None: + # User is probably being clever and trying to access something + # they don't have access to. + raise Http404 context['content'] = module.get_html() else: log.warning("Couldn't find a section descriptor for course_id '{0}'," diff --git a/lms/templates/module-error.html b/lms/templates/module-error.html index 7c731db17a..2a51f5b11a 100644 --- a/lms/templates/module-error.html +++ b/lms/templates/module-error.html @@ -2,12 +2,10 @@
We're sorry, this module is temporarily unavailable. Our staff is working to fix it as soon as possible. Please email us at technical@mitx.mit.edu to report any problems or downtime.
-% if is_staff: -Error: ${error | h}
Raw data: ${data | h}
-% endif