mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-16 14:33:00 +00:00
Merge pull request #4779 from krassowski/support-allow_unauthenticated_access-false
Support forbidding unauthenticated access (`allow_unauthenticated_access = False`)
This commit is contained in:
14
jupyterhub/singleuser/_decorator.py
Normal file
14
jupyterhub/singleuser/_decorator.py
Normal 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"]
|
@@ -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
|
||||||
|
@@ -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 = {
|
||||||
|
@@ -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(
|
||||||
|
Reference in New Issue
Block a user