From 8002da7a66d0630ef4e1fd1a88cc93c4f070e83d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Behmo?= Date: Tue, 12 Aug 2025 21:32:47 +0200 Subject: [PATCH] feat: HTTP basic authentication This is performed via two configuration settings. Note however that we don't yet support automatic reloading of credentials. This is unfortunate because there is no way to reload tutor Deck... --- README.rst | 5 ++++ tutordeck/plugin.py | 2 ++ tutordeck/server/app.py | 56 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 62 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index ac6dcb9..55b95da 100644 --- a/README.rst +++ b/README.rst @@ -73,6 +73,11 @@ Restart platform via GUI to apply changes: .. image:: https://github.com/overhangio/tutor-deck/raw/release/images/apply.png :alt: Apply Image +You may add HTTP basic authentication by defining the following Tutor settings:: + + tutor config save --set DECK_AUTH_USERNAME=myusername \ + --set DECK_AUTH_PASSWORD=s3cr3tpassw0rd + Troubleshooting *************** diff --git a/tutordeck/plugin.py b/tutordeck/plugin.py index cc81bdc..a3aa20a 100644 --- a/tutordeck/plugin.py +++ b/tutordeck/plugin.py @@ -14,6 +14,8 @@ from .server import app hooks.Filters.CONFIG_DEFAULTS.add_items( [ ("DECK_VERSION", __version__), + ("DECK_AUTH_USERNAME", None), + ("DECK_AUTH_PASSWORD", None), ] ) diff --git a/tutordeck/server/app.py b/tutordeck/server/app.py index 8005665..082db35 100644 --- a/tutordeck/server/app.py +++ b/tutordeck/server/app.py @@ -46,12 +46,66 @@ def run(root: str, **app_kwargs: t.Any) -> None: tutorclient.logger.addHandler(handler) tutorclient.logger.setLevel(logging.INFO) + # Configure authentication + HttpAuthCredentials.load_credentials() + # TODO app.run() should be called only in development app.run(**app_kwargs) +class HttpAuthCredentials: + USERNAME: str = "" + PASSWORD: str = "" + + @classmethod + def load_credentials(cls) -> None: + """ + Note that credentials will not be automatically reloaded on configuration change. + + TODO reload credentials automatically when needed. + """ + config = tutorclient.Project.get_config() + cls.USERNAME = t.cast(str, config.get("DECK_AUTH_USERNAME", "")) + cls.PASSWORD = t.cast(str, config.get("DECK_AUTH_PASSWORD", "")) + + @classmethod + def is_auth_success(cls) -> bool: + """ + Returns True if the current request has the right HTTP basic auth credentials. + """ + if not cls.USERNAME or not cls.PASSWORD: + # No credential required + return True + + if not request.authorization: + # No credential was provided + return False + + # Check provided credentials + username = request.authorization.parameters.get("username") + password = request.authorization.parameters.get("password") + return username == cls.USERNAME and password == cls.PASSWORD + + +@app.before_request +def http_basic_auth() -> None | tuple[str, int, dict[str, str]]: + """ + Check authentication headers if necessary. + """ + if not HttpAuthCredentials.is_auth_success(): + # https://quart.palletsprojects.com/en/latest/reference/response_values/#tuple-str-int-dict-str-str + # https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/401 + # https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Authentication#authentication_schemes + return "", 401, {"WWW-Authenticate": "basic"} + + return None + + @app.before_request async def before_request() -> None: + """ + Store installed and enabled plugins as global attributes. + """ # Shared views and template context g.installed_plugins = tutorclient.Client.installed_plugins() g.enabled_plugins = tutorclient.Client.enabled_plugins() @@ -222,7 +276,7 @@ async def plugin_upgrade(name: str) -> BaseResponse: @app.post("/plugins/update") -async def plugins_update() -> WerkzeugResponse: +async def plugins_update() -> BaseResponse: tutorclient.CliPool.run_sequential(["plugins", "update"]) return redirect(url_for("plugin_store"))