diff --git a/docs/usage/multi_user.md b/docs/usage/multi_user.md index 7ded0e03..57c0b6af 100644 --- a/docs/usage/multi_user.md +++ b/docs/usage/multi_user.md @@ -1,8 +1,12 @@ -Jupyverse supports multiple users working collaboratively. Depending on the chosen authentication method, access to the server can be finer-grained. For instance, it is possible to require users to create an account before they can log in, and to give them permissions restricting access to specific resources. It is also possible to let them log in as anonymous users. The authentication method largely depends on the level of security you want. +Jupyverse supports multiple users working collaboratively. Depending on the chosen authentication method, access to the server can be finer-grained. For instance, it is possible to require users to create an account before they can log in, and to give them permissions restricting access to specific resources. It is also possible to let them log in as anonymous users. The authentication method largely depends on the desired level of security. ## Collaborative editing -The first thing to do is to allow collaborative editing when launching Jupyverse: +The first thing to do is to install the Jupyter collaboration package: +```bash +pip install jupyter-collaboration +``` +Jupyverse must then be launched in collaborative mode: ```bash jupyverse --set frontend.collaborative=true ``` diff --git a/jupyverse_api/jupyverse_api/lab/__init__.py b/jupyverse_api/jupyverse_api/lab/__init__.py index b9679d36..245e2000 100644 --- a/jupyverse_api/jupyverse_api/lab/__init__.py +++ b/jupyverse_api/jupyverse_api/lab/__init__.py @@ -15,6 +15,7 @@ class Lab(Router, ABC): prefix_dir: Path jlab_dir: Path + labextensions_dir: Path extensions_dir: Path redirect_after_root: str @@ -36,6 +37,7 @@ def __init__(self, app: App, auth: Auth, jupyterlab_config: Optional[JupyterLabC self.jlab_dir = Path(jupyterlab_module.__file__).parents[1] / "dev_mode" else: self.jlab_dir = self.prefix_dir / "share" / "jupyter" / "lab" + self.labextensions_dir = self.prefix_dir / "share" / "jupyter" / "labextensions" for ext in self.federated_extensions: name = ext["name"] self.mount( diff --git a/plugins/auth/fps_auth/routes.py b/plugins/auth/fps_auth/routes.py index fb1bd8ca..b633adcc 100644 --- a/plugins/auth/fps_auth/routes.py +++ b/plugins/auth/fps_auth/routes.py @@ -1,6 +1,7 @@ import contextlib import json import logging +import random from typing import Any, Callable, Dict, List, Optional, Tuple from fastapi import APIRouter, Depends, Request @@ -90,6 +91,10 @@ async def get_api_me( keys = ["username", "name", "display_name", "initials", "avatar_url", "color"] identity = {k: getattr(user, k) for k in keys} + if not identity["name"] and not identity["display_name"]: + moon = get_anonymous_username() + identity["name"] = f"Anonymous {moon}" + identity["display_name"] = f"Anonymous {moon}" return { "identity": identity, "permissions": checked_permissions, @@ -148,3 +153,95 @@ def websocket_auth( return backend.websocket_auth(permissions) return _Auth() + + +# From https://en.wikipedia.org/wiki/Moons_of_Jupiter +moons_of_jupyter = ( + "Metis", + "Adrastea", + "Amalthea", + "Thebe", + "Io", + "Europa", + "Ganymede", + "Callisto", + "Themisto", + "Leda", + "Ersa", + "Pandia", + "Himalia", + "Lysithea", + "Elara", + "Dia", + "Carpo", + "Valetudo", + "Euporie", + "Eupheme", + # 'S/2003 J 18', + # 'S/2010 J 2', + "Helike", + # 'S/2003 J 16', + # 'S/2003 J 2', + "Euanthe", + # 'S/2017 J 7', + "Hermippe", + "Praxidike", + "Thyone", + "Thelxinoe", + # 'S/2017 J 3', + "Ananke", + "Mneme", + # 'S/2016 J 1', + "Orthosie", + "Harpalyke", + "Iocaste", + # 'S/2017 J 9', + # 'S/2003 J 12', + # 'S/2003 J 4', + "Erinome", + "Aitne", + "Herse", + "Taygete", + # 'S/2017 J 2', + # 'S/2017 J 6', + "Eukelade", + "Carme", + # 'S/2003 J 19', + "Isonoe", + # 'S/2003 J 10', + "Autonoe", + "Philophrosyne", + "Cyllene", + "Pasithee", + # 'S/2010 J 1', + "Pasiphae", + "Sponde", + # 'S/2017 J 8', + "Eurydome", + # 'S/2017 J 5', + "Kalyke", + "Hegemone", + "Kale", + "Kallichore", + # 'S/2011 J 1', + # 'S/2017 J 1', + "Chaldene", + "Arche", + "Eirene", + "Kore", + # 'S/2011 J 2', + # 'S/2003 J 9', + "Megaclite", + "Aoede", + # 'S/2003 J 23', + "Callirrhoe", + "Sinope", +) + + +def get_anonymous_username() -> str: + """ + Get a random user-name based on the moons of Jupyter. + This function returns names like "Anonymous Io" or "Anonymous Metis". + """ + return moons_of_jupyter[random.randint(0, len(moons_of_jupyter) - 1)] diff --git a/plugins/contents/fps_contents/fileid.py b/plugins/contents/fps_contents/fileid.py index 89b20a93..be50ce4a 100644 --- a/plugins/contents/fps_contents/fileid.py +++ b/plugins/contents/fps_contents/fileid.py @@ -35,7 +35,7 @@ class FileIdManager(metaclass=Singleton): watchers: Dict[str, List[Watcher]] lock: asyncio.Lock - def __init__(self, db_path: str = "fileid.db"): + def __init__(self, db_path: str = ".fileid.db"): self.db_path = db_path self.initialized = asyncio.Event() self.watchers = {} diff --git a/plugins/jupyterlab/pyproject.toml b/plugins/jupyterlab/pyproject.toml index 369e524b..40fb2de6 100644 --- a/plugins/jupyterlab/pyproject.toml +++ b/plugins/jupyterlab/pyproject.toml @@ -8,7 +8,7 @@ description = "An FPS plugin for the JupyterLab API" keywords = [ "jupyter", "server", "fastapi", "plugins" ] requires-python = ">=3.8" dependencies = [ - "jupyterlab >=4.0.0rc0,<5", + "jupyterlab >=4.0.0,<5", "jupyverse-api >=0.1.2,<1", ] dynamic = [ "version",] diff --git a/plugins/lab/fps_lab/routes.py b/plugins/lab/fps_lab/routes.py index 3a5cfa64..00aefc8d 100644 --- a/plugins/lab/fps_lab/routes.py +++ b/plugins/lab/fps_lab/routes.py @@ -158,31 +158,39 @@ async def change_setting( return Response(status_code=HTTPStatus.NO_CONTENT.value) async def get_settings(self, user: User): - with open(self.jlab_dir / "static" / "package.json") as f: - package = json.load(f) if user: user_settings = json.loads(user.settings) else: user_settings = {} settings = [] - for path in (self.jlab_dir / "schemas" / "@jupyterlab").glob("*/*.json"): - with open(path) as f: - schema = json.load(f) - key = f"{path.parent.name}:{path.stem}" - setting = { - "id": f"@jupyterlab/{key}", - "schema": schema, - "version": package["version"], - "raw": "{}", - "settings": {}, - "warning": None, - "last_modified": None, - "created": None, - } - if key in user_settings: - setting.update(user_settings[key]) - setting["settings"] = json5.loads(user_settings[key]["raw"]) - settings.append(setting) + schemas = [self.jlab_dir / "schemas"] + for d1 in self.labextensions_dir.iterdir(): + if (d1 / "schemas").exists(): + schemas.append(d1 / "schemas") + for d2 in d1.iterdir(): + if (d2 / "schemas").exists(): + schemas.append(d2 / "schemas") + for s in schemas: + for d1 in s.iterdir(): + for d2 in d1.iterdir(): + package = json.loads((d2 / "package.json.orig").read_text()) + for path in [p for p in d2.iterdir() if p.suffix == ".json"]: + schema = json.loads(path.read_text()) + key = f"{path.parent.name}:{path.stem}" + setting = { + "id": f"{d1.name}/{key}", + "schema": schema, + "version": package["version"], + "raw": "{}", + "settings": {}, + "warning": None, + "last_modified": None, + "created": None, + } + if key in user_settings: + setting.update(user_settings[key]) + setting["settings"] = json5.loads(user_settings[key]["raw"]) + settings.append(setting) return {"settings": settings} def get_federated_extensions(self, extensions_dir: Path) -> Tuple[List, List]: