diff --git a/README.rst b/README.rst index bd2fbf8181b9d65dbb59da724825891b1628cd6d..893f9e2d17a26da6e3548ebeed91ce5cf477a264 100644 --- a/README.rst +++ b/README.rst @@ -139,9 +139,11 @@ Instructions: .. admonition:: Testing using ``ngrok`` - When launching LTI 1.3 requests through ``ngrok``, make sure you set ``DCS_SESSION_COOKIE_SAMESITE = 'None'`` in your - ``devstack.py`` (located in /edx/app/edxapp/edx-platform/(lms|cms)/envs``) when doing LTI 1.3 launches in the - devstack through ngrok. Do not forget to restart your services after updating the ``.py`` files. + When launching LTI 1.3 requests through ``ngrok``, make sure your LMS is serving session cookies marked as + ``Secure`` and with the ``SameSite`` attribute set to ``None``. You can do this by changing ``SESSION_COOKIE_SECURE: true`` + and ``DCS_SESSION_COOKIE_SAMESITE: None`` in your ``lms.yml`` configuration files. Note that this will break logins + for locally accessed URLs in the devstack. + Custom LTI Parameters ===================== @@ -367,6 +369,12 @@ Changelog Please See the [releases tab](https://github.com/edx/xblock-lti-consumer/releases) for the complete changelog. +3.4.5 - 2022-03-16 +------------------ + +* Fix LTI Deep Linking return endpoint permission checking method by replacing the old one with the proper + Studio API call. + 3.4.4 - 2022-03-03 ------------------ diff --git a/lti_consumer/__init__.py b/lti_consumer/__init__.py index ed92eb5d89f723f051797376ec443aaf8e65e40b..4b73aa061889e46046f750289a2d912294dc2686 100644 --- a/lti_consumer/__init__.py +++ b/lti_consumer/__init__.py @@ -4,4 +4,4 @@ Runtime will load the XBlock class from here. from .apps import LTIConsumerApp from .lti_xblock import LtiConsumerXBlock -__version__ = '3.4.4' +__version__ = '3.4.5' diff --git a/lti_consumer/plugin/compat.py b/lti_consumer/plugin/compat.py index d64a5aeab8cfd479cc357c57ff7391a90dd323a9..e2acf520a25bc4c6550ee3c547d472d504f93cde 100644 --- a/lti_consumer/plugin/compat.py +++ b/lti_consumer/plugin/compat.py @@ -113,6 +113,18 @@ def user_has_access(*args, **kwargs): return has_access(*args, **kwargs) +def user_has_studio_write_access(*args, **kwargs): + """ + Import and run `has_studio_write_access` from common modules. + + Used to check if someone saving deep linking content has the + correct write permissions for a given. + """ + # pylint: disable=import-error,import-outside-toplevel + from common.djangoapps.student.auth import has_studio_write_access + return has_studio_write_access(*args, **kwargs) + + def get_course_by_id(course_key): """ Import and run `get_course_by_id` from LMS diff --git a/lti_consumer/plugin/views.py b/lti_consumer/plugin/views.py index 4cc4bd8777c23c3e204699a69c6ca7223219de2d..abe52d27ecf61b5783db58c1cec9767a22159c4c 100644 --- a/lti_consumer/plugin/views.py +++ b/lti_consumer/plugin/views.py @@ -63,13 +63,6 @@ from lti_consumer.utils import _ log = logging.getLogger(__name__) -def user_has_staff_access(user, course_key): - """ - Check if an user has write permissions to a given course. - """ - return compat.user_has_access(user, "staff", course_key) - - def has_block_access(user, block, course_key): """ Checks if a user has access to given xblock. @@ -183,26 +176,31 @@ def access_token_endpoint(request, usage_id=None): @require_http_methods(["POST"]) def deep_linking_response_endpoint(request, lti_config_id=None): """ - Deep Linking response endpoint where tool can send back + Deep Linking response endpoint where tool can send back Deep Linking + content selected by instructions in the tool's UI. + + For this feature to work, the LMS session cookies need to be Secure + and have the `SameSite` attribute set to `None`, otherwise we won't + be able to check user permissions. """ try: # Retrieve LTI configuration lti_config = LtiConfiguration.objects.get(id=lti_config_id) - # First, check if the user has sufficient permissions to - # save LTI Deep Linking content through the student.auth API. - course_key = lti_config.location.course_key - if not user_has_staff_access(request.user, course_key): - raise PermissionDenied() - # Get LTI consumer lti_consumer = lti_config.get_lti_consumer() - # Retrieve Deep Linking return message and validate parameters + # Validate Deep Linking return message and return decoded message content_items = lti_consumer.check_and_decode_deep_linking_token( request.POST.get("JWT") ) + # Check if the user has sufficient permissions to + # save LTI Deep Linking content through the student.auth API. + course_key = lti_config.location.course_key + if not compat.user_has_studio_write_access(request.user, course_key): + raise PermissionDenied() + # On a transaction, clear older DeepLinking selections, then # verify and save each content item passed from the tool. with transaction.atomic(): diff --git a/lti_consumer/tests/unit/plugin/test_views_lti_deep_linking.py b/lti_consumer/tests/unit/plugin/test_views_lti_deep_linking.py index 58f77660bd02c159a1fb988b8138ed127217089d..77505f9a28c2fe20d171c97c75fc1839dd26c433 100644 --- a/lti_consumer/tests/unit/plugin/test_views_lti_deep_linking.py +++ b/lti_consumer/tests/unit/plugin/test_views_lti_deep_linking.py @@ -89,10 +89,17 @@ class LtiDeepLinkingResponseEndpointTestCase(LtiDeepLinkingTestCase): super().setUp() # Patch method that calls platform core to ask for user permissions - studio_access_patcher = patch('lti_consumer.plugin.views.user_has_staff_access') - self.addCleanup(studio_access_patcher.stop) - self._mock_has_studio_write_acess = studio_access_patcher.start() - self._mock_has_studio_write_acess.return_value = True + compat_mock = patch("lti_consumer.signals.compat") + self.addCleanup(compat_mock.stop) + self._compat_mock = compat_mock.start() + self._compat_mock.user_has_studio_write_access.return_value = True + + has_studio_write_acess_patcher = patch( + 'lti_consumer.plugin.views.compat.user_has_studio_write_access', + return_value=True + ) + self.addCleanup(has_studio_write_acess_patcher.stop) + self._mock_has_studio_write_acess = has_studio_write_acess_patcher.start() # Deep Linking response endpoint self.url = '/lti_consumer/v1/lti/{}/lti-dl/response'.format(self.lti_config.id)