Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ These are notable changes in edx-platform. This is a rolling list of changes,
in roughly chronological order, most recent first. Add your entries at or near
the top. Include a label indicating the component affected.

Blades: Make LTI module not send grade_back_url if has_score=False. BLD-561.

Blades: Show answer for imageresponse. BLD-21.

Blades: LTI additional Python tests. LTI must use HTTPS for
Expand Down
4 changes: 2 additions & 2 deletions common/djangoapps/student/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ class AnonymousUserId(models.Model):

Purpose of this table is to provide user by anonymous_user_id.

We are generating anonymous_user_id using md5 algorithm, so resulting length will always be 16 bytes.
http://docs.python.org/2/library/md5.html#md5.digest_size
We generate anonymous_user_id using md5 algorithm,
and use result in hex form, so its length is equal to 32 bytes.
"""
user = models.ForeignKey(User, db_index=True)
anonymous_user_id = models.CharField(unique=True, max_length=32)
Expand Down
25 changes: 21 additions & 4 deletions common/lib/xmodule/xmodule/lti_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,11 +355,15 @@ def oauth_params(self, custom_parameters, client_key, client_secret):

# Parameters required for grading:
u'resource_link_id': self.get_resource_link_id(),
u'lis_outcome_service_url': self.get_outcome_service_url(),
u'lis_result_sourcedid': self.get_lis_result_sourcedid(),

}

if self.has_score:
body.update({
u'lis_outcome_service_url': self.get_outcome_service_url()
})

# Appending custom parameter for signing.
body.update(custom_parameters)

Expand Down Expand Up @@ -483,17 +487,24 @@ def grade_handler(self, request, dispatch):
try:
imsx_messageIdentifier, sourcedId, score, action = self.parse_grade_xml_body(request.body)
except Exception:
log.debug("[LTI]: Request body XML parsing error.")
failure_values['imsx_description'] = 'Request body XML parsing error.'
return Response(response_xml_template.format(**failure_values), content_type="application/xml")

# Verify OAuth signing.
try:
self.verify_oauth_body_sign(request)
except (ValueError, LTIError):
failure_values['imsx_messageIdentifier'] = escape(imsx_messageIdentifier)
failure_values['imsx_description'] = 'OAuth verification error.'
return Response(response_xml_template.format(**failure_values), content_type="application/xml")


real_user = self.system.get_real_user(urllib.unquote(sourcedId.split(':')[-1]))
if not real_user: # that means we can't save to database, as we do not have real user id.
failure_values['imsx_messageIdentifier'] = escape(imsx_messageIdentifier)
failure_values['imsx_description'] = 'User not found.'
return Response(response_xml_template.format(**failure_values), content_type="application/xml")

if action == 'replaceResultRequest':
self.system.publish(
event={
Expand All @@ -510,9 +521,11 @@ def grade_handler(self, request, dispatch):
'imsx_messageIdentifier': escape(imsx_messageIdentifier),
'response': '<replaceResultResponse/>'
}
log.debug("[LTI]: Grade is saved.")
return Response(response_xml_template.format(**values), content_type="application/xml")

unsupported_values['imsx_messageIdentifier'] = escape(imsx_messageIdentifier)
log.debug("[LTI]: Incorrect action.")
return Response(response_xml_template.format(**unsupported_values), content_type='application/xml')


Expand Down Expand Up @@ -541,6 +554,7 @@ def parse_grade_xml_body(cls, body):
# Raise exception if score is not float or not in range 0.0-1.0 regarding spec.
score = float(score)
if not 0 <= score <= 1:
log.debug("[LTI]: Score not in range.")
raise LTIError

return imsx_messageIdentifier, sourcedId, score, action
Expand Down Expand Up @@ -582,8 +596,11 @@ def verify_oauth_body_sign(self, request):
params=oauth_headers.items(),
signature=oauth_signature
)
if (oauth_body_hash != oauth_headers.get('oauth_body_hash') or
not signature.verify_hmac_sha1(mock_request, client_secret)):
if oauth_body_hash != oauth_headers.get('oauth_body_hash'):
log.debug("[LTI]: OAuth body hash verification is failed.")
raise LTIError
if not signature.verify_hmac_sha1(mock_request, client_secret):
log.debug("[LTI]: OAuth signature verification is failed.")
raise LTIError

def get_client_key_secret(self):
Expand Down
28 changes: 24 additions & 4 deletions common/lib/xmodule/xmodule/tests/test_lti_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ def test_authorization_header_not_present(self):
expected_response = {
'action': None,
'code_major': 'failure',
'description': 'The request has failed.',
'description': 'OAuth verification error.',
'messageIdentifier': self.DEFAULTS['messageIdentifier'],
}

Expand All @@ -133,7 +133,27 @@ def test_authorization_header_empty(self):
expected_response = {
'action': None,
'code_major': 'failure',
'description': 'The request has failed.',
'description': 'OAuth verification error.',
'messageIdentifier': self.DEFAULTS['messageIdentifier'],
}
self.assertEqual(response.status_code, 200)
self.assertDictEqual(expected_response, real_response)

def test_real_user_is_none(self):
"""
If we have no real user, we should send back failure response.
"""
self.xmodule.verify_oauth_body_sign = Mock()
self.xmodule.has_score = True
self.system.get_real_user = Mock(return_value=None)
request = Request(self.environ)
request.body = self.get_request_body()
response = self.xmodule.grade_handler(request, '')
real_response = self.get_response_values(response)
expected_response = {
'action': None,
'code_major': 'failure',
'description': 'User not found.',
'messageIdentifier': self.DEFAULTS['messageIdentifier'],
}
self.assertEqual(response.status_code, 200)
Expand All @@ -151,7 +171,7 @@ def test_grade_not_in_range(self):
expected_response = {
'action': None,
'code_major': 'failure',
'description': 'The request has failed.',
'description': 'Request body XML parsing error.',
'messageIdentifier': 'unknown',
}
self.assertEqual(response.status_code, 200)
Expand All @@ -169,7 +189,7 @@ def test_bad_grade_decimal(self):
expected_response = {
'action': None,
'code_major': 'failure',
'description': 'The request has failed.',
'description': 'Request body XML parsing error.',
'messageIdentifier': 'unknown',
}
self.assertEqual(response.status_code, 200)
Expand Down
3 changes: 3 additions & 0 deletions common/lib/xmodule/xmodule/x_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -1016,6 +1016,9 @@ def __init__(

error_descriptor_class - The class to use to render XModules with errors

get_real_user - function that takes `anonymous_student_id` and returns real user_id,
associated with `anonymous_student_id`.

"""

