mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-16 06:22:59 +00:00
flesh out REST API
can now list/view/add/create/modify users and start/stop single-user servers
This commit is contained in:
@@ -1,7 +1,9 @@
|
|||||||
from . import auth, users
|
from .base import *
|
||||||
from .auth import *
|
from .auth import *
|
||||||
from .users import *
|
from .users import *
|
||||||
|
|
||||||
|
from . import auth, users
|
||||||
|
|
||||||
default_handlers = []
|
default_handlers = []
|
||||||
for mod in (auth, users):
|
for mod in (auth, users):
|
||||||
default_handlers.extend(mod.default_handlers)
|
default_handlers.extend(mod.default_handlers)
|
||||||
|
@@ -6,12 +6,13 @@
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
from tornado import web
|
from tornado import web
|
||||||
from ..handlers import BaseHandler
|
|
||||||
from .. import orm
|
from .. import orm
|
||||||
from ..utils import token_authenticated
|
from ..utils import token_authenticated
|
||||||
|
from .base import APIHandler
|
||||||
|
|
||||||
|
|
||||||
class AuthorizationsAPIHandler(BaseHandler):
|
|
||||||
|
class AuthorizationsAPIHandler(APIHandler):
|
||||||
@token_authenticated
|
@token_authenticated
|
||||||
def get(self, token):
|
def get(self, token):
|
||||||
orm_token = self.db.query(orm.CookieToken).filter(orm.CookieToken.token == token).first()
|
orm_token = self.db.query(orm.CookieToken).filter(orm.CookieToken.token == token).first()
|
||||||
|
53
jupyterhub/apihandlers/base.py
Normal file
53
jupyterhub/apihandlers/base.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
"""Base API handlers"""
|
||||||
|
# Copyright (c) Jupyter Development Team.
|
||||||
|
# Distributed under the terms of the Modified BSD License.
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
try:
|
||||||
|
# py3
|
||||||
|
from http.client import responses
|
||||||
|
except ImportError:
|
||||||
|
from httplib import responses
|
||||||
|
|
||||||
|
from tornado import web
|
||||||
|
|
||||||
|
from ..handlers import BaseHandler
|
||||||
|
|
||||||
|
class APIHandler(BaseHandler):
|
||||||
|
def get_json_body(self):
|
||||||
|
"""Return the body of the request as JSON data."""
|
||||||
|
if not self.request.body:
|
||||||
|
return None
|
||||||
|
body = self.request.body.strip().decode(u'utf-8')
|
||||||
|
try:
|
||||||
|
model = json.loads(body)
|
||||||
|
except Exception:
|
||||||
|
self.log.debug("Bad JSON: %r", body)
|
||||||
|
self.log.error("Couldn't parse JSON", exc_info=True)
|
||||||
|
raise web.HTTPError(400, 'Invalid JSON in body of request')
|
||||||
|
return model
|
||||||
|
|
||||||
|
|
||||||
|
def write_error(self, status_code, **kwargs):
|
||||||
|
"""Write JSON errors instead of HTML"""
|
||||||
|
exc_info = kwargs.get('exc_info')
|
||||||
|
message = ''
|
||||||
|
status_message = responses.get(status_code, 'Unknown Error')
|
||||||
|
if exc_info:
|
||||||
|
exception = exc_info[1]
|
||||||
|
# get the custom message, if defined
|
||||||
|
try:
|
||||||
|
message = exception.log_message % exception.args
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# construct the custom reason, if defined
|
||||||
|
reason = getattr(exception, 'reason', '')
|
||||||
|
if reason:
|
||||||
|
status_message = reason
|
||||||
|
|
||||||
|
self.write(json.dumps({
|
||||||
|
'status': status_code,
|
||||||
|
'message': message or status_message,
|
||||||
|
}))
|
@@ -5,25 +5,132 @@
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from ..handlers import BaseHandler
|
from tornado import gen, web
|
||||||
|
|
||||||
from .. import orm
|
from .. import orm
|
||||||
from ..utils import admin_only
|
from ..utils import admin_only, authenticated_403
|
||||||
|
from .base import APIHandler
|
||||||
|
|
||||||
|
try:
|
||||||
|
basestring
|
||||||
|
except NameError:
|
||||||
|
basestring = str # py3
|
||||||
|
|
||||||
class UserListAPIHandler(BaseHandler):
|
class BaseUserHandler(APIHandler):
|
||||||
|
|
||||||
|
def user_model(self, user):
|
||||||
|
return {
|
||||||
|
'name': user.name,
|
||||||
|
'admin': user.admin,
|
||||||
|
'server': user.server.base_url if user.server else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
_model_types = {
|
||||||
|
'name': basestring,
|
||||||
|
'admin': bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _check_user_model(self, model):
|
||||||
|
if not isinstance(model, dict):
|
||||||
|
raise web.HTTPError(400, "Invalid JSON data: %r" % model)
|
||||||
|
if not set(model).issubset(set(self._model_types)):
|
||||||
|
raise web.HTTPError(400, "Invalid JSON keys: %r" % model)
|
||||||
|
for key, value in model.items():
|
||||||
|
if not isinstance(value, self._model_types[key]):
|
||||||
|
raise web.HTTPError(400, "user.%s must be %s, not: %r" % (
|
||||||
|
key, self._model_types[key], type(value)
|
||||||
|
))
|
||||||
|
|
||||||
|
class UserListAPIHandler(BaseUserHandler):
|
||||||
@admin_only
|
@admin_only
|
||||||
def get(self):
|
def get(self):
|
||||||
users = list(self.db.query(orm.User))
|
users = self.db.query(orm.User)
|
||||||
|
data = [ self.user_model(u) for u in users ]
|
||||||
data = []
|
|
||||||
for user in users:
|
|
||||||
data.append({
|
|
||||||
'name': user.name,
|
|
||||||
'server': user.server.base_url if user.server else None,
|
|
||||||
})
|
|
||||||
|
|
||||||
self.write(json.dumps(data))
|
self.write(json.dumps(data))
|
||||||
|
|
||||||
|
|
||||||
|
def admin_or_self(method):
|
||||||
|
"""Decorator for restricting access to either the target user or admin"""
|
||||||
|
def m(self, name):
|
||||||
|
current = self.get_current_user()
|
||||||
|
if current is None:
|
||||||
|
raise web.HTTPError(403)
|
||||||
|
if not (current.name == name or current.admin):
|
||||||
|
raise web.HTTPError(403)
|
||||||
|
|
||||||
|
# raise 404 if not found
|
||||||
|
if not self.find_user(name):
|
||||||
|
raise web.HTTPError(404)
|
||||||
|
return method(self, name)
|
||||||
|
return m
|
||||||
|
|
||||||
|
class UserAPIHandler(BaseUserHandler):
|
||||||
|
|
||||||
|
@admin_or_self
|
||||||
|
def get(self, name):
|
||||||
|
user = self.find_user(name)
|
||||||
|
self.write(json.dumps(self.user_model(user)))
|
||||||
|
|
||||||
|
@admin_only
|
||||||
|
def post(self, name):
|
||||||
|
data = self.get_json_body()
|
||||||
|
user = self.find_user(name)
|
||||||
|
if user is not None:
|
||||||
|
raise web.HTTPError(400, "User %s already exists" % name)
|
||||||
|
|
||||||
|
user = self.user_from_username(name)
|
||||||
|
if data:
|
||||||
|
self._check_user_model(data)
|
||||||
|
if 'admin' in data:
|
||||||
|
user.admin = data['admin']
|
||||||
|
self.db.commit()
|
||||||
|
self.write(json.dumps(self.user_model(user)))
|
||||||
|
self.set_status(201)
|
||||||
|
|
||||||
|
@admin_only
|
||||||
|
def delete(self, name):
|
||||||
|
user = self.find_user(name)
|
||||||
|
if user is None:
|
||||||
|
raise web.HTTPError(404)
|
||||||
|
if user.name == self.get_current_user().name:
|
||||||
|
raise web.HTTPError(400, "Cannot delete yourself!")
|
||||||
|
self.set_status(204)
|
||||||
|
|
||||||
|
@admin_only
|
||||||
|
def patch(self, name):
|
||||||
|
user = self.find_user(name)
|
||||||
|
if user is None:
|
||||||
|
raise web.HTTPError(404)
|
||||||
|
data = self.get_json_body()
|
||||||
|
self._check_user_model(data)
|
||||||
|
for key, value in data.items():
|
||||||
|
setattr(user, key, value)
|
||||||
|
self.db.commit()
|
||||||
|
self.write(json.dumps(self.user_model(user)))
|
||||||
|
|
||||||
|
|
||||||
|
class UserServerAPIHandler(BaseUserHandler):
|
||||||
|
@gen.coroutine
|
||||||
|
@admin_or_self
|
||||||
|
def post(self, name):
|
||||||
|
user = self.find_user(name)
|
||||||
|
if user.spawner:
|
||||||
|
raise web.HTTPError(400, "%s's server is already running" % name)
|
||||||
|
else:
|
||||||
|
yield self.spawn_single_user(user)
|
||||||
|
self.set_status(201)
|
||||||
|
|
||||||
|
@gen.coroutine
|
||||||
|
@admin_or_self
|
||||||
|
def delete(self, name):
|
||||||
|
user = self.find_user(name)
|
||||||
|
if user.spawner is None:
|
||||||
|
raise web.HTTPError(400, "%s's server is not running" % name)
|
||||||
|
yield self.stop_single_user(user)
|
||||||
|
self.set_status(204)
|
||||||
|
|
||||||
default_handlers = [
|
default_handlers = [
|
||||||
(r"/api/users", UserListAPIHandler),
|
(r"/api/users", UserListAPIHandler),
|
||||||
|
(r"/api/users/([^/]+)", UserAPIHandler),
|
||||||
|
(r"/api/users/([^/]+)/server", UserServerAPIHandler),
|
||||||
]
|
]
|
||||||
|
@@ -229,6 +229,10 @@ class JupyterHubApp(Application):
|
|||||||
# TODO: load state from db for resume
|
# TODO: load state from db for resume
|
||||||
# TODO: if not resuming, clear existing db contents
|
# TODO: if not resuming, clear existing db contents
|
||||||
self.db = orm.new_session(self.db_url, echo=self.debug_db)
|
self.db = orm.new_session(self.db_url, echo=self.debug_db)
|
||||||
|
for name in self.admin_users:
|
||||||
|
user = orm.User(name=name, admin=True)
|
||||||
|
self.db.add(user)
|
||||||
|
self.db.commit()
|
||||||
|
|
||||||
def init_hub(self):
|
def init_hub(self):
|
||||||
"""Load the Hub config into the database"""
|
"""Load the Hub config into the database"""
|
||||||
@@ -279,8 +283,7 @@ class JupyterHubApp(Application):
|
|||||||
if self.ssl_cert:
|
if self.ssl_cert:
|
||||||
cmd.extend(['--ssl-cert', self.ssl_cert])
|
cmd.extend(['--ssl-cert', self.ssl_cert])
|
||||||
|
|
||||||
|
self.proxy_process = Popen(cmd, env=env)
|
||||||
self.proxy = Popen(cmd, env=env)
|
|
||||||
|
|
||||||
def init_tornado_settings(self):
|
def init_tornado_settings(self):
|
||||||
"""Set up the tornado settings dict."""
|
"""Set up the tornado settings dict."""
|
||||||
@@ -330,7 +333,7 @@ class JupyterHubApp(Application):
|
|||||||
@gen.coroutine
|
@gen.coroutine
|
||||||
def cleanup(self):
|
def cleanup(self):
|
||||||
self.log.info("Cleaning up proxy...")
|
self.log.info("Cleaning up proxy...")
|
||||||
self.proxy.terminate()
|
self.proxy_process.terminate()
|
||||||
self.log.info("Cleaning up single-user servers...")
|
self.log.info("Cleaning up single-user servers...")
|
||||||
|
|
||||||
# request (async) process termination
|
# request (async) process termination
|
||||||
|
@@ -92,12 +92,18 @@ class BaseHandler(RequestHandler):
|
|||||||
return user
|
return user
|
||||||
return self.get_current_user_cookie()
|
return self.get_current_user_cookie()
|
||||||
|
|
||||||
|
def find_user(self, name):
|
||||||
|
"""Get a user by name
|
||||||
|
|
||||||
|
return None if no such user
|
||||||
|
"""
|
||||||
|
return self.db.query(orm.User).filter(orm.User.name==name).first()
|
||||||
|
|
||||||
def user_from_username(self, username):
|
def user_from_username(self, username):
|
||||||
"""Get ORM User for username"""
|
"""Get ORM User for username"""
|
||||||
user = self.db.query(orm.User).filter(orm.User.name==username).first()
|
user = self.find_user(username)
|
||||||
if user is None:
|
if user is None:
|
||||||
admin = (not self.admin_users) or username in self.admin_users
|
user = orm.User(name=username)
|
||||||
user = orm.User(name=username, admin=admin)
|
|
||||||
self.db.add(user)
|
self.db.add(user)
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
return user
|
return user
|
||||||
@@ -165,6 +171,18 @@ class BaseHandler(RequestHandler):
|
|||||||
yield wait_for_server(user.server.ip, user.server.port)
|
yield wait_for_server(user.server.ip, user.server.port)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
|
|
||||||
|
@gen.coroutine
|
||||||
|
def notify_proxy_delete(self, user):
|
||||||
|
proxy = self.db.query(orm.Proxy).first()
|
||||||
|
r = requests.delete(
|
||||||
|
url_path_join(
|
||||||
|
proxy.api_server.url,
|
||||||
|
user.server.base_url,
|
||||||
|
),
|
||||||
|
headers={'Authorization': "token %s" % proxy.auth_token},
|
||||||
|
)
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
@gen.coroutine
|
@gen.coroutine
|
||||||
def spawn_single_user(self, user):
|
def spawn_single_user(self, user):
|
||||||
user.server = orm.Server(
|
user.server = orm.Server(
|
||||||
@@ -194,6 +212,20 @@ class BaseHandler(RequestHandler):
|
|||||||
self.notify_proxy(user)
|
self.notify_proxy(user)
|
||||||
raise gen.Return(user)
|
raise gen.Return(user)
|
||||||
|
|
||||||
|
@gen.coroutine
|
||||||
|
def stop_single_user(self, user):
|
||||||
|
if user.spawner is None:
|
||||||
|
return
|
||||||
|
status = yield user.spawner.poll()
|
||||||
|
yield user.spawner.stop()
|
||||||
|
self.notify_proxy_delete(user)
|
||||||
|
user.state = {}
|
||||||
|
user.spawner = None
|
||||||
|
user.server = None
|
||||||
|
self.db.commit()
|
||||||
|
|
||||||
|
raise gen.Return(user)
|
||||||
|
|
||||||
#---------------------------------------------------------------
|
#---------------------------------------------------------------
|
||||||
# template rendering
|
# template rendering
|
||||||
#---------------------------------------------------------------
|
#---------------------------------------------------------------
|
||||||
|
@@ -75,7 +75,7 @@ class Server(Base):
|
|||||||
def host(self):
|
def host(self):
|
||||||
return "{proto}://{ip}:{port}".format(
|
return "{proto}://{ip}:{port}".format(
|
||||||
proto=self.proto,
|
proto=self.proto,
|
||||||
ip=self.ip,
|
ip=self.ip or '*',
|
||||||
port=self.port,
|
port=self.port,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -1,12 +1,13 @@
|
|||||||
"""mock utilities for testing"""
|
"""mock utilities for testing"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
except ImportError:
|
except ImportError:
|
||||||
import mock
|
import mock
|
||||||
|
|
||||||
import getpass
|
|
||||||
import threading
|
|
||||||
|
|
||||||
from tornado.ioloop import IOLoop
|
from tornado.ioloop import IOLoop
|
||||||
|
|
||||||
from IPython.utils.py3compat import unicode_type
|
from IPython.utils.py3compat import unicode_type
|
||||||
@@ -30,13 +31,17 @@ def mock_authenticate(username, password, service='login'):
|
|||||||
|
|
||||||
class MockSpawner(LocalProcessSpawner):
|
class MockSpawner(LocalProcessSpawner):
|
||||||
|
|
||||||
def make_preexec_fn(self):
|
def make_preexec_fn(self, *a, **kw):
|
||||||
# skip the setuid stuff
|
# skip the setuid stuff
|
||||||
return
|
return
|
||||||
|
|
||||||
def _set_user_changed(self, name, old, new):
|
def _set_user_changed(self, name, old, new):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def _cmd_default(self):
|
||||||
|
return [sys.executable, '-m', 'jupyterhub.tests.mocksu']
|
||||||
|
|
||||||
|
|
||||||
class MockPAMAuthenticator(PAMAuthenticator):
|
class MockPAMAuthenticator(PAMAuthenticator):
|
||||||
def authenticate(self, *args, **kwargs):
|
def authenticate(self, *args, **kwargs):
|
||||||
with mock.patch('simplepam.authenticate', mock_authenticate):
|
with mock.patch('simplepam.authenticate', mock_authenticate):
|
||||||
@@ -44,14 +49,19 @@ class MockPAMAuthenticator(PAMAuthenticator):
|
|||||||
|
|
||||||
class MockHubApp(JupyterHubApp):
|
class MockHubApp(JupyterHubApp):
|
||||||
"""HubApp with various mock bits"""
|
"""HubApp with various mock bits"""
|
||||||
# def start_proxy(self):
|
|
||||||
# pass
|
def _ip_default(self):
|
||||||
|
return 'localhost'
|
||||||
|
|
||||||
def _authenticator_default(self):
|
def _authenticator_default(self):
|
||||||
return '%s.%s' % (__name__, 'MockPAMAuthenticator')
|
return '%s.%s' % (__name__, 'MockPAMAuthenticator')
|
||||||
|
|
||||||
def _spawner_class_default(self):
|
def _spawner_class_default(self):
|
||||||
return '%s.%s' % (__name__, 'MockSpawner')
|
return '%s.%s' % (__name__, 'MockSpawner')
|
||||||
|
|
||||||
|
def _admin_users_default(self):
|
||||||
|
return {'admin'}
|
||||||
|
|
||||||
def start(self, argv=None):
|
def start(self, argv=None):
|
||||||
evt = threading.Event()
|
evt = threading.Event()
|
||||||
def _start():
|
def _start():
|
||||||
@@ -59,10 +69,8 @@ class MockHubApp(JupyterHubApp):
|
|||||||
# put initialize in start for SQLAlchemy threading reasons
|
# put initialize in start for SQLAlchemy threading reasons
|
||||||
super(MockHubApp, self).initialize(argv=argv)
|
super(MockHubApp, self).initialize(argv=argv)
|
||||||
|
|
||||||
# add some initial users - 1 admin, 1 non-admin
|
# add an initial user
|
||||||
admin = orm.User(name='admin', admin=True)
|
|
||||||
user = orm.User(name='user')
|
user = orm.User(name='user')
|
||||||
self.db.add(admin)
|
|
||||||
self.db.add(user)
|
self.db.add(user)
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
self.io_loop.add_callback(evt.set)
|
self.io_loop.add_callback(evt.set)
|
||||||
|
@@ -1,10 +1,15 @@
|
|||||||
"""Tests for the REST API"""
|
"""Tests for the REST API"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from ..utils import url_path_join as ujoin
|
from ..utils import url_path_join as ujoin
|
||||||
from .. import orm
|
from .. import orm
|
||||||
|
|
||||||
|
def find_user(db, name):
|
||||||
|
return db.query(orm.User).filter(orm.User.name==name).first()
|
||||||
|
|
||||||
def add_user(db, **kwargs):
|
def add_user(db, **kwargs):
|
||||||
user = orm.User(**kwargs)
|
user = orm.User(**kwargs)
|
||||||
db.add(user)
|
db.add(user)
|
||||||
@@ -12,7 +17,7 @@ def add_user(db, **kwargs):
|
|||||||
return user
|
return user
|
||||||
|
|
||||||
def auth_header(db, name):
|
def auth_header(db, name):
|
||||||
user = db.query(orm.User).filter(orm.User.name==name).first()
|
user = find_user(db, name)
|
||||||
if user is None:
|
if user is None:
|
||||||
user = add_user(db, name=name)
|
user = add_user(db, name=name)
|
||||||
if not user.api_tokens:
|
if not user.api_tokens:
|
||||||
@@ -72,10 +77,12 @@ def test_get_users(app):
|
|||||||
assert sorted(r.json(), key=lambda d: d['name']) == [
|
assert sorted(r.json(), key=lambda d: d['name']) == [
|
||||||
{
|
{
|
||||||
'name': 'admin',
|
'name': 'admin',
|
||||||
|
'admin': True,
|
||||||
'server': None,
|
'server': None,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'user',
|
'name': 'user',
|
||||||
|
'admin': False,
|
||||||
'server': None,
|
'server': None,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -84,3 +91,79 @@ def test_get_users(app):
|
|||||||
headers=auth_header(db, 'user'),
|
headers=auth_header(db, 'user'),
|
||||||
)
|
)
|
||||||
assert r.status_code == 403
|
assert r.status_code == 403
|
||||||
|
|
||||||
|
def test_add_user(app):
|
||||||
|
db = app.db
|
||||||
|
name = 'newuser'
|
||||||
|
r = api_request(app, 'users', name, method='post')
|
||||||
|
assert r.status_code == 201
|
||||||
|
user = find_user(db, name)
|
||||||
|
assert user is not None
|
||||||
|
assert user.name == name
|
||||||
|
assert not user.admin
|
||||||
|
|
||||||
|
def test_add_admin(app):
|
||||||
|
db = app.db
|
||||||
|
name = 'newadmin'
|
||||||
|
r = api_request(app, 'users', name, method='post',
|
||||||
|
data=json.dumps({'admin': True}),
|
||||||
|
)
|
||||||
|
assert r.status_code == 201
|
||||||
|
user = find_user(db, name)
|
||||||
|
assert user is not None
|
||||||
|
assert user.name == name
|
||||||
|
assert user.admin
|
||||||
|
|
||||||
|
def test_delete_user(app):
|
||||||
|
db = app.db
|
||||||
|
mal = add_user(db, name='mal')
|
||||||
|
r = api_request(app, 'users', 'mal', method='delete')
|
||||||
|
assert r.status_code == 204
|
||||||
|
|
||||||
|
|
||||||
|
def test_make_admin(app):
|
||||||
|
db = app.db
|
||||||
|
name = 'admin2'
|
||||||
|
r = api_request(app, 'users', name, method='post')
|
||||||
|
assert r.status_code == 201
|
||||||
|
user = find_user(db, name)
|
||||||
|
assert user is not None
|
||||||
|
assert user.name == name
|
||||||
|
assert not user.admin
|
||||||
|
|
||||||
|
r = api_request(app, 'users', name, method='patch',
|
||||||
|
data=json.dumps({'admin': True})
|
||||||
|
)
|
||||||
|
assert r.status_code == 200
|
||||||
|
user = find_user(db, name)
|
||||||
|
assert user is not None
|
||||||
|
assert user.name == name
|
||||||
|
assert user.admin
|
||||||
|
|
||||||
|
|
||||||
|
def test_spawn(app, io_loop):
|
||||||
|
db = app.db
|
||||||
|
name = 'wash'
|
||||||
|
user = add_user(db, name=name)
|
||||||
|
r = api_request(app, 'users', name, 'server', method='post')
|
||||||
|
assert r.status_code == 201
|
||||||
|
assert user.spawner is not None
|
||||||
|
status = io_loop.run_sync(user.spawner.poll)
|
||||||
|
assert status is None
|
||||||
|
|
||||||
|
assert user.server.base_url == '/user/%s' % name
|
||||||
|
r = requests.get(ujoin(app.proxy.public_server.url, user.server.base_url))
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert r.text == user.server.base_url
|
||||||
|
|
||||||
|
r = requests.get(ujoin(app.proxy.public_server.url, user.server.base_url, 'args'))
|
||||||
|
assert r.status_code == 200
|
||||||
|
argv = r.json()
|
||||||
|
for expected in ['--user=%s' % name, '--base-url=%s' % user.server.base_url]:
|
||||||
|
assert expected in argv
|
||||||
|
|
||||||
|
r = api_request(app, 'users', name, 'server', method='delete')
|
||||||
|
assert r.status_code == 204
|
||||||
|
|
||||||
|
assert user.spawner is None
|
||||||
|
|
@@ -38,7 +38,7 @@ def auth_decorator(check_auth):
|
|||||||
def decorator(method):
|
def decorator(method):
|
||||||
def decorated(self, *args, **kwargs):
|
def decorated(self, *args, **kwargs):
|
||||||
check_auth(self)
|
check_auth(self)
|
||||||
return method(self, *args)
|
return method(self, *args, **kwargs)
|
||||||
decorated.__name__ = method.__name__
|
decorated.__name__ = method.__name__
|
||||||
decorated.__doc__ = method.__doc__
|
decorated.__doc__ = method.__doc__
|
||||||
return decorated
|
return decorated
|
||||||
|
Reference in New Issue
Block a user