Skip to content

Commit

Permalink
Source code cosmetics
Browse files Browse the repository at this point in the history
  • Loading branch information
jeflem committed Sep 9, 2023
1 parent b42025b commit 5476279
Show file tree
Hide file tree
Showing 5 changed files with 222 additions and 50 deletions.
86 changes: 79 additions & 7 deletions images/ananke-base/assets/jupyterhub_config.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
# Configuration file for jupyterhub.

import fcntl
import json
import logging
import subprocess
import json
import fcntl
from glob import glob

c = get_config()
from ltiauthenticator.lti13.auth import LTI13Authenticator
from ltiauthenticator.lti13.handlers import LTI13CallbackHandler

c = get_config() # noqa

logging.basicConfig(
level=logging.INFO,
Expand Down Expand Up @@ -39,7 +42,29 @@

c.post_auth_hook_callbacks = []

async def post_auth_callback(authenticator, handler, authentication):
async def post_auth_callback(authenticator: LTI13Authenticator, handler: LTI13CallbackHandler, authentication: dict) -> dict:
"""
Optional hook to run necessary bootstrapping tasks.
If any of these tasks returns `True` the JupyterHub will be restarted.
Parameters
----------
authenticator : LTI13Authenticator
The JupyterHub LTI 1.3 Authenticator.
handler : LTI13CallbackHandler
Handles JupyterHub authentication requests responses according to the LTI 1.3 standard.
authentication : dict
The authentication dict for the user.
Returns
-------
dict
The (altered) authentication dict for the user.
Notes
-----
The username on Debian has to start with a letter, which is why the letter u is prefixed.
"""

logging.debug('Running post authentication hooks')

Expand All @@ -63,9 +88,33 @@ async def post_auth_callback(authenticator, handler, authentication):
# write LTI data to logs
#-------------------------------------------------------------------------------

async def log_lti_data(authenticator, handler, authentication):
async def log_lti_data(authenticator: LTI13Authenticator, handler: LTI13CallbackHandler, authentication: dict) -> bool:
"""
Additional bootstrapping function to log LTI relevant data.
This hook will be called within the post_auth_callback() function and will always return `False`.
This behaviour is necessary for the check if a restart is needed, see post_auth_callback().
Parameters
----------
authenticator : LTI13Authenticator
The JupyterHub LTI 1.3 Authenticator.
handler : LTI13CallbackHandler
Handles JupyterHub authentication requests responses according to the LTI 1.3 standard.
authentication : dict
The authentication dict for the user.
Returns
-------
bool
Always `False`, as no restart of the JupyterHub is required.
Notes
-----
The parameters `handler` and `authentication` have to be supplied, even though they are not accessed.
"""

logging.debug('Received following LTI data: ' + str(authentication.get('auth_state')))
logging.debug(f'Received following LTI data: {authentication.get("auth_state")}')

return False

c.post_auth_hook_callbacks.append(log_lti_data)
Expand All @@ -80,7 +129,30 @@ async def log_lti_data(authenticator, handler, authentication):
user_data = json.load(f)
logging.debug(str(len(user_data)) + ' users in data base')

async def update_user_data(authenticator, handler, authentication):
async def update_user_data(authenticator: LTI13Authenticator, handler: LTI13CallbackHandler, authentication: dict) -> False:
"""
Additional bootstrapping function to update the user database if necessary.
This hook will be called within the post_auth_callback() function and will always return `False`.
This behaviour is necessary for the check if a restart is needed, see post_auth_callback().
Parameters
----------
authenticator : LTI13Authenticator
The JupyterHub LTI 1.3 Authenticator.
handler : LTI13CallbackHandler
Handles JupyterHub authentication requests responses according to the LTI 1.3 standard.
authentication : dict
The authentication dict for the user.
Returns
-------
bool
Always `False`, as no restart of the JupyterHub is required.
Notes
-----
The parameters `handler` and `authentication` have to be supplied, even though they are not accessed.
"""

username = authentication.get('name')
logging.debug(f'Looking up user {username} in data base.')
Expand Down
37 changes: 19 additions & 18 deletions images/ananke-nbgrader/assets/kore/kore.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,25 @@
from flask import Flask
from flask import request as flask_request
from flask import session as flask_session
from flask import jsonify as flask_jsonify
from flask import redirect as flask_redirect
from flask import make_response
from flask_session import Session
from flask import render_template

from functools import wraps
import secrets
import datetime
import json
import os
from jwcrypto.jwk import JWK
import logging
import os
import re
import secrets
import subprocess
import time
from jwcrypto.jwk import JWK
import requests
import jwt
import datetime
from functools import wraps
from tempfile import mkdtemp
import subprocess

import jwt
import requests
from flask import Flask
from flask import jsonify as flask_jsonify
from flask import make_response
from flask import redirect as flask_redirect
from flask import render_template
from flask import request as flask_request
from flask import session as flask_session
from flask_session import Session
from jupyterhub.services.auth import HubOAuth
from nbgrader.api import Gradebook
from traitlets.config import Config
Expand Down Expand Up @@ -69,7 +68,9 @@
auth = HubOAuth(api_token=kore_token, cache_max_age=60)

def authenticated(f):
"""Decorator for authenticating with the Hub via OAuth"""
"""
Decorator function for authenticating with the Hub via OAuth.
"""

@wraps(f)
def decorated(*args, **kwargs):
Expand Down
130 changes: 109 additions & 21 deletions images/ananke-nbgrader/assets/kore/kore_jhub_config.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
# configure nbgrader and Kore

import asyncio
import fcntl
import json
import logging
import os
import asyncio
import pwd
import re
import subprocess
import json
import fcntl
import secrets
import subprocess
import sys
import pwd
from typing import NoReturn

from ltiauthenticator.lti13.auth import LTI13Authenticator
from ltiauthenticator.lti13.handlers import LTI13CallbackHandler
from nbgrader.api import Gradebook # noqa
from nbgrader.apps import NbGraderAPI # noqa
from traitlets.config import Config

sys.path.append('/opt/kore')
Expand Down Expand Up @@ -56,25 +61,109 @@
instructors = []
logging.debug('found {} instructors'.format(len(instructors)))

# post authentication callback for nbgrader configuration
async def nbgrader_post_auth(authenticator, handler, authentication):

def get_dir_owner(path):
''' Return uid and gid of directory. '''
# post-authentication callback for nbgrader configuration
async def nbgrader_post_auth(authenticator: LTI13Authenticator, handler: LTI13CallbackHandler, authentication: dict) -> bool:
"""
This hook does a multiple of tasks, depending on the parameters from the authentication dict.
(1) If the user is an instructor
(1.1) ~ and it is the first login of the instructor to the `JupyterHub`, then
(1.1.1) the nbgrader extensions get enabled for the instructor and
(1.1.1) the user (instructor) is added to the corresponding database.
(1.2) ~, then the LTI parameters are written to file.
(2) The course parameters are generated, see the make_course_id function of the kore_utils.py file.
(3) Check if a grader for the supplied course is present.
(3.1) Where, if the user is an instructor and the grader does not yet exist, then
(3.1.1) the grader user is created on the OS,
(3.1.2) the nbgrader extensions are enabled,
(3.1.3) the nbgrader config is generated and written to file and
(3.1.4) the home directory creation and permission relevant task are handled.
(4) If the user is an instructor and the course is not present as a service on the `JupyterHub`, then
(4.1) the course gets added as a service,
(4.2) the user (instructor) is added to the formgrade group of the service.
(5) If the user is a student and the grader exists, then the user (student) gets added to the course.
(6) Finally a bool is returned which indicates if the `JupyterHub` has to be restarted.
Parameters
----------
authenticator : LTI13Authenticator
The JupyterHub LTI 1.3 Authenticator.
handler : LTI13CallbackHandler
Handles JupyterHub authentication requests responses according to the LTI 1.3 standard.
authentication : dict
The authentication dict for the user.
Returns
-------
bool
True if a restart of the JupyterHub is necessary, False otherwise.
"""

def get_dir_owner(path) -> tuple[int, int] | tuple[None, None]:
"""
Return uid and gid of given directory.
Parameters
----------
path : str
Path to the directory.
Returns
-------
tuple[int, int] | tuple[None, None]
Depending on the fact if the supplied directory exists, the uid and the gid are returned as tuple of ints, a tuple of None otherwise.
"""

if os.path.isdir(path):
info = os.stat(path)
return info.st_uid, info.st_gid
else:
return None, None

def set_dir_owner(path, uid, gid):
''' Set directory's owner recursively if uid and gid both are not None. '''
def set_dir_owner(path: str, uid: int, gid: int) -> NoReturn:
"""
Set directory's owner recursively if uid and gid both are not None.
Parameters
----------
path : str
Path to the directory.
uid : int
The user identifier (uid) of the directory.
gid : int
The group identifier (gid) of the directory.
Returns
-------
NoReturn
"""

if uid and gid:
os.system(f'chown -R {uid}:{gid} {path}')

async def run_as_user(username, cmd, args):
''' Run command as dynamic user in username's home directory.
cmd is a string. args is a list of strings. '''
async def run_as_user(username: str, cmd: str, args: list[str]) -> NoReturn:
"""
Run command as dynamic user in usernames home directory.
cmd is a string. args is a list of strings.
Parameters
----------
username : str
The user as which to execute a command.
cmd : str
The command to be executed.
args : list[str]
Additional arguments for the supplied command.
Returns
-------
NoReturn
"""

systemd_run = ['systemd-run',
'--wait',
f'--unit=post-auth-hook-{username}',
Expand Down Expand Up @@ -102,12 +191,11 @@ async def run_as_user(username, cmd, args):

# activate nbgrader extensions for instructor
logging.debug('activating nbgrader extensions for instructor')
if is_instructor:
uid, gid = get_dir_owner(user_home)
await run_as_user(username, 'jupyter', ['server', 'extension', 'enable', '--user', 'nbgrader.server_extensions.course_list'])
await run_as_user(username, 'jupyter', ['labextension', 'disable', '--level=user', 'nbgrader:course-list'])
await run_as_user(username, 'jupyter', ['labextension', 'enable', '--level=user', 'nbgrader:course-list'])
set_dir_owner(user_home, uid, gid)
uid, gid = get_dir_owner(user_home)
await run_as_user(username, 'jupyter', ['server', 'extension', 'enable', '--user', 'nbgrader.server_extensions.course_list'])
await run_as_user(username, 'jupyter', ['labextension', 'disable', '--level=user', 'nbgrader:course-list'])
await run_as_user(username, 'jupyter', ['labextension', 'enable', '--level=user', 'nbgrader:course-list'])
set_dir_owner(user_home, uid, gid)

# add instructor to list
instructors.append(username)
Expand Down
18 changes: 14 additions & 4 deletions images/ananke-nbgrader/assets/kore/kore_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,22 @@
def course_id_to_grader_user(course_id):

return course_id[0:32]
#h = hashlib.shake_256(course_id.encode())
#return ('c-' + h.hexdigest(4) + '-' + '-'.join(course_id.split('-')[3:]))[:32] # Debian usernames are limited to 32 characters


def make_course_id(lti_state):
''' Make course ID, course title, grader username from LTI data.'''
def make_course_id(lti_state: dict) -> tuple[str, str, str]:
"""
Make course ID, course title, grader username from LTI data.
Parameters
----------
lti_state : dict
The authentication dict for the user.
Returns
-------
tuple[str, str, str]
The returned tuple contains the course id, course title and the grader user as strings.
"""

deployment_id = lti_state.get('https://purl.imsglobal.org/spec/lti/claim/deployment_id', '0')
resource_link_id = lti_state.get('https://purl.imsglobal.org/spec/lti/claim/resource_link').get('id')
Expand Down
1 change: 1 addition & 0 deletions images/ananke-nbgrader/assets/kore/make_keys.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# create new key pair for LTI communication

import json

from Crypto.PublicKey import RSA
from jwcrypto.jwk import JWK

Expand Down

0 comments on commit 5476279

Please sign in to comment.