Merge pull request #30 from minrk/restapi

getting started with REST API
This commit is contained in:
Min RK
2014-09-14 16:53:24 -07:00
23 changed files with 927 additions and 134 deletions

View File

@@ -1 +1,9 @@
from .base import *
from .auth import *
from .users import *
from . import auth, users
default_handlers = []
for mod in (auth, users):
default_handlers.extend(mod.default_handlers)

View File

@@ -6,12 +6,13 @@
import json
from tornado import web
from ..handlers import BaseHandler
from .. import orm
from ..utils import token_authenticated
from .base import APIHandler
class AuthorizationsAPIHandler(BaseHandler):
class AuthorizationsAPIHandler(APIHandler):
@token_authenticated
def get(self, token):
orm_token = self.db.query(orm.CookieToken).filter(orm.CookieToken.token == token).first()
@@ -20,3 +21,7 @@ class AuthorizationsAPIHandler(BaseHandler):
self.write(json.dumps({
'user' : orm_token.user.name,
}))
default_handlers = [
(r"/api/authorizations/([^/]+)", AuthorizationsAPIHandler),
]

View 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,
}))

View File

@@ -0,0 +1,136 @@
"""Authorization handlers"""
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
import json
from tornado import gen, web
from .. import orm
from ..utils import admin_only, authenticated_403
from .base import APIHandler
try:
basestring
except NameError:
basestring = str # py3
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
def get(self):
users = self.db.query(orm.User)
data = [ self.user_model(u) for u in users ]
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 = [
(r"/api/users", UserListAPIHandler),
(r"/api/users/([^/]+)", UserAPIHandler),
(r"/api/users/([^/]+)/server", UserServerAPIHandler),
]

View File

@@ -14,29 +14,19 @@ from jinja2 import Environment, FileSystemLoader
import tornado.httpserver
import tornado.options
from tornado.ioloop import IOLoop
from tornado.log import LogFormatter, app_log
from tornado.log import LogFormatter
from tornado import gen, web
from IPython.utils.traitlets import (
Unicode, Integer, Dict, TraitError, List, Instance, Bool, Bytes, Any,
DottedObjectName,
Unicode, Integer, Dict, TraitError, List, Bool, Bytes, Any,
DottedObjectName, Set,
)
from IPython.config import Application, catch_config_error
from IPython.utils.importstring import import_item
here = os.path.dirname(__file__)
from .handlers import (
Template404,
RootHandler,
LoginHandler,
LogoutHandler,
UserHandler,
)
from .apihandlers import (
AuthorizationsAPIHandler,
)
from . import handlers, apihandlers
from . import orm
from ._data import DATA_FILES_PATH
@@ -158,6 +148,12 @@ class JupyterHubApp(Application):
debug_db = Bool(False)
db = Any()
admin_users = Set(config=True,
help="""list of usernames of admin users
If unspecified, all users are admin.
"""
)
tornado_settings = Dict(config=True)
handlers = List()
@@ -214,25 +210,26 @@ class JupyterHubApp(Application):
return handlers
def init_handlers(self):
handlers = [
(r"/", RootHandler),
(r"/login", LoginHandler),
(r"/logout", LogoutHandler),
(r"/api/authorizations/([^/]+)", AuthorizationsAPIHandler),
]
self.handlers = self.add_url_prefix(self.hub_prefix, handlers)
h = []
h.extend(handlers.default_handlers)
h.extend(apihandlers.default_handlers)
self.handlers = self.add_url_prefix(self.hub_prefix, h)
# some extra handlers, outside hub_prefix
self.handlers.extend([
(r"/user/([^/]+)/?.*", UserHandler),
(r"/?", web.RedirectHandler, {"url" : self.hub_prefix, "permanent": False}),
(r"(?!%s).*" % self.hub_prefix, handlers.PrefixRedirectHandler),
(r'(.*)', handlers.Template404),
])
self.handlers.append(
(r'(.*)', Template404)
)
def init_db(self):
# TODO: load state from db for resume
# TODO: if not resuming, clear existing db contents
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):
"""Load the Hub config into the database"""
@@ -283,8 +280,7 @@ class JupyterHubApp(Application):
if self.ssl_cert:
cmd.extend(['--ssl-cert', self.ssl_cert])
self.proxy = Popen(cmd, env=env)
self.proxy_process = Popen(cmd, env=env)
def init_tornado_settings(self):
"""Set up the tornado settings dict."""
@@ -300,6 +296,7 @@ class JupyterHubApp(Application):
log=self.log,
db=self.db,
hub=self.hub,
admin_users=self.admin_users,
authenticator=import_item(self.authenticator)(config=self.config),
spawner_class=import_item(self.spawner_class),
base_url=base_url,
@@ -333,7 +330,7 @@ class JupyterHubApp(Application):
@gen.coroutine
def cleanup(self):
self.log.info("Cleaning up proxy...")
self.proxy.terminate()
self.proxy_process.terminate()
self.log.info("Cleaning up single-user servers...")
# request (async) process termination