# Right now, usage_store is unused, and field_data is always supplanted
Expand Down
114 changes: 50 additions & 64 deletions lms/djangoapps/courseware/mock_lti_server/mock_lti_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ class MockLTIRequestHandler(BaseHTTPRequestHandler):
protocol = "HTTP/1.0"
callback_url = None


def log_message(self, format, *args):
"""Log an arbitrary message."""
# Code copied from BaseHTTPServer.py. Changed to write to sys.stdout
Expand All @@ -49,18 +48,13 @@ def do_GET(self):

Used for checking LTI Provider started correctly.
'''

self.send_response(200, 'OK')
self.send_header('Content-type', 'html')
self.end_headers()

response_str = """<html><head><title>TEST TITLE</title></head>
<body>This is LTI Provider.</body></html>"""

self.wfile.write(response_str)



def do_POST(self):
'''
Handle a POST request from the client and sends response back.
Expand All @@ -72,38 +66,17 @@ def do_POST(self):
# Respond to request with correct lti endpoint:
elif self._is_correct_lti_request():
self.post_dict = self._post_dict()
correct_keys = [
'user_id',
'role',
'oauth_nonce',
'oauth_timestamp',
'oauth_consumer_key',
'lti_version',
'oauth_signature_method',
'oauth_version',
'oauth_signature',
'lti_message_type',
'oauth_callback',
'lis_outcome_service_url',
'lis_result_sourcedid',
'launch_presentation_return_url',
# 'lis_person_sourcedid', optional, not used now.
'resource_link_id',
]
if sorted(correct_keys) != sorted(self.post_dict.keys()):
status_message = "Incorrect LTI header"
params = {k: v for k, v in self.post_dict.items() if k != 'oauth_signature'}
if self.server.check_oauth_signature(params, self.post_dict.get('oauth_signature', "")):
status_message = "This is LTI tool. Success."
# set data for grades what need to be stored as server data
if 'lis_outcome_service_url' in self.post_dict:
self.server.grade_data = {
'callback_url': self.post_dict.get('lis_outcome_service_url'),
'sourcedId': self.post_dict.get('lis_result_sourcedid')
}
else:
params = {k: v for k, v in self.post_dict.items() if k != 'oauth_signature'}
if self.server.check_oauth_signature(params, self.post_dict['oauth_signature']):
status_message = "This is LTI tool. Success."
else:
status_message = "Wrong LTI signature"
# set data for grades
# what need to be stored as server data
self.server.grade_data = {
'callback_url': self.post_dict.get('lis_outcome_service_url'),
'sourcedId': self.post_dict.get('lis_result_sourcedid')
}
status_message = "Wrong LTI signature"
self._send_response(status_message, 200)
else:
status_message = "Invalid request URL"
Expand Down Expand Up @@ -141,17 +114,17 @@ def _post_dict(self):
self.server.cookie = {}
referer = urlparse.urlparse(self.headers.getheader('referer'))
self.server.referer_host = "{}://{}".format(referer.scheme, referer.netloc)
self.server.referer_netloc = referer.netloc
return post_dict

