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
This commit is contained in:
Min RK
2015-01-27 17:04:30 -08:00
parent 6dc65d55a5
commit 6b9f73ba1f
8 changed files with 72 additions and 5 deletions

View File

@@ -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),
]

View File

@@ -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,

View File

@@ -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'],

View File

@@ -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)

View File

@@ -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) {
@@ -32,6 +34,23 @@ 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);

View File

@@ -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});

View File

@@ -15,10 +15,13 @@
<tr class="user-row" data-user="{{u.name}}" data-admin="{{u.admin}}">
<td class="name-col col-sm-2">{{u.name}}</td>
<td class="admin-col col-sm-2">{% if u.admin %}admin{% endif %}</td>
<td class="time-col col-sm-4">{{u.last_activity.isoformat() + 'Z'}}</td>
<td class="server-col col-sm-2 text-center">
<td class="time-col col-sm-3">{{u.last_activity.isoformat() + 'Z'}}</td>
<td class="server-col col-sm-3 text-center">
{% if u.server %}
<span class="stop-server btn btn-xs btn-danger">stop server</span>
{% if admin_access %}
<span class="access-server btn btn-xs btn-success">access server</span>
{% endif %}
{% else %}
<span class="start-server btn btn-xs btn-success">start server</span>
{% endif %}

View File

@@ -54,6 +54,7 @@
<script type="text/javascript">
window.jhdata = {
base_url: "{{base_url}}",
prefix: "{{prefix}}",
{% if user %}
user: "{{user.name}}",
{% endif %}