View File

@@ -1,2 +1,8 @@
from .base import *
from .login import *
from . import base, pages, login
default_handlers = []
for mod in (base, pages, login):
default_handlers.extend(mod.default_handlers)

View File

@@ -56,6 +56,10 @@ class BaseHandler(RequestHandler):
# Login and cookie-related
#---------------------------------------------------------------
@property
def admin_users(self):
return self.settings.setdefault('admin_users', set())
def get_current_user_token(self):
"""get_current_user from Authorization header token"""
auth_header = self.request.headers.get('Authorization', '')
@@ -88,10 +92,16 @@ class BaseHandler(RequestHandler):
return user
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):
"""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:
user = orm.User(name=username)
self.db.add(user)
@@ -104,13 +114,13 @@ class BaseHandler(RequestHandler):
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_login_cookies(self, user):
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:
cookie_token = user.new_cookie_token()
self.db.add(cookie_token)
self.db.commit()
self.set_cookie(
user.server.cookie_name,
cookie_token.token,
@@ -118,6 +128,7 @@ class BaseHandler(RequestHandler):
)
# create and set a new cookie token for the hub
if not self.get_current_user_cookie():
cookie_token = user.new_cookie_token()
self.db.add(cookie_token)
self.db.commit()
@@ -161,6 +172,18 @@ class BaseHandler(RequestHandler):
yield wait_for_server(user.server.ip, user.server.port)
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
def spawn_single_user(self, user):
user.server = orm.Server(
@@ -190,6 +213,21 @@ class BaseHandler(RequestHandler):
self.notify_proxy(user)
raise gen.Return(user)
@gen.coroutine
def stop_single_user(self, user):
if user.spawner is None:
return
status = yield user.spawner.poll()
if status is None:
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
#---------------------------------------------------------------
@@ -203,16 +241,12 @@ class BaseHandler(RequestHandler):
template = self.get_template(name)
return template.render(**ns)
@property
def logged_in(self):
"""Is a user currently logged in?"""
return self.get_current_user() is not None
@property
def template_namespace(self):
user = self.get_current_user()
return dict(
base_url=self.hub.server.base_url,
logged_in=self.logged_in,
user=user,
login_url=self.settings['login_url'],
static_url=self.static_url,
)
@@ -260,33 +294,46 @@ class Template404(BaseHandler):
raise web.HTTPError(404)
class RootHandler(BaseHandler):
"""Render the Hub root page."""
@web.authenticated
def get(self):
user = self.get_current_user()
html = self.render_template('index.html',
server_running = user.server is not None,
server_url = '/user/%s' % user.name,
)
self.finish(html)
class PrefixRedirectHandler(BaseHandler):
"""Redirect anything outside a prefix inside.
class UserHandler(BaseHandler):
"""Respawn single-user server after logging in.
This handler shouldn't be called if the proxy is set up correctly.
Redirects /foo to /prefix/foo, etc.
"""
@web.authenticated
def get(self):
self.redirect(url_path_join(
self.hub.server.base_url, self.request.path,
), permanent=False)
class UserSpawnHandler(BaseHandler):
"""Requests to /user/name handled by the Hub
should result in spawning the single-user server and
being redirected to the original.
"""
@gen.coroutine
def get(self, name):
self.log.warn("Hub serving single-user url: %s", self.request.path)
current_user = self.get_current_user()
if current_user and current_user.name == name:
self.spawn_single_user(current_user)
self.redirect('')
# logged in, spawn the server
if current_user.spawner:
status = yield current_user.spawner.poll()
if status is not None:
yield self.spawn_single_user(current_user)
else:
self.log.warn("Hub serving single-user url: %s", self.request.path)
yield self.spawn_single_user(current_user)
# set login cookie anew
self.set_login_cookie(current_user)
self.redirect(url_path_join(
self.base_url, 'user', name,
))
else:
# not logged in to the right user,
# clear any cookies and reload (will redirect to login)
self.clear_login_cookie()
self.redirect(url_concat(self.settings['login_url'], {
'next' : self.request.path,
}))
self.redirect(url_concat(
self.settings['login_url'],
{'next': self.request.path,
}), permanent=False)
default_handlers = [
(r'/user/([^/]+)/?.*', UserSpawnHandler),
]

View File

@@ -28,8 +28,12 @@ class LoginHandler(BaseHandler):
)
def get(self):
if self.get_argument('next', False) and self.get_current_user():
self.redirect(self.get_argument('next'), permanent=False)
next_url = self.get_argument('next', False)
if next_url and self.get_current_user():
# set new login cookie
# because single-user cookie may have been cleared or incorrect
self.set_login_cookie(self.get_current_user())
self.redirect(next_url, permanent=False)
else:
username = self.get_argument('username', default='')
self.finish(self._render(username=username))
@@ -46,7 +50,7 @@ class LoginHandler(BaseHandler):
if authorized:
user = self.user_from_username(username)
yield self.spawn_single_user(user)
self.set_login_cookies(user)
self.set_login_cookie(user)
next_url = self.get_argument('next', default='') or self.hub.server.base_url
self.redirect(next_url)
else:
@@ -56,3 +60,8 @@ class LoginHandler(BaseHandler):
username=username,
)
self.finish(html)
default_handlers = [
(r"/login", LoginHandler),
(r"/logout", LogoutHandler),
]

View File

@@ -0,0 +1,55 @@
"""Basic html-rendering handlers."""
# Copyright (c) IPython Development Team.
# Distributed under the terms of the Modified BSD License.
from tornado import web
from .. import orm
from ..utils import admin_only, url_path_join
from .base import BaseHandler
class RootHandler(BaseHandler):
"""Render the Hub root page.
Currently redirects to home if logged in,
shows big fat login button otherwise.
"""
def get(self):
if self.get_current_user():
self.redirect(
url_path_join(self.hub.server.base_url, 'home'),
permanent=False,
)
return
html = self.render_template('index.html',
login_url=self.settings['login_url'],
)
self.finish(html)
class HomeHandler(BaseHandler):
"""Render the user's home page."""
@web.authenticated
def get(self):
html = self.render_template('home.html',
user=self.get_current_user(),
)
self.finish(html)
class AdminHandler(BaseHandler):
"""Render the admin page."""
@admin_only
def get(self):
html = self.render_template('admin.html',
user=self.get_current_user(),
users=self.db.query(orm.User),
)
self.finish(html)
default_handlers = [
(r'/', RootHandler),
(r'/home', HomeHandler),
(r'/admin', AdminHandler),
]

View File

