Merge pull request #4779 from krassowski/support-allow_unauthenticated_access-false

Support forbidding unauthenticated access (`allow_unauthenticated_access = False`)
This commit is contained in:
Min RK
2024-04-10 13:19:45 +02:00
committed by GitHub
4 changed files with 78 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
from typing import Any, Callable, TypeVar
try:
from jupyter_server.auth.decorator import allow_unauthenticated
except ImportError:
FuncT = TypeVar("FuncT", bound=Callable[..., Any])
# if using an older jupyter-server version this can be a no-op,
# as these do not support marking endpoints anyways
def allow_unauthenticated(method: FuncT) -> FuncT:
return method
__all__ = ["allow_unauthenticated"]

View File

@@ -51,6 +51,7 @@ from jupyterhub.utils import (
url_path_join, url_path_join,
) )
from ._decorator import allow_unauthenticated
from ._disable_user_config import _disable_user_config from ._disable_user_config import _disable_user_config
SINGLEUSER_TEMPLATES_DIR = str(Path(__file__).parent.resolve().joinpath("templates")) SINGLEUSER_TEMPLATES_DIR = str(Path(__file__).parent.resolve().joinpath("templates"))
@@ -68,6 +69,7 @@ def _exclude_home(path_list):
class JupyterHubLogoutHandler(LogoutHandler): class JupyterHubLogoutHandler(LogoutHandler):
@allow_unauthenticated
def get(self): def get(self):
hub_auth = self.identity_provider.hub_auth hub_auth = self.identity_provider.hub_auth
# clear token stored in single-user cookie (set by hub_auth) # clear token stored in single-user cookie (set by hub_auth)
@@ -95,6 +97,10 @@ class JupyterHubOAuthCallbackHandler(HubOAuthCallbackHandler):
def initialize(self, hub_auth): def initialize(self, hub_auth):
self.hub_auth = hub_auth self.hub_auth = hub_auth
@allow_unauthenticated
async def get(self):
return await super().get()
class JupyterHubIdentityProvider(IdentityProvider): class JupyterHubIdentityProvider(IdentityProvider):
"""Identity Provider for JupyterHub OAuth """Identity Provider for JupyterHub OAuth

View File

@@ -52,6 +52,7 @@ from ..utils import (
make_ssl_context, make_ssl_context,
url_path_join, url_path_join,
) )
from ._decorator import allow_unauthenticated
from ._disable_user_config import _disable_user_config, _exclude_home from ._disable_user_config import _disable_user_config, _exclude_home
# Authenticate requests with the Hub # Authenticate requests with the Hub
@@ -132,6 +133,7 @@ class JupyterHubLoginHandlerMixin:
class JupyterHubLogoutHandlerMixin: class JupyterHubLogoutHandlerMixin:
@allow_unauthenticated
def get(self): def get(self):
self.settings['hub_auth'].clear_cookie(self) self.settings['hub_auth'].clear_cookie(self)
self.redirect( self.redirect(
@@ -147,6 +149,10 @@ class OAuthCallbackHandlerMixin(HubOAuthCallbackHandler):
def hub_auth(self): def hub_auth(self):
return self.settings['hub_auth'] return self.settings['hub_auth']
@allow_unauthenticated
async def get(self):
return await super().get()
# register new hub related command-line aliases # register new hub related command-line aliases
aliases = { aliases = {

View File

@@ -2,6 +2,7 @@
import os import os
import sys import sys
import warnings
from contextlib import nullcontext from contextlib import nullcontext
from pathlib import Path from pathlib import Path
from pprint import pprint from pprint import pprint
@@ -291,6 +292,57 @@ async def test_notebook_dir(
raise ValueError(f"No contents check for {notebook_dir=}") raise ValueError(f"No contents check for {notebook_dir=}")
@pytest.mark.parametrize("extension", [True, False])
@pytest.mark.skipif(IS_JUPYVERSE, reason="jupyverse has no auth configuration")
async def test_forbid_unauthenticated_access(
request, app, tmp_path, user, full_spawn, extension
):
try:
from jupyter_server.auth.decorator import allow_unauthenticated # noqa
except ImportError:
pytest.skip("needs jupyter-server 2.13")
from jupyter_server.utils import JupyterServerAuthWarning
# login, start the server
cookies = await app.login_user('nandy')
s = AsyncSession()
s.cookies = cookies
user = app.users['nandy']
# stop spawner, if running:
if user.running:
await user.stop()
# start with new config:
user.spawner.default_url = "/jupyterhub-test-info"
if extension:
user.spawner.environment["JUPYTERHUB_SINGLEUSER_EXTENSION"] = "1"
else:
user.spawner.environment["JUPYTERHUB_SINGLEUSER_EXTENSION"] = "0"
# make sure it's resolved to start
tmp_path = tmp_path.resolve()
real_home_dir = tmp_path / "realhome"
real_home_dir.mkdir()
# make symlink to test resolution
home_dir = tmp_path / "home"
home_dir.symlink_to(real_home_dir)
# home_dir is defined on SimpleSpawner
user.spawner.home_dir = str(home_dir)
jupyter_config_dir = home_dir / ".jupyter"
jupyter_config_dir.mkdir()
# verify config paths
with (jupyter_config_dir / "jupyter_server_config.py").open("w") as f:
f.write("c.ServerApp.allow_unauthenticated_access = False")
# If there are core endpoints (added by jupyterhub) without decorators,
# spawn will error out. If there are extension endpoints without decorators
# these will be logged as warnings.
with warnings.catch_warnings():
warnings.simplefilter("error", JupyterServerAuthWarning)
await user.spawn()
@pytest.mark.skipif(IS_JUPYVERSE, reason="jupyverse has no --help-all") @pytest.mark.skipif(IS_JUPYVERSE, reason="jupyverse has no --help-all")
def test_help_output(): def test_help_output():
out = check_output( out = check_output(