Skip to content

Commit fdfaba4

Browse files
minrkZsailer
authored andcommitted
allow get_user to be async
careful to deprecate overridden get_current_user without ignoring auth Needs some changes due to early steps that are called before prepare, but must now be moved to prepare due to the reliance on auth info. - setting CORS headers (set_default_headers) - check_xsrf_cookie - check_origin now that get_user is async, we have to re-run these bits in prepare after user is authenticated
1 parent d1e061e commit fdfaba4

File tree

5 files changed

+66
-16
lines changed

5 files changed

+66
-16
lines changed

jupyter_server/auth/identity.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ class IdentityProvider(LoggingConfigurable):
9595
"""
9696
Interface for providing identity
9797
98+
_may_ be a coroutine.
99+
98100
Two principle methods:
99101
100102
- :meth:`~.IdentityProvider.get_user` returns a :class:`~.User` object

jupyter_server/auth/login.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ def is_token_authenticated(cls, handler):
152152
"""
153153
if getattr(handler, "_user_id", None) is None:
154154
# ensure get_user has been called, so we know if we're token-authenticated
155-
handler.get_current_user()
155+
handler.current_user
156156
return getattr(handler, "_token_authenticated", False)
157157

158158
@classmethod

jupyter_server/base/handlers.py

Lines changed: 61 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# Distributed under the terms of the Modified BSD License.
44
import datetime
55
import functools
6+
import inspect
67
import ipaddress
78
import json
89
import mimetypes
@@ -134,7 +135,21 @@ def clear_login_cookie(self):
134135
self.force_clear_cookie(self.cookie_name)
135136

136137
def get_current_user(self):
137-
return self.identity_provider.get_user(self)
138+
clsname = self.__class__.__name__
139+
msg = (
140+
f"Calling `{clsname}.get_current_user()` directly is deprecated in jupyter-server 2.0."
141+
" Use `self.current_user` instead (works in all versions)."
142+
)
143+
if hasattr(self, "_jupyter_current_user"):
144+
# backward-compat: return _jupyter_current_user
145+
warnings.warn(
146+
msg,
147+
DeprecationWarning,
148+
stacklevel=2,
149+
)
150+
return self._jupyter_current_user
151+
# haven't called get_user in prepare, raise
152+
raise RuntimeError(msg)
138153

139154
def skip_check_origin(self):
140155
"""Ask my login_handler if I should skip the origin_check
@@ -164,7 +179,7 @@ def cookie_name(self):
164179
@property
165180
def logged_in(self):
166181
"""Is a user currently logged in?"""
167-
user = self.get_current_user()
182+
user = self.current_user
168183
return user and not user == "anonymous"
169184

170185
@property
@@ -346,6 +361,13 @@ def allow_credentials(self):
346361
def set_default_headers(self):
347362
"""Add CORS headers, if defined"""
348363
super().set_default_headers()
364+
365+
def set_cors_headers(self):
366+
"""Add CORS headers, if defined
367+
368+
Now that current_user is async (jupyter-server 2.0),
369+
must be called at the end of prepare(), instead of in set_default_headers.
370+
"""
349371
if self.allow_origin:
350372
self.set_header("Access-Control-Allow-Origin", self.allow_origin)
351373
elif self.allow_origin_pat:
@@ -484,6 +506,9 @@ def check_referer(self):
484506

485507
def check_xsrf_cookie(self):
486508
"""Bypass xsrf cookie checks when token-authenticated"""
509+
if not hasattr(self, "_jupyter_current_user"):
510+
# Called too early, will be checked later
511+
return
487512
if self.token_authenticated or self.settings.get("disable_check_xsrf", False):
488513
# Token-authenticated requests do not need additional XSRF-check
489514
# Servers without authentication are vulnerable to XSRF
@@ -543,9 +568,40 @@ def check_host(self):
543568
)
544569
return allow
545570

546-
def prepare(self):
571+
async def prepare(self):
547572
if not self.check_host():
548573
raise web.HTTPError(403)
574+
575+
from jupyter_server.auth import IdentityProvider
576+
577+
if (
578+
type(self.identity_provider) is IdentityProvider
579+
and inspect.getmodule(self.get_current_user).__name__ != __name__
580+
):
581+
# check for overridden get_current_user + default IdentityProvider
582+
# deprecated way to override auth (e.g. JupyterHub < 3.0)
583+
# allow deprecated, overridden get_current_user
584+
warnings.warn(
585+
"Overriding JupyterHandler.get_current_user is deprecated in jupyter-server 2.0."
586+
" Use an IdentityProvider class.",
587+
DeprecationWarning,
588+
# stacklevel not useful here
589+
)
590+
user = self.get_current_user()
591+
else:
592+
user = self.identity_provider.get_user(self)
593+
if inspect.isawaitable(user):
594+
# IdentityProvider.get_user _may_ be async
595+
user = await user
596+
597+
# self.current_user for tornado's @web.authenticated
598+
# self._jupyter_current_user for backward-compat in deprecated get_current_user calls
599+
# and our own private checks for whether .current_user has been set
600+
self.current_user = self._jupyter_current_user = user
601+
# complete initial steps which require auth to resolve first:
602+
self.set_cors_headers()
603+
if self.request.method not in {"GET", "HEAD", "OPTIONS"}:
604+
self.check_xsrf_cookie()
549605
return super().prepare()
550606

551607
# ---------------------------------------------------------------
@@ -638,10 +694,10 @@ def write_error(self, status_code, **kwargs):
638694
class APIHandler(JupyterHandler):
639695
"""Base class for API handlers"""
640696

641-
def prepare(self):
697+
async def prepare(self):
698+
await super().prepare()
642699
if not self.check_origin():
643700
raise web.HTTPError(404)
644-
return super().prepare()
645701

646702
def write_error(self, status_code, **kwargs):
647703
"""APIHandler errors are JSON, not human pages"""
@@ -663,14 +719,6 @@ def write_error(self, status_code, **kwargs):
663719
self.log.warning(reply["message"])
664720
self.finish(json.dumps(reply))
665721

666-
def get_current_user(self):
667-
"""Raise 403 on API handlers instead of redirecting to human login page"""
668-
# preserve _user_cache so we don't raise more than once
669-
if hasattr(self, "_user_cache"):
670-
return self._user_cache
671-
self._user_cache = user = super().get_current_user()
672-
return user
673-
674722
def get_login_url(self):
675723
# if get_login_url is invoked in an API handler,
676724
# that means @web.authenticated is trying to trigger a redirect.

jupyter_server/base/zmqhandlers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@ def pre_get(self):
313313
the websocket finishes completing.
314314
"""
315315
# authenticate the request before opening the websocket
316-
user = self.get_current_user()
316+
user = self.current_user
317317
if user is None:
318318
self.log.warning("Couldn't authenticate WebSocket connection")
319319
raise web.HTTPError(403)

jupyter_server/gateway/handlers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def authenticate(self):
4848
the websocket finishes completing.
4949
"""
5050
# authenticate the request before opening the websocket
51-
if self.get_current_user() is None:
51+
if self.current_user is None:
5252
self.log.warning("Couldn't authenticate WebSocket connection")
5353
raise web.HTTPError(403)
5454

0 commit comments

Comments
 (0)