@@ -8,7 +8,7 @@ import uuid
from sqlalchemy.types import TypeDecorator, VARCHAR
from sqlalchemy import (
Column, Integer, String, ForeignKey, Unicode, Binary,
Column, Integer, String, ForeignKey, Unicode, Binary, Boolean,
)
from sqlalchemy.ext.declarative import declarative_base, declared_attr
from sqlalchemy.orm import sessionmaker, relationship, backref
@@ -75,7 +75,7 @@ class Server(Base):
def host(self):
return "{proto}://{ip}:{port}".format(
proto=self.proto,
ip=self.ip,
ip=self.ip or '*',
port=self.port,
)
@@ -157,6 +157,7 @@ class User(Base):
# should we allow multiple servers per user?
_server_id = Column(Integer, ForeignKey('servers.id'))
server = relationship(Server, primaryjoin=_server_id == Server.id)
admin = Column(Boolean, default=False)
api_tokens = relationship("APIToken", backref="user")
cookie_tokens = relationship("CookieToken", backref="user")

View File

@@ -38,12 +38,12 @@ def db():
@fixture
def io_loop():
"""Get the current IOLoop"""
ioloop.IOLoop.clear_current()
return ioloop.IOLoop.current()
loop = ioloop.IOLoop()
loop.make_current()
return loop
@fixture
@fixture(scope='module')
def app(request):
app = MockHubApp()
app.start([])

View File

@@ -1,12 +1,13 @@
"""mock utilities for testing"""
import sys
import threading
try:
from unittest import mock
except ImportError:
import mock
import getpass
import threading
from tornado.ioloop import IOLoop
from IPython.utils.py3compat import unicode_type
@@ -30,13 +31,17 @@ def mock_authenticate(username, password, service='login'):
class MockSpawner(LocalProcessSpawner):
def make_preexec_fn(self):
def make_preexec_fn(self, *a, **kw):
# skip the setuid stuff
return
def _set_user_changed(self, name, old, new):
pass
def _cmd_default(self):
return [sys.executable, '-m', 'jupyterhub.tests.mocksu']
class MockPAMAuthenticator(PAMAuthenticator):
def authenticate(self, *args, **kwargs):
with mock.patch('simplepam.authenticate', mock_authenticate):
@@ -44,26 +49,30 @@ class MockPAMAuthenticator(PAMAuthenticator):
class MockHubApp(JupyterHubApp):
"""HubApp with various mock bits"""
# def start_proxy(self):
# pass
def _ip_default(self):
return 'localhost'
def _authenticator_default(self):
return '%s.%s' % (__name__, 'MockPAMAuthenticator')
def _spawner_class_default(self):
return '%s.%s' % (__name__, 'MockSpawner')
def _admin_users_default(self):
return {'admin'}
def start(self, argv=None):
evt = threading.Event()
def _start():
self.io_loop = IOLoop.current()
# put initialize in start for SQLAlchemy threading reasons
super(MockHubApp, self).initialize(argv=argv)
user = orm.User(name=getpass.getuser())
# add an initial user
user = orm.User(name='user')
self.db.add(user)
self.db.commit()
token = user.new_api_token()
self.db.add(token)
self.db.commit()
self.io_loop.add_callback(evt.set)
super(MockHubApp, self).start()
@@ -72,6 +81,6 @@ class MockHubApp(JupyterHubApp):
evt.wait(timeout=5)
def stop(self):
self.io_loop.stop()
self.io_loop.add_callback(self.io_loop.stop)
self._thread.join()

View File

@@ -0,0 +1,40 @@
"""Mock single-user server for testing
basic HTTP Server that echos URLs back,
and allow retrieval of sys.argv.
"""
import argparse
import json
import sys
from tornado import web, httpserver, ioloop
class EchoHandler(web.RequestHandler):
def get(self):
self.write(self.request.path)
class ArgsHandler(web.RequestHandler):
def get(self):
self.write(json.dumps(sys.argv))
def main(args):
app = web.Application([
(r'.*/args', ArgsHandler),
(r'.*', EchoHandler),
])
server = httpserver.HTTPServer(app)
server.listen(args.port)
try:
ioloop.IOLoop.instance().start()
except KeyboardInterrupt:
print('\nInterrupted')
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--port', type=int)
args, extra = parser.parse_known_args()
main(args)

View File

