From 3f65147607726030018b00819e037b8a80c6d4e7 Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Sat, 10 Feb 2024 15:11:37 +0000 Subject: [PATCH] Add `allow_unauthenticated_access` traitlet and `@allow_unauthenticated` --- jupyter_server/auth/decorator.py | 27 +++++++++++++++++++++++++++ jupyter_server/auth/login.py | 4 ++++ jupyter_server/auth/logout.py | 2 ++ jupyter_server/base/handlers.py | 10 ++++++++++ jupyter_server/serverapp.py | 15 +++++++++++++++ 5 files changed, 58 insertions(+) diff --git a/jupyter_server/auth/decorator.py b/jupyter_server/auth/decorator.py index a92866b4e8..9fdc115c78 100644 --- a/jupyter_server/auth/decorator.py +++ b/jupyter_server/auth/decorator.py @@ -85,3 +85,30 @@ async def inner(self, *args, **kwargs): return cast(FuncT, wrapper(method)) return cast(FuncT, wrapper) + + +def allow_unauthenticated(method: FuncT) -> FuncT: + """A decorator for tornado.web.RequestHandler methods + that allows any user to make the following request. + + Selectively disables the 'authentication' layer of REST API which + is active when `ServerApp.allow_unauthenticated_access = False`. + + To be used exclusively on endpoints which may be considered public, + for example the logic page handler. + + .. versionadded:: 2.13 + + Parameters + ---------- + method : bound callable + the endpoint method to remove authentication from. + """ + + @wraps(method) + async def wrapper(self, *args, **kwargs): + return method(self, *args, **kwargs) + + setattr(wrapper, "__allow_unauthenticated", True) + + return cast(FuncT, wrapper) diff --git a/jupyter_server/auth/login.py b/jupyter_server/auth/login.py index 22832df341..0628681224 100644 --- a/jupyter_server/auth/login.py +++ b/jupyter_server/auth/login.py @@ -9,6 +9,7 @@ from tornado.escape import url_escape from ..base.handlers import JupyterHandler +from .decorator import allow_unauthenticated from .security import passwd_check, set_password @@ -73,6 +74,7 @@ def _redirect_safe(self, url, default=None): url = default self.redirect(url) + @allow_unauthenticated def get(self): """Get the login form.""" if self.current_user: @@ -81,6 +83,7 @@ def get(self): else: self._render() + @allow_unauthenticated def post(self): """Post a login.""" user = self.current_user = self.identity_provider.process_login_form(self) @@ -110,6 +113,7 @@ def passwd_check(self, a, b): """Check a passwd.""" return passwd_check(a, b) + @allow_unauthenticated def post(self): """Post a login form.""" typed_password = self.get_argument("password", default="") diff --git a/jupyter_server/auth/logout.py b/jupyter_server/auth/logout.py index 3db7f796ba..584404cf4c 100644 --- a/jupyter_server/auth/logout.py +++ b/jupyter_server/auth/logout.py @@ -3,11 +3,13 @@ # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. from ..base.handlers import JupyterHandler +from .decorator import allow_unauthenticated class LogoutHandler(JupyterHandler): """An auth logout handler.""" + @allow_unauthenticated def get(self): """Handle a logout.""" self.identity_provider.clear_login_cookie(self) diff --git a/jupyter_server/base/handlers.py b/jupyter_server/base/handlers.py index 10f36b7679..48991b9ab6 100644 --- a/jupyter_server/base/handlers.py +++ b/jupyter_server/base/handlers.py @@ -630,6 +630,16 @@ async def prepare(self) -> Awaitable[None] | None: # type:ignore[override] self.set_cors_headers() if self.request.method not in {"GET", "HEAD", "OPTIONS"}: self.check_xsrf_cookie() + + if not self.settings.get("allow_unauthenticated_access", False): + if not self.request.method: + raise HTTPError(403) + method = getattr(self, self.request.method.lower()) + if not getattr(method, "__allow_unauthenticated", False): + # reuse `web.authenticated` logic, which redirects to the login + # page on GET and HEAD and otherwise raises 403 + return web.authenticated(lambda _method: None)(self) + return super().prepare() # --------------------------------------------------------------- diff --git a/jupyter_server/serverapp.py b/jupyter_server/serverapp.py index 9b451310ca..a7ceca4f1e 100644 --- a/jupyter_server/serverapp.py +++ b/jupyter_server/serverapp.py @@ -1214,6 +1214,21 @@ def _deprecated_password_config(self, change: t.Any) -> None: """, ) + allow_unauthenticated_access = Bool( + True, + config=True, + help="""Allow requests unauthenticated access to endpoints without authentication rules. + + When set to `True` (default in jupyter-server 2.0, subject to change + in the future), any request to an endpoint without an authentication rule + (either `@tornado.web.authenticated`, or `@allow_unauthenticated`) + will be permitted, regardless of whether user has logged in or not. + + When set to `False`, logging in will be required for access to each endpoint, + excluding the endpoints marked with `@allow_unauthenticated` decorator. + """, + ) + allow_remote_access = Bool( config=True, help="""Allow requests where the Host header doesn't point to a local server