Skip to content
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

Qcportal jupyterhub #882

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions Dockerfile.jupyterhub
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
FROM ubuntu:22.04

ENV DEBIAN_FRONTEND=noninteractive

EXPOSE 8000

RUN apt-get upgrade -y && apt-get update -y && apt-get install -yqq --no-install-recommends python3-pip && pip3 install --upgrade pip
RUN apt-get install npm nodejs -y \
&& npm install -g configurable-http-proxy \
&& pip3 install jupyterhub \
&& pip3 install --upgrade notebook \
&& pip3 install --upgrade jupyterlab \
&& pip3 install --upgrade qcportal \
&& rm -rf /var/lib/apt/lists/* /var/log/* /var/tmp/* ~/.npm

ADD docker/jupyterhub_config.py /etc/jupyterhub_config.py
ADD docker/qcarchive_authenticator.py /etc/qcarchive_authenticator.py

ENV PYTHONPATH="${PYTHONPATH}:/etc"

CMD ["jupyterhub", "--ip=0.0.0.0", "--port=8000", "--no-ssl", "-f", "/etc/jupyterhub_config.py"]
26 changes: 26 additions & 0 deletions docker/jupyterhub_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""Jupyterhub Config file for hosting QCPortal.

This authenticates using QCArchive server usernames and passwords.
"""

import os

c = get_config() # noqa

# Get the base url from the environment
base_url = os.getenv("QCFRACTAL_JHUB_BASE_URL", "/")
c.JupyterHub.base_url = base_url

from qcarchive_authenticator import QCArchiveAuthenticator

c.JupyterHub.authenticator_class = QCArchiveAuthenticator

# don't cache static files
c.JupyterHub.tornado_settings = {
"no_cache_static": True,
"slow_spawn_timeout": 0,
}

c.JupyterHub.allow_named_servers = True

c.Authenticator.allow_all = True
96 changes: 96 additions & 0 deletions docker/qcarchive_authenticator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
from tornado import gen
import os
from jupyterhub.auth import Authenticator
import requests
import jwt
from subprocess import PIPE, STDOUT, Popen
import pwd
import warnings

class QCArchiveAuthenticator(Authenticator):

@gen.coroutine
def authenticate(self, handler, data):
base_address = os.getenv("QCFRACTAL_ADDRESS")

if base_address is None:
raise RuntimeError(f"QCArchive address returned None.")

login_address = f"{base_address}/auth/v1/login"
uinfo_address = f"{base_address}/api/v1/users/{data['username']}"

# Log in to get JWT
body = {"username": data["username"], "password": data["password"]}

# 'verify' means whether or not to verify SSL certificates
r = requests.post(login_address, json=body, verify=True)

if r.status_code != 200:
try:
details = r.json()
except:
details = {"msg": r.reason}
raise RuntimeError(f"Request failed: {details['msg']}", r.status_code, details)

print("Successfully logged in!")

# Grab access token, use to get all user information
access_token = r.json()["access_token"]

headers = {"Authorization": f"Bearer {access_token}"}
r = requests.get(uinfo_address, headers=headers)

if r.status_code != 200:
fail_msg = r.json()["msg"]
raise RuntimeError(
f"Unable to get user information: {r.status_code} - {fail_msg}"
)

uinfo = r.json()
username = uinfo["username"]
user_id = uinfo["id"]
role = uinfo["role"]

# Check if the user has a home directory.
home_dir = False
if os.path.isdir(f"/home/{username}"):
home_dir = True

# Check if the user exists already.
user_exists = True
try:
pwd.getpwnam(username)
except KeyError:
print(f'User {username} does not exist.')
user_exists = False

# Create the user if they do not exist.
if not user_exists:
cmd = [
"adduser",
"-q",
"--disabled-password",
"--uid",
f"{100000+user_id}",
username,
]
p = Popen(cmd, stdout=PIPE, stderr=STDOUT)
p.wait()


# If they did not have a home directory before user creation, clone the QCArchive demos.
if not home_dir:
cmd = ["git", "clone", "https://github.com/MolSSI/QCArchiveDemos.git", f"/home/{username}/QCArchiveDemos"]
p = Popen(cmd, stdout=PIPE, stderr=STDOUT)
p.wait()

cmd = ["chown", f"{username}", f"/home/{username}/QCArchiveDemos"]
p = Popen(cmd, stdout=PIPE, stderr=STDOUT)
p.wait()

os.environ["QCPORTAL_ADDRESS"] = base_address
os.environ["QCPORTAL_USERNAME"] = username
os.environ["QCORTAL_PASSWORD"] = data["password"]
os.environ["QCPORTAL_VERIFY"] = os.getenv("QCFRACTAL_VERIFY", "True")

return username
Loading