@@ -1,17 +1,40 @@
"""Tests for the REST API"""
import json
import requests
from ..utils import url_path_join as ujoin
from .. import orm
def find_user(db, name):
return db.query(orm.User).filter(orm.User.name==name).first()
def add_user(db, **kwargs):
user = orm.User(**kwargs)
db.add(user)
db.commit()
return user
def auth_header(db, name):
user = find_user(db, name)
if user is None:
user = add_user(db, name=name)
if not user.api_tokens:
token = user.new_api_token()
db.add(token)
db.commit()
else:
token = user.api_tokens[0]
return {'Authorization': 'token %s' % token.token}
def api_request(app, *api_path, **kwargs):
"""Make an API request"""
base_url = app.hub.server.url
token = app.db.query(orm.APIToken).first()
kwargs.setdefault('headers', {})
kwargs['headers'].setdefault('Authorization', 'token %s' % token.token)
headers = kwargs.setdefault('headers', {})
if 'Authorization' not in headers:
headers.update(auth_header(app.db, 'admin'))
url = ujoin(base_url, 'api', *api_path)
method = kwargs.pop('method', 'get')
@@ -47,3 +70,100 @@ def test_auth_api(app):
assert r.status_code == 403
def test_get_users(app):
db = app.db
r = api_request(app, 'users')
assert r.status_code == 200
assert sorted(r.json(), key=lambda d: d['name']) == [
{
'name': 'admin',
'admin': True,
'server': None,
},
{
'name': 'user',
'admin': False,
'server': None,
}
]
r = api_request(app, 'users',
headers=auth_header(db, 'user'),
)
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

View File

@@ -29,24 +29,42 @@ def wait_for_server(ip, port, timeout=10):
else:
break
def auth_decorator(check_auth):
"""Make an authentication decorator
def token_authenticated(method):
"""decorator for a method authenticated only by the Authorization token header"""
def check_token(self, *args, **kwargs):
I heard you like decorators, so I put a decorator
in your decorator, so you can decorate while you decorate.
"""
def decorator(method):
def decorated(self, *args, **kwargs):
check_auth(self)
return method(self, *args, **kwargs)
decorated.__name__ = method.__name__
decorated.__doc__ = method.__doc__
return decorated
decorator.__name__ = check_auth.__name__
decorator.__doc__ = check_auth.__doc__
return decorator
@auth_decorator
def token_authenticated(self):
"""decorator for a method authenticated only by the Authorization token header
(no cookies)
"""
if self.get_current_user_token() is None:
raise web.HTTPError(403)
return method(self, *args, **kwargs)
check_token.__name__ = method.__name__
check_token.__doc__ = method.__doc__
return check_token
def authenticated_403(method):
"""decorator like web.authenticated, but raise 403 instead of redirect to login"""
def check_user(self, *args, **kwargs):
@auth_decorator
def authenticated_403(self):
"""like web.authenticated, but raise 403 instead of redirect to login"""
if self.get_current_user() is None:
raise web.HTTPError(403)
return method(self, *args, **kwargs)
check_user.__name__ = method.__name__
check_user.__doc__ = method.__doc__
return check_user
@auth_decorator
def admin_only(self):
"""decorator for restricting access to admin users"""
user = self.get_current_user()
if user is None or not user.admin:
raise web.HTTPError(403)

View File

@@ -0,0 +1,19 @@
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
require(["jquery", "jhapi"], function ($, JHAPI) {
"use strict";
var base_url = window.jhdata.base_url;
var user = window.jhdata.user;
var api = new JHAPI(base_url);
$("#stop").click(function () {
api.stop_server(user, {
success: function () {
$("#stop").hide();
}
});
});
});

View File

