From 6dc65d55a5c1f88ad39259d13da0a6bd76d67076 Mon Sep 17 00:00:00 2001 From: Min RK Date: Tue, 27 Jan 2015 16:32:15 -0800 Subject: [PATCH 1/2] split cookie setting into sub-methods so they can be reused --- jupyterhub/handlers/base.py | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/jupyterhub/handlers/base.py b/jupyterhub/handlers/base.py index 3c401a54..d8620f8c 100644 --- a/jupyterhub/handlers/base.py +++ b/jupyterhub/handlers/base.py @@ -127,23 +127,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): From 6b9f73ba1fd5cd42fe47287018613e986ef8d554 Mon Sep 17 00:00:00 2001 From: Min RK Date: Tue, 27 Jan 2015 17:04:30 -0800 Subject: [PATCH 2/2] add JupyterHub.admin_access optionally allow admin users to login to user servers by visiting a special admin-only URL that sets the relevant cookie - disabled by default - an 'access server' button is added to the admin panel, which sets the necessary cookie to log in to the user server --- jupyterhub/apihandlers/users.py | 24 +++++++++++++++++++++++- jupyterhub/app.py | 7 +++++++ jupyterhub/handlers/base.py | 1 + jupyterhub/handlers/pages.py | 1 + share/jupyter/hub/static/js/admin.js | 23 +++++++++++++++++++++-- share/jupyter/hub/static/js/jhapi.js | 13 +++++++++++++ share/jupyter/hub/templates/admin.html | 7 +++++-- share/jupyter/hub/templates/page.html | 1 + 8 files changed, 72 insertions(+), 5 deletions(-) diff --git a/jupyterhub/apihandlers/users.py b/jupyterhub/apihandlers/users.py index 6ff84529..caf089eb 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): @@ -147,8 +147,30 @@ class UserServerAPIHandler(BaseUserHandler): yield self.stop_single_user(user) self.set_status(204) +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 57f95b7a..00df1099 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -321,6 +321,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 @@ -708,6 +714,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 d8620f8c..376653d9 100644 --- a/jupyterhub/handlers/base.py +++ b/jupyterhub/handlers/base.py @@ -244,6 +244,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 4088116d..68947ded 100644 --- a/share/jupyter/hub/templates/page.html +++ b/share/jupyter/hub/templates/page.html @@ -54,6 +54,7 @@