-
Notifications
You must be signed in to change notification settings - Fork 140
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
LTI 1.3 #1023
base: master
Are you sure you want to change the base?
LTI 1.3 #1023
Conversation
@@ -68,7 +68,7 @@ def _get_signer(self, app): | |||
|
|||
def open_session(self, app, request): | |||
# Check for cookieless session in the path | |||
path_session = re.match(r"(/@)([a-f0-9A-F_]*)(@)", request.path) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried to look into the code for other instances of this kind of check by searching for "/@" . I think I found all of them.
3591e25
to
609a552
Compare
Looks like the CI is broken btw. |
Yep, we still have to fix that 😞 |
""" {name: key} for the LTI customers """ | ||
return self._lti_keys if self._is_lti else {} | ||
def lti_config(self): | ||
""" LTI Tool config dictionary. Specs are at https://github.com/dmitry-viskov/pylti1.3/blob/master/README.rst?plain=1#L70-L98 """ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There will be documentation part of this PR on this.
from pylti1p3.launch_data_storage.base import LaunchDataStorage | ||
|
||
|
||
class MongoLTILaunchDataStorage(LaunchDataStorage): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could be used to store only the LTI Launch ID in the user session and retrieve the rest from the database.
e53b0e2
to
9a6d192
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Globally this LGTM. The comments are for me, except if you want to address them in the meantime.
I won't merge it now as I'd rather have this merged in a clean main branch with tasksets removed, but it will as soon as this new branch is ready. I should also validate this on several platforms before being sure nothing has been missed.
As we retire LTI1.1 in the same operation, we'll probably also have to produce migration scripts for LTI configurations. That could include automated key generation.
@@ -93,7 +93,7 @@ def add_page(self, pattern, classname_or_viewfunc): | |||
if not self._loaded: | |||
raise PluginManagerNotLoadedException() | |||
|
|||
self._flask_app.add_url_rule("/<cookieless:sessionid>" + pattern[1:], view_func=classname_or_viewfunc) | |||
self._flask_app.add_url_rule("/" + pattern[1:], view_func=classname_or_viewfunc) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
self._flask_app.add_url_rule("/" + pattern[1:], view_func=classname_or_viewfunc) | |
self._flask_app.add_url_rule(pattern, view_func=classname_or_viewfunc) |
flask.g.lti_session = self._database.lti_sessions.find_one({'session_id': flask.request.args['session_id']}) | ||
if 'lti_session' not in flask.g or not flask.g.lti_session: | ||
flask.g.lti_session = {} | ||
return flask.g.lti_session |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should document somewhere that flask.g
has the same lifetime than a request and that it should therefore be safe to use it this way in a multi-process, multi-user production deployment.
if app.user_manager.session_is_lti(): | ||
lti_session_id = flask.request.args.get('session_id', flask.g.get('lti_session_id')) | ||
app.user_manager._database.lti_sessions.find_one_and_update({'session_id': lti_session_id}, {'$set': flask.g.lti_session}, upsert=True) | ||
# TODO(mp): Find whether the session should be dropped instead? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Based on what is done in mong_sessions.py
, the session should be dropped at the opening stage, according to the expiration time.
yield '/'.join([iss, client_config['client_id'], deployment_id]) | ||
|
||
def lti_keyset_hash(self, issuer: str, client_id: str) -> str: | ||
return hashlib.md5((issuer + client_id).encode('utf-8')).digest().hex() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not probable but maybe we should add a delimiter char to avoid conflicts with different sets of issuer and client ids.
cookieless = False | ||
sid = request.cookies.get(self.get_cookie_name(app)) | ||
if lti_session or is_lti_launch: | ||
return None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My understanding here is that, before, we returned the result of self.session_class(sid=None)
while here we return directly None
. Is that equivalent ?
@@ -211,7 +220,7 @@ def POST(self, *args, **kwargs): | |||
if not self.user_manager.session_username() and not self.__class__.__name__ == "ProfilePage": | |||
return redirect("/preferences/profile") | |||
|
|||
if not self.is_lti_page and self.user_manager.session_lti_info() is not None: # lti session | |||
if not self.is_lti_page and self.user_manager.session_lti_info() is not None: # lti session, TODO(mp): Not sure whether it is still needed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if not self.is_lti_page and self.user_manager.session_lti_info() is not None: # lti session, TODO(mp): Not sure whether it is still needed | |
if not self.is_lti_page and self.user_manager.session_lti_info() is not None: # lti session |
data=None, error=_("Missing LTI session id")) | ||
def _get_lti_session_data(self): | ||
if not self.user_manager.session_is_lti(): | ||
return self.template_helper.render("lti_bind.html", success=False, data=None, error=_("Missing LTI session id")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The method returns a tuple below, not here. Besides, I would avoid returning rendered templates directly and try to gather these calls to render
in one unique method, even if this is not the case for most of the existing code.
super().__init__(*args, **kwargs) | ||
|
||
def can_set_keys_expiration(self) -> bool: | ||
return False # TODO(mp): I think it's reasonable to clean LTI Launch messages further than a week away tho |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed.
can_report_grades = message_launch.has_ags() and tool_conf.get_iss_config(iss=message_launch.get_iss(), | ||
client_id=message_launch.get_client_id()).get('auth_token_url') | ||
|
||
session_id = hashlib.sha256(launch_id.encode('utf-8')).digest().hex() # TODO(mp): Make this more secure |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This depends on what the launch id is based on. However, as it is probably shared externally, it would indeed be more secure to add some private hint in the session id generation to avoid session hijacking. Maybe based on the flask session secret parameter.
|
||
if ags.can_put_grade(): | ||
sc = Grade() | ||
# TODO(mp): Is there a better timestamp to set with the score? Submission time? Grading time? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe it's better to always set the score as the latest for the platform (and therefore keep the datetime.now()
). This way we ensure the score shown on INGInious is the one that is stored by the platform.
This PR updates the LTI support of INGInious to LTI 1.3, along with a couple of internal changes.
The support of LTI 1.1 is dropped by these changes so all integrations must be updated when integrating this code into running instances.
I'll detail the contributions as they are integrated into the PR.
7a3a4d6 has been tested with a test instance of the syllabus and INGInious with the existing LTI code. I tried submitting code to a task and browsing submissions.
9b1d6e5 updates the basic features (i.e. not the Outcome service) to LTI 1.3. This has been tested with an ongoing patch to the syllabus and saLTIre.
11ebad4 implements the LTI Assignment and Grade service to report grades back to the LTI Platform as a modern replacement to the Outcome service. This has been tested with the test tools of IMS Global as I couldn't configure saLTIre properly so the message carrying the grade could be authenticated by the platform. I suppose IMS Global checks this properly.
262f9fd gives a try at making a clear separation between regular Flask sessions and LTI sessions. Both are still backed by the database. The
UserManager
becomes the access point to the session now. Tested with the syllabus, saLTIre and IMS Global.