@@ -0,0 +1,89 @@
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
define(['jquery', 'utils'], function ($, utils) {
"use strict";
var JHAPI = function (base_url) {
this.base_url = base_url;
};
var default_options = {
type: 'GET',
headers: {'Content-Type': 'application/json'},
cache: false,
dataType : "json",
processData: false,
success: null,
error: utils.log_jax_error,
};
var update = function (d1, d2) {
$.map(d2, function (i, key) {
d1[key] = d2[key];
});
return d1;
};
var ajax_defaults = function (options) {
var d = {};
update(d, default_options);
update(d, options);
return d;
};
JHAPI.prototype.api_request = function (path, options) {
options = options || {};
options = ajax_defaults(options || {});
var url = utils.url_path_join(
this.base_url,
'api',
utils.encode_uri_components(path)
);
$.ajax(url, options);
};
JHAPI.prototype.start_server = function (user, options) {
options = options || {};
options.update({
type: 'POST',
});
this.api_request(
utils.url_path_join('users', user, 'server'),
options
);
};
JHAPI.prototype.stop_server = function (user, options) {
options = update(options || {}, {
type: 'DELETE',
});
this.api_request(
utils.url_path_join('users', user, 'server'),
options
);
};
JHAPI.prototype.list_users = function (options) {
this.api_request('users', options);
};
JHAPI.prototype.get_user = function (user, options) {
this.api_request(
utils.url_path_join('users', user),
options
);
};
JHAPI.prototype.delete_user = function (user, options) {
options = update(options || {}, {
type: 'DELETE',
});
this.api_request(
utils.url_path_join('users', user),
options
);
};
return JHAPI;
});

View File

@@ -0,0 +1,127 @@
// Based on IPython's base.js.utils
// Original Copyright (c) IPython Development Team.
// Distributed under the terms of the Modified BSD License.
// Modifications Copyright (c) Juptyer Development Team.
// Distributed under the terms of the Modified BSD License.
define(['jquery'], function($){
"use strict";
var url_path_join = function () {
// join a sequence of url components with '/'
var url = '';
for (var i = 0; i < arguments.length; i++) {
if (arguments[i] === '') {
continue;
}
if (url.length > 0 && url[url.length-1] != '/') {
url = url + '/' + arguments[i];
} else {
url = url + arguments[i];
}
}
url = url.replace(/\/\/+/, '/');
return url;
};
var parse_url = function (url) {
// an `a` element with an href allows attr-access to the parsed segments of a URL
// a = parse_url("http://localhost:8888/path/name#hash")
// a.protocol = "http:"
// a.host = "localhost:8888"
// a.hostname = "localhost"
// a.port = 8888
// a.pathname = "/path/name"
// a.hash = "#hash"
var a = document.createElement("a");
a.href = url;
return a;
};
var encode_uri_components = function (uri) {
// encode just the components of a multi-segment uri,
// leaving '/' separators
return uri.split('/').map(encodeURIComponent).join('/');
};
var url_join_encode = function () {
// join a sequence of url components with '/',
// encoding each component with encodeURIComponent
return encode_uri_components(url_path_join.apply(null, arguments));
};
var escape_html = function (text) {
// escape text to HTML
return $("<div/>").text(text).html();
};
var get_body_data = function(key) {
// get a url-encoded item from body.data and decode it
// we should never have any encoded URLs anywhere else in code
// until we are building an actual request
return decodeURIComponent($('body').data(key));
};
// http://stackoverflow.com/questions/2400935/browser-detection-in-javascript
var browser = (function() {
if (typeof navigator === 'undefined') {
// navigator undefined in node
return 'None';
}
var N= navigator.appName, ua= navigator.userAgent, tem;
var M= ua.match(/(opera|chrome|safari|firefox|msie)\/?\s*(\.?\d+(\.\d+)*)/i);
if (M && (tem= ua.match(/version\/([\.\d]+)/i)) !== null) M[2]= tem[1];
M= M? [M[1], M[2]]: [N, navigator.appVersion,'-?'];
return M;
})();
// http://stackoverflow.com/questions/11219582/how-to-detect-my-browser-version-and-operating-system-using-javascript
var platform = (function () {
if (typeof navigator === 'undefined') {
// navigator undefined in node
return 'None';
}
var OSName="None";
if (navigator.appVersion.indexOf("Win")!=-1) OSName="Windows";
if (navigator.appVersion.indexOf("Mac")!=-1) OSName="MacOS";
if (navigator.appVersion.indexOf("X11")!=-1) OSName="UNIX";
if (navigator.appVersion.indexOf("Linux")!=-1) OSName="Linux";
return OSName;
})();
var ajax_error_msg = function (jqXHR) {
// Return a JSON error message if there is one,
// otherwise the basic HTTP status text.
if (jqXHR.responseJSON && jqXHR.responseJSON.message) {
return jqXHR.responseJSON.message;
} else {
return jqXHR.statusText;
}
};
var log_ajax_error = function (jqXHR, status, error) {
// log ajax failures with informative messages
var msg = "API request failed (" + jqXHR.status + "): ";
console.log(jqXHR);
msg += ajax_error_msg(jqXHR);
console.log(msg);
};
var utils = {
url_path_join : url_path_join,
url_join_encode : url_join_encode,
encode_uri_components : encode_uri_components,
escape_html : escape_html,
get_body_data : get_body_data,
parse_url : parse_url,
browser : browser,
platform: platform,
ajax_error_msg : ajax_error_msg,
log_ajax_error : log_ajax_error,
};
return utils;
});

