-
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
Make LTI http service take an LTIRegistration #6596
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,6 +10,7 @@ | |
|
||
from typing import TypedDict | ||
|
||
from lms.models import LTIRegistration | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This code is not really used in the code base yet, from the beginning on the same files (where GH doesn't allow me to post a comment):
|
||
from lms.services.ltia_http import LTIAHTTPService | ||
|
||
|
||
|
@@ -31,20 +32,22 @@ class LTINamesRolesService: | |
"https://purl.imsglobal.org/spec/lti-nrps/scope/contextmembership.readonly" | ||
] | ||
|
||
def __init__(self, service_url: str, ltia_http_service: LTIAHTTPService): | ||
self._service_url = service_url | ||
def __init__(self, ltia_http_service: LTIAHTTPService): | ||
self._ltia_service = ltia_http_service | ||
|
||
def get_context_memberships(self) -> list[Member]: | ||
def get_context_memberships( | ||
self, lti_registration: LTIRegistration, service_url: str | ||
) -> list[Member]: | ||
""" | ||
Get all the memberships of a context (a course). | ||
|
||
The course is defined by the service URL which will obtain | ||
from a LTI launch parameter and is always linked to an specific context. | ||
""" | ||
response = self._ltia_service.request( | ||
lti_registration, | ||
"GET", | ||
self._service_url, | ||
service_url, | ||
scopes=self.LTIA_SCOPES, | ||
headers={ | ||
"Accept": "application/vnd.ims.lti-nrps.v2.membershipcontainer+json" | ||
|
@@ -55,9 +58,4 @@ def get_context_memberships(self) -> list[Member]: | |
|
||
|
||
def factory(_context, request): | ||
return LTINamesRolesService( | ||
service_url=request.lti_jwt.get( | ||
"https://purl.imsglobal.org/spec/lti-nrps/claim/namesroleservice", {} | ||
).get("context_memberships_url"), | ||
ltia_http_service=request.find_service(LTIAHTTPService), | ||
) | ||
return LTINamesRolesService(ltia_http_service=request.find_service(LTIAHTTPService)) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,42 +15,52 @@ | |
class LTIAHTTPService: | ||
"""Send LTI Advantage requests and return the responses.""" | ||
|
||
def __init__( # noqa: PLR0913 | ||
def __init__( | ||
self, | ||
lti_registration: LTIRegistration, | ||
plugin: MiscPlugin, | ||
jwt_service: JWTService, | ||
http, | ||
jwt_oauth2_token_service: JWTOAuth2TokenService, | ||
): | ||
self._lti_registration = lti_registration | ||
self._jwt_service = jwt_service | ||
self._http = http | ||
self._plugin = plugin | ||
self._jwt_oauth2_token_service = jwt_oauth2_token_service | ||
|
||
def request(self, method, url, scopes, headers=None, **kwargs): | ||
def request( # noqa: PLR0913 | ||
self, | ||
lti_registration: LTIRegistration, | ||
method, | ||
url, | ||
scopes, | ||
headers=None, | ||
**kwargs, | ||
): | ||
headers = headers or {} | ||
|
||
assert "Authorization" not in headers | ||
|
||
access_token = self._get_access_token(scopes) | ||
access_token = self._get_access_token(lti_registration, scopes) | ||
headers["Authorization"] = f"Bearer {access_token}" | ||
|
||
return self._http.request(method, url, headers=headers, **kwargs) | ||
|
||
def _get_access_token(self, scopes: list[str]) -> str: | ||
def _get_access_token( | ||
self, lti_registration: LTIRegistration, scopes: list[str] | ||
) -> str: | ||
"""Get a valid access token from the DB or get a new one from the LMS.""" | ||
token = self._jwt_oauth2_token_service.get_token(self._lti_registration, scopes) | ||
token = self._jwt_oauth2_token_service.get_token(lti_registration, scopes) | ||
if not token: | ||
LOG.debug("Requesting new LTIA JWT token") | ||
token = self._get_new_access_token(scopes) | ||
token = self._get_new_access_token(lti_registration, scopes) | ||
else: | ||
LOG.debug("Using cached LTIA JWT token") | ||
|
||
return token.access_token | ||
|
||
def _get_new_access_token(self, scopes: list[str]) -> JWTOAuth2Token: | ||
def _get_new_access_token( | ||
self, lti_registration: LTIRegistration, scopes: list[str] | ||
) -> JWTOAuth2Token: | ||
""" | ||
Get an access token from the LMS to use in LTA services. | ||
|
||
|
@@ -62,15 +72,15 @@ def _get_new_access_token(self, scopes: list[str]) -> JWTOAuth2Token: | |
{ | ||
"exp": now + timedelta(hours=1), | ||
"iat": now, | ||
"iss": self._lti_registration.client_id, | ||
"sub": self._lti_registration.client_id, | ||
"aud": self._plugin.get_ltia_aud_claim(self._lti_registration), | ||
"iss": lti_registration.client_id, | ||
"sub": lti_registration.client_id, | ||
"aud": self._plugin.get_ltia_aud_claim(lti_registration), | ||
"jti": uuid.uuid4().hex, | ||
} | ||
) | ||
|
||
response = self._http.post( | ||
self._lti_registration.token_url, | ||
lti_registration.token_url, | ||
data={ | ||
"grant_type": "client_credentials", | ||
"client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", | ||
|
@@ -87,7 +97,7 @@ def _get_new_access_token(self, scopes: list[str]) -> JWTOAuth2Token: | |
raise | ||
|
||
token = self._jwt_oauth2_token_service.save_token( | ||
lti_registration=self._lti_registration, | ||
lti_registration=lti_registration, | ||
scopes=scopes, | ||
access_token=token_data["access_token"], | ||
expires_in=token_data["expires_in"], | ||
|
@@ -97,7 +107,6 @@ def _get_new_access_token(self, scopes: list[str]) -> JWTOAuth2Token: | |
|
||
def factory(_context, request): | ||
return LTIAHTTPService( | ||
request.lti_user.application_instance.lti_registration, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In general I don't like the existing pattern of scoping services to one request, AI or similar. They become only useful in one context when they could be more useful just passing more parameters to the relevant functions. Here we are planning to call this from a celery task, for many different registrations. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed. Services scoped to the request tend to accidentally become stateful, and later refactors or dependencies between services end up producing accidental crossed data or even memory leaks. With truly stateless services where the contextual information is passed via method arguments, we could just have a single persistent instance shared by all requests, as you mentioned above. |
||
request.product.plugin.misc, | ||
request.find_service(JWTService), | ||
request.find_service(name="http"), | ||
|
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.
Pass the registration again on existing ltia_service calls.