Skip to content

Commit 682c90e

Browse files
Merge pull request #278 from openedx/mroytman/MST-1585-LtiError-user_id-LTI-1.1-launch
Handle LtiError Error During LTI 1.1 Launch When Calling user_id for Unauthenticated User
2 parents 6e5a4ae + e52699f commit 682c90e

6 files changed

Lines changed: 99 additions & 41 deletions

File tree

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__ = '4.4.0'
7+
__version__ = '4.5.0'

lti_consumer/lti_xblock.py

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -745,7 +745,11 @@ def role(self):
745745
"""
746746
Get system user role and convert it to LTI role.
747747
"""
748-
role = self.runtime.service(self, 'user').get_current_user().opt_attrs.get('edx-platform.user_role', 'student')
748+
user = self.runtime.service(self, 'user').get_current_user()
749+
if not user.opt_attrs["edx-platform.is_authenticated"]:
750+
raise LtiError(self.ugettext("Could not get user data for current request"))
751+
752+
role = user.opt_attrs.get('edx-platform.user_role', 'student')
749753
return ROLE_MAP.get(role, 'Student,Learner')
750754

751755
@property
@@ -791,27 +795,7 @@ def user_id(self):
791795
'edx-platform.anonymous_user_id', None)
792796

793797
if user_id is None:
794-
# TODO: Remove the supplemental log messaging. The logs were amended to help diagnose the bug in MST-1540:
795-
# https://2u-internal.atlassian.net/browse/MST-1540. The bug is not easily reproducible in production or
796-
# development, so these logs were added to determine whether the currently requesting user is an
797-
# AnonymousUser or a currently authenticated user.
798-
# pylint: disable=import-outside-toplevel
799-
from crum import get_current_request
800-
request = get_current_request()
801-
802-
# A test (lti_consumer.tests.unit.test_lti_xblock.TestProperties.test_user_id_none) calls this function
803-
# directly, outside the scope of a request, causing it to fail if we assume this function runs within the
804-
# context of a request. It's easier to modify the code here to handle the case when there is no request
805-
# object to appease the test than to modify the test, because these changes to the log are temporary and
806-
# will be removed shortly.
807-
request_user_id = None
808-
request_user_authenticated = None
809-
if request:
810-
request_user_id = request.user.id
811-
request_user_authenticated = request.user.is_authenticated
812-
raise LtiError(self.ugettext(f"Could not get user id for current request"
813-
f" and currently requesting user id is: {request_user_id}"
814-
f" and is_authenticated is: {request_user_authenticated}"))
798+
raise LtiError(self.ugettext("Could not get user id for current request"))
815799
return str(user_id)
816800

817801
def get_icon_class(self):
@@ -998,6 +982,10 @@ def extract_real_user_data(self):
998982
Extract and return real user data from the runtime
999983
"""
1000984
user = self.runtime.service(self, 'user').get_current_user()
985+
986+
if not user.opt_attrs["edx-platform.is_authenticated"]:
987+
raise LtiError(self.ugettext("Could not get user data for current request"))
988+
1001989
user_data = {
1002990
'user_email': None,
1003991
'user_username': None,
@@ -1100,10 +1088,21 @@ def lti_launch_handler(self, request, suffix=''): # pylint: disable=unused-argu
11001088
Returns:
11011089
webob.response: HTML LTI launch form
11021090
"""
1103-
real_user_data = self.extract_real_user_data()
1104-
11051091
lti_consumer = self._get_lti_consumer()
11061092

1093+
# Occassionally, users try to do an LTI launch while they are unauthenticated. It is not known why this occurs.
1094+
# Sometimes, it is due to a web crawlers; other times, it is due to actual users of the platform. Regardless,
1095+
# return a 400 response with an appropriate error template.
1096+
try:
1097+
real_user_data = self.extract_real_user_data()
1098+
user_id = self.user_id
1099+
role = self.role
1100+
result_sourcedid = self.lis_result_sourcedid
1101+
except LtiError:
1102+
loader = ResourceLoader(__name__)
1103+
template = loader.render_django_template('/templates/html/lti_launch_error.html')
1104+
return Response(template, status=400, content_type='text/html')
1105+
11071106
username = None
11081107
email = None
11091108
if self.ask_to_send_username and real_user_data['user_username']:
@@ -1112,12 +1111,13 @@ def lti_launch_handler(self, request, suffix=''): # pylint: disable=unused-argu
11121111
email = real_user_data['user_email']
11131112

11141113
lti_consumer.set_user_data(
1115-
self.user_id,
1116-
self.role,
1117-
result_sourcedid=self.lis_result_sourcedid,
1114+
user_id,
1115+
role,
1116+
result_sourcedid=result_sourcedid,
11181117
person_sourcedid=username,
11191118
person_contact_email_primary=email
11201119
)
1120+
11211121
lti_consumer.set_context_data(
11221122
self.context_id,
11231123
self.course.display_name_with_default,

lti_consumer/plugin/views.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ def launch_gate_endpoint(request, suffix=None): # pylint: disable=unused-argume
150150
usage_id = request.GET.get('login_hint')
151151
if not usage_id:
152152
log.info('The `login_hint` query param in the request is missing or empty.')
153-
return render(request, 'html/lti_1p3_launch_error.html', status=HTTP_400_BAD_REQUEST)
153+
return render(request, 'html/lti_launch_error.html', status=HTTP_400_BAD_REQUEST)
154154

155155
try:
156156
usage_key = UsageKey.from_string(usage_id)
@@ -161,7 +161,7 @@ def launch_gate_endpoint(request, suffix=None): # pylint: disable=unused-argume
161161
exc,
162162
exc_info=True
163163
)
164-
return render(request, 'html/lti_1p3_launch_error.html', status=HTTP_404_NOT_FOUND)
164+
return render(request, 'html/lti_launch_error.html', status=HTTP_404_NOT_FOUND)
165165