def _send_graded_result(self):

"""
Send grade request.
"""
values = {
'textString': 0.5,
'sourcedId': self.server.grade_data['sourcedId'],
'imsx_messageIdentifier': uuid4().hex,
}

payload = textwrap.dedent("""
<?xml version = "1.0" encoding = "UTF-8"?>
<imsx_POXEnvelopeRequest xmlns="http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0">
Expand Down Expand Up @@ -208,40 +181,53 @@ def _send_response(self, message, status_code):
Send message back to the client
'''
self._send_head(status_code)
if self.server.grade_data['callback_url']:
response_str = """<html><head><title>TEST TITLE</title></head>
<body>
<div><h2>Graded IFrame loaded</h2> \
<h3>Server response is:</h3>\
<h3 class="result">{}</h3></div>
<form action="{url}/grade" method="post">
<input type="submit" name="submit-button" value="Submit">
</form>

</body></html>""".format(message, url="http://%s:%s" % self.server.server_address)
else:
response_str = """<html><head><title>TEST TITLE</title></head>
<body>
<div><h2>IFrame loaded</h2> \
<h3>Server response is:</h3>\
<h3 class="result">{}</h3></div>
</body></html>""".format(message)

# Log the response
logger.debug("LTI: sent response {}".format(response_str))
if getattr(self.server, 'grade_data', False): # lti can be graded
response_str = textwrap.dedent("""
<html>
<head>
<title>TEST TITLE</title>
</head>
<body>
<div>
<h2>Graded IFrame loaded</h2>
<h3>Server response is:</h3>
<h3 class="result">{}</h3>
</div>
<form action="{url}/grade" method="post">
<input type="submit" name="submit-button" value="Submit">
</form>
</body>
</html>
""").format(message, url="http://%s:%s" % self.server.server_address)
else: # lti can't be graded
response_str = textwrap.dedent("""
<html>
<head>
<title>TEST TITLE</title>
</head>
<body>
<div>
<h2>IFrame loaded</h2>
<h3>Server response is:</h3>
<h3 class="result">{}</h3>
</div>
</body>
</html>
""").format(message)

logger.debug("LTI: sent response {}".format(response_str))
self.wfile.write(response_str)

def _is_correct_lti_request(self):
'''If url to LTI tool is correct.'''
'''
If url to LTI tool is correct.
'''
return self.server.oauth_settings['lti_endpoint'] in self.path

def oauth_sign(self, url, body):
"""
Signs request and returns signed body and headers.

"""

client = oauthlib.oauth1.Client(
client_key=unicode(self.server.oauth_settings['client_key']),
client_secret=unicode(self.server.oauth_settings['client_secret'])
Expand Down
Loading