import logging import os import re import subprocess from typing import Dict, Optional from oauthenticator.generic import GenericOAuthenticator from jupyterhub.auth import DummyAuthenticator from dockerspawner import DockerSpawner c = get_config() # Authenticator selection (environment-driven) c.Authenticator.enable_auth_state = False auth_strategy = os.environ.get("JUPYTERHUB_AUTH_STRATEGY", "oauth").strip().lower() if auth_strategy == "dummy": c.JupyterHub.authenticator_class = DummyAuthenticator dummy_password = os.environ.get("JUPYTERHUB_DUMMY_PASSWORD") if dummy_password: c.DummyAuthenticator.password = dummy_password else: c.JupyterHub.authenticator_class = GenericOAuthenticator c.GenericOAuthenticator.client_id = os.environ.get("DSP_OAUTH_CLIENT_ID", "") c.GenericOAuthenticator.client_secret = os.environ.get("DSP_OAUTH_CLIENT_SECRET", "") c.GenericOAuthenticator.authorize_url = os.environ.get("DSP_OAUTH_AUTHORIZE_URL", "") c.GenericOAuthenticator.token_url = os.environ.get("DSP_OAUTH_TOKEN_URL", "") c.GenericOAuthenticator.userdata_url = os.environ.get("DSP_OAUTH_USERINFO_URL", "") c.GenericOAuthenticator.oauth_callback_url = os.environ.get("JUPYTERHUB_OAUTH_CALLBACK", "") c.GenericOAuthenticator.scope = ["profile"] c.GenericOAuthenticator.username_claim = "hub_username" c.GenericOAuthenticator.username_key = "hub_username" c.GenericOAuthenticator.auto_login = True # Explicitly acknowledge HTTP when running behind an external TLS terminator. c.JupyterHub.confirm_no_ssl = True c.Spawner.http_timeout = int(os.getenv("JUPYTERHUB_HTTP_TIMEOUT", "90")) c.Spawner.start_timeout = int(os.getenv("JUPYTERHUB_START_TIMEOUT", "90")) def _env_bool(name: str, default: bool = False) -> bool: value = os.getenv(name) if value is None: return default return value.strip().lower() in {"1", "true", "yes", "on"} tornado_settings = getattr(c.JupyterHub, "tornado_settings", {}) if isinstance(tornado_settings, dict): merged_settings = tornado_settings.copy() else: merged_settings = {} default_frame_ancestors = ["'self'", "http://localhost:8082", "http://127.0.0.1:8082"] app_origin_values = [value.rstrip("/") for value in os.getenv("DSP_APP_ORIGINS", "").split() if value] extra_frame_ancestors = [value.rstrip("/") for value in os.getenv("DSP_FRAME_ANCESTORS", "").split() if value] frame_ancestors = " ".join( dict.fromkeys(default_frame_ancestors + app_origin_values + extra_frame_ancestors) ) header_settings = { "Content-Security-Policy": f"frame-ancestors {frame_ancestors}", "X-Frame-Options": "ALLOWALL", } existing_headers = merged_settings.get("headers", {}) existing_headers.update(header_settings) merged_settings["headers"] = existing_headers external_url = os.getenv("JUPYTER_EXTERNAL_URL", "") cookie_secure_default = external_url.startswith("https://") merged_settings["cookie_options"] = { "SameSite": "None", "Secure": _env_bool("JUPYTERHUB_COOKIE_SECURE", cookie_secure_default), } c.JupyterHub.tornado_settings = merged_settings # Single-user server configuration c.JupyterHub.spawner_class = DockerSpawner c.DockerSpawner.image = os.environ.get("DSP_JH_IMAGE", "jupyter/minimal-notebook:python-3.11") c.DockerSpawner.remove_containers = True c.DockerSpawner.cmd = ["start-singleuser.sh"] c.DockerSpawner.notebook_dir = "/home/jovyan/work" c.DockerSpawner.network_name = os.environ.get("DSP_JH_NETWORK", "dsp_default") def _workspace_volume(username: str) -> Dict[str, str]: safe = re.sub(r"[^a-zA-Z0-9._-]+", "-", username) host_root = os.environ.get("DSP_WORKSPACE_ROOT", "/var/www/html/uploads/jupyter_workspace") volumes: Dict[str, str] = {f"{host_root}/{safe}": "/home/jovyan/work"} r_scripts_root = os.environ.get("DSP_R_SCRIPTS_ROOT", "/var/www/html/r_scripts") if os.path.isdir(r_scripts_root): volumes[r_scripts_root] = "/home/jovyan/work/r_scripts" return volumes def _extract_person_id(username: str) -> Optional[str]: match = re.search(r"(\d+)$", username or "") return match.group(1) if match else None def _run_sync(person_id: str) -> None: command = [ "docker", "exec", os.environ.get("DSP_APP_CONTAINER", "dsp_app"), "php", "/var/www/html/scripts/trigger_workspace_sync.php", person_id, ] subprocess.run(command, check=False) async def pre_spawn_hook(spawner): username = spawner.user.name spawner.volumes = _workspace_volume(username) person_id = _extract_person_id(username) if person_id: _run_sync(person_id) c.DockerSpawner.pre_spawn_hook = pre_spawn_hook _cull_token = os.environ.get("JUPYTERHUB_CULL_API_TOKEN") if _cull_token: c.JupyterHub.services = [ { "name": "cull-idle", "command": [ "python", "-m", "jupyterhub_idle_culler", "--timeout=3600", "--cull-every=600", "--concurrency=10", ], "api_token": _cull_token, "admin": True, } ] else: logging.warning("JUPYTERHUB_CULL_API_TOKEN not set; idle culler disabled.")