diff --git a/jupyterhub/apihandlers/users.py b/jupyterhub/apihandlers/users.py index 990fbdcc..9fea47bd 100644 --- a/jupyterhub/apihandlers/users.py +++ b/jupyterhub/apihandlers/users.py @@ -8,7 +8,7 @@ import json from tornado import gen, web from .. import orm -from ..utils import admin_only, authenticated_403 +from ..utils import admin_only from .base import APIHandler class BaseUserHandler(APIHandler): @@ -159,8 +159,30 @@ class UserServerAPIHandler(BaseUserHandler): status = 202 if user.stop_pending else 204 self.set_status(status) +class UserAdminAccessAPIHandler(BaseUserHandler): + """Grant admins access to single-user servers + + This handler sets the necessary cookie for an admin to login to a single-user server. + """ + @admin_only + def post(self, name): + current = self.get_current_user() + self.log.warn("Admin user %s has requested access to %s's server", + current.name, name, + ) + if not self.settings.get('admin_access', False): + raise web.HTTPError(403, "admin access to user servers disabled") + user = self.find_user(name) + if user is None: + raise web.HTTPError(404) + if user.server is None: + raise web.HTTPError(400, "%s has no server running" % name) + self.set_server_cookie(user) + + default_handlers = [ (r"/api/users", UserListAPIHandler), (r"/api/users/([^/]+)", UserAPIHandler), (r"/api/users/([^/]+)/server", UserServerAPIHandler), + (r"/api/users/([^/]+)/admin-access", UserAdminAccessAPIHandler), ] diff --git a/jupyterhub/app.py b/jupyterhub/app.py index d71f5635..23fab837 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -322,6 +322,12 @@ class JupyterHub(Application): db = Any() session_factory = Any() + admin_access = Bool(False, config=True, + help="""Grant admin users permission to access single-user servers. + + Users should be properly informed if this is enabled. + """ + ) admin_users = Set(config=True, help="""set of usernames of admin users @@ -717,6 +723,7 @@ class JupyterHub(Application): proxy=self.proxy, hub=self.hub, admin_users=self.admin_users, + admin_access=self.admin_access, authenticator=self.authenticator, spawner_class=self.spawner_class, base_url=self.base_url, diff --git a/jupyterhub/handlers/base.py b/jupyterhub/handlers/base.py index a1561fa1..8d784775 100644 --- a/jupyterhub/handlers/base.py +++ b/jupyterhub/handlers/base.py @@ -131,23 +131,31 @@ class BaseHandler(RequestHandler): if user and user.server: self.clear_cookie(user.server.cookie_name, path=user.server.base_url) self.clear_cookie(self.hub.server.cookie_name, path=self.hub.server.base_url) - + + def set_server_cookie(self, user): + """set the login cookie for the single-user server""" + self.set_secure_cookie( + user.server.cookie_name, + user.cookie_id, + path=user.server.base_url, + ) + + def set_hub_cookie(self, user): + """set the login cookie for the Hub""" + self.set_secure_cookie( + self.hub.server.cookie_name, + user.cookie_id, + path=self.hub.server.base_url) + def set_login_cookie(self, user): """Set login cookies for the Hub and single-user server.""" # create and set a new cookie token for the single-user server if user.server: - self.set_secure_cookie( - user.server.cookie_name, - user.cookie_id, - path=user.server.base_url, - ) + self.set_server_cookie(user) # create and set a new cookie token for the hub if not self.get_current_user_cookie(): - self.set_secure_cookie( - self.hub.server.cookie_name, - user.cookie_id, - path=self.hub.server.base_url) + self.set_hub_cookie(user) @gen.coroutine def authenticate(self, data): @@ -278,6 +286,7 @@ class BaseHandler(RequestHandler): user = self.get_current_user() return dict( base_url=self.hub.server.base_url, + prefix=self.base_url, user=user, login_url=self.settings['login_url'], logout_url=self.settings['logout_url'], diff --git a/jupyterhub/handlers/pages.py b/jupyterhub/handlers/pages.py index b5792946..b1cc258a 100644 --- a/jupyterhub/handlers/pages.py +++ b/jupyterhub/handlers/pages.py @@ -48,6 +48,7 @@ class AdminHandler(BaseHandler): html = self.render_template('admin.html', user=self.get_current_user(), users=self.db.query(orm.User), + admin_access=self.settings.get('admin_access', False), ) self.finish(html) diff --git a/share/jupyter/hub/static/js/admin.js b/share/jupyter/hub/static/js/admin.js index e76e9c79..22390345 100644 --- a/share/jupyter/hub/static/js/admin.js +++ b/share/jupyter/hub/static/js/admin.js @@ -1,10 +1,12 @@ // Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. -require(["jquery", "bootstrap", "moment", "jhapi"], function ($, bs, moment, JHAPI) { +require(["jquery", "bootstrap", "moment", "jhapi", "utils"], function ($, bs, moment, JHAPI, utils) { "use strict"; var base_url = window.jhdata.base_url; + var prefix = window.jhdata.prefix; + var api = new JHAPI(base_url); var get_row = function (element) { @@ -31,7 +33,24 @@ require(["jquery", "bootstrap", "moment", "jhapi"], function ($, bs, moment, JHA } }); }); - + + $(".access-server").click(function () { + var el = $(this); + var row = get_row(el); + var user = row.data('user'); + var w = window.open(); + api.admin_access(user, { + async: false, + success: function () { + w.location = utils.url_path_join(prefix, 'user', user); + }, + error: function (xhr, err) { + w.close(); + console.error("Failed to gain access to server", err); + } + }); + }); + $(".start-server").click(function () { var el = $(this); var row = get_row(el); diff --git a/share/jupyter/hub/static/js/jhapi.js b/share/jupyter/hub/static/js/jhapi.js index 1e7cf0f1..4bdbf63f 100644 --- a/share/jupyter/hub/static/js/jhapi.js +++ b/share/jupyter/hub/static/js/jhapi.js @@ -100,6 +100,19 @@ define(['jquery', 'utils'], function ($, utils) { ); }; + JHAPI.prototype.admin_access = function (user, options) { + options = options || {}; + options = update(options, { + type: 'POST', + dataType: null, + }); + + this.api_request( + utils.url_path_join('users', user, 'admin-access'), + options + ); + }; + JHAPI.prototype.delete_user = function (user, options) { options = options || {}; options = update(options, {type: 'DELETE', dataType: null}); diff --git a/share/jupyter/hub/templates/admin.html b/share/jupyter/hub/templates/admin.html index 2142cb3a..43f78dfa 100644 --- a/share/jupyter/hub/templates/admin.html +++ b/share/jupyter/hub/templates/admin.html @@ -15,10 +15,13 @@ {{u.name}} {% if u.admin %}admin{% endif %} - {{u.last_activity.isoformat() + 'Z'}} - + {{u.last_activity.isoformat() + 'Z'}} + {% if u.server %} stop server + {% if admin_access %} + access server + {% endif %} {% else %} start server {% endif %} diff --git a/share/jupyter/hub/templates/page.html b/share/jupyter/hub/templates/page.html index 014efea1..a84c3d46 100644 --- a/share/jupyter/hub/templates/page.html +++ b/share/jupyter/hub/templates/page.html @@ -57,6 +57,7 @@