From 5f626268effd6925793fa9b5effa01efd9622cd2 Mon Sep 17 00:00:00 2001 From: Min RK Date: Thu, 9 Jan 2020 13:04:45 +0100 Subject: [PATCH 1/3] trigger auth_state_hook prior to options form - allow auth_state_hook to be async - trigger it prior to start and options_form serving, rather than on home page --- jupyterhub/handlers/pages.py | 10 +++++++--- jupyterhub/spawner.py | 4 ++-- jupyterhub/user.py | 7 ++++++- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/jupyterhub/handlers/pages.py b/jupyterhub/handlers/pages.py index 734c257d..e0a7ddfb 100644 --- a/jupyterhub/handlers/pages.py +++ b/jupyterhub/handlers/pages.py @@ -67,9 +67,6 @@ class HomeHandler(BaseHandler): else: url = url_path_join(self.hub.base_url, 'spawn', user.escaped_name) - auth_state = await user.get_auth_state() - user.spawner.run_auth_state_hook(auth_state) - html = self.render_template( 'home.html', user=user, @@ -149,6 +146,7 @@ class SpawnHandler(BaseHandler): server_name = '' spawner = user.spawners[server_name] + # resolve `?next=...`, falling back on the spawn-pending url # must not be /user/server for named servers, # which may get handled by the default server if they aren't ready yet @@ -175,6 +173,12 @@ class SpawnHandler(BaseHandler): # Add handler to spawner here so you can access query params in form rendering. spawner.handler = self + + # auth_state may be an input to options form, + # so resolve the auth state hook here + auth_state = await user.get_auth_state() + await spawner.run_auth_state_hook(auth_state) + spawner_options_form = await spawner.get_options_form() if spawner_options_form: self.log.debug("Serving options form for %s", spawner._log_name) diff --git a/jupyterhub/spawner.py b/jupyterhub/spawner.py index 4639cb69..438fe7d1 100644 --- a/jupyterhub/spawner.py +++ b/jupyterhub/spawner.py @@ -972,11 +972,11 @@ class Spawner(LoggingConfigurable): except Exception: self.log.exception("post_stop_hook failed with exception: %s", self) - def run_auth_state_hook(self, auth_state): + async def run_auth_state_hook(self, auth_state): """Run the auth_state_hook if defined""" if self.auth_state_hook is not None: try: - return self.auth_state_hook(self, auth_state) + await maybe_future(self.auth_state_hook(self, auth_state)) except Exception: self.log.exception("auth_stop_hook failed with exception: %s", self) diff --git a/jupyterhub/user.py b/jupyterhub/user.py index 4fca536a..d6419639 100644 --- a/jupyterhub/user.py +++ b/jupyterhub/user.py @@ -535,12 +535,17 @@ class User: # trigger pre-spawn hook on authenticator authenticator = self.authenticator try: + spawner._start_pending = True + if authenticator: # pre_spawn_start can thow errors that can lead to a redirect loop # if left uncaught (see https://github.com/jupyterhub/jupyterhub/issues/2683) await maybe_future(authenticator.pre_spawn_start(self, spawner)) - spawner._start_pending = True + # trigger auth_state hook + auth_state = await self.get_auth_state() + await spawner.run_auth_state_hook(auth_state) + # update spawner start time, and activity for both spawner and user self.last_activity = ( spawner.orm_spawner.started From b80906b8c8c8598fdc3ed57722723d522a9dbd3f Mon Sep 17 00:00:00 2001 From: Min RK Date: Fri, 17 Jan 2020 10:54:24 +0100 Subject: [PATCH 2/3] make auth_state available to page templates --- jupyterhub/apihandlers/auth.py | 10 ++++++++-- jupyterhub/handlers/base.py | 7 ++++++- jupyterhub/handlers/pages.py | 28 +++++++++++++++++++++++----- 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/jupyterhub/apihandlers/auth.py b/jupyterhub/apihandlers/auth.py index cf1eba14..15a4d783 100644 --- a/jupyterhub/apihandlers/auth.py +++ b/jupyterhub/apihandlers/auth.py @@ -223,7 +223,7 @@ class OAuthAuthorizeHandler(OAuthHandler, BaseHandler): return True @web.authenticated - def get(self): + async def get(self): """GET /oauth/authorization Render oauth confirmation page: @@ -251,8 +251,14 @@ class OAuthAuthorizeHandler(OAuthHandler, BaseHandler): return # Render oauth 'Authorize application...' page + auth_state = await self.current_user.get_auth_state() self.write( - self.render_template("oauth.html", scopes=scopes, oauth_client=client) + self.render_template( + "oauth.html", + auth_state=auth_state, + scopes=scopes, + oauth_client=client, + ) ) # Errors that should be shown to the user on the provider website diff --git a/jupyterhub/handlers/base.py b/jupyterhub/handlers/base.py index bf9f877f..730a6bdd 100644 --- a/jupyterhub/handlers/base.py +++ b/jupyterhub/handlers/base.py @@ -1431,8 +1431,13 @@ class UserUrlHandler(BaseHandler): url_path_join(self.hub.base_url, "spawn", user.escaped_name, server_name), {"next": self.request.uri}, ) + auth_state = await user.get_auth_state() html = self.render_template( - "not_running.html", user=user, server_name=server_name, spawn_url=spawn_url + "not_running.html", + user=user, + server_name=server_name, + spawn_url=spawn_url, + auth_state=auth_state, ) self.finish(html) diff --git a/jupyterhub/handlers/pages.py b/jupyterhub/handlers/pages.py index e0a7ddfb..196ed8a1 100644 --- a/jupyterhub/handlers/pages.py +++ b/jupyterhub/handlers/pages.py @@ -67,8 +67,10 @@ class HomeHandler(BaseHandler): else: url = url_path_join(self.hub.base_url, 'spawn', user.escaped_name) + auth_state = await user.get_auth_state() html = self.render_template( 'home.html', + auth_state=auth_state, user=user, url=url, allow_named_servers=self.allow_named_servers, @@ -91,10 +93,12 @@ class SpawnHandler(BaseHandler): default_url = None - def _render_form(self, for_user, spawner_options_form, message=''): + async def _render_form(self, for_user, spawner_options_form, message=''): + auth_state = await for_user.get_auth_state() return self.render_template( 'spawn.html', for_user=for_user, + auth_state=auth_state, spawner_options_form=spawner_options_form, error_message=message, url=self.request.uri, @@ -305,6 +309,8 @@ class SpawnPendingHandler(BaseHandler): # if spawning fails for any reason, point users to /hub/home to retry self.extra_error_html = self.spawn_home_error + auth_state = await user.get_auth_state() + # First, check for previous failure. if ( not spawner.active @@ -324,6 +330,7 @@ class SpawnPendingHandler(BaseHandler): html = self.render_template( "not_running.html", user=user, + auth_state=auth_state, server_name=server_name, spawn_url=spawn_url, failed=True, @@ -345,7 +352,11 @@ class SpawnPendingHandler(BaseHandler): else: page = "spawn_pending.html" html = self.render_template( - page, user=user, spawner=spawner, progress_url=spawner._progress_url + page, + user=user, + spawner=spawner, + progress_url=spawner._progress_url, + auth_state=auth_state, ) self.finish(html) return @@ -370,6 +381,7 @@ class SpawnPendingHandler(BaseHandler): html = self.render_template( "not_running.html", user=user, + auth_state=auth_state, server_name=server_name, spawn_url=spawn_url, ) @@ -389,7 +401,7 @@ class AdminHandler(BaseHandler): @web.authenticated @admin_only - def get(self): + async def get(self): available = {'name', 'admin', 'running', 'last_activity'} default_sort = ['admin', 'name'] mapping = {'running': orm.Spawner.server_id} @@ -438,9 +450,11 @@ class AdminHandler(BaseHandler): for u in users: running.extend(s for s in u.spawners.values() if s.active) + auth_state = await self.current_user.get_auth_state() html = self.render_template( 'admin.html', current_user=self.current_user, + auth_state=auth_state, admin_access=self.settings.get('admin_access', False), users=users, running=running, @@ -456,7 +470,7 @@ class TokenPageHandler(BaseHandler): """Handler for page requesting new API tokens""" @web.authenticated - def get(self): + async def get(self): never = datetime(1900, 1, 1) user = self.current_user @@ -525,8 +539,12 @@ class TokenPageHandler(BaseHandler): oauth_clients = sorted(oauth_clients, key=sort_key, reverse=True) + auth_state = await self.current_user.get_auth_state() html = self.render_template( - 'token.html', api_tokens=api_tokens, oauth_clients=oauth_clients + 'token.html', + api_tokens=api_tokens, + oauth_clients=oauth_clients, + auth_state=auth_state, ) self.finish(html) From 58f18bffff52bb904986023f873e9fc5d47c7bba Mon Sep 17 00:00:00 2001 From: Min RK Date: Fri, 17 Jan 2020 12:08:20 +0100 Subject: [PATCH 3/3] _render_form is async --- jupyterhub/handlers/pages.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jupyterhub/handlers/pages.py b/jupyterhub/handlers/pages.py index 196ed8a1..3ca03653 100644 --- a/jupyterhub/handlers/pages.py +++ b/jupyterhub/handlers/pages.py @@ -186,7 +186,7 @@ class SpawnHandler(BaseHandler): spawner_options_form = await spawner.get_options_form() if spawner_options_form: self.log.debug("Serving options form for %s", spawner._log_name) - form = self._render_form( + form = await self._render_form( for_user=user, spawner_options_form=spawner_options_form ) self.finish(form) @@ -248,7 +248,7 @@ class SpawnHandler(BaseHandler): "Failed to spawn single-user server with form", exc_info=True ) spawner_options_form = await user.spawner.get_options_form() - form = self._render_form( + form = await self._render_form( for_user=user, spawner_options_form=spawner_options_form, message=str(e) ) self.finish(form)