-
Notifications
You must be signed in to change notification settings - Fork 13
lti changes #644
base: main
Are you sure you want to change the base?
lti changes #644
Changes from 1 commit
7ec5396
ee0dfd7
d07623c
f30091c
32db3a5
070ca4f
4554626
2ad280f
938df6a
b8944e5
48a6467
7ac98ac
abe0606
47bf382
089e1c3
508d95d
28d73e9
96390e1
6a7c7a1
5bfdada
0216198
40f79ca
980b972
cee4464
e2aceec
db33df4
784dd48
5d3a3f4
8a4c151
972352e
aa9d7da
e1fdbb5
98d951e
29cb562
fb09de6
0405223
e2fb9a7
92db213
56d5cbb
9e6533a
7ba436a
13d4ad2
c365961
a4691ea
a533e56
08463b3
9da1fb6
7c53d06
83856ec
b336d3f
a39211c
819dbb7
6f05a41
839a1a8
d29cbd7
17ea6c3
1d8a951
9fbb70e
1351cc1
6c0ce3f
3abcc7f
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 |
---|---|---|
|
@@ -5,8 +5,6 @@ | |
from nbgrader.api import Course | ||
from nbgrader.api import Gradebook | ||
from nbgrader.api import InvalidEntry | ||
from sqlalchemy_utils import create_database | ||
from sqlalchemy_utils import database_exists | ||
|
||
from illumidesk.authenticators.utils import LTIUtils | ||
|
||
|
@@ -18,24 +16,23 @@ | |
nbgrader_db_port = os.environ.get("POSTGRES_NBGRADER_PORT") or 5432 | ||
nbgrader_db_password = os.environ.get("POSTGRES_NBGRADER_PASSWORD") | ||
nbgrader_db_user = os.environ.get("POSTGRES_NBGRADER_USER") | ||
nbgrader_db_name = os.environ.get("POSTGRES_NBGRADER_DATABASE") | ||
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. If database does not exist, I am getting the following error
|
||
mnt_root = os.environ.get("ILLUMIDESK_MNT_ROOT", "/illumidesk-courses") | ||
|
||
org_name = os.environ.get("ORGANIZATION_NAME") or "my-org" | ||
|
||
if not org_name: | ||
raise EnvironmentError("ORGANIZATION_NAME env-var is not set") | ||
|
||
CAMPUS_ID = os.environ.get("CAMPUS_ID") | ||
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. if the database does exist in rds instance like in the case of
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. if the database does exist in rds instance like in the case of
|
||
if not CAMPUS_ID: | ||
raise EnvironmentError("CAMPUS_ID env-var is not set") | ||
|
||
def nbgrader_format_db_url(course_id: str) -> str: | ||
def nbgrader_format_db_url() -> str: | ||
""" | ||
Returns the nbgrader database url with the format: <org_name>_<course-id> | ||
|
||
Args: | ||
course_id: the course id (usually associated with the course label) from which the launch was initiated. | ||
Returns the nbgrader database url | ||
""" | ||
course_id = LTIUtils().normalize_string(course_id) | ||
database_name = f"{org_name}_{course_id}" | ||
return f"postgresql://{nbgrader_db_user}:{nbgrader_db_password}@{nbgrader_db_host}:{nbgrader_db_port}/{database_name}" | ||
return f"postgresql://{nbgrader_db_user}:{nbgrader_db_password}@{nbgrader_db_host}:{nbgrader_db_port}/{nbgrader_db_name}" | ||
|
||
|
||
class NbGraderServiceHelper: | ||
|
@@ -51,7 +48,7 @@ class NbGraderServiceHelper: | |
database_name: the database name | ||
""" | ||
|
||
def __init__(self, course_id: str, check_database_exists: bool = False): | ||
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. So what I'm understanding here is that all courses will be in one database instead of a database-specific for each org_course. This assumes that the database exists by default. 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. right, the DB will be common and will be maintained by the migrations in admin-dashboard code base 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. we may need to add back the |
||
def __init__(self, course_id: str): | ||
if not course_id: | ||
raise ValueError("course_id missing") | ||
|
||
|
@@ -62,56 +59,48 @@ def __init__(self, course_id: str, check_database_exists: bool = False): | |
self.uid = int(os.environ.get("NB_GRADER_UID") or "10001") | ||
self.gid = int(os.environ.get("NB_GRADER_GID") or "100") | ||
|
||
self.db_url = nbgrader_format_db_url(course_id) | ||
self.database_name = f"{org_name}_{self.course_id}" | ||
if check_database_exists: | ||
self.create_database_if_not_exists() | ||
|
||
def create_database_if_not_exists(self) -> None: | ||
"""Creates a new database if it doesn't exist""" | ||
conn_uri = nbgrader_format_db_url(self.course_id) | ||
|
||
if not database_exists(conn_uri): | ||
logger.debug("db not exist, create database") | ||
create_database(conn_uri) | ||
self.db_url = nbgrader_format_db_url() | ||
|
||
def add_user_to_nbgrader_gradebook(self, username: str, lms_user_id: str) -> None: | ||
def add_user_to_nbgrader_gradebook(self, email: str, external_user_id: str, source: str, source_type: str, role: str) -> None: | ||
""" | ||
Adds a user to the nbgrader gradebook database for the course. | ||
|
||
Args: | ||
username: The user's username | ||
lms_user_id: The user's id on the LMS | ||
email: The user's email | ||
external_user_id: The user's id on the external system | ||
source: source from where user was authenticated | ||
source_type: source_type | ||
Raises: | ||
InvalidEntry: when there was an error adding the user to the database | ||
""" | ||
if not username: | ||
raise ValueError("username missing") | ||
if not lms_user_id: | ||
raise ValueError("lms_user_id missing") | ||
if not email: | ||
raise ValueError("email missing") | ||
if not external_user_id: | ||
raise ValueError("external_user_id missing") | ||
|
||
with Gradebook(self.db_url, course_id=self.course_id) as gb: | ||
with Gradebook(self.db_url, course_id=self.course_id, campus_id=CAMPUS_ID) as gb: | ||
try: | ||
gb.update_or_create_student(username, lms_user_id=lms_user_id) | ||
user = gb.update_or_create_user_by_email(email, role=role, external_user_id=external_user_id, source=source, source_type=source_type) | ||
logger.debug( | ||
"Added user %s with lms_user_id %s to gradebook" | ||
% (username, lms_user_id) | ||
"Added user %s with external_user_id %s to gradebook" | ||
% (email, external_user_id) | ||
) | ||
return user.to_dict() | ||
except InvalidEntry as e: | ||
logger.debug("Error during adding student to gradebook: %s" % e) | ||
|
||
def update_course(self, **kwargs) -> None: | ||
""" | ||
Updates the course in nbgrader database | ||
""" | ||
with Gradebook(self.db_url, course_id=self.course_id) as gb: | ||
with Gradebook(self.db_url, course_id=self.course_id, campus_id=CAMPUS_ID) as gb: | ||
gb.update_course(self.course_id, **kwargs) | ||
|
||
def get_course(self) -> Course: | ||
""" | ||
Gets the course model instance | ||
""" | ||
with Gradebook(self.db_url, course_id=self.course_id) as gb: | ||
with Gradebook(self.db_url, course_id=self.course_id, campus_id=CAMPUS_ID) as gb: | ||
course = gb.check_course(self.course_id) | ||
logger.debug(f"course got from db:{course}") | ||
return course | ||
|
@@ -131,7 +120,7 @@ def register_assignment(self, assignment_name: str, **kwargs: dict) -> Assignmen | |
"Assignment name normalized %s to save in gradebook" % assignment_name | ||
) | ||
assignment = None | ||
with Gradebook(self.db_url, course_id=self.course_id) as gb: | ||
with Gradebook(self.db_url, course_id=self.course_id, campus_id=CAMPUS_ID) as gb: | ||
try: | ||
assignment = gb.update_or_create_assignment(assignment_name, **kwargs) | ||
logger.debug("Added assignment %s to gradebook" % assignment_name) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -60,7 +60,7 @@ async def setup_course_hook_lti11( | |
jupyterhub_api = JupyterHubAPI() | ||
|
||
# normalize the name and course_id strings in authentication dictionary | ||
username = authentication["name"] | ||
name = authentication["name"] | ||
lms_user_id = authentication["auth_state"]["user_id"] | ||
user_role = authentication["auth_state"]["roles"].split(",")[0] | ||
course_id = lti_utils.normalize_string( | ||
|
@@ -69,25 +69,28 @@ async def setup_course_hook_lti11( | |
nb_service = NbGraderServiceHelper(course_id, True) | ||
|
||
# register the user (it doesn't matter if it is a student or instructor) with her/his lms_user_id in nbgrader | ||
nb_service.add_user_to_nbgrader_gradebook(username, lms_user_id) | ||
role = "STUDENT" | ||
if not user_is_a_student(user_role) and user_is_an_instructor(user_role): | ||
role = "INSTRUCTOR" | ||
nb_service.add_user_to_nbgrader_gradebook(name, lms_user_id, role) | ||
# TODO: verify the logic to simplify groups creation and membership | ||
if user_is_a_student(user_role): | ||
if role == "STUDENT": | ||
try: | ||
# assign the user to 'nbgrader-<course_id>' group in jupyterhub and gradebook | ||
await jupyterhub_api.add_student_to_jupyterhub_group(course_id, username) | ||
await jupyterhub_api.add_student_to_jupyterhub_group(course_id, lms_user_id) | ||
except AddJupyterHubUserException as e: | ||
logger.error( | ||
"An error when adding student username: %s to course_id: %s with exception %s", | ||
(username, course_id, e), | ||
"An error when adding student user_id: %s to course_id: %s with exception %s", | ||
(lms_user_id, course_id, e), | ||
) | ||
elif user_is_an_instructor(user_role): | ||
elif role == "INSTRUCTOR": | ||
try: | ||
# assign the user in 'formgrade-<course_id>' group | ||
await jupyterhub_api.add_instructor_to_jupyterhub_group(course_id, username) | ||
await jupyterhub_api.add_instructor_to_jupyterhub_group(course_id, lms_user_id) | ||
except AddJupyterHubUserException as e: | ||
logger.error( | ||
"An error when adding instructor username: %s to course_id: %s with exception %s", | ||
(username, course_id, e), | ||
"An error when adding instructor user_id: %s to course_id: %s with exception %s", | ||
(lms_user_id, course_id, e), | ||
) | ||
|
||
# launch the new grader-notebook as a service | ||
|
@@ -128,30 +131,33 @@ async def setup_course_hook( | |
# normalize the name and course_id strings in authentication dictionary | ||
course_id = lti_utils.normalize_string(authentication["auth_state"]["course_id"]) | ||
nb_service = NbGraderServiceHelper(course_id, True) | ||
username = lti_utils.normalize_string(authentication["name"]) | ||
lms_user_id = authentication["auth_state"]["lms_user_id"] | ||
email = authentication["auth_state"]["email"] | ||
user_role = authentication["auth_state"]["user_role"] | ||
source = authentication["auth_state"]["source"] | ||
source_type = authentication["auth_state"]["source_type"] | ||
|
||
# register the user (it doesn't matter if it is a student or instructor) with her/his lms_user_id in nbgrader | ||
nb_service.add_user_to_nbgrader_gradebook(username, lms_user_id) | ||
user = nb_service.add_user_to_nbgrader_gradebook(email, lms_user_id, source, source_type) | ||
authentication["name"] = user["id"] | ||
# TODO: verify the logic to simplify groups creation and membership | ||
if user_is_a_student(user_role): | ||
try: | ||
# assign the user to 'nbgrader-<course_id>' group in jupyterhub and gradebook | ||
await jupyterhub_api.add_student_to_jupyterhub_group(course_id, username) | ||
await jupyterhub_api.add_student_to_jupyterhub_group(course_id, user["id"]) | ||
except AddJupyterHubUserException as e: | ||
logger.error( | ||
"An error when adding student username: %s to course_id: %s with exception %s", | ||
(username, course_id, e), | ||
"An error when adding student user_id: %s to course_id: %s with exception %s", | ||
(lms_user_id, course_id, e), | ||
) | ||
elif user_is_an_instructor(user_role): | ||
try: | ||
# assign the user in 'formgrade-<course_id>' group | ||
await jupyterhub_api.add_instructor_to_jupyterhub_group(course_id, username) | ||
await jupyterhub_api.add_instructor_to_jupyterhub_group(course_id, user["id"]) | ||
except AddJupyterHubUserException as e: | ||
logger.error( | ||
"An error when adding instructor username: %s to course_id: %s with exception %s", | ||
(username, course_id, e), | ||
"An error when adding instructor user_id: %s to course_id: %s with exception %s", | ||
(lms_user_id, course_id, e), | ||
) | ||
|
||
# launch the new grader-notebook as a service | ||
|
@@ -163,6 +169,48 @@ async def setup_course_hook( | |
return authentication | ||
|
||
|
||
async def setup_user_hook_auth0( | ||
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. Would the custom authenticator also need it's own Class that inherits from Oauthenticator in order to decode jwt claims? 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. I think it should not be needed, this method is more list a post auth hook |
||
authenticator: Authenticator, | ||
handler: RequestHandler, | ||
authentication: Dict[str, str], | ||
) -> Dict[str, str]: | ||
""" | ||
Calls the microservice to create a new user in case it does not exist. | ||
The data needed is received from auth_state within authentication object. This | ||
function assumes that the required k/v's in the auth_state dictionary are available, | ||
since the Authenticator(s) validates the data beforehand. | ||
|
||
This function requires `Authenticator.enable_auth_state = True` and is intended | ||
to be used as a post_auth_hook. | ||
|
||
Args: | ||
authenticator: the JupyterHub Authenticator object | ||
handler: the JupyterHub handler object | ||
authentication: the authentication object returned by the | ||
authenticator class | ||
|
||
Returns: | ||
authentication (Required): updated authentication object | ||
""" | ||
lti_utils = LTIUtils() | ||
|
||
# normalize the name and course_id strings in authentication dictionary | ||
course_id = lti_utils.normalize_string(authentication["auth_state"]["course_id"]) | ||
nb_service = NbGraderServiceHelper(course_id, True) | ||
oauth_user = authentication["auth_state"]["oauth_user"] or {} | ||
|
||
lms_user_id = oauth_user.get("user_id") or oauth_user.get("username") or oauth_user.get("sub") | ||
email = oauth_user.get("email") or oauth_user.get("sub") | ||
# user_role = authentication["auth_state"]["user_role"] | ||
source = "auth0" | ||
source_type = "generic-oauth" | ||
|
||
# register the user (it doesn't matter if it is a student or instructor) with her/his lms_user_id in nbgrader | ||
user = nb_service.add_user_to_nbgrader_gradebook(email, lms_user_id, source, source_type) | ||
authentication["name"] = user["id"] | ||
return authentication | ||
|
||
|
||
class LTI13Authenticator(OAuthenticator): | ||
"""Custom authenticator used with LTI 1.3 requests""" | ||
|
||
|
@@ -235,7 +283,9 @@ async def authenticate( # noqa: C901 | |
course_id = lti_utils.normalize_string(course_id) | ||
self.log.debug("Normalized course label is %s" % course_id) | ||
username = "" | ||
email = "" | ||
if "email" in jwt_decoded and jwt_decoded["email"]: | ||
email = jwt_decoded["email"] | ||
username = lti_utils.email_to_username(jwt_decoded["email"]) | ||
elif "name" in jwt_decoded and jwt_decoded["name"]: | ||
username = jwt_decoded["name"] | ||
|
@@ -294,6 +344,7 @@ async def authenticate( # noqa: C901 | |
await process_resource_link_lti_13(self.log, course_id, jwt_decoded) | ||
|
||
lms_user_id = jwt_decoded["sub"] if "sub" in jwt_decoded else username | ||
lms_id = (jwt_decoded.get("https://purl.imsglobal.org/spec/lti/claim/tool_platform") or {}).get("guid") | ||
|
||
# ensure the username is normalized | ||
self.log.debug("username is %s" % username) | ||
|
@@ -303,14 +354,19 @@ async def authenticate( # noqa: C901 | |
# ensure the user name is normalized | ||
username_normalized = lti_utils.normalize_string(username) | ||
self.log.debug("Assigned username is: %s" % username_normalized) | ||
self.log.debug("Assigned id is: %s for username %s" % (lms_user_id, username)) | ||
|
||
return { | ||
"name": username_normalized, | ||
"name": email or lms_user_id, | ||
"auth_state": { | ||
"course_id": course_id, | ||
"user_role": user_role, | ||
"lms_user_id": lms_user_id, | ||
"username": username, | ||
"email": email or lms_user_id, | ||
"launch_return_url": launch_return_url, | ||
"source": lms_id, | ||
"source_type": "lti13" | ||
}, # noqa: E231 | ||
} | ||
|
||
|
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.
Has the removal of
GRADER_EXCHANGE_SHARED_PVC
been tested? I was still having issues after I removed the deployment. That being said I was testing it without efs access points as we didn't need that in the Oregon cluster. I can try again with a new image and approve this afterward. Let me know if you have a new image, if not I'll fetch this PR and create the image and once tested, approve this.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 have created a new image of this with pr-71 tag