View File

@@ -0,0 +1,13 @@
{% extends "page.html" %}
{% block main %}
<div class="container">
<div class="row">
<div class="text-center">
<a class="btn btn-lg btn-primary">Administrate!</a>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,29 @@
{% extends "page.html" %}
{% block main %}
<div class="container">
<div class="row">
<div class="text-center">
{% if user.server %}
<a id="stop" class="btn btn-lg btn-danger">Stop My Server</a>
{% endif %}
<a id="start" class="btn btn-lg btn-success"
href="{{base_url}}user/{{user.name}}/"
>
My Server
</a>
{% if user.admin %}
<a id="admin" class="btn btn-lg btn-primary" href="{{base_url}}admin">Admin</a>
{% endif %}
</div>
</div>
</div>
{% endblock %}
{% block script %}
<script type="text/javascript">
require(["home"]);
</script>
{% endblock %}

View File

@@ -1,11 +1,14 @@
{% extends "page.html" %}
{% block login_widget %}
{% endblock %}
{% block main %}
<div class="container">
<div class="row">
<div class="text-center">
<a id="start" class="btn btn-lg btn-primary" href="{{server_url}}">Dashboard</a>
<a id="login" class="btn btn-lg btn-primary" href="{{login_url}}">Log in</a>
</div>
</div>
</div>

View File

@@ -1,6 +1,5 @@
{% extends "page.html" %}
{% block login_widget %}
{% endblock %}

View File

@@ -15,11 +15,12 @@
<script src="{{static_url("components/requirejs/require.js") }}" type="text/javascript" charset="utf-8"></script>
<script>
require.config({
baseUrl: '{{static_url("", include_version=False)}}',
baseUrl: '{{static_url("js", include_version=False)}}',
paths: {
jquery: 'components/jquery/jquery.min',
bootstrap: 'components/bootstrap/js/bootstrap.min',
moment: "components/moment/moment",
components: '../components',
jquery: '../components/jquery/jquery.min',
bootstrap: '../components/bootstrap/js/bootstrap.min',
moment: "../components/moment/moment",
},
shim: {
bootstrap: {
@@ -30,12 +31,21 @@
});
</script>
<script type="text/javascript">
window.jhdata = {
base_url: "{{base_url}}",
{% if user %}
user: "{{user.name}}",
{% endif %}
}
</script>
{% block meta %}
{% endblock %}
</head>
<body {% block params %}{% endblock %}>
<body>
<noscript>
<div id='noscript'>
@@ -51,7 +61,7 @@
{% block login_widget %}
<span id="login_widget">
{% if logged_in %}
{% if user %}
<a id="logout" class="btn navbar-btn btn-default pull-right" href="{{base_url}}logout">Logout</a>
{% else %}
<a id="login" class="btn navbar-btn btn-default pull-right" href="{{base_url}}login">Login</a>