-
Notifications
You must be signed in to change notification settings - Fork 14
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
Launch Canvas Studio assignments using an admin account #6139
Conversation
19e9004
to
6c8becd
Compare
253acdf
to
7f657f0
Compare
b509491
to
c4956ea
Compare
c4956ea
to
5966b86
Compare
The Canvas Studio API has a restriction that only the owner of a video can get a download URL or a transcript using it. This means that other instructors and students in a course would not be able to launch a Canvas Studio assignment if we used their authentication to request the video URL. As a workaround, use an admin account to request the download URL and transcript when launching an assignment. This requires us to configure an admin account to use for this purpose in Hypothesis settings, and for that user to authenticate with Hypothesis at least once, eg. by creating or launching an assignment, before any teachers or students launch an assignment.
5966b86
to
e9d974a
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.
What this PR does is a bit hacky but it's a necessary work around to work with the Canvas Studio API.
As I mentioned elsewhere that we might even apply the same workaround (using a token for a user other than the current one ) in the future, for example in course copy.
Pointed to a possible refactor to avoid making queries to User from outside UserSerivce. That relatively straightforward and we use that pattern elsewhere. We could in any case do that in follow up PR
I also mention the possibility of a refactor in the OAuth2Service factory, avoid passing there the current requests details and move those to explicit parameters in .get/.save. That's probably something to discuss beforehand as it might be a bigger change.
assert not self._is_admin() | ||
|
||
admin_email = self._admin_email() | ||
admin_user = ( |
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.
IMO this should be refactored into UserService
That could be done in a number of way, my prefered option would be:
public UserService.search taking optional:
- application_instance
- user_id
Update the existing private _user_search_query to support, conditally, all of these parameters.
Some examples:
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.
A difference from the search
APIs is that here I'm using one_or_none
both to get a single item instead of a list, and as an assert that we never have ambiguity over the results. This could still go in UserService
, but it would be more of a "fetch"/"lookup" API.
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.
You can do one_or_none in a search type interface (but it will be uglier) but you can't do search in a fetch/lookup interface.
One alternative we haven't used yet would be to expose the search query method returning a query object instead of one or a list of models.
That way the centralization benefits of having it over UserService will still be there (eg I we wanted to exclude an hypothetical deleted=True
it will be a one line change) but you still get the flexibility of other services executing the query themselves (ie they can use .one() .one_or_none() or even use the query as a subquery...)
@@ -316,6 +403,56 @@ def _api_url(self, path: str) -> str: | |||
site = self._canvas_studio_site() | |||
return f"{site}/api/public/{path}" | |||
|
|||
def _admin_email(self) -> str: |
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.
Arguably this could look nicer with a @property
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.
@property
are a bit more tricky to mock so that could be good reason to prefer not to use them.
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.
Do we have strong conventions in the codebase about when to use properties and when to use methods without arguments? On the JS side we rarely use getters (although they are certainly "allowed"), which I think is mostly a convention arising from developers' backgrounds / preferences.
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.
Nah, mostly personal preference, I'd say obj.value()
looks ugly but again, personal preference.
) | ||
return admin_email | ||
|
||
def _is_admin(self) -> bool: |
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.
Same here, @property
would look nicer
return OAuth2TokenService( | ||
request.db, request.lti_user.application_instance, request.lti_user.user_id | ||
request.db, | ||
request.lti_user.application_instance, |
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 IMO a very extended attipattern in the LMS code base
The service factories create services that are only useful in one context, here only useful to fetch the current user in LTI launch context.
Saving one method parameter complicates things everywhere else.
I reckon we should aim to take these explicitly as method parameters making the LTI use case just a tight more complex (ie passing a value that's readily available everywhere as request.lti_user) but making more complex use cases like this one possible without having to come up with new patterns and heachaches.
That being said, depending on the service that could be costly refactor.
That being said, parametrizing the factory as one here also works 👍
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.
That being said, depending on the service that could be costly refactor.
At first glance it looks mostly like a tedious refactor rather than complex one.
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 service factories are automatically invoked by the pyramid_services
machinery when calling request.find_service
, so that presumably means they have to be able to work without required extra arguments?
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 point is that these should not be service arguments but extra service method parameters, ie making this not the "Current user token service" but "User token service".
I reckon in this case, as this is the first time we have this need the refactor will look much more verbose as this ties into the HTTPService as well. Calling the service factory does the trick thought.
The Canvas Studio API has a constraint that only the owner of a video or an admin can use the APIs for getting a video download/playback URL or the transcript. Before this PR, only the instructor who uploaded a video could actually launch the assignment.
In #6160 a new application instance setting
canvas_studio.admin_email
was added, which specifies the email address of a user with admin permissions in Canvas Studio. In this PR, when an assignment is launched, an API token belonging to this user is used to fetch the video and transcript. In order for this to work, the admin user must authenticate Canvas Studio once by configuring or launching an assignment. The user does not have to be an administrator within Canvas itself, they just need to have the necessary permissions with Canvas Studio.This solution is not ideal because it requires extra work for end-users to set up, and if the admin user ever revokes API access in Canvas Studio, launching assignments will break for everyone, until the admin user re-authenticates.
Summary of changes:
OAuthHTTPService
which use a specified user ID, instead of the user ID associated with the current requestCanvasStudioService
to create two instances ofOAuthHTTPService
, one for the current user and one for the admin userCanvasStudioService
which fetch the transcript and video to use the admin user instance ofOAuthHTTPService
, if the current user is not the admin userTesting:
There are a variety of error scenarios that we need to handle:
canvas_studio.admin_email
setting is not configuredSee test cases for
CanvasStudioService
for the messages that each of these should generate.