From f982515d3fe333323823b4794b9e7fb723ca3ae2 Mon Sep 17 00:00:00 2001 From: Vismayak Mohanarajan Date: Thu, 11 Jul 2024 15:46:04 -0500 Subject: [PATCH] Integration of Jupyter through docker-compose (#1088) * Working Jupyterhub Integration * Linting * Handling logout * Updating docker-compose to keep jupyterhub version static * Incorporating feedback * Working Jupyterhub Integration * Linting * Handling logout * Updating docker-compose to keep jupyterhub version static * Incorporating feedback * Fix conflict * Fix config * Creating a traefik setup for prod docker-compose * Make it work for dev set up * Updating .env ecample * better file names * Updated prod webpack and a shell script for prod setup --------- Co-authored-by: Luigi Marini Co-authored-by: Chen Wang --- .gitignore | 1 + README.md | 4 +- docker-compose.dev.yml | 5 + docker-compose.jupyter-dev.yml | 30 +++ docker-compose.jupyter.yml | 33 +++ docker-dev.sh | 9 + docker-prod.sh | 10 + docs/docs/devs/getstarted.md | 2 + frontend/package.json | 2 +- frontend/src/app.config.ts | 5 + frontend/src/components/Layout.tsx | 19 ++ frontend/webpack.config.dev.js | 4 + frontend/webpack.config.prod.js | 4 + jupyterhub/.env-example | 14 ++ jupyterhub/Dockerfile.jupyterhub | 14 ++ .../customauthenticator/__init__.py | 0 .../customauthenticator/custom.py | 225 ++++++++++++++++++ jupyterhub/authenticator/setup.py | 12 + jupyterhub/authenticator/test_jwt.py | 41 ++++ jupyterhub/jupyterhub_config.py | 107 +++++++++ jupyterhub/jupyterhub_dev_config.py | 98 ++++++++ 21 files changed, 637 insertions(+), 2 deletions(-) create mode 100644 docker-compose.jupyter-dev.yml create mode 100644 docker-compose.jupyter.yml create mode 100755 docker-prod.sh create mode 100644 jupyterhub/.env-example create mode 100644 jupyterhub/Dockerfile.jupyterhub create mode 100644 jupyterhub/authenticator/customauthenticator/__init__.py create mode 100644 jupyterhub/authenticator/customauthenticator/custom.py create mode 100644 jupyterhub/authenticator/setup.py create mode 100644 jupyterhub/authenticator/test_jwt.py create mode 100644 jupyterhub/jupyterhub_config.py create mode 100644 jupyterhub/jupyterhub_dev_config.py diff --git a/.gitignore b/.gitignore index 986f39ae1..aa73ebb11 100644 --- a/.gitignore +++ b/.gitignore @@ -82,3 +82,4 @@ secrets.yaml # faker official.csv fact.png +jupyterhub/.env-dev diff --git a/README.md b/README.md index e779590d0..425aa2dc4 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,9 @@ There is a few other documentation links available on the [website](https://clow ## Installation The easiest way of running Clowder v2 is checking out the [code](https://github.com/clowder-framework/clowder2) -and running `docker compose up` in the main directory. +and running `docker compose up` in the main directory. If you would like to run Clowder with JupyterHub, +you can use our script `docker-prod.sh` to start the services. Run `./docker-prod.sh prod up` to start the services +and `./docker-prod.sh prod down` to stop them. Helm charts are available for running Clowder v2 on Kubernetes. See the [helm](https://github.com/clowder-framework/clowder2/tree/main/deployments/kubernetes/charts) directory for more information. diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 92df388f8..61dcc8bf0 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -67,6 +67,8 @@ services: postgres: image: postgres + networks: + - clowder2 volumes: - postgres_data:/var/lib/postgresql/data environment: @@ -79,6 +81,8 @@ services: volumes: - ./scripts/keycloak/clowder-realm-dev.json:/opt/keycloak/data/import/realm.json:ro - ./scripts/keycloak/clowder-theme/:/opt/keycloak/themes/clowder-theme/:ro + networks: + - clowder2 command: - start-dev - --http-relative-path /keycloak @@ -172,6 +176,7 @@ services: networks: clowder2: + name: clowder2 ## By default this config uses default local driver, ## For custom volumes replace with volume driver configuration. diff --git a/docker-compose.jupyter-dev.yml b/docker-compose.jupyter-dev.yml new file mode 100644 index 000000000..bea03e898 --- /dev/null +++ b/docker-compose.jupyter-dev.yml @@ -0,0 +1,30 @@ +version: '3' +services: + jupyterhub: + build: + context: jupyterhub + dockerfile: Dockerfile.jupyterhub + args: + JUPYTERHUB_VERSION: 4 + restart: always + networks: + - clowder2 + volumes: + # The JupyterHub configuration file + - ./jupyterhub/jupyterhub_dev_config.py:/srv/jupyterhub/jupyterhub_config.py:ro + # Bind Docker socket on the hostso we can connect to the daemon from + # within the container + - /var/run/docker.sock:/var/run/docker.sock:rw + # Bind Docker volume on host for JupyterHub database and cookie secrets + - jupyterhub-data:/data + ports: + - "8765:8000" + env_file: + - jupyterhub/.env-dev + command: jupyterhub -f /srv/jupyterhub/jupyterhub_config.py + + depends_on: + - keycloak + +volumes: + jupyterhub-data: diff --git a/docker-compose.jupyter.yml b/docker-compose.jupyter.yml new file mode 100644 index 000000000..b8f02659c --- /dev/null +++ b/docker-compose.jupyter.yml @@ -0,0 +1,33 @@ +version: '3' +services: + jupyterhub: + build: + context: jupyterhub + dockerfile: Dockerfile.jupyterhub + args: + JUPYTERHUB_VERSION: 4 + restart: always + networks: + - clowder2 + volumes: + # The JupyterHub configuration file + - ./jupyterhub/jupyterhub_config.py:/srv/jupyterhub/jupyterhub_config.py:ro + # Bind Docker socket on the hostso we can connect to the daemon from + # within the container + - /var/run/docker.sock:/var/run/docker.sock:rw + # Bind Docker volume on host for JupyterHub database and cookie secrets + - jupyterhub_data:/data + env_file: + - jupyterhub/.env + labels: + - "traefik.enable=true" + - "traefik.http.routers.jupyterhub.rule=PathPrefix(`/jupyterhub`)" + - "traefik.http.services.jupyterhub.loadbalancer.server.port=8000" + + command: jupyterhub -f /srv/jupyterhub/jupyterhub_config.py + + depends_on: + - keycloak + +volumes: + jupyterhub_data: diff --git a/docker-dev.sh b/docker-dev.sh index 44f50afab..eb775df60 100755 --- a/docker-dev.sh +++ b/docker-dev.sh @@ -7,3 +7,12 @@ if [ "$1" = "down" ] then docker-compose -f docker-compose.dev.yml -p clowder2-dev down fi +if [ "$1" = "jupyter" ] && [ "$2" = "up" ] +then + docker-compose -f docker-compose.dev.yml -f docker-compose.jupyter-dev.yml -p clowder2-dev up -d --build +fi + +if [ "$1" = "jupyter" ] && [ "$2" = "down" ] +then + docker-compose -f docker-compose.dev.yml -f docker-compose.jupyter-dev.yml -p clowder2-dev down +fi diff --git a/docker-prod.sh b/docker-prod.sh new file mode 100755 index 000000000..e661a9a31 --- /dev/null +++ b/docker-prod.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env sh +if [ "$1" = "prod" ] && [ "$2" = "up" ] +then + docker-compose -f docker-compose.yml -f docker-compose.jupyter.yml up -d +fi + +if [ "$1" = "prod" ] && [ "$2" = "down" ] +then + docker-compose -f docker-compose.yml -f docker-compose.jupyter.yml down +fi diff --git a/docs/docs/devs/getstarted.md b/docs/docs/devs/getstarted.md index bbf8f1633..6eb3b72aa 100644 --- a/docs/docs/devs/getstarted.md +++ b/docs/docs/devs/getstarted.md @@ -47,6 +47,8 @@ section below). - Running `docker-compose logs -f` displays the live logs for all containers. To view the logs of individual containers, provide the container name. For example, for viewing the backend module logs, run `docker-compose logs -f backend`. - Running `./docker-dev.sh down` brings down the required services. +- If you want to run the jupyterhub, you can run `./docker-dev.sh jupyter up`. The jupyterhub will be available at + `http://localhost:8765`. You can bring it down using `./docker-dev.sh jupyter down`. **Note:** `./docker-dev.sh` sets the project name flag to `-p clowder2-dev`. This is so that the dev containers don't get mixed with the production containers if the user is running both on the same machine using `docker-compose.yml`. diff --git a/frontend/package.json b/frontend/package.json index 0c9f543b4..2681aa035 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,7 +12,7 @@ "start-message": "babel-node tools/startMessage.js", "prestart": "npm-run-all --parallel start-message", "start": "npm-run-all --parallel open:src", - "start:dev": "export CLOWDER_REMOTE_HOSTNAME=http://localhost:8000 && npm run start", + "start:dev": "export CLOWDER_REMOTE_HOSTNAME=http://localhost:8000 && export JUPYTERHUB_URL=http://localhost:8765 && npm run start", "open:src": "babel-node tools/srcServer.js", "open:dist": "babel-node tools/distServer.js", "lint:watch": "npm run lint --watch", diff --git a/frontend/src/app.config.ts b/frontend/src/app.config.ts index 2ab77149d..7c325852c 100644 --- a/frontend/src/app.config.ts +++ b/frontend/src/app.config.ts @@ -9,6 +9,7 @@ interface Config { hostname: string; apikey: string; GHIssueBaseURL: string; + jupyterHubURL: string; KeycloakBaseURL: string; KeycloakLogin: string; KeycloakLogout: string; @@ -66,6 +67,10 @@ config["KeycloakRegister"] = `${config.KeycloakBaseURL}/register`; config["searchEndpoint"] = `${hostname}/api/v2/elasticsearch`; config["publicSearchEndpoint"] = `${hostname}/api/v2/public_elasticsearch`; +// jupterhub +const localJupyterhubURL: string = `${config.hostname}/jupyterhub`; +config["jupyterHubURL"] = process.env.JUPYTERHUB_URL || localJupyterhubURL; + // refresh token time interval config["refreshTokenInterval"] = 1000 * 60; // 1 minute // updated extractor logs diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 106b5834e..a96716a61 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -22,6 +22,7 @@ import { RootState } from "../types/data"; import { AddBox, Explore } from "@material-ui/icons"; import HistoryIcon from "@mui/icons-material/History"; import GroupIcon from "@mui/icons-material/Group"; +import MenuBookIcon from "@mui/icons-material/MenuBook"; import Gravatar from "react-gravatar"; import PersonIcon from "@mui/icons-material/Person"; import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; @@ -40,6 +41,8 @@ import AdminPanelSettingsIcon from "@mui/icons-material/AdminPanelSettings"; import { Footer } from "./navigation/Footer"; import BuildIcon from "@mui/icons-material/Build"; +import config from "../app.config"; + const drawerWidth = 240; const Main = styled("main", { shouldForwardProp: (prop) => prop !== "open" })<{ @@ -422,6 +425,22 @@ export default function PersistentDrawerLeft(props) { + {/*TODO: Need to make link dynamic */} + + + + + + + + + + diff --git a/frontend/webpack.config.dev.js b/frontend/webpack.config.dev.js index 688ca512b..6511e871f 100644 --- a/frontend/webpack.config.dev.js +++ b/frontend/webpack.config.dev.js @@ -8,6 +8,9 @@ import ESLintPlugin from "eslint-webpack-plugin"; console.log( `the current CLOWDER_REMOTE_HOSTNAME environment variable is ${process.env.CLOWDER_REMOTE_HOSTNAME}` ); +console.log( + `the JupyterHub URL is set to ${process.env.JUPYTERHUB_URL}` +) export default { mode: "development", @@ -40,6 +43,7 @@ export default { CLOWDER_REMOTE_HOSTNAME: JSON.stringify( process.env.CLOWDER_REMOTE_HOSTNAME ), + JUPYTERHUB_URL: JSON.stringify(process.env.JUPYTERHUB_URL), APIKEY: JSON.stringify(process.env.APIKEY), KeycloakBaseURL: JSON.stringify(process.env.KeycloakBaseURL), }, diff --git a/frontend/webpack.config.prod.js b/frontend/webpack.config.prod.js index 8102de89c..67ccf827b 100644 --- a/frontend/webpack.config.prod.js +++ b/frontend/webpack.config.prod.js @@ -11,6 +11,9 @@ import TerserPlugin from "terser-webpack-plugin"; console.log( `the current CLOWDER_REMOTE_HOSTNAME environment variable is ${process.env.CLOWDER_REMOTE_HOSTNAME}` ); +console.log( + `the JupyterHub URL is set to ${process.env.JUPYTERHUB_URL}` +) export default { mode: "production", @@ -47,6 +50,7 @@ export default { CLOWDER_REMOTE_HOSTNAME: JSON.stringify( process.env.CLOWDER_REMOTE_HOSTNAME ), + JUPYTERHUB_URL: JSON.stringify(process.env.JUPYTERHUB_URL), APIKEY: JSON.stringify(process.env.APIKEY), KeycloakBaseURL: JSON.stringify(process.env.KeycloakBaseURL), }, diff --git a/jupyterhub/.env-example b/jupyterhub/.env-example new file mode 100644 index 000000000..949459b1f --- /dev/null +++ b/jupyterhub/.env-example @@ -0,0 +1,14 @@ +# Example configuration file for Clowder JupyterHub +KEYCLOAK_HOSTNAME="keycloak:8080/keycloak" +# Development mode use the following line instead +#KEYCLOAK_HOSTNAME="keycloak:8080/keycloak" +KEYCLOAK_AUDIENCE="clowder" +KEYCLOAK_REALM="clowder" +JUPYTERHUB_ADMIN="admin" +#Change network name to the one created by docker-compose +DOCKER_NETWORK_NAME="clowder2_clowder2" +DOCKER_NOTEBOOK_IMAGE="quay.io/jupyter/base-notebook:latest" +DOCKER_NOTEBOOK_DIR="/home/jovyan/work" +JUPYTERHUB_CRYPT_KEY="" +CLOWDER_URL="localhost" +PROD_DEPLOYMENT="false" diff --git a/jupyterhub/Dockerfile.jupyterhub b/jupyterhub/Dockerfile.jupyterhub new file mode 100644 index 000000000..349782ad7 --- /dev/null +++ b/jupyterhub/Dockerfile.jupyterhub @@ -0,0 +1,14 @@ +ARG JUPYTERHUB_VERSION +FROM quay.io/jupyterhub/jupyterhub:$JUPYTERHUB_VERSION + +# Install dockerspawner, +# hadolint ignore=DL3013 +RUN python3 -m pip install --no-cache-dir \ + dockerspawner + +# Install custom authenticator +WORKDIR /tmp/authenticator/ +COPY authenticator /tmp/authenticator/ +RUN pip3 install /tmp/authenticator + +CMD ["jupyterhub", "-f", "/srv/jupyterhub/jupyterhub_config.py"] diff --git a/jupyterhub/authenticator/customauthenticator/__init__.py b/jupyterhub/authenticator/customauthenticator/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/jupyterhub/authenticator/customauthenticator/custom.py b/jupyterhub/authenticator/customauthenticator/custom.py new file mode 100644 index 000000000..7dfb05962 --- /dev/null +++ b/jupyterhub/authenticator/customauthenticator/custom.py @@ -0,0 +1,225 @@ +import json +import os +import urllib.parse + +from jose import jwt +from jose.exceptions import ExpiredSignatureError, JWTClaimsError, JWTError +from tornado import web +from traitlets import Unicode + +from jupyterhub.auth import Authenticator +from jupyterhub.handlers import LoginHandler, LogoutHandler + + +class CustomTokenAuthenticator(Authenticator): + """ + Accept the authenticated Access Token from cookie. + """ + + auth_cookie_header = Unicode( + os.environ.get("AUTH_COOKIE_HEADER", ""), + config=True, + help="the cookie header we put in browser to retrieve token", + ) + + auth_username_key = Unicode( + os.environ.get("AUTH_USERNAME_KEY", ""), + config=True, + help="the key to retreive username from the json", + ) + + landing_page_login_url = Unicode( + os.environ.get("LANDING_PAGE_LOGIN_URL", ""), + config=True, + help="the landing page login entry", + ) + + landing_page_logout_url = Unicode( + os.environ.get("LANDING_PAGE_LOGOUT", ""), + config=True, + help="the landing page logout entry", + ) + + keycloak_url = Unicode( + os.environ.get("KEYCLOAK_URL", ""), + config=True, + help="the URL where keycloak is installed", + ) + + keycloak_audience = Unicode( + os.environ.get("KEYCLOAK_AUDIENCE", ""), + config=True, + help="the audience for keycloak to check", + ) + + keycloak_pem_key = Unicode( + os.environ.get("KEYCLOAK_PEM_KEY", ""), + config=True, + help="the RSA pem key with proper header and footer (deprecated)", + ) + + space_service_url = Unicode( + os.environ.get("SPACE_SERVICE_URL", ""), + config=True, + help="the internal space service url", + ) + + quotas = None + + def get_handlers(self, app): + return [ + (r"/", LoginHandler), + (r"/user", LoginHandler), + (r"/lab", LoginHandler), + (r"/login", LoginHandler), + (r"/logout", CustomTokenLogoutHandler), + ] + + def get_keycloak_pem(self): + if not self.keycloak_url: + raise web.HTTPError( + 500, log_message="JupyterHub is not correctly configured." + ) + + # fetch the key + response = urllib.request.urlopen(self.keycloak_url) + if response.code >= 200 or response <= 299: + encoding = response.info().get_content_charset("utf-8") + result = json.loads(response.read().decode(encoding)) + self.keycloak_pem_key = ( + f"-----BEGIN PUBLIC KEY-----\n" + f"{result['public_key']}\n" + f"-----END PUBLIC KEY-----" + ) + else: + raise web.HTTPError(500, log_message="Could not get key from keycloak.") + + def check_jwt_token(self, access_token): + # make sure we have the pem cert + if not self.keycloak_pem_key: + self.get_keycloak_pem() + + # make sure audience is set + if not self.keycloak_audience: + raise web.HTTPError( + 403, log_message="JupyterHub is not correctly configured." + ) + + # no token in the cookie + if not access_token: + raise web.HTTPError(401, log_message="Please login to access Clowder.") + + # make sure it is a valid token + if len(access_token.split(" ")) != 2 or access_token.split(" ")[0] != "Bearer": + raise web.HTTPError( + 403, log_message="Token format not valid, it has to be bearer xxxx!" + ) + + # decode jwt token instead of sending it to userinfo endpoint: + access_token = access_token.split(" ")[1] + public_key = self.keycloak_pem_key + audience = self.keycloak_audience + try: + resp_json = jwt.decode(access_token, public_key, audience=audience) + except ExpiredSignatureError: + raise web.HTTPError( + 403, + log_message="JWT Expired Signature Error: token signature has expired", + ) + except JWTClaimsError: + raise web.HTTPError( + 403, log_message="JWT Claims Error: token signature is invalid" + ) + except JWTError: + raise web.HTTPError( + 403, log_message="JWT Error: token signature is invalid" + ) + except Exception: + raise web.HTTPError(403, log_message="Not a valid jwt token!") + + # make sure we know username + if self.auth_username_key not in resp_json.keys(): + raise web.HTTPError( + 500, + log_message=f"Required field {self.auth_username_key} does not exist in jwt token", + ) + username = resp_json[self.auth_username_key] + + self.log.info(f"username={username}") + return {"name": username} + + async def authenticate(self, handler, data): + self.log.info("Authenticate") + try: + access_token = urllib.parse.unquote( + handler.get_cookie(self.auth_cookie_header, "") + ) + if not access_token: + raise web.HTTPError(401, log_message="Please login to access Clowder.") + + # check token and authorization + user = self.check_jwt_token(access_token) + return user + except web.HTTPError as e: + if e.log_message: + error_msg = urllib.parse.quote(e.log_message.encode("utf-8")) + else: + error_msg = ( + urllib.parse.quote(f"Error {e}".encode("utf-8")) + + ". Please login to access Clowder." + ) + handler.redirect(f"{self.landing_page_login_url}?error={error_msg}") + + # async def pre_spawn_start(self, user, spawner): + # auth_state = await user.get_auth_state() + # if not auth_state: + # self.log.error("No auth state") + # return + # + # spawner.environment['NB_USER'] = user.name + # spawner.environment['NB_UID'] = str(auth_state['uid']) + # + # quota = self.find_quota(user, auth_state) + # if "cpu" in quota: + # spawner.cpu_guarantee = quota["cpu"][0] + # spawner.cpu_limit = quota["cpu"][1] + # else: + # spawner.cpu_guarantee = 1 + # spawner.cpu_limit = 2 + # if "mem" in quota: + # spawner.mem_guarantee = f"{quota['mem'][0]}G" + # spawner.mem_limit = f"{quota['mem'][1]}G" + # else: + # spawner.mem_guarantee = "2G" + # spawner.mem_limit = "4G" + + +# +# # This is called from the jupyterlab so there is no cookies that this depends on +# async def refresh_user(self, user, handler): +# self.log.info("Refresh User") +# try: +# access_token = urllib.parse.unquote(handler.get_cookie(self.auth_cookie_header, "")) +# # if no token present +# if not access_token: +# return False +# +# # if token present, check token and authorization +# if self.check_jwt_token(access_token): +# True +# return False +# except: +# self.log.exception("Error in refresh user") +# return False + + +class CustomTokenLogoutHandler(LogoutHandler): + async def handle_logout(self): + # remove clowder token on logout + self.log.info("Remove clowder token on logout") + self.log.info( + "You have logged out of Clowder system from Clowder . Please login again if you want to use " + "Clowder components." + ) + self.set_cookie(self.authenticator.auth_cookie_header, "") + self.redirect(f"{self.authenticator.landing_page_logout_url}") diff --git a/jupyterhub/authenticator/setup.py b/jupyterhub/authenticator/setup.py new file mode 100644 index 000000000..0314f36d2 --- /dev/null +++ b/jupyterhub/authenticator/setup.py @@ -0,0 +1,12 @@ +from setuptools import setup + +setup( + name="customauthenticator", + version="0.8.0", + description="Custom Authenticator for JupyterHub", + author="cwang138", + author_email="cwang138@illinois.edu", + license="MPL 2.0", + packages=["customauthenticator"], + install_requires=["jupyterhub", "pyjwt", "requests", "python-jose"], +) diff --git a/jupyterhub/authenticator/test_jwt.py b/jupyterhub/authenticator/test_jwt.py new file mode 100644 index 000000000..e86f6bc96 --- /dev/null +++ b/jupyterhub/authenticator/test_jwt.py @@ -0,0 +1,41 @@ +import json +import urllib.request + +from jose import jwt +from jose.exceptions import ExpiredSignatureError, JWTClaimsError, JWTError + +response = urllib.request.urlopen("") + +if response.code >= 200 or response <= 299: + encoding = response.info().get_content_charset("utf-8") + result = json.loads(response.read().decode(encoding)) + public_key = ( + f"-----BEGIN PUBLIC KEY-----\n" + f"{result['public_key']}\n" + f"-----END PUBLIC KEY-----" + ) +else: + print("Could not get key from keycloak.") + + +access_token = "" + +# make sure it is a valid token +if len(access_token.split(" ")) != 2 or access_token.split(" ")[0] != "Bearer": + print("Token format not valid, it has to be bearer xxxx!") + +# decode jwt token instead of sending it to userinfo endpoint: +access_token = access_token.split(" ")[1] + +try: + decoded = jwt.decode(access_token, public_key, audience="clowder") + print(decoded) + +except ExpiredSignatureError: + print("JWT Expired Signature Error: token signature has expired") +except JWTClaimsError: + print("JWT Claims Error: token signature is invalid") +except JWTError: + print("JWT Error: token signature is invalid") +except Exception: + print("Not a valid jwt token!") diff --git a/jupyterhub/jupyterhub_config.py b/jupyterhub/jupyterhub_config.py new file mode 100644 index 000000000..20242be38 --- /dev/null +++ b/jupyterhub/jupyterhub_config.py @@ -0,0 +1,107 @@ +# Copyright (c) Jupyter Development Team. +# Distributed under the terms of the Modified BSD License. + +# Configuration file for JupyterHub +import os + +from customauthenticator.custom import CustomTokenAuthenticator + +c = get_config() # noqa: F821 + +# We rely on environment variables to configure JupyterHub so that we +# avoid having to rebuild the JupyterHub container every time we change a +# configuration parameter. + +# Base URL of the Hub +c.JupyterHub.base_url = "/jupyterhub" + + +# Important proxy settings to work with Traefik +c.JupyterHub.proxy_class = "jupyterhub.proxy.ConfigurableHTTPProxy" +c.ConfigurableHTTPProxy.command = ["configurable-http-proxy"] + + +# Spawn single-user servers as Docker containers +c.JupyterHub.spawner_class = "dockerspawner.DockerSpawner" + +# Spawn containers from this image +c.DockerSpawner.image = os.environ["DOCKER_NOTEBOOK_IMAGE"] + +# Connect containers to this Docker network +network_name = os.environ["DOCKER_NETWORK_NAME"] +c.DockerSpawner.use_internal_ip = True +c.DockerSpawner.network_name = network_name + +# Explicitly set notebook directory because we'll be mounting a volume to it. +# Most `jupyter/docker-stacks` *-notebook images run the Notebook server as +# user `jovyan`, and set the notebook directory to `/home/jovyan/work`. +# We follow the same convention. +notebook_dir = os.environ.get("DOCKER_NOTEBOOK_DIR", "/home/jovyan/work") +c.DockerSpawner.notebook_dir = notebook_dir + +# Mount the real user's Docker volume on the host to the notebook user's +# notebook directory in the container +c.DockerSpawner.volumes = {"jupyterhub-user-{username}": notebook_dir} + +# Remove containers once they are stopped +c.DockerSpawner.remove = True + +# For debugging arguments passed to spawned containers +c.DockerSpawner.debug = True + +# User containers will access hub by container name on the Docker network +c.JupyterHub.hub_ip = "jupyterhub" +c.JupyterHub.hub_port = 8080 + +# Persist hub data on volume mounted inside container +# c.JupyterHub.cookie_secret_file = "/data/jupyterhub_cookie_secret" +c.JupyterHub.db_url = "sqlite:////data/jupyterhub.sqlite" + +# # Authenticate users with Native Authenticator +# c.JupyterHub.authenticator_class = "nativeauthenticator.NativeAuthenticator" +# +# # Allow anyone to sign-up without approval +# c.NativeAuthenticator.open_signup = True + +# Authenticate with Custom Token Authenticator +c.Spawner.cmd = ["start.sh", "jupyterhub-singleuser", "--allow-root"] +c.KubeSpawner.args = ["--allow-root"] +c.JupyterHub.authenticator_class = CustomTokenAuthenticator +# TODO:Change this keycloak_url as required + +c.CustomTokenAuthenticator.auth_cookie_header = "Authorization" +c.CustomTokenAuthenticator.auth_username_key = "preferred_username" +c.CustomTokenAuthenticator.auth_uid_number_key = "uid_number" +c.CustomTokenAuthenticator.enable_auth_state = True +c.CustomTokenAuthenticator.auto_login = True + +if os.getenv("PROD_DEPLOYMENT") == "true": + c.CustomTokenAuthenticator.keycloak_url = "https://%s/realms/%s/" % ( + os.getenv("KEYCLOAK_HOSTNAME"), + os.getenv("KEYCLOAK_REALM"), + ) + c.CustomTokenAuthenticator.landing_page_login_url = "https://" + os.getenv( + "KEYCLOAK_HOSTNAME" + ) + c.CustomTokenAuthenticator.landing_page_logout_url = ( + "https://" + os.getenv("CLOWDER_URL") + "/auth/logout" + ) + +else: + c.CustomTokenAuthenticator.keycloak_url = "http://%s/realms/%s/" % ( + os.getenv("KEYCLOAK_HOSTNAME"), + os.getenv("KEYCLOAK_REALM"), + ) + c.CustomTokenAuthenticator.landing_page_login_url = "http://" + os.getenv( + "KEYCLOAK_HOSTNAME" + ) + c.CustomTokenAuthenticator.landing_page_logout_url = ( + "http://" + os.getenv("CLOWDER_URL") + "/auth/logout" + ) + +c.JupyterHub.cookie_secret = os.getenv("JUPYTERHUB_CRYPT_KEY") + +# Allowed admins +admin = os.environ.get("JUPYTERHUB_ADMIN") +if admin: + c.Authenticator.admin_users = [admin] diff --git a/jupyterhub/jupyterhub_dev_config.py b/jupyterhub/jupyterhub_dev_config.py new file mode 100644 index 000000000..136f80962 --- /dev/null +++ b/jupyterhub/jupyterhub_dev_config.py @@ -0,0 +1,98 @@ +# Copyright (c) Jupyter Development Team. +# Distributed under the terms of the Modified BSD License. + +# Configuration file for JupyterHub +import os + +from customauthenticator.custom import CustomTokenAuthenticator + +c = get_config() # noqa: F821 + +# We rely on environment variables to configure JupyterHub so that we +# avoid having to rebuild the JupyterHub container every time we change a +# configuration parameter. + +# Spawn single-user servers as Docker containers +c.JupyterHub.spawner_class = "dockerspawner.DockerSpawner" + +# Spawn containers from this image +c.DockerSpawner.image = os.environ["DOCKER_NOTEBOOK_IMAGE"] + +# Connect containers to this Docker network +network_name = os.environ["DOCKER_NETWORK_NAME"] +c.DockerSpawner.use_internal_ip = True +c.DockerSpawner.network_name = network_name + +# Explicitly set notebook directory because we'll be mounting a volume to it. +# Most `jupyter/docker-stacks` *-notebook images run the Notebook server as +# user `jovyan`, and set the notebook directory to `/home/jovyan/work`. +# We follow the same convention. +notebook_dir = os.environ.get("DOCKER_NOTEBOOK_DIR", "/home/jovyan/work") +c.DockerSpawner.notebook_dir = notebook_dir + +# Mount the real user's Docker volume on the host to the notebook user's +# notebook directory in the container +c.DockerSpawner.volumes = {"jupyterhub-user-{username}": notebook_dir} + +# Remove containers once they are stopped +c.DockerSpawner.remove = True + +# For debugging arguments passed to spawned containers +c.DockerSpawner.debug = True + +# User containers will access hub by container name on the Docker network +c.JupyterHub.hub_ip = "jupyterhub" +c.JupyterHub.hub_port = 8080 + +# Persist hub data on volume mounted inside container +# c.JupyterHub.cookie_secret_file = "/data/jupyterhub_cookie_secret" +c.JupyterHub.db_url = "sqlite:////data/jupyterhub.sqlite" + +# # Authenticate users with Native Authenticator +# c.JupyterHub.authenticator_class = "nativeauthenticator.NativeAuthenticator" +# +# # Allow anyone to sign-up without approval +# c.NativeAuthenticator.open_signup = True + +# Authenticate with Custom Token Authenticator +c.Spawner.cmd = ["start.sh", "jupyterhub-singleuser", "--allow-root"] +c.KubeSpawner.args = ["--allow-root"] +c.JupyterHub.authenticator_class = CustomTokenAuthenticator +# TODO:Change this keycloak_url as required + +c.CustomTokenAuthenticator.auth_cookie_header = "Authorization" +c.CustomTokenAuthenticator.auth_username_key = "preferred_username" +c.CustomTokenAuthenticator.auth_uid_number_key = "uid_number" +c.CustomTokenAuthenticator.enable_auth_state = True +c.CustomTokenAuthenticator.auto_login = True + +if os.getenv("PROD_DEPLOYMENT") == "true": + c.CustomTokenAuthenticator.keycloak_url = "https://%s/realms/%s/" % ( + os.getenv("KEYCLOAK_HOSTNAME"), + os.getenv("KEYCLOAK_REALM"), + ) + c.CustomTokenAuthenticator.landing_page_login_url = "https://" + os.getenv( + "KEYCLOAK_HOSTNAME" + ) + c.CustomTokenAuthenticator.landing_page_logout_url = ( + "https://" + os.getenv("CLOWDER_URL") + "/auth/logout" + ) + +else: + c.CustomTokenAuthenticator.keycloak_url = "http://%s/realms/%s/" % ( + os.getenv("KEYCLOAK_HOSTNAME"), + os.getenv("KEYCLOAK_REALM"), + ) + c.CustomTokenAuthenticator.landing_page_login_url = "http://" + os.getenv( + "KEYCLOAK_HOSTNAME" + ) + c.CustomTokenAuthenticator.landing_page_logout_url = ( + "http://" + os.getenv("CLOWDER_URL") + "/auth/logout" + ) + +c.JupyterHub.cookie_secret = os.getenv("JUPYTERHUB_CRYPT_KEY") + +# Allowed admins +admin = os.environ.get("JUPYTERHUB_ADMIN") +if admin: + c.Authenticator.admin_users = [admin]