Skip to content

Commit bfd9c08

Browse files
Merge pull request #307 from openedx/mroytman/MST-1717-external-id-LTI-1.1-launches
Add course flag to send external_user_id as user_id in LTI 1.1 XBlock launches
2 parents 9c04004 + cff7744 commit bfd9c08

6 files changed

Lines changed: 106 additions & 8 deletions

File tree

CHANGELOG.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@ Please See the [releases tab](https://github.com/edx/xblock-lti-consumer/release
1515

1616
Unreleased
1717
~~~~~~~~~~
18+
6.4.0 - 2022-11-18
19+
------------------
20+
Adds support for sending an external_user_id in LTI 1.1 XBlock launches. When the
21+
lti_consumer.enable_external_user_id_1p1_launches CourseWaffleFlag is enabled, the LTI 1.1 launch will send an
22+
external_user_id as the user_id attribute of the launch. When the lti_consumer.enable_external_user_id_1p1_launches
23+
CourseWaffleFlag is disabled, the LTI 1.1 launch will continue to send the anonymous_user_id. The external_user_id is
24+
defined, created, and stored by the external_user_ids Djangoapp in edx-platform.
1825

1926
6.3.0 - 2022-11-16
2027
------------------

lti_consumer/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44
from .apps import LTIConsumerApp
55
from .lti_xblock import LtiConsumerXBlock
66

7-
__version__ = '6.3.0'
7+
__version__ = '6.4.0'

lti_consumer/lti_xblock.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,13 @@
7575
from .outcomes import OutcomeService
7676
from .plugin import compat
7777
from .track import track_event
78-
from .utils import _, resolve_custom_parameter_template, external_config_filter_enabled, database_config_enabled
78+
from .utils import (
79+
_,
80+
resolve_custom_parameter_template,
81+
external_config_filter_enabled,
82+
external_user_id_1p1_launches_enabled,
83+
database_config_enabled,
84+
)
7985

8086
log = logging.getLogger(__name__)
8187

@@ -826,6 +832,22 @@ def external_user_id(self):
826832
raise LtiError(self.ugettext("Could not get user id for current request"))
827833
return str(user_id)
828834

835+
def get_lti_1p1_user_id(self):
836+
"""
837+
Returns the user ID to send to an LTI tool during an LTI 1.1 launch. If the
838+
enable_external_user_id_1p1_launches CourseWaffleFlag is enabled for the course, returns the external_user_id
839+
defined by the external_user_ids Djangoapp. Otherwise, returns the anonymous_user_id.
840+
841+
This addresses cases where LTI tools require a static, opaque user_id that is consistent across contexts. On an
842+
opt-in basis, courses can be set up to send the external_user_id instead of the anonymous_user_id. Note that
843+
toggling this flag in a running course carries the risk of breaking the LTI integrations in the course. This
844+
flag should also only be enabled for new courses in which no LTI attempts have been made.
845+
"""
846+
if external_user_id_1p1_launches_enabled(self.location.course_key): # pylint: disable=no-member
847+
return self.external_user_id
848+
849+
return self.anonymous_user_id
850+
829851
@property
830852
def resource_link_id(self):
831853
"""
@@ -875,7 +897,7 @@ def lis_result_sourcedid(self):
875897
return "{context}:{resource_link}:{user_id}".format(
876898
context=urllib.parse.quote(self.context_id),
877899
resource_link=self.resource_link_id,
878-
user_id=self.anonymous_user_id
900+
user_id=self.get_lti_1p1_user_id()
879901
)
880902

881903
@property
@@ -1112,7 +1134,7 @@ def lti_launch_handler(self, request, suffix=''): # pylint: disable=unused-argu
11121134
# return a 400 response with an appropriate error template.
11131135
try:
11141136
real_user_data = self.extract_real_user_data()
1115-
user_id = self.anonymous_user_id
1137+
user_id = self.get_lti_1p1_user_id()
11161138
role = self.role
11171139

11181140
# Convert the LMS role into an LTI 1.1 role.

lti_consumer/plugin/compat.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,17 @@
3131
# .. toggle_warning: None.
3232
ENABLE_EXTERNAL_CONFIG_FILTER = 'enable_external_config_filter'
3333

34+
# .. toggle_name: lti_consumer.enable_external_user_id_1p1_launches
35+
# .. toggle_implementation: CourseWaffleFlag
36+
# .. toggle_default: False
37+
# .. toggle_description: Enables sending a user's external user ID, as created and stored by the external_user_ids
38+
# Djangoapp, instead of an anonymous user ID in LTI 1.1 launches.
39+
# .. toggle_use_cases: open_edx
40+
# .. toggle_creation_date: 2022-11-18
41+
# .. toggle_tickets: https://github.com/openedx/xblock-lti-consumer/pull/307
42+
# .. toggle_warning: None.
43+
ENABLE_EXTERNAL_USER_ID_1P1_LAUNCHES = 'enable_external_user_id_1p1_launches'
44+
3445
# Waffle Flags
3546
# .. toggle_name: lti_consumer.enable_database_config
3647
# .. toggle_implementation: CourseWaffleFlag
@@ -54,6 +65,15 @@ def get_external_config_waffle_flag():
5465
return CourseWaffleFlag(f'{WAFFLE_NAMESPACE}.{ENABLE_EXTERNAL_CONFIG_FILTER}', __name__)
5566

5667

68+
def get_external_user_id_1p1_launches_waffle_flag():
69+
"""
70+
Import and return Waffle flag for enabling sending external user IDs in LTI 1.1 launches.
71+
"""
72+
# pylint: disable=import-error,import-outside-toplevel
73+
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag
74+
return CourseWaffleFlag(f'{WAFFLE_NAMESPACE}.{ENABLE_EXTERNAL_USER_ID_1P1_LAUNCHES}', __name__)
75+
76+
5777
def get_database_config_waffle_flag():
5878
# pylint: disable=import-error,import-outside-toplevel
5979
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag

lti_consumer/tests/unit/test_lti_xblock.py

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,6 @@ class TestProperties(TestLtiConsumerXBlock):
8585
"""
8686
Unit tests for LtiConsumerXBlock properties
8787
"""
88-
8988
def test_descriptor(self):
9089
"""
9190
Test `descriptor` returns the XBLock object
@@ -304,13 +303,14 @@ def test_resource_link_id(self):
304303

305304
@patch('lti_consumer.lti_xblock.LtiConsumerXBlock.context_id')
306305
@patch('lti_consumer.lti_xblock.LtiConsumerXBlock.resource_link_id')
307-
@patch('lti_consumer.lti_xblock.LtiConsumerXBlock.anonymous_user_id', PropertyMock(return_value=FAKE_USER_ID))
308-
def test_lis_result_sourcedid(self, mock_resource_link_id, mock_context_id):
306+
@patch('lti_consumer.lti_xblock.LtiConsumerXBlock.get_lti_1p1_user_id')
307+
def test_lis_result_sourcedid(self, mock_get_external_user_id, mock_resource_link_id, mock_context_id):
309308
"""
310309
Test `lis_result_sourcedid` returns appropriate string
311310
"""
312311
mock_resource_link_id.__get__ = Mock(return_value='resource_link_id')
313312
mock_context_id.__get__ = Mock(return_value='context_id')
313+
mock_get_external_user_id.return_value = FAKE_USER_ID
314314

315315
self.assertEqual(self.xblock.lis_result_sourcedid, f"context_id:resource_link_id:{FAKE_USER_ID}")
316316

@@ -668,6 +668,35 @@ def test_unauthenticated_user(self):
668668
self.xblock.extract_real_user_data()
669669

670670

671+
@ddt.ddt
672+
class TestGetLti1p1UserId(TestLtiConsumerXBlock):
673+
""" Unit tests for the get_lti_1p1_user_id method"""
674+
def setUp(self):
675+
super().setUp()
676+
677+
# Mock out the anonymous_user_id and external_user_id properties.
678+
fake_user = Mock()
679+
fake_user.opt_attrs = {
680+
'edx-platform.user_id': 1,
681+
'edx-platform.user_role': 'studnent',
682+
'edx-platform.is_authenticated': True,
683+
'edx-platform.anonymous_user_id': 'anonymous_user_id',
684+
}
685+
self.xblock.runtime.service(self, 'user').get_current_user = Mock(return_value=fake_user)
686+
self.xblock.runtime.service(self, 'user').get_external_user_id = Mock(return_value="external_user_id")
687+
688+
@ddt.data(
689+
(True, 'external_user_id'),
690+
(False, 'anonymous_user_id'),
691+
)
692+
@ddt.unpack
693+
def test_external_user_id_flag_enabled(self, external_user_id_1p1_launches_enabled_value, expected_value):
694+
with patch('lti_consumer.lti_xblock.external_user_id_1p1_launches_enabled') as \
695+
external_user_id_1p1_launches_enabled:
696+
external_user_id_1p1_launches_enabled.return_value = external_user_id_1p1_launches_enabled_value
697+
self.assertEqual(self.xblock.get_lti_1p1_user_id(), expected_value)
698+
699+
671700
class TestStudentView(TestLtiConsumerXBlock):
672701
"""
673702
Unit tests for LtiConsumerXBlock.student_view()
@@ -783,6 +812,11 @@ def setUp(self):
783812

784813
self.xblock.runtime.service(self, 'user').get_current_user = Mock(return_value=fake_user)
785814

815+
self.mock_external_user_ids_patcher = patch("lti_consumer.lti_xblock.external_user_id_1p1_launches_enabled")
816+
self.mock_external_user_ids_patcher_enabled = self.mock_external_user_ids_patcher.start()
817+
self.mock_external_user_ids_patcher_enabled.return_value = False
818+
self.addCleanup(self.mock_external_user_ids_patcher.stop)
819+
786820
@patch('lti_consumer.lti_xblock.LtiConsumerXBlock.course')
787821
@patch('lti_consumer.lti_xblock.LtiConsumerXBlock.anonymous_user_id', PropertyMock(return_value=FAKE_USER_ID))
788822
def test_generate_launch_request_called(self, mock_course):

lti_consumer/utils.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@
88
from django.conf import settings
99
from edx_django_utils.cache import get_cache_key, TieredCache
1010

11-
from lti_consumer.plugin.compat import get_external_config_waffle_flag, get_database_config_waffle_flag
11+
from lti_consumer.plugin.compat import (
12+
get_external_config_waffle_flag,
13+
get_external_user_id_1p1_launches_waffle_flag,
14+
get_database_config_waffle_flag,
15+
)
1216
from lti_consumer.lti_1p3.constants import LTI_1P3_CONTEXT_TYPE
1317
from lti_consumer.lti_1p3.exceptions import InvalidClaimValue, MissingRequiredClaim
1418

@@ -187,6 +191,17 @@ def external_config_filter_enabled(course_key):
187191
return get_external_config_waffle_flag().is_enabled(course_key)
188192

189193

194+
def external_user_id_1p1_launches_enabled(course_key):
195+
"""
196+
Returns whether the lti_consumer.enable_external_user_id_1p1_launches CourseWaffleFlag is enabled.
197+
Returns True if sending external user IDs in LTI 1.1 launches is enabled for the course via the CourseWaffleFlag.
198+
199+
Arguments:
200+
course_key (opaque_keys.edx.locator.CourseLocator): Course Key
201+
"""
202+
return get_external_user_id_1p1_launches_waffle_flag().is_enabled(course_key)
203+
204+
190205
def database_config_enabled(course_key):
191206
"""
192207
Return whether the lti_consumer.enable_database_config WaffleFlag is enabled. Return True if it is enabled;

0 commit comments

Comments
 (0)