166166
try:
167167
lti_config = LtiConfiguration.objects.get(
@@ -173,7 +173,7 @@ def launch_gate_endpoint(request, suffix=None): # pylint: disable=unused-argume
173173

174174
if lti_config.version != LtiConfiguration.LTI_1P3:
175175
log.error("The LTI Version of configuration %s is not LTI 1.3", lti_config)
176-
return render(request, 'html/lti_1p3_launch_error.html', status=HTTP_404_NOT_FOUND)
176+
return render(request, 'html/lti_launch_error.html', status=HTTP_404_NOT_FOUND)
177177

178178
context = {}
179179

@@ -269,7 +269,7 @@ def launch_gate_endpoint(request, suffix=None): # pylint: disable=unused-argume
269269
exc,
270270
exc_info=True
271271
)
272-
return render(request, 'html/lti_1p3_launch_error.html', context, status=HTTP_400_BAD_REQUEST)
272+
return render(request, 'html/lti_launch_error.html', context, status=HTTP_400_BAD_REQUEST)
273273
except AssertionError as exc:
274274
log.warning(
275275
"Permission on LTI block %r denied for user %r: %s",

lti_consumer/templates/html/lti_1p3_launch_error.html renamed to lti_consumer/templates/html/lti_launch_error.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
</head>
88
<body>
99
<p>
10-
<b>{% trans "There was an error while launching the LTI 1.3 tool." %}</b>
10+
<b>{% trans "There was an error while launching the LTI tool." %}</b>
1111
</p>
1212
<p>
1313
{% trans "If you're seeing this on a live course, please contact the course staff." %}

lti_consumer/tests/unit/plugin/test_views.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ def test_launch_callback_endpoint_fails(self):
196196
self.assertEqual(response.status_code, 400)
197197

198198
response_body = response.content.decode('utf-8')
199-
self.assertIn("There was an error while launching the LTI 1.3 tool.", response_body)
199+
self.assertIn("There was an error while launching the LTI tool.", response_body)
200200
self.assertNotIn("% trans", response_body)
201201

202202
with patch(

lti_consumer/tests/unit/test_lti_xblock.py

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -146,29 +146,40 @@ def test_role(self):
146146
"""
147147
fake_user = Mock()
148148
fake_user.opt_attrs = {
149-
'edx-platform.user_role': 'student'
149+
'edx-platform.user_role': 'student',
150+
'edx-platform.is_authenticated': True,
150151
}
151152
self.xblock.runtime.service(self, 'user').get_current_user = Mock(return_value=fake_user)
152153
self.assertEqual(self.xblock.role, 'Student,Learner')
153154

154155
fake_user.opt_attrs = {
155-
'edx-platform.user_role': 'guest'
156+
'edx-platform.user_role': 'guest',
157+
'edx-platform.is_authenticated': True,
156158
}
157159
self.xblock.runtime.service(self, 'user').get_current_user = Mock(return_value=fake_user)
158160
self.assertEqual(self.xblock.role, 'Student,Learner')
159161

160162
fake_user.opt_attrs = {
161-
'edx-platform.user_role': 'staff'
163+
'edx-platform.user_role': 'staff',
164+
'edx-platform.is_authenticated': True,
162165
}
163166
self.xblock.runtime.service(self, 'user').get_current_user = Mock(return_value=fake_user)
164167
self.assertEqual(self.xblock.role, 'Administrator')
165168

166169
fake_user.opt_attrs = {
167-
'edx-platform.user_role': 'instructor'
170+
'edx-platform.user_role': 'instructor',
171+
'edx-platform.is_authenticated': True,
168172
}
169173
self.xblock.runtime.service(self, 'user').get_current_user = Mock(return_value=fake_user)
170174
self.assertEqual(self.xblock.role, 'Instructor')
171175

176+
fake_user.opt_attrs = {
177+
'edx-platform.user_role': 'student',
178+
'edx-platform.is_authenticated': False,
179+
}
180+
with self.assertRaises(LtiError):
181+
_ = self.xblock.role
182+
172183
def test_course(self):
173184
"""
174185
Test `course` calls modulestore.get_course
@@ -593,7 +604,8 @@ def test_get_real_user_callable(self):
593604
fake_user.emails = [fake_user_email]
594605
fake_username = 'fake'
595606
fake_user.opt_attrs = {
596-
"edx-platform.username": fake_username
607+
"edx-platform.username": fake_username,
608+
"edx-platform.is_authenticated": True,
597609
}
598610

599611
self.xblock.runtime.service(self, 'user').get_current_user = Mock(return_value=fake_user)
@@ -616,14 +628,29 @@ def test_get_real_user_callable_with_language_preference(self):
616628
fake_user.opt_attrs = {
617629
"edx-platform.user_preferences": {
618630
"pref-lang": "en"
619-
}
631+
},
632+
"edx-platform.is_authenticated": True,
620633
}
621634

622635
self.xblock.runtime.service(self, 'user').get_current_user = Mock(return_value=fake_user)
623636

624637
real_user_data = self.xblock.extract_real_user_data()
625638
self.assertEqual(real_user_data['user_language'], pref_language)
626639

640+
def test_unauthenticated_user(self):
641+
"""
642+
Test that an LtiError is raised when the user is unauthenticated.
643+
"""
644+
fake_user = Mock()
645+
fake_user.opt_attrs = {
646+
"edx-platform.is_authenticated": False,
647+
}
648+
649+
self.xblock.runtime.service(self, 'user').get_current_user = Mock(return_value=fake_user)
650+
651+
with self.assertRaises(LtiError):
652+
self.xblock.extract_real_user_data()
653+
627654

628655
class TestStudentView(TestLtiConsumerXBlock):
629656
"""
@@ -734,7 +761,8 @@ def setUp(self):
734761
fake_user.emails = [fake_user_email]
735762
fake_username = 'fake'
736763
fake_user.opt_attrs = {
737-
"edx-platform.username": fake_username
764+
"edx-platform.username": fake_username,
765+
"edx-platform.is_authenticated": True,
738766
}
739767

740768
self.xblock.runtime.service(self, 'user').get_current_user = Mock(return_value=fake_user)
@@ -757,6 +785,36 @@ def test_generate_launch_request_called(self, mock_course):
757785
self.assertEqual(response.status_code, 200)
758786
self.assertEqual(response.content_type, 'text/html')
759787

788+
@patch('lti_consumer.lti_xblock.LtiConsumerXBlock.course')
789+
def test_lti_launch_handler_unauthenticated(self, mock_course):
790+
"""
791+
Test that a 400 response an an appropriate template is rendered when a user is unauthenticated
792+
during an LTI launch according to the LMS's user service.
793+
"""
794+
provider = 'lti_provider'
795+
key = 'test'
796+
secret = 'secret'
797+
type(mock_course).lti_passports = PropertyMock(return_value=[f"{provider}:{key}:{secret}"])
798+
799+
fake_user = Mock()
800+
fake_user_email = 'abc@example.com'
801+
fake_user.emails = [fake_user_email]
802+
fake_username = 'fake'
803+
fake_user.opt_attrs = {
804+
"edx-platform.username": fake_username,
805+
"edx-platform.is_authenticated": True,
806+
}
807+
self.xblock.runtime.service(self, 'user').get_current_user = Mock(return_value=fake_user)
808+
809+
request = make_request('', 'GET')
810+
response = self.xblock.lti_launch_handler(request)
811+
812+
self.assertEqual(response.status_code, 400)
813+
self.assertEqual(response.content_type, 'text/html')
814+
815+
response_body = response.body.decode('utf-8')
816+
self.assertIn("There was an error while launching the LTI tool.", response_body)
817+
760818
@patch('lti_consumer.lti_xblock.LtiConsumerXBlock.course')
761819
@patch('lti_consumer.lti_xblock.LtiConsumerXBlock.user_id', PropertyMock(return_value=FAKE_USER_ID))
762820
def test_publish_tracking_event(self, mock_course):

0 commit comments

Comments
 (0)