ff-ltitoolkit 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ff_ltitoolkit-0.1.0.dist-info/METADATA +98 -0
- ff_ltitoolkit-0.1.0.dist-info/RECORD +94 -0
- ff_ltitoolkit-0.1.0.dist-info/WHEEL +4 -0
- ff_ltitoolkit-0.1.0.dist-info/licenses/LICENSE +21 -0
- ltitoolkit/__init__.py +20 -0
- ltitoolkit/adapters/__init__.py +11 -0
- ltitoolkit/adapters/brightspace/__init__.py +35 -0
- ltitoolkit/adapters/brightspace/client.py +176 -0
- ltitoolkit/adapters/canvas/__init__.py +27 -0
- ltitoolkit/adapters/canvas/client.py +142 -0
- ltitoolkit/advantage/__init__.py +9 -0
- ltitoolkit/advantage/service.py +96 -0
- ltitoolkit/core/__init__.py +19 -0
- ltitoolkit/core/actions.py +6 -0
- ltitoolkit/core/assignments_grades.py +300 -0
- ltitoolkit/core/contrib/__init__.py +0 -0
- ltitoolkit/core/contrib/django/__init__.py +5 -0
- ltitoolkit/core/contrib/django/cookie.py +56 -0
- ltitoolkit/core/contrib/django/launch_data_storage/__init__.py +0 -0
- ltitoolkit/core/contrib/django/launch_data_storage/cache.py +10 -0
- ltitoolkit/core/contrib/django/lti1p3_tool_config/__init__.py +139 -0
- ltitoolkit/core/contrib/django/lti1p3_tool_config/admin.py +48 -0
- ltitoolkit/core/contrib/django/lti1p3_tool_config/apps.py +6 -0
- ltitoolkit/core/contrib/django/lti1p3_tool_config/migrations/0001_initial.py +168 -0
- ltitoolkit/core/contrib/django/lti1p3_tool_config/migrations/__init__.py +0 -0
- ltitoolkit/core/contrib/django/lti1p3_tool_config/models.py +185 -0
- ltitoolkit/core/contrib/django/message_launch.py +39 -0
- ltitoolkit/core/contrib/django/oidc_login.py +41 -0
- ltitoolkit/core/contrib/django/redirect.py +34 -0
- ltitoolkit/core/contrib/django/request.py +32 -0
- ltitoolkit/core/contrib/django/session.py +5 -0
- ltitoolkit/core/contrib/flask/__init__.py +7 -0
- ltitoolkit/core/contrib/flask/cookie.py +34 -0
- ltitoolkit/core/contrib/flask/launch_data_storage/__init__.py +0 -0
- ltitoolkit/core/contrib/flask/launch_data_storage/cache.py +9 -0
- ltitoolkit/core/contrib/flask/message_launch.py +32 -0
- ltitoolkit/core/contrib/flask/oidc_login.py +31 -0
- ltitoolkit/core/contrib/flask/redirect.py +34 -0
- ltitoolkit/core/contrib/flask/request.py +40 -0
- ltitoolkit/core/contrib/flask/session.py +5 -0
- ltitoolkit/core/contrib/py.typed +0 -0
- ltitoolkit/core/cookie.py +17 -0
- ltitoolkit/core/cookies_allowed_check.py +151 -0
- ltitoolkit/core/course_groups.py +115 -0
- ltitoolkit/core/deep_link.py +100 -0
- ltitoolkit/core/deep_link_resource.py +96 -0
- ltitoolkit/core/deployment.py +13 -0
- ltitoolkit/core/exception.py +16 -0
- ltitoolkit/core/grade.py +143 -0
- ltitoolkit/core/launch_data_storage/__init__.py +0 -0
- ltitoolkit/core/launch_data_storage/base.py +75 -0
- ltitoolkit/core/launch_data_storage/cache.py +43 -0
- ltitoolkit/core/launch_data_storage/session.py +29 -0
- ltitoolkit/core/lineitem.py +205 -0
- ltitoolkit/core/message_launch.py +828 -0
- ltitoolkit/core/message_validators/__init__.py +13 -0
- ltitoolkit/core/message_validators/abstract.py +25 -0
- ltitoolkit/core/message_validators/deep_link.py +34 -0
- ltitoolkit/core/message_validators/privacy_launch.py +40 -0
- ltitoolkit/core/message_validators/resource_message.py +21 -0
- ltitoolkit/core/message_validators/submission_review.py +45 -0
- ltitoolkit/core/names_roles.py +97 -0
- ltitoolkit/core/oidc_login.py +275 -0
- ltitoolkit/core/py.typed +0 -0
- ltitoolkit/core/redirect.py +24 -0
- ltitoolkit/core/registration.py +119 -0
- ltitoolkit/core/request.py +17 -0
- ltitoolkit/core/roles.py +109 -0
- ltitoolkit/core/service_connector.py +144 -0
- ltitoolkit/core/session.py +70 -0
- ltitoolkit/core/tool_config/__init__.py +4 -0
- ltitoolkit/core/tool_config/abstract.py +117 -0
- ltitoolkit/core/tool_config/dict.py +253 -0
- ltitoolkit/core/tool_config/json_file.py +100 -0
- ltitoolkit/core/tool_config/py.typed +0 -0
- ltitoolkit/core/utils.py +10 -0
- ltitoolkit/dynamic_registration/__init__.py +39 -0
- ltitoolkit/dynamic_registration/models.py +192 -0
- ltitoolkit/dynamic_registration/service.py +156 -0
- ltitoolkit/dynamic_registration/store.py +40 -0
- ltitoolkit/dynamic_registration/tool_conf.py +102 -0
- ltitoolkit/exceptions.py +42 -0
- ltitoolkit/fastapi/__init__.py +30 -0
- ltitoolkit/fastapi/cookie.py +53 -0
- ltitoolkit/fastapi/dynamic_registration.py +40 -0
- ltitoolkit/fastapi/message_launch.py +60 -0
- ltitoolkit/fastapi/oidc_login.py +47 -0
- ltitoolkit/fastapi/redirect.py +54 -0
- ltitoolkit/fastapi/request.py +77 -0
- ltitoolkit/fastapi/session.py +13 -0
- ltitoolkit/http.py +80 -0
- ltitoolkit/token/__init__.py +20 -0
- ltitoolkit/token/cache.py +47 -0
- ltitoolkit/token/service.py +165 -0
|
@@ -0,0 +1,828 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import hashlib
|
|
3
|
+
import json
|
|
4
|
+
import typing as t
|
|
5
|
+
import uuid
|
|
6
|
+
from abc import ABCMeta, abstractmethod
|
|
7
|
+
|
|
8
|
+
import jwt # type: ignore
|
|
9
|
+
import requests
|
|
10
|
+
import typing_extensions as te
|
|
11
|
+
from jwcrypto.jwk import JWK # type: ignore
|
|
12
|
+
|
|
13
|
+
from .actions import Action
|
|
14
|
+
from .assignments_grades import AssignmentsGradesService, TAssignmentsGradersData
|
|
15
|
+
from .cookie import CookieService
|
|
16
|
+
from .course_groups import CourseGroupsService, TGroupsServiceData
|
|
17
|
+
from .deep_link import DeepLink, TDeepLinkData
|
|
18
|
+
from .exception import LtiException
|
|
19
|
+
from .launch_data_storage.base import DisableSessionId, LaunchDataStorage
|
|
20
|
+
from .message_validators import get_validators
|
|
21
|
+
from .message_validators.deep_link import DeepLinkMessageValidator
|
|
22
|
+
from .message_validators.privacy_launch import PrivacyLaunchValidator
|
|
23
|
+
from .message_validators.resource_message import ResourceMessageValidator
|
|
24
|
+
from .message_validators.submission_review import SubmissionReviewLaunchValidator
|
|
25
|
+
from .names_roles import NamesRolesProvisioningService, TNamesAndRolesData
|
|
26
|
+
from .roles import (
|
|
27
|
+
StaffRole,
|
|
28
|
+
StudentRole,
|
|
29
|
+
TeacherRole,
|
|
30
|
+
TeachingAssistantRole,
|
|
31
|
+
DesignerRole,
|
|
32
|
+
ObserverRole,
|
|
33
|
+
TransientRole,
|
|
34
|
+
)
|
|
35
|
+
from .registration import Registration, TKeySet
|
|
36
|
+
from .request import Request
|
|
37
|
+
from .session import SessionService
|
|
38
|
+
from .service_connector import ServiceConnector, REQUESTS_USER_AGENT
|
|
39
|
+
from .tool_config import ToolConfAbstract
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
TResourceLinkClaim = te.TypedDict(
|
|
43
|
+
"TResourceLinkClaim",
|
|
44
|
+
{
|
|
45
|
+
# Required data
|
|
46
|
+
"id": str,
|
|
47
|
+
# Optional data
|
|
48
|
+
"description": str,
|
|
49
|
+
"title": str,
|
|
50
|
+
},
|
|
51
|
+
total=False,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
TContextClaim = te.TypedDict(
|
|
55
|
+
"TContextClaim",
|
|
56
|
+
{
|
|
57
|
+
# Required data
|
|
58
|
+
"id": str,
|
|
59
|
+
# Optional data
|
|
60
|
+
"label": str,
|
|
61
|
+
"title": str,
|
|
62
|
+
"type": t.List[str],
|
|
63
|
+
},
|
|
64
|
+
total=False,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
TToolPlatformClaim = te.TypedDict(
|
|
68
|
+
"TToolPlatformClaim",
|
|
69
|
+
{
|
|
70
|
+
# Required data
|
|
71
|
+
"guid": str,
|
|
72
|
+
# Optional data
|
|
73
|
+
"contact_email": str,
|
|
74
|
+
"description": str,
|
|
75
|
+
"name": str,
|
|
76
|
+
"url": str,
|
|
77
|
+
"product_family_code": str,
|
|
78
|
+
"version": str,
|
|
79
|
+
},
|
|
80
|
+
total=False,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
TLearningInformationServicesClaim = te.TypedDict(
|
|
84
|
+
"TLearningInformationServicesClaim",
|
|
85
|
+
{
|
|
86
|
+
"person_sourcedid": str,
|
|
87
|
+
"course_offering_sourcedid": str,
|
|
88
|
+
"course_section_sourcedid": str,
|
|
89
|
+
},
|
|
90
|
+
total=False,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
TMigrationClaim = te.TypedDict(
|
|
94
|
+
"TMigrationClaim",
|
|
95
|
+
{
|
|
96
|
+
# Required data
|
|
97
|
+
"oauth_consumer_key": str,
|
|
98
|
+
# Optional data
|
|
99
|
+
"oauth_consumer_key_sign": str,
|
|
100
|
+
"user_id": str,
|
|
101
|
+
"context_id": str,
|
|
102
|
+
"tool_consumer_instance_guid ": str,
|
|
103
|
+
"resource_link_id": str,
|
|
104
|
+
},
|
|
105
|
+
total=False,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
TForUserClaim = te.TypedDict(
|
|
109
|
+
"TForUserClaim",
|
|
110
|
+
{
|
|
111
|
+
# Required data
|
|
112
|
+
"user_id": str,
|
|
113
|
+
# Optional data
|
|
114
|
+
"person_sourcedId": str,
|
|
115
|
+
"given_name": str,
|
|
116
|
+
"family_name": str,
|
|
117
|
+
"name": str,
|
|
118
|
+
"email": str,
|
|
119
|
+
"roles": t.List[str],
|
|
120
|
+
},
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
TLaunchData = te.TypedDict(
|
|
124
|
+
"TLaunchData",
|
|
125
|
+
{
|
|
126
|
+
# Required data
|
|
127
|
+
"iss": str,
|
|
128
|
+
"nonce": str,
|
|
129
|
+
"aud": t.Union[t.List[str], str],
|
|
130
|
+
"https://purl.imsglobal.org/spec/lti/claim/message_type": te.Literal[
|
|
131
|
+
"LtiResourceLinkRequest",
|
|
132
|
+
"LtiDeepLinkingRequest",
|
|
133
|
+
"DataPrivacyLaunchRequest",
|
|
134
|
+
"LtiSubmissionReviewRequest",
|
|
135
|
+
],
|
|
136
|
+
"https://purl.imsglobal.org/spec/lti/claim/version": te.Literal["1.3.0"],
|
|
137
|
+
"https://purl.imsglobal.org/spec/lti/claim/deployment_id": str,
|
|
138
|
+
"https://purl.imsglobal.org/spec/lti/claim/target_link_uri": str,
|
|
139
|
+
"https://purl.imsglobal.org/spec/lti/claim/resource_link": TResourceLinkClaim,
|
|
140
|
+
"https://purl.imsglobal.org/spec/lti/claim/roles": t.List[str],
|
|
141
|
+
"sub": str,
|
|
142
|
+
# Optional data
|
|
143
|
+
"given_name": str,
|
|
144
|
+
"family_name": str,
|
|
145
|
+
"name": str,
|
|
146
|
+
"email": str,
|
|
147
|
+
"https://purl.imsglobal.org/spec/lti/claim/context": TContextClaim,
|
|
148
|
+
"https://purl.imsglobal.org/spec/lti/claim/lis": TLearningInformationServicesClaim,
|
|
149
|
+
"https://purl.imsglobal.org/spec/lti/claim/custom": t.Mapping[str, str],
|
|
150
|
+
"https://purl.imsglobal.org/spec/lti-dl/claim/deep_linking_settings": TDeepLinkData,
|
|
151
|
+
"https://purl.imsglobal.org/spec/lti-gs/claim/groupsservice": TGroupsServiceData,
|
|
152
|
+
"https://purl.imsglobal.org/spec/lti-nrps/claim/namesroleservice": TNamesAndRolesData,
|
|
153
|
+
"https://purl.imsglobal.org/spec/lti-ags/claim/endpoint": TAssignmentsGradersData,
|
|
154
|
+
"https://purl.imsglobal.org/spec/lti/claim/tool_platform": TToolPlatformClaim,
|
|
155
|
+
"https://purl.imsglobal.org/spec/lti/claim/role_scope_mentor": t.List[str],
|
|
156
|
+
"https://purl.imsglobal.org/spec/lti/claim/lti1p1": TMigrationClaim,
|
|
157
|
+
"https://purl.imsglobal.org/spec/lti/claim/for_user": TForUserClaim,
|
|
158
|
+
},
|
|
159
|
+
total=False,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
TJwtHeader = te.TypedDict(
|
|
163
|
+
"TJwtHeader",
|
|
164
|
+
{
|
|
165
|
+
"kid": str,
|
|
166
|
+
"alg": str,
|
|
167
|
+
},
|
|
168
|
+
total=False,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
TJwtData = te.TypedDict(
|
|
172
|
+
"TJwtData",
|
|
173
|
+
{
|
|
174
|
+
"header": TJwtHeader,
|
|
175
|
+
"body": TLaunchData,
|
|
176
|
+
},
|
|
177
|
+
total=False,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
REQ = t.TypeVar("REQ", bound=Request)
|
|
181
|
+
TCONF = t.TypeVar("TCONF", bound=ToolConfAbstract)
|
|
182
|
+
SES = t.TypeVar("SES", bound=SessionService)
|
|
183
|
+
COOK = t.TypeVar("COOK", bound=CookieService)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class MessageLaunch(t.Generic[REQ, TCONF, SES, COOK]):
|
|
187
|
+
__metaclass__ = ABCMeta
|
|
188
|
+
_request: REQ
|
|
189
|
+
_tool_config: TCONF
|
|
190
|
+
_session_service: SES
|
|
191
|
+
_cookie_service: COOK
|
|
192
|
+
_jwt: TJwtData
|
|
193
|
+
_jwt_verify_options: t.Dict[str, bool]
|
|
194
|
+
_registration: t.Optional[Registration]
|
|
195
|
+
_launch_id: str
|
|
196
|
+
_validated: bool = False
|
|
197
|
+
_auto_validation: bool = True
|
|
198
|
+
_restored: bool = False
|
|
199
|
+
_id_token_hash: t.Optional[str]
|
|
200
|
+
_public_key_cache_data_storage: t.Optional[LaunchDataStorage[t.Any]] = None
|
|
201
|
+
_public_key_cache_lifetime: t.Optional[int] = None
|
|
202
|
+
|
|
203
|
+
def __init__(
|
|
204
|
+
self,
|
|
205
|
+
request: REQ,
|
|
206
|
+
tool_config: TCONF,
|
|
207
|
+
session_service: t.Optional[SES] = None,
|
|
208
|
+
cookie_service: t.Optional[COOK] = None,
|
|
209
|
+
launch_data_storage: t.Optional[LaunchDataStorage[t.Any]] = None,
|
|
210
|
+
requests_session: t.Optional[requests.Session] = None,
|
|
211
|
+
):
|
|
212
|
+
self._request = request
|
|
213
|
+
self._tool_config = tool_config
|
|
214
|
+
|
|
215
|
+
assert session_service is not None, "Session Service must be set"
|
|
216
|
+
assert cookie_service is not None, "Cookie Service must be set"
|
|
217
|
+
|
|
218
|
+
self._session_service = session_service
|
|
219
|
+
self._cookie_service = cookie_service
|
|
220
|
+
self._launch_id = "lti1p3-launch-" + str(uuid.uuid4())
|
|
221
|
+
self._jwt = {}
|
|
222
|
+
self._jwt_verify_options = {"verify_aud": False}
|
|
223
|
+
self._id_token_hash = None
|
|
224
|
+
self._validated = False
|
|
225
|
+
self._auto_validation = True
|
|
226
|
+
self._restored = False
|
|
227
|
+
self._public_key_cache_data_storage = None
|
|
228
|
+
self._public_key_cache_lifetime = None
|
|
229
|
+
if requests_session:
|
|
230
|
+
self._requests_session = requests_session
|
|
231
|
+
else:
|
|
232
|
+
self._requests_session = requests.Session()
|
|
233
|
+
self._requests_session.headers["User-Agent"] = REQUESTS_USER_AGENT
|
|
234
|
+
|
|
235
|
+
if launch_data_storage:
|
|
236
|
+
self.set_launch_data_storage(launch_data_storage)
|
|
237
|
+
|
|
238
|
+
@abstractmethod
|
|
239
|
+
def _get_request_param(self, key: str) -> str:
|
|
240
|
+
raise NotImplementedError
|
|
241
|
+
|
|
242
|
+
def set_launch_id(self, launch_id: str) -> "MessageLaunch":
|
|
243
|
+
self._launch_id = launch_id
|
|
244
|
+
return self
|
|
245
|
+
|
|
246
|
+
def set_auto_validation(self, enable: bool) -> "MessageLaunch":
|
|
247
|
+
self._auto_validation = enable
|
|
248
|
+
return self
|
|
249
|
+
|
|
250
|
+
def set_jwt(self, val: TJwtData) -> "MessageLaunch":
|
|
251
|
+
self._jwt = val
|
|
252
|
+
return self
|
|
253
|
+
|
|
254
|
+
def set_jwt_verify_options(self, val: t.Dict[str, bool]) -> "MessageLaunch":
|
|
255
|
+
self._jwt_verify_options = val
|
|
256
|
+
return self
|
|
257
|
+
|
|
258
|
+
def set_restored(self) -> "MessageLaunch":
|
|
259
|
+
self._restored = True
|
|
260
|
+
return self
|
|
261
|
+
|
|
262
|
+
def get_session_service(self) -> SES:
|
|
263
|
+
return self._session_service
|
|
264
|
+
|
|
265
|
+
def get_iss(self) -> str:
|
|
266
|
+
iss = self._get_jwt_body().get("iss")
|
|
267
|
+
if not iss:
|
|
268
|
+
raise LtiException('"iss" is empty')
|
|
269
|
+
return iss
|
|
270
|
+
|
|
271
|
+
def get_client_id(self) -> str:
|
|
272
|
+
jwt_body = self._get_jwt_body()
|
|
273
|
+
aud = jwt_body.get("aud")
|
|
274
|
+
return aud[0] if isinstance(aud, list) else aud # type: ignore
|
|
275
|
+
|
|
276
|
+
@classmethod
|
|
277
|
+
def from_cache(
|
|
278
|
+
cls,
|
|
279
|
+
launch_id: str,
|
|
280
|
+
request: REQ,
|
|
281
|
+
tool_config: TCONF,
|
|
282
|
+
session_service: t.Optional[SES] = None,
|
|
283
|
+
cookie_service: t.Optional[COOK] = None,
|
|
284
|
+
launch_data_storage: t.Optional[LaunchDataStorage[t.Any]] = None,
|
|
285
|
+
requests_session: t.Optional[requests.Session] = None,
|
|
286
|
+
) -> "MessageLaunch":
|
|
287
|
+
obj = cls(
|
|
288
|
+
request,
|
|
289
|
+
tool_config,
|
|
290
|
+
session_service=session_service,
|
|
291
|
+
cookie_service=cookie_service,
|
|
292
|
+
launch_data_storage=launch_data_storage,
|
|
293
|
+
requests_session=requests_session,
|
|
294
|
+
)
|
|
295
|
+
launch_data = obj.get_session_service().get_launch_data(launch_id)
|
|
296
|
+
if not launch_data:
|
|
297
|
+
raise LtiException("Launch data not found")
|
|
298
|
+
return (
|
|
299
|
+
obj.set_launch_id(launch_id)
|
|
300
|
+
.set_auto_validation(enable=False)
|
|
301
|
+
.set_jwt(t.cast(TJwtData, {"body": launch_data}))
|
|
302
|
+
.set_restored()
|
|
303
|
+
.validate_registration()
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
def validate(self) -> "MessageLaunch":
|
|
307
|
+
"""
|
|
308
|
+
Validates all aspects of an incoming LTI message launch and caches the launch if successful.
|
|
309
|
+
"""
|
|
310
|
+
if self._restored:
|
|
311
|
+
raise LtiException("Can't validate restored launch")
|
|
312
|
+
self._validated = True
|
|
313
|
+
try:
|
|
314
|
+
return (
|
|
315
|
+
self.validate_state()
|
|
316
|
+
.validate_jwt_format()
|
|
317
|
+
.validate_nonce()
|
|
318
|
+
.validate_registration()
|
|
319
|
+
.validate_jwt_signature()
|
|
320
|
+
.validate_deployment()
|
|
321
|
+
.validate_message()
|
|
322
|
+
.save_launch_data()
|
|
323
|
+
)
|
|
324
|
+
except Exception:
|
|
325
|
+
self._validated = False
|
|
326
|
+
raise
|
|
327
|
+
|
|
328
|
+
def _get_jwt_body(self) -> TLaunchData:
|
|
329
|
+
if not self._validated and self._auto_validation:
|
|
330
|
+
self.validate()
|
|
331
|
+
return self._jwt.get("body", {})
|
|
332
|
+
|
|
333
|
+
def _get_iss(self) -> str:
|
|
334
|
+
iss = self._get_jwt_body().get("iss")
|
|
335
|
+
if not iss:
|
|
336
|
+
raise LtiException('"iss" is empty')
|
|
337
|
+
return iss
|
|
338
|
+
|
|
339
|
+
def _get_id_token(self) -> str:
|
|
340
|
+
id_token = self._get_request_param("id_token")
|
|
341
|
+
if not id_token:
|
|
342
|
+
raise LtiException("Missing id_token")
|
|
343
|
+
return id_token
|
|
344
|
+
|
|
345
|
+
def _get_id_token_hash(self) -> str:
|
|
346
|
+
if not self._id_token_hash:
|
|
347
|
+
id_token = self._get_id_token()
|
|
348
|
+
id_token_param = id_token.encode("utf8")
|
|
349
|
+
self._id_token_hash = hashlib.md5(id_token_param).hexdigest()
|
|
350
|
+
return self._id_token_hash
|
|
351
|
+
|
|
352
|
+
def _get_deployment_id(self) -> str:
|
|
353
|
+
deployment_id = self._get_jwt_body().get(
|
|
354
|
+
"https://purl.imsglobal.org/spec/lti/claim/deployment_id"
|
|
355
|
+
)
|
|
356
|
+
if not deployment_id:
|
|
357
|
+
raise LtiException("deployment_id is not set in jwt body")
|
|
358
|
+
return deployment_id
|
|
359
|
+
|
|
360
|
+
def get_service_connector(self) -> ServiceConnector:
|
|
361
|
+
assert self._registration is not None, "Registration not yet set"
|
|
362
|
+
return ServiceConnector(self._registration, self._requests_session)
|
|
363
|
+
|
|
364
|
+
def has_nrps(self) -> bool:
|
|
365
|
+
"""
|
|
366
|
+
Returns whether or not the current launch can use the names and roles service.
|
|
367
|
+
|
|
368
|
+
:return: bool Returns a boolean indicating the availability of names and roles.
|
|
369
|
+
"""
|
|
370
|
+
return (
|
|
371
|
+
self._get_jwt_body()
|
|
372
|
+
.get("https://purl.imsglobal.org/spec/lti-nrps/claim/namesroleservice", {})
|
|
373
|
+
.get("context_memberships_url", None)
|
|
374
|
+
is not None
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
def get_nrps(self) -> NamesRolesProvisioningService:
|
|
378
|
+
"""
|
|
379
|
+
Fetches an instance of the names and roles service for the current launch.
|
|
380
|
+
|
|
381
|
+
:return: NamesRolesProvisioningService
|
|
382
|
+
"""
|
|
383
|
+
assert self._registration is not None, "Registration not yet set"
|
|
384
|
+
connector = self.get_service_connector()
|
|
385
|
+
names_role_service = self._get_jwt_body().get(
|
|
386
|
+
"https://purl.imsglobal.org/spec/lti-nrps/claim/namesroleservice"
|
|
387
|
+
)
|
|
388
|
+
if not names_role_service:
|
|
389
|
+
raise LtiException("namesroleservice is not set in jwt body")
|
|
390
|
+
return NamesRolesProvisioningService(connector, names_role_service)
|
|
391
|
+
|
|
392
|
+
def has_ags(self) -> bool:
|
|
393
|
+
"""
|
|
394
|
+
Returns whether or not the current launch can use the assignments and grades service.
|
|
395
|
+
|
|
396
|
+
:return: bool Returns a boolean indicating the availability of assignments and grades.
|
|
397
|
+
"""
|
|
398
|
+
return (
|
|
399
|
+
self._get_jwt_body().get(
|
|
400
|
+
"https://purl.imsglobal.org/spec/lti-ags/claim/endpoint", None
|
|
401
|
+
)
|
|
402
|
+
is not None
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
def get_ags(self) -> AssignmentsGradesService:
|
|
406
|
+
"""
|
|
407
|
+
Fetches an instance of the assignments and grades service for the current launch.
|
|
408
|
+
|
|
409
|
+
:return: AssignmentsGradesService
|
|
410
|
+
"""
|
|
411
|
+
assert self._registration is not None, "Registration not yet set"
|
|
412
|
+
connector = self.get_service_connector()
|
|
413
|
+
endpoint = self._get_jwt_body().get(
|
|
414
|
+
"https://purl.imsglobal.org/spec/lti-ags/claim/endpoint"
|
|
415
|
+
)
|
|
416
|
+
if not endpoint:
|
|
417
|
+
raise LtiException("endpoint is not set in jwt body")
|
|
418
|
+
return AssignmentsGradesService(connector, endpoint)
|
|
419
|
+
|
|
420
|
+
def has_cgs(self) -> bool:
|
|
421
|
+
"""
|
|
422
|
+
Returns whether or not the current launch can use the course groups service.
|
|
423
|
+
|
|
424
|
+
:return: bool Returns a boolean indicating the availability of groups.
|
|
425
|
+
"""
|
|
426
|
+
groups_service_data = self._get_jwt_body().get(
|
|
427
|
+
"https://purl.imsglobal.org/spec/lti-gs/claim/groupsservice", {}
|
|
428
|
+
)
|
|
429
|
+
return groups_service_data.get("context_groups_url", None) is not None
|
|
430
|
+
|
|
431
|
+
def get_cgs(self) -> CourseGroupsService:
|
|
432
|
+
"""
|
|
433
|
+
Fetches an instance of the course groups service for the current launch.
|
|
434
|
+
|
|
435
|
+
:return:
|
|
436
|
+
"""
|
|
437
|
+
assert self._registration is not None, "Registration not yet set"
|
|
438
|
+
connector = self.get_service_connector()
|
|
439
|
+
groups_service_data = self._get_jwt_body().get(
|
|
440
|
+
"https://purl.imsglobal.org/spec/lti-gs/claim/groupsservice"
|
|
441
|
+
)
|
|
442
|
+
if not groups_service_data:
|
|
443
|
+
raise LtiException("groupsservice is not set in jwt body")
|
|
444
|
+
context_groups_url = groups_service_data.get("context_groups_url", None)
|
|
445
|
+
if not context_groups_url:
|
|
446
|
+
raise LtiException("context_groups_url is not set in groupsservice section")
|
|
447
|
+
return CourseGroupsService(connector, groups_service_data)
|
|
448
|
+
|
|
449
|
+
def get_deep_link(self) -> DeepLink:
|
|
450
|
+
"""
|
|
451
|
+
Fetches a deep link that can be used to construct a deep linking response.
|
|
452
|
+
|
|
453
|
+
:return: DeepLink
|
|
454
|
+
"""
|
|
455
|
+
assert self._registration is not None, "Registration not yet set"
|
|
456
|
+
|
|
457
|
+
deployment_id = self._get_deployment_id()
|
|
458
|
+
deep_linking_settings = self._get_jwt_body().get(
|
|
459
|
+
"https://purl.imsglobal.org/spec/lti-dl/claim/deep_linking_settings"
|
|
460
|
+
)
|
|
461
|
+
if not deep_linking_settings:
|
|
462
|
+
raise LtiException("deep_linking_settings is not set in jwt body")
|
|
463
|
+
|
|
464
|
+
return DeepLink(self._registration, deployment_id, deep_linking_settings)
|
|
465
|
+
|
|
466
|
+
def get_data_privacy_launch_user(self) -> t.Optional[TForUserClaim]:
|
|
467
|
+
"""
|
|
468
|
+
Applicable for DataPrivacyLaunchRequest only. Returns information about user
|
|
469
|
+
who's data the launch is intended to action upon, for instance the student
|
|
470
|
+
who has requested their data be removed under GDPR's right to be forgotten.
|
|
471
|
+
|
|
472
|
+
:return: dict
|
|
473
|
+
"""
|
|
474
|
+
jwt_body = self._get_jwt_body()
|
|
475
|
+
return jwt_body.get("https://purl.imsglobal.org/spec/lti/claim/for_user")
|
|
476
|
+
|
|
477
|
+
def get_submission_review_user(self) -> t.Optional[TForUserClaim]:
|
|
478
|
+
"""
|
|
479
|
+
Applicable for LtiSubmissionReviewRequest only. Returns information about user
|
|
480
|
+
who's submission should be displayed for review.
|
|
481
|
+
|
|
482
|
+
:return: dict
|
|
483
|
+
"""
|
|
484
|
+
jwt_body = self._get_jwt_body()
|
|
485
|
+
return jwt_body.get("https://purl.imsglobal.org/spec/lti/claim/for_user")
|
|
486
|
+
|
|
487
|
+
def is_deep_link_launch(self) -> bool:
|
|
488
|
+
"""
|
|
489
|
+
Returns whether or not the current launch is a deep linking launch.
|
|
490
|
+
|
|
491
|
+
:return: bool Returns true if the current launch is a deep linking launch.
|
|
492
|
+
"""
|
|
493
|
+
jwt_body = self._get_jwt_body()
|
|
494
|
+
return DeepLinkMessageValidator().can_validate(jwt_body)
|
|
495
|
+
|
|
496
|
+
def is_resource_launch(self) -> bool:
|
|
497
|
+
"""
|
|
498
|
+
Returns whether or not the current launch is a resource launch.
|
|
499
|
+
|
|
500
|
+
:return: bool Returns true if the current launch is a resource launch.
|
|
501
|
+
"""
|
|
502
|
+
jwt_body = self._get_jwt_body()
|
|
503
|
+
return ResourceMessageValidator().can_validate(jwt_body)
|
|
504
|
+
|
|
505
|
+
def is_data_privacy_launch(self) -> bool:
|
|
506
|
+
"""
|
|
507
|
+
Returns whether or not the current launch is a data privacy launch.
|
|
508
|
+
|
|
509
|
+
:return: bool Returns true if the current launch is a data privacy launch.
|
|
510
|
+
"""
|
|
511
|
+
jwt_body = self._get_jwt_body()
|
|
512
|
+
return PrivacyLaunchValidator().can_validate(jwt_body)
|
|
513
|
+
|
|
514
|
+
def is_submission_review_launch(self) -> bool:
|
|
515
|
+
"""
|
|
516
|
+
Returns whether or not the current launch is a submission review launch.
|
|
517
|
+
|
|
518
|
+
:return: bool Returns true if the current launch is a submission review launch.
|
|
519
|
+
"""
|
|
520
|
+
jwt_body = self._get_jwt_body()
|
|
521
|
+
return SubmissionReviewLaunchValidator().can_validate(jwt_body)
|
|
522
|
+
|
|
523
|
+
def get_launch_data(self) -> TLaunchData:
|
|
524
|
+
"""
|
|
525
|
+
Fetches the decoded body of the JWT used in the current launch.
|
|
526
|
+
|
|
527
|
+
:return: dict Returns the decoded json body of the launch
|
|
528
|
+
"""
|
|
529
|
+
return self._get_jwt_body()
|
|
530
|
+
|
|
531
|
+
def get_launch_id(self) -> str:
|
|
532
|
+
"""
|
|
533
|
+
Get the unique launch id for the current launch.
|
|
534
|
+
|
|
535
|
+
:return: str A unique identifier used to re-reference the current launch in subsequent requests.
|
|
536
|
+
"""
|
|
537
|
+
return self._launch_id
|
|
538
|
+
|
|
539
|
+
def get_tool_conf(self) -> TCONF:
|
|
540
|
+
return self._tool_config
|
|
541
|
+
|
|
542
|
+
@staticmethod
|
|
543
|
+
def urlsafe_b64decode(val: str) -> str:
|
|
544
|
+
remainder = len(val) % 4
|
|
545
|
+
if remainder > 0:
|
|
546
|
+
padlen = 4 - remainder
|
|
547
|
+
val = val + ("=" * padlen)
|
|
548
|
+
tmp = val.translate(str.maketrans("-_", "+/")) # type: ignore
|
|
549
|
+
return base64.b64decode(tmp).decode("utf-8") # type: ignore
|
|
550
|
+
|
|
551
|
+
def set_public_key_caching(
|
|
552
|
+
self, data_storage: LaunchDataStorage[t.Any], cache_lifetime: int = 7200
|
|
553
|
+
):
|
|
554
|
+
self._public_key_cache_data_storage = data_storage
|
|
555
|
+
self._public_key_cache_lifetime = cache_lifetime
|
|
556
|
+
|
|
557
|
+
def fetch_public_key(self, key_set_url: str) -> TKeySet:
|
|
558
|
+
cache_key = (
|
|
559
|
+
"key-set-url-" + hashlib.md5(key_set_url.encode("utf-8")).hexdigest()
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
with DisableSessionId(self._public_key_cache_data_storage):
|
|
563
|
+
if self._public_key_cache_data_storage:
|
|
564
|
+
public_key = self._public_key_cache_data_storage.get_value(cache_key)
|
|
565
|
+
if public_key:
|
|
566
|
+
return public_key
|
|
567
|
+
|
|
568
|
+
try:
|
|
569
|
+
resp = self._requests_session.get(key_set_url)
|
|
570
|
+
except requests.exceptions.RequestException as e:
|
|
571
|
+
raise LtiException(
|
|
572
|
+
f"Error during fetch URL {key_set_url}: {str(e)}"
|
|
573
|
+
) from e
|
|
574
|
+
try:
|
|
575
|
+
public_key = resp.json()
|
|
576
|
+
if self._public_key_cache_data_storage:
|
|
577
|
+
self._public_key_cache_data_storage.set_value(
|
|
578
|
+
cache_key, public_key, self._public_key_cache_lifetime
|
|
579
|
+
)
|
|
580
|
+
return public_key
|
|
581
|
+
except ValueError as e:
|
|
582
|
+
raise LtiException(
|
|
583
|
+
f"Invalid response from {key_set_url}. Must be JSON: {resp.text}"
|
|
584
|
+
) from e
|
|
585
|
+
|
|
586
|
+
def get_public_key(self) -> t.Tuple[str, str]:
|
|
587
|
+
assert self._registration is not None, "Registration not yet set"
|
|
588
|
+
public_key_set = self._registration.get_key_set()
|
|
589
|
+
key_set_url = self._registration.get_key_set_url()
|
|
590
|
+
|
|
591
|
+
if not public_key_set:
|
|
592
|
+
assert (
|
|
593
|
+
key_set_url is not None
|
|
594
|
+
), "If public_key_set is not set, public_set_url should be set"
|
|
595
|
+
if key_set_url.startswith(("http://", "https://")):
|
|
596
|
+
public_key_set = self.fetch_public_key(key_set_url)
|
|
597
|
+
self._registration.set_key_set(public_key_set)
|
|
598
|
+
else:
|
|
599
|
+
raise LtiException("Invalid URL: " + key_set_url)
|
|
600
|
+
|
|
601
|
+
# Find key used to sign the JWT (matches the KID in the header)
|
|
602
|
+
kid = self._jwt.get("header", {}).get("kid", None)
|
|
603
|
+
alg = self._jwt.get("header", {}).get("alg", None)
|
|
604
|
+
|
|
605
|
+
if not kid:
|
|
606
|
+
raise LtiException("JWT KID not found")
|
|
607
|
+
if not alg:
|
|
608
|
+
raise LtiException("JWT ALG not found")
|
|
609
|
+
|
|
610
|
+
for key in public_key_set["keys"]:
|
|
611
|
+
key_kid = key.get("kid")
|
|
612
|
+
key_alg = key.get("alg", "RS256")
|
|
613
|
+
if key_kid and key_kid == kid and key_alg == alg:
|
|
614
|
+
try:
|
|
615
|
+
key_json = json.dumps(key)
|
|
616
|
+
jwk_obj = JWK.from_json(key_json)
|
|
617
|
+
public_key = jwk_obj.export_to_pem()
|
|
618
|
+
return public_key, key_alg
|
|
619
|
+
except (ValueError, TypeError) as e:
|
|
620
|
+
raise LtiException("Can't convert JWT key to PEM format") from e
|
|
621
|
+
|
|
622
|
+
# Could not find public key with a matching kid and alg.
|
|
623
|
+
raise LtiException("Unable to find public key")
|
|
624
|
+
|
|
625
|
+
def validate_state(self) -> "MessageLaunch":
|
|
626
|
+
# Check State for OIDC.
|
|
627
|
+
state_from_request = self._get_request_param("state")
|
|
628
|
+
if not state_from_request:
|
|
629
|
+
raise LtiException("Missing state param")
|
|
630
|
+
|
|
631
|
+
id_token_hash = self._get_id_token_hash()
|
|
632
|
+
if not self._session_service.check_state_is_valid(
|
|
633
|
+
state_from_request, id_token_hash
|
|
634
|
+
):
|
|
635
|
+
state_from_cookie = self._cookie_service.get_cookie(state_from_request)
|
|
636
|
+
if state_from_request != state_from_cookie:
|
|
637
|
+
# Error if state doesn't match.
|
|
638
|
+
raise LtiException("State not found")
|
|
639
|
+
|
|
640
|
+
return self
|
|
641
|
+
|
|
642
|
+
def validate_jwt_format(self) -> "MessageLaunch":
|
|
643
|
+
id_token = self._get_id_token()
|
|
644
|
+
jwt_parts = id_token.split(".")
|
|
645
|
+
|
|
646
|
+
if len(jwt_parts) != 3:
|
|
647
|
+
# Invalid number of parts in JWT.
|
|
648
|
+
raise LtiException("Invalid id_token, JWT must contain 3 parts")
|
|
649
|
+
|
|
650
|
+
try:
|
|
651
|
+
# Decode JWT headers.
|
|
652
|
+
header = self.urlsafe_b64decode(jwt_parts[0])
|
|
653
|
+
self._jwt["header"] = json.loads(header)
|
|
654
|
+
|
|
655
|
+
# Decode JWT body.
|
|
656
|
+
body = self.urlsafe_b64decode(jwt_parts[1])
|
|
657
|
+
self._jwt["body"] = json.loads(body)
|
|
658
|
+
except Exception as e:
|
|
659
|
+
raise LtiException("Invalid JWT format, can't be decoded") from e
|
|
660
|
+
|
|
661
|
+
return self
|
|
662
|
+
|
|
663
|
+
def validate_nonce(self) -> "MessageLaunch":
|
|
664
|
+
nonce = self._get_jwt_body().get("nonce")
|
|
665
|
+
if not nonce:
|
|
666
|
+
raise LtiException('"nonce" is empty')
|
|
667
|
+
|
|
668
|
+
res = self._session_service.check_nonce(nonce)
|
|
669
|
+
if not res:
|
|
670
|
+
raise LtiException("Invalid Nonce")
|
|
671
|
+
|
|
672
|
+
return self
|
|
673
|
+
|
|
674
|
+
def validate_registration(self) -> "MessageLaunch":
|
|
675
|
+
iss = self.get_iss()
|
|
676
|
+
jwt_body = self._get_jwt_body()
|
|
677
|
+
client_id = self.get_client_id()
|
|
678
|
+
|
|
679
|
+
# Mypy doesn't support higher kinded types yet so it thinks that all
|
|
680
|
+
# generic attrs have type `Any`. See issue:
|
|
681
|
+
# https://github.com/python/mypy/issues/8228
|
|
682
|
+
config: ToolConfAbstract[REQ] = self._tool_config
|
|
683
|
+
req: REQ = self._request
|
|
684
|
+
|
|
685
|
+
# Find registration
|
|
686
|
+
if config.check_iss_has_one_client(iss):
|
|
687
|
+
self._registration = config.find_registration(
|
|
688
|
+
iss, action=Action.MESSAGE_LAUNCH, request=req, jwt_body=jwt_body
|
|
689
|
+
)
|
|
690
|
+
else:
|
|
691
|
+
self._registration = config.find_registration_by_params(
|
|
692
|
+
iss,
|
|
693
|
+
client_id,
|
|
694
|
+
action=Action.MESSAGE_LAUNCH,
|
|
695
|
+
request=req,
|
|
696
|
+
jwt_body=jwt_body,
|
|
697
|
+
)
|
|
698
|
+
|
|
699
|
+
if not self._registration:
|
|
700
|
+
raise LtiException("Registration not found.")
|
|
701
|
+
|
|
702
|
+
# Check client id
|
|
703
|
+
if client_id != self._registration.get_client_id():
|
|
704
|
+
raise LtiException("Client id not registered for this issuer")
|
|
705
|
+
|
|
706
|
+
return self
|
|
707
|
+
|
|
708
|
+
def validate_jwt_signature(self) -> "MessageLaunch":
|
|
709
|
+
id_token = self._get_id_token()
|
|
710
|
+
|
|
711
|
+
# Fetch public key object
|
|
712
|
+
public_key, key_alg = self.get_public_key()
|
|
713
|
+
|
|
714
|
+
try:
|
|
715
|
+
jwt.decode(
|
|
716
|
+
id_token,
|
|
717
|
+
public_key,
|
|
718
|
+
algorithms=[key_alg],
|
|
719
|
+
options=self._jwt_verify_options,
|
|
720
|
+
)
|
|
721
|
+
except jwt.InvalidTokenError as e:
|
|
722
|
+
raise LtiException(f"Can't decode id_token: {str(e)}") from e
|
|
723
|
+
|
|
724
|
+
return self
|
|
725
|
+
|
|
726
|
+
def validate_deployment(self) -> "MessageLaunch":
|
|
727
|
+
iss = self.get_iss()
|
|
728
|
+
client_id = self.get_client_id()
|
|
729
|
+
deployment_id = self._get_deployment_id()
|
|
730
|
+
tool_config: ToolConfAbstract = self._tool_config
|
|
731
|
+
|
|
732
|
+
# Find deployment.
|
|
733
|
+
if tool_config.check_iss_has_one_client(iss):
|
|
734
|
+
deployment = tool_config.find_deployment(iss, deployment_id)
|
|
735
|
+
else:
|
|
736
|
+
deployment = tool_config.find_deployment_by_params(
|
|
737
|
+
iss, deployment_id, client_id
|
|
738
|
+
)
|
|
739
|
+
if not deployment:
|
|
740
|
+
raise LtiException("Unable to find deployment")
|
|
741
|
+
|
|
742
|
+
return self
|
|
743
|
+
|
|
744
|
+
def validate_message(self) -> "MessageLaunch":
|
|
745
|
+
jwt_body = self._get_jwt_body()
|
|
746
|
+
message_type = jwt_body.get(
|
|
747
|
+
"https://purl.imsglobal.org/spec/lti/claim/message_type", None
|
|
748
|
+
)
|
|
749
|
+
if not message_type:
|
|
750
|
+
raise LtiException("Invalid message type")
|
|
751
|
+
|
|
752
|
+
validators = get_validators()
|
|
753
|
+
validated = False
|
|
754
|
+
for validator in validators:
|
|
755
|
+
if validator.can_validate(jwt_body):
|
|
756
|
+
if validated:
|
|
757
|
+
raise LtiException("Validator conflict")
|
|
758
|
+
validated = True
|
|
759
|
+
res = validator.validate(jwt_body)
|
|
760
|
+
if not res:
|
|
761
|
+
raise LtiException("Message validation failed")
|
|
762
|
+
|
|
763
|
+
if not validated:
|
|
764
|
+
raise LtiException("Unrecognized message type")
|
|
765
|
+
|
|
766
|
+
return self
|
|
767
|
+
|
|
768
|
+
def set_launch_data_storage(
|
|
769
|
+
self, data_storage: LaunchDataStorage[t.Any]
|
|
770
|
+
) -> "MessageLaunch":
|
|
771
|
+
data_storage.set_request(self._request)
|
|
772
|
+
session_cookie_name = data_storage.get_session_cookie_name()
|
|
773
|
+
if session_cookie_name:
|
|
774
|
+
session_id = self._cookie_service.get_cookie(session_cookie_name)
|
|
775
|
+
if session_id:
|
|
776
|
+
data_storage.set_session_id(session_id)
|
|
777
|
+
else:
|
|
778
|
+
raise LtiException(f"Missing %s cookie {session_cookie_name}")
|
|
779
|
+
self._session_service.set_data_storage(data_storage)
|
|
780
|
+
return self
|
|
781
|
+
|
|
782
|
+
def set_launch_data_lifetime(self, time_sec: int) -> "MessageLaunch":
|
|
783
|
+
self._session_service.set_launch_data_lifetime(time_sec)
|
|
784
|
+
return self
|
|
785
|
+
|
|
786
|
+
def save_launch_data(self) -> "MessageLaunch":
|
|
787
|
+
state_from_request = self._get_request_param("state")
|
|
788
|
+
id_token_hash = self._get_id_token_hash()
|
|
789
|
+
|
|
790
|
+
self._session_service.save_launch_data(self._launch_id, self._jwt["body"])
|
|
791
|
+
self._session_service.set_state_valid(state_from_request, id_token_hash)
|
|
792
|
+
return self
|
|
793
|
+
|
|
794
|
+
def get_params_from_login(self):
|
|
795
|
+
state = self._get_request_param("state")
|
|
796
|
+
return self._session_service.get_state_params(state)
|
|
797
|
+
|
|
798
|
+
def check_jwt_body_is_empty(self) -> bool:
|
|
799
|
+
jwt_body = self._get_jwt_body()
|
|
800
|
+
return not jwt_body
|
|
801
|
+
|
|
802
|
+
def check_staff_access(self) -> bool:
|
|
803
|
+
jwt_body = self._get_jwt_body()
|
|
804
|
+
return StaffRole(jwt_body).check()
|
|
805
|
+
|
|
806
|
+
def check_student_access(self) -> bool:
|
|
807
|
+
jwt_body = self._get_jwt_body()
|
|
808
|
+
return StudentRole(jwt_body).check()
|
|
809
|
+
|
|
810
|
+
def check_teacher_access(self) -> bool:
|
|
811
|
+
jwt_body = self._get_jwt_body()
|
|
812
|
+
return TeacherRole(jwt_body).check()
|
|
813
|
+
|
|
814
|
+
def check_teaching_assistant_access(self) -> bool:
|
|
815
|
+
jwt_body = self._get_jwt_body()
|
|
816
|
+
return TeachingAssistantRole(jwt_body).check()
|
|
817
|
+
|
|
818
|
+
def check_designer_access(self) -> bool:
|
|
819
|
+
jwt_body = self._get_jwt_body()
|
|
820
|
+
return DesignerRole(jwt_body).check()
|
|
821
|
+
|
|
822
|
+
def check_observer_access(self) -> bool:
|
|
823
|
+
jwt_body = self._get_jwt_body()
|
|
824
|
+
return ObserverRole(jwt_body).check()
|
|
825
|
+
|
|
826
|
+
def check_transient(self) -> bool:
|
|
827
|
+
jwt_body = self._get_jwt_body()
|
|
828
|
+
return TransientRole(jwt_body).check()
|