|
71 | 71 | from .exceptions import LtiError |
72 | 72 | from .lti_1p1.consumer import LtiConsumer1p1, parse_result_json, LTI_PARAMETERS |
73 | 73 | from .lti_1p1.oauth import log_authorization_header |
74 | | -from .lti_1p3.exceptions import ( |
75 | | - Lti1p3Exception, |
76 | | - UnsupportedGrantType, |
77 | | - MalformedJwtToken, |
78 | | - MissingRequiredClaim, |
79 | | - NoSuitableKeys, |
80 | | - TokenSignatureExpired, |
81 | | - UnknownClientId, |
82 | | -) |
83 | | -from .lti_1p3.constants import LTI_1P3_CONTEXT_TYPE |
84 | 74 | from .outcomes import OutcomeService |
85 | 75 | from .track import track_event |
86 | 76 | from .utils import _, resolve_custom_parameter_template, external_config_filter_enabled, database_config_enabled |
@@ -1168,187 +1158,29 @@ def lti_launch_handler(self, request, suffix=''): # pylint: disable=unused-argu |
1168 | 1158 | template = loader.render_django_template('/templates/html/lti_launch.html', context) |
1169 | 1159 | return Response(template, content_type='text/html') |
1170 | 1160 |
|
1171 | | - @XBlock.handler |
1172 | | - def lti_1p3_launch_callback(self, request, suffix=''): # pylint: disable=unused-argument |
1173 | | - """ |
1174 | | - XBlock handler for launching the LTI 1.3 tool. |
1175 | | -
|
1176 | | - This endpoint is only valid when a LTI 1.3 tool is being used. |
1177 | | -
|
1178 | | - Returns: |
1179 | | - webob.response: HTML LTI launch form or error page if misconfigured |
1180 | | - """ |
1181 | | - if self.lti_version != "lti_1p3": |
1182 | | - return Response(status=404) |
1183 | | - |
1184 | | - loader = ResourceLoader(__name__) |
1185 | | - context = {} |
1186 | | - |
1187 | | - user_role = self.runtime.get_user_role() |
1188 | | - lti_consumer = self._get_lti_consumer() |
1189 | | - |
1190 | | - try: |
1191 | | - # Pass user data |
1192 | | - lti_consumer.set_user_data( |
1193 | | - user_id=self.external_user_id, |
1194 | | - # Pass django user role to library |
1195 | | - role=user_role |
1196 | | - ) |
1197 | | - |
1198 | | - # Set launch context |
1199 | | - # Hardcoded for now, but we need to translate from |
1200 | | - # self.launch_target to one of the LTI compliant names, |
1201 | | - # either `iframe`, `frame` or `window` |
1202 | | - # This is optional though |
1203 | | - lti_consumer.set_launch_presentation_claim('iframe') |
1204 | | - |
1205 | | - # Set context claim |
1206 | | - # This is optional |
1207 | | - context_title = " - ".join([ |
1208 | | - self.course.display_name_with_default, |
1209 | | - self.course.display_org_with_default |
1210 | | - ]) |
1211 | | - lti_consumer.set_context_claim( |
1212 | | - self.context_id, |
1213 | | - context_types=[LTI_1P3_CONTEXT_TYPE.course_offering], |
1214 | | - context_title=context_title, |
1215 | | - context_label=self.context_id |
1216 | | - ) |
1217 | | - |
1218 | | - # Retrieve preflight response |
1219 | | - preflight_response = dict(request.GET) |
1220 | | - lti_message_hint = preflight_response.get('lti_message_hint', '') |
1221 | | - |
1222 | | - # Set LTI Launch URL |
1223 | | - launch_url = self.lti_1p3_launch_url |
1224 | | - if self.config_type == 'database': |
1225 | | - launch_url = lti_consumer.launch_url |
1226 | | - context.update({'launch_url': launch_url}) |
1227 | | - |
1228 | | - # Modify LTI Launch URL dependind on launch type |
1229 | | - # Deep Linking Launch - Configuration flow launched by |
1230 | | - # course creators to set up content. |
1231 | | - lti_advantage_deep_linking_enabled = lti_consumer.lti_dl_enabled() |
1232 | | - if lti_advantage_deep_linking_enabled and lti_message_hint == 'deep_linking_launch': |
1233 | | - # Check if the user is staff before LTI doing deep linking launch. |
1234 | | - # If not, raise exception and display error page |
1235 | | - if user_role not in ['instructor', 'staff']: |
1236 | | - raise AssertionError('Deep Linking can only be performed by instructors and staff.') |
1237 | | - # Set deep linking launch |
1238 | | - context.update({'launch_url': lti_consumer.lti_dl.deep_linking_launch_url}) |
1239 | | - |
1240 | | - # Deep Linking ltiResourceLink content presentation |
1241 | | - # When content type is `ltiResourceLink`, the tool will be launched with |
1242 | | - # different parameters, set by instructors when running the DL configuration flow. |
1243 | | - elif lti_advantage_deep_linking_enabled and 'deep_linking_content_launch' in lti_message_hint: |
1244 | | - # Retrieve Deep Linking parameters using lti_message_hint parameter. |
1245 | | - deep_linking_id = lti_message_hint.split(':')[1] |
1246 | | - from lti_consumer.api import get_deep_linking_data # pylint: disable=import-outside-toplevel |
1247 | | - dl_params = get_deep_linking_data(deep_linking_id, block=self) |
1248 | | - |
1249 | | - # Modify LTI launch and set ltiResourceLink parameters |
1250 | | - lti_consumer.set_dl_content_launch_parameters( |
1251 | | - url=dl_params.get('url'), |
1252 | | - custom=dl_params.get('custom') |
1253 | | - ) |
1254 | | - |
1255 | | - # Update context with LTI launch parameters |
1256 | | - context.update({ |
1257 | | - "preflight_response": preflight_response, |
1258 | | - "launch_request": lti_consumer.generate_launch_request( |
1259 | | - resource_link=str(self.location), # pylint: disable=no-member |
1260 | | - preflight_response=preflight_response |
1261 | | - ) |
1262 | | - }) |
1263 | | - |
1264 | | - # emit tracking event |
1265 | | - event = { |
1266 | | - 'lti_version': self.lti_version, |
1267 | | - 'user_roles': user_role, |
1268 | | - 'launch_url': self.lti_1p3_launch_url, |
1269 | | - } |
1270 | | - track_event('xblock.launch_request', event) |
1271 | | - |
1272 | | - template = loader.render_mako_template('/templates/html/lti_1p3_launch.html', context) |
1273 | | - return Response(template, content_type='text/html') |
1274 | | - except (Lti1p3Exception, LtiError, NotImplementedError, TypeError, ValueError) as exc: |
1275 | | - log.warning( |
1276 | | - "Error preparing LTI 1.3 launch for block %r: %s", |
1277 | | - str(self.location), # pylint: disable=no-member |
1278 | | - exc, |
1279 | | - exc_info=True, |
1280 | | - ) |
1281 | | - template = loader.render_django_template('/templates/html/lti_1p3_launch_error.html', context) |
1282 | | - return Response(template, status=400, content_type='text/html') |
1283 | | - except AssertionError as exc: |
1284 | | - log.warning( |
1285 | | - "Permission on LTI block %r denied for user %r: %s", |
1286 | | - str(self.location), # pylint: disable=no-member |
1287 | | - self.external_user_id, |
1288 | | - exc, |
1289 | | - exc_info=True |
1290 | | - ) |
1291 | | - template = loader.render_django_template('/templates/html/lti_1p3_permission_error.html', context) |
1292 | | - return Response(template, status=403, content_type='text/html') |
1293 | | - |
1294 | 1161 | @XBlock.handler |
1295 | 1162 | def lti_1p3_access_token(self, request, suffix=''): # pylint: disable=unused-argument |
1296 | 1163 | """ |
1297 | 1164 | XBlock handler for creating access tokens for the LTI 1.3 tool. |
1298 | | -
|
1299 | 1165 | This endpoint is only valid when a LTI 1.3 tool is being used. |
1300 | | -
|
1301 | 1166 | Returns: |
1302 | | - webob.response: |
| 1167 | + django.http.HttpResponse: |
1303 | 1168 | Either an access token or error message detailing the failure. |
1304 | 1169 | All responses are RFC 6749 compliant. |
1305 | | -
|
1306 | 1170 | References: |
1307 | 1171 | Sucess: https://tools.ietf.org/html/rfc6749#section-4.4.3 |
1308 | 1172 | Failure: https://tools.ietf.org/html/rfc6749#section-5.2 |
1309 | 1173 | """ |
1310 | 1174 | if self.lti_version != "lti_1p3": |
1311 | 1175 | return Response(status=404) |
1312 | | - if request.method != "POST": |
1313 | | - return Response(status=405) |
1314 | 1176 |
|
1315 | | - lti_consumer = self._get_lti_consumer() |
1316 | | - try: |
1317 | | - token = lti_consumer.access_token( |
1318 | | - dict(urllib.parse.parse_qsl( |
1319 | | - request.body.decode('utf-8'), |
1320 | | - keep_blank_values=True |
1321 | | - )) |
1322 | | - ) |
1323 | | - # The returned `token` is compliant with RFC 6749 so we just |
1324 | | - # need to return a 200 OK response with the token as Json body |
1325 | | - return Response(json_body=token, content_type="application/json") |
1326 | | - |
1327 | | - # Handle errors and return a proper response |
1328 | | - except MissingRequiredClaim: |
1329 | | - # Missing request attibutes |
1330 | | - return Response( |
1331 | | - json_body={"error": "invalid_request"}, |
1332 | | - status=400 |
1333 | | - ) |
1334 | | - except (MalformedJwtToken, TokenSignatureExpired): |
1335 | | - # Triggered when a invalid grant token is used |
1336 | | - return Response( |
1337 | | - json_body={"error": "invalid_grant"}, |
1338 | | - status=400, |
1339 | | - ) |
1340 | | - except (NoSuitableKeys, UnknownClientId): |
1341 | | - # Client ID is not registered in the block or |
1342 | | - # isn't possible to validate token using available keys. |
1343 | | - return Response( |
1344 | | - json_body={"error": "invalid_client"}, |
1345 | | - status=400, |
1346 | | - ) |
1347 | | - except UnsupportedGrantType: |
1348 | | - return Response( |
1349 | | - json_body={"error": "unsupported_grant_type"}, |
1350 | | - status=400, |
1351 | | - ) |
| 1177 | + # Asserting that the consumer can be created. This makes sure that the LtiConfiguration |
| 1178 | + # object exists before calling the Django View |
| 1179 | + assert self._get_lti_consumer() |
| 1180 | + # Runtime import because this can only be run in the LMS/Studio Django |
| 1181 | + # environments. Importing the views on the top level will cause RuntimeErorr |
| 1182 | + from lti_consumer.plugin.views import access_token_endpoint # pylint: disable=import-outside-toplevel |
| 1183 | + return access_token_endpoint(request, usage_id=str(self.location)) # pylint: disable=no-member |
1352 | 1184 |
|
1353 | 1185 | @XBlock.handler |
1354 | 1186 | def outcome_service_handler(self, request, suffix=''): # pylint: disable=unused-argument |
|
0 commit comments