Commit b5ab6450 by John Eskew

Merge pull request #11331 from CredoReference/invalid-studio-user-permissions

Invalid StudioPermissionsService object in API to show/save xblock settings in CMS
parents 04b80f04 bab44282
......@@ -23,6 +23,8 @@ from mock import Mock
from opaque_keys.edx.locator import CourseKey, LibraryLocator
from openedx.core.djangoapps.content.course_structures.tests import SignalDisconnectTestMixin
from xblock_django.user_service import DjangoXBlockUserService
from xmodule.x_module import STUDIO_VIEW
from student import auth
class LibraryTestCase(ModuleStoreTestCase):
......@@ -30,16 +32,22 @@ class LibraryTestCase(ModuleStoreTestCase):
Common functionality for content libraries tests
"""
def setUp(self):
user_password = super(LibraryTestCase, self).setUp()
self.user_password = super(LibraryTestCase, self).setUp()
self.client = AjaxEnabledTestClient()
self.client.login(username=self.user.username, password=user_password)
self._login_as_staff_user(logout_first=False)
self.lib_key = self._create_library()
self.library = modulestore().get_library(self.lib_key)
self.session_data = {} # Used by _bind_module
def _login_as_staff_user(self, logout_first=True):
""" Login as a staff user """
if logout_first:
self.client.logout()
self.client.login(username=self.user.username, password=self.user_password)
def _create_library(self, org="org", library="lib", display_name="Test Library"):
"""
Helper method used to create a library. Uses the REST API.
......@@ -729,6 +737,64 @@ class TestLibraryAccess(SignalDisconnectTestMixin, LibraryTestCase):
lc_block = self._refresh_children(lc_block, status_code_expected=200 if expected_result else 403)
self.assertEqual(len(lc_block.children), 1 if expected_result else 0)
def test_studio_user_permissions(self):
"""
Test that user could attach to the problem only libraries that he has access (or which were created by him).
This test was created on the basis of bug described in the pull requests on github:
https://github.com/edx/edx-platform/pull/11331
https://github.com/edx/edx-platform/pull/11611
"""
self._create_library(org='admin_org_1', library='lib_adm_1', display_name='admin_lib_1')
self._create_library(org='admin_org_2', library='lib_adm_2', display_name='admin_lib_2')
self._login_as_non_staff_user()
self._create_library(org='staff_org_1', library='lib_staff_1', display_name='staff_lib_1')
self._create_library(org='staff_org_2', library='lib_staff_2', display_name='staff_lib_2')
with modulestore().default_store(ModuleStoreEnum.Type.split):
course = CourseFactory.create()
instructor_role = CourseInstructorRole(course.id)
auth.add_users(self.user, instructor_role, self.non_staff_user)
lib_block = ItemFactory.create(
category='library_content',
parent_location=course.location,
user_id=self.non_staff_user.id,
publish_item=False
)
def _get_settings_html():
"""
Helper function to get block settings HTML
Used to check the available libraries.
"""
edit_view_url = reverse_usage_url("xblock_view_handler", lib_block.location, {"view_name": STUDIO_VIEW})
resp = self.client.get_json(edit_view_url)
self.assertEquals(resp.status_code, 200)
return parse_json(resp)['html']
self._login_as_staff_user()
staff_settings_html = _get_settings_html()
self.assertIn('staff_lib_1', staff_settings_html)
self.assertIn('staff_lib_2', staff_settings_html)
self.assertIn('admin_lib_1', staff_settings_html)
self.assertIn('admin_lib_2', staff_settings_html)
self._login_as_non_staff_user()
response = self.client.get_json(LIBRARY_REST_URL)
staff_libs = parse_json(response)
self.assertEquals(2, len(staff_libs))
non_staff_settings_html = _get_settings_html()
self.assertIn('staff_lib_1', non_staff_settings_html)
self.assertIn('staff_lib_2', non_staff_settings_html)
self.assertNotIn('admin_lib_1', non_staff_settings_html)
self.assertNotIn('admin_lib_2', non_staff_settings_html)
@ddt.ddt
@override_settings(SEARCH_ENGINE=None)
......
......@@ -21,7 +21,7 @@ from xblock.runtime import Mixologist
from contentstore.utils import get_lms_link_for_item
from contentstore.views.helpers import get_parent_xblock, is_unit, xblock_type_display_name
from contentstore.views.item import create_xblock_info, add_container_page_publishing_info
from contentstore.views.item import create_xblock_info, add_container_page_publishing_info, StudioEditModuleRuntime
from opaque_keys.edx.keys import UsageKey
......@@ -330,6 +330,7 @@ def component_handler(request, usage_key_string, handler, suffix=''):
usage_key = UsageKey.from_string(usage_key_string)
descriptor = modulestore().get_item(usage_key)
descriptor.xmodule_runtime = StudioEditModuleRuntime(request.user)
# Let the module handle the AJAX
req = django_to_webob_request(request)
......
......@@ -21,6 +21,7 @@ from opaque_keys.edx.locator import LibraryUsageLocator
from pytz import UTC
from xblock.fields import Scope
from xblock.fragment import Fragment
from xblock_django.user_service import DjangoXBlockUserService
from cms.lib.xblock.authoring_mixin import VISIBILITY_VIEW
from contentstore.utils import (
......@@ -51,6 +52,7 @@ from xmodule.modulestore.inheritance import own_metadata
from xmodule.tabs import CourseTabList
from xmodule.x_module import PREVIEW_VIEWS, STUDIO_VIEW, STUDENT_VIEW, DEPRECATION_VSCOMPAT_EVENT
__all__ = [
'orphan_handler', 'xblock_handler', 'xblock_view_handler', 'xblock_outline_handler', 'xblock_container_handler'
]
......@@ -198,6 +200,49 @@ def xblock_handler(request, usage_key_string):
)
class StudioPermissionsService(object):
"""
Service that can provide information about a user's permissions.
Deprecated. To be replaced by a more general authorization service.
Only used by LibraryContentDescriptor (and library_tools.py).
"""
def __init__(self, user):
self._user = user
def can_read(self, course_key):
""" Does the user have read access to the given course/library? """
return has_studio_read_access(self._user, course_key)
def can_write(self, course_key):
""" Does the user have read access to the given course/library? """
return has_studio_write_access(self._user, course_key)
class StudioEditModuleRuntime(object):
"""
An extremely minimal ModuleSystem shim used for XBlock edits and studio_view.
(i.e. whenever we're not using PreviewModuleSystem.) This is required to make information
about the current user (especially permissions) available via services as needed.
"""
def __init__(self, user):
self._user = user
def service(self, block, service_name):
"""
This block is not bound to a user but some blocks (LibraryContentModule) may need
user-specific services to check for permissions, etc.
If we return None here, CombinedSystem will load services from the descriptor runtime.
"""
if block.service_declaration(service_name) is not None:
if service_name == "user":
return DjangoXBlockUserService(self._user)
if service_name == "studio_user_permissions":
return StudioPermissionsService(self._user)
return None
@require_http_methods(("GET"))
@login_required
@expect_json
......@@ -231,6 +276,9 @@ def xblock_view_handler(request, usage_key_string, view_name):
))
if view_name in (STUDIO_VIEW, VISIBILITY_VIEW):
if view_name == STUDIO_VIEW and xblock.xmodule_runtime is None:
xblock.xmodule_runtime = StudioEditModuleRuntime(request.user)
try:
fragment = xblock.render(view_name)
# catch exceptions indiscriminately, since after this point they escape the
......@@ -375,6 +423,7 @@ def _update_with_callback(xblock, user, old_metadata=None, old_content=None):
old_metadata = own_metadata(xblock)
if old_content is None:
old_content = xblock.get_explicitly_set_fields_by_scope(Scope.content)
xblock.xmodule_runtime = StudioEditModuleRuntime(user)
xblock.editor_saved(user, old_metadata, old_content)
# Update after the callback so any changes made in the callback will get persisted.
......@@ -624,6 +673,7 @@ def _duplicate_item(parent_usage_key, duplicate_source_usage_key, user, display_
# Allow an XBlock to do anything fancy it may need to when duplicated from another block.
# These blocks may handle their own children or parenting if needed. Let them return booleans to
# let us know if we need to handle these or not.
dest_module.xmodule_runtime = StudioEditModuleRuntime(user)
children_handled = dest_module.studio_post_duplicate(store, source_item)
# Children are not automatically copied over (and not all xblocks have a 'children' attribute).
......
......@@ -16,7 +16,6 @@ from xmodule.x_module import PREVIEW_VIEWS, STUDENT_VIEW, AUTHOR_VIEW
from xmodule.contentstore.django import contentstore
from xmodule.error_module import ErrorDescriptor
from xmodule.exceptions import NotFoundError, ProcessingError
from xmodule.library_tools import LibraryToolsService
from xmodule.services import SettingsService
from xmodule.modulestore.django import modulestore, ModuleI18nService
from xmodule.mixin import wrap_with_license
......@@ -150,28 +149,6 @@ class PreviewModuleSystem(ModuleSystem): # pylint: disable=abstract-method
return result
class StudioPermissionsService(object):
"""
Service that can provide information about a user's permissions.
Deprecated. To be replaced by a more general authorization service.
Only used by LibraryContentDescriptor (and library_tools.py).
"""
def __init__(self, request):
super(StudioPermissionsService, self).__init__()
self._request = request
def can_read(self, course_key):
""" Does the user have read access to the given course/library? """
return has_studio_read_access(self._request.user, course_key)
def can_write(self, course_key):
""" Does the user have read access to the given course/library? """
return has_studio_write_access(self._request.user, course_key)
def _preview_module_system(request, descriptor, field_data):
"""
Returns a ModuleSystem for the specified descriptor that is specialized for
......@@ -213,8 +190,6 @@ def _preview_module_system(request, descriptor, field_data):
# stick the license wrapper in front
wrappers.insert(0, wrap_with_license)
descriptor.runtime._services['studio_user_permissions'] = StudioPermissionsService(request) # pylint: disable=protected-access
return PreviewModuleSystem(
static_url=settings.STATIC_URL,
# TODO (cpennington): Do we want to track how instructors are using the preview problems?
......@@ -241,7 +216,6 @@ def _preview_module_system(request, descriptor, field_data):
services={
"field-data": field_data,
"i18n": ModuleI18nService,
"library_tools": LibraryToolsService(modulestore()),
"settings": SettingsService(),
"user": DjangoXBlockUserService(request.user),
},
......
......@@ -576,12 +576,10 @@ class LibraryContentDescriptor(LibraryContentFields, MakoModuleDescriptor, XmlDe
"""
lib_tools = self.runtime.service(self, 'library_tools')
user_perms = self.runtime.service(self, 'studio_user_permissions')
all_libraries = lib_tools.list_available_libraries()
if user_perms:
all_libraries = [
(key, name) for key, name in all_libraries
if user_perms.can_read(key) or self.source_library_id == unicode(key)
]
all_libraries = [
(key, name) for key, name in lib_tools.list_available_libraries()
if user_perms.can_read(key) or self.source_library_id == unicode(key)
]
all_libraries.sort(key=lambda entry: entry[1]) # Sort by name
if self.source_library_id and self.source_library_key not in [entry[0] for entry in all_libraries]:
all_libraries.append((self.source_library_id, _(u"Invalid Library")))
......
......@@ -1482,8 +1482,9 @@ class DescriptorSystem(MetricsMixin, ConfigurableFragmentWrapper, Runtime):
"""
potential_set = set(super(DescriptorSystem, self).applicable_aside_types(block))
if getattr(block, 'xmodule_runtime', None) is not None:
application_set = set(block.xmodule_runtime.applicable_aside_types(block))
return list(potential_set.intersection(application_set))
if hasattr(block.xmodule_runtime, 'applicable_aside_types'):
application_set = set(block.xmodule_runtime.applicable_aside_types(block))
return list(potential_set.intersection(application_set))
return list(potential_set)
def resource_url(self, resource):
......
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