mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-18 15:33:02 +00:00
store the multi-user state in a database
via SQLAlchemy ORM
This commit is contained in:
147
multiuser/app.py
147
multiuser/app.py
@@ -9,10 +9,12 @@ import tornado.options
|
|||||||
from tornado import web
|
from tornado import web
|
||||||
|
|
||||||
from IPython.utils.traitlets import (
|
from IPython.utils.traitlets import (
|
||||||
Unicode, Integer, Dict, TraitError, List,
|
Unicode, Integer, Dict, TraitError, List, Instance, Bool, Bytes, Any,
|
||||||
|
DottedObjectName,
|
||||||
)
|
)
|
||||||
from IPython.config import Application
|
from IPython.config import Application
|
||||||
from IPython.html import utils
|
from IPython.html.utils import url_path_join
|
||||||
|
from IPython.utils.importstring import import_item
|
||||||
|
|
||||||
here = os.path.dirname(__file__)
|
here = os.path.dirname(__file__)
|
||||||
|
|
||||||
@@ -24,35 +26,79 @@ from .handlers import (
|
|||||||
UserHandler,
|
UserHandler,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .user import UserManager
|
from . import db
|
||||||
|
|
||||||
class MultiUserAuthenticationApp(Application):
|
# from .user import UserManager
|
||||||
|
|
||||||
|
class MultiUserApp(Application):
|
||||||
|
|
||||||
|
ip = Unicode('localhost', config=True,
|
||||||
|
help="The public facing ip of the proxy"
|
||||||
|
)
|
||||||
port = Integer(8000, config=True,
|
port = Integer(8000, config=True,
|
||||||
help="The public facing port of the proxy"
|
help="The public facing port of the proxy"
|
||||||
)
|
)
|
||||||
|
base_url = Unicode('/', config=True,
|
||||||
|
help="The base URL of the entire application"
|
||||||
|
)
|
||||||
|
proxy_auth_token = Unicode(config=True,
|
||||||
|
help="The Proxy Auth token"
|
||||||
|
)
|
||||||
|
def _proxy_auth_token_default(self):
|
||||||
|
return db.new_token()
|
||||||
|
|
||||||
|
proxy_api_ip = Unicode('localhost', config=True,
|
||||||
|
help="The ip for the proxy API handlers"
|
||||||
|
)
|
||||||
proxy_api_port = Integer(config=True,
|
proxy_api_port = Integer(config=True,
|
||||||
help="The port for the proxy API handlers"
|
help="The port for the proxy API handlers"
|
||||||
)
|
)
|
||||||
def _proxy_api_port_default(self):
|
def _proxy_api_port_default(self):
|
||||||
return self.port + 1
|
return self.port + 1
|
||||||
|
|
||||||
multiuser_port = Integer(8081, config=True,
|
hub_port = Integer(8081, config=True,
|
||||||
help="The port for this process"
|
help="The port for this process"
|
||||||
)
|
)
|
||||||
|
hub_ip = Unicode('localhost', config=True,
|
||||||
multiuser_prefix = Unicode('/multiuser/', config=True,
|
help="The ip for this process"
|
||||||
help="The prefix for the multi-user server. Must not be '/'"
|
|
||||||
)
|
)
|
||||||
def _multiuser_prefix_changed(self, name, old, new):
|
|
||||||
|
hub_prefix = Unicode('/hub/', config=True,
|
||||||
|
help="The prefix for the hub server. Must not be '/'"
|
||||||
|
)
|
||||||
|
def _hub_prefix_default(self):
|
||||||
|
return url_path_join(self.base_url, '/hub/')
|
||||||
|
|
||||||
|
def _hub_prefix_changed(self, name, old, new):
|
||||||
if new == '/':
|
if new == '/':
|
||||||
raise TraitError("'/' is not a valid multi-user prefix")
|
raise TraitError("'/' is not a valid hub prefix")
|
||||||
newnew = new
|
newnew = new
|
||||||
if not new.startswith('/'):
|
if not new.startswith('/'):
|
||||||
newnew = '/' + new
|
newnew = '/' + new
|
||||||
if not newnew.endswith('/'):
|
if not newnew.endswith('/'):
|
||||||
newnew = newnew + '/'
|
newnew = newnew + '/'
|
||||||
|
if not newnew.startswith(self.base_url):
|
||||||
|
newnew = url_path_join(self.base_url, newnew)
|
||||||
if newnew != new:
|
if newnew != new:
|
||||||
self.multiuser_prefix = newnew
|
self.hub_prefix = newnew
|
||||||
|
|
||||||
|
cookie_secret = Bytes(config=True)
|
||||||
|
def _cookie_secret_default(self):
|
||||||
|
return b'secret!'
|
||||||
|
|
||||||
|
# spawning subprocesses
|
||||||
|
spawner_class = DottedObjectName("multiuser.spawner.ProcessSpawner")
|
||||||
|
def _spawner_class_changed(self, name, old, new):
|
||||||
|
self.spawner = import_item(new)
|
||||||
|
|
||||||
|
spawner = Any()
|
||||||
|
def _spawner_default(self):
|
||||||
|
return import_item(self.spawner_class)
|
||||||
|
|
||||||
|
|
||||||
|
db_url = Unicode('sqlite:///:memory:', config=True)
|
||||||
|
debug_db = Bool(False)
|
||||||
|
db = Any()
|
||||||
|
|
||||||
tornado_settings = Dict(config=True)
|
tornado_settings = Dict(config=True)
|
||||||
|
|
||||||
@@ -62,7 +108,7 @@ class MultiUserAuthenticationApp(Application):
|
|||||||
"""add a url prefix to handlers"""
|
"""add a url prefix to handlers"""
|
||||||
for i, tup in enumerate(handlers):
|
for i, tup in enumerate(handlers):
|
||||||
lis = list(tup)
|
lis = list(tup)
|
||||||
lis[0] = utils.url_path_join(prefix, tup[0])
|
lis[0] = url_path_join(prefix, tup[0])
|
||||||
handlers[i] = tuple(lis)
|
handlers[i] = tuple(lis)
|
||||||
return handlers
|
return handlers
|
||||||
|
|
||||||
@@ -73,38 +119,62 @@ class MultiUserAuthenticationApp(Application):
|
|||||||
(r"/logout", LogoutHandler),
|
(r"/logout", LogoutHandler),
|
||||||
(r"/api/authorizations/([^/]+)", AuthorizationsHandler),
|
(r"/api/authorizations/([^/]+)", AuthorizationsHandler),
|
||||||
]
|
]
|
||||||
self.handlers = self.add_url_prefix(self.multiuser_prefix, handlers)
|
self.handlers = self.add_url_prefix(self.hub_prefix, handlers)
|
||||||
self.handlers.extend([
|
self.handlers.extend([
|
||||||
(r"/user/([^/]+)/?.*", UserHandler),
|
(r"/user/([^/]+)/?.*", UserHandler),
|
||||||
(r"/", web.RedirectHandler, {"url" : self.multiuser_prefix}),
|
(r"/", web.RedirectHandler, {"url" : self.hub_prefix}),
|
||||||
])
|
])
|
||||||
|
|
||||||
def init_user_manager(self):
|
def init_db(self):
|
||||||
self.user_manager = UserManager(proxy_port=self.proxy_api_port)
|
# TODO: load state from db for resume
|
||||||
|
self.db = db.new_session(self.db_url, echo=self.debug_db)
|
||||||
|
|
||||||
|
def init_hub(self):
|
||||||
|
self.hub = db.Hub(
|
||||||
|
server=db.Server(
|
||||||
|
ip=self.hub_ip,
|
||||||
|
port=self.hub_port,
|
||||||
|
base_url=self.hub_prefix,
|
||||||
|
cookie_secret=self.cookie_secret,
|
||||||
|
cookie_name='jupyter-hub-token',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.db.add(self.hub)
|
||||||
|
self.db.commit()
|
||||||
|
|
||||||
def init_proxy(self):
|
def init_proxy(self):
|
||||||
|
self.proxy = db.Proxy(
|
||||||
|
public_server=db.Server(
|
||||||
|
ip=self.ip,
|
||||||
|
port=self.port,
|
||||||
|
),
|
||||||
|
api_server=db.Server(
|
||||||
|
ip=self.proxy_api_ip,
|
||||||
|
port=self.proxy_api_port,
|
||||||
|
base_url='/api/routes/'
|
||||||
|
),
|
||||||
|
auth_token = db.new_token(),
|
||||||
|
)
|
||||||
|
self.db.add(self.proxy)
|
||||||
|
self.db.commit()
|
||||||
|
|
||||||
|
def start_proxy(self):
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
env['CONFIGPROXY_AUTH_TOKEN'] = self.user_manager.proxy_auth_token
|
env['CONFIGPROXY_AUTH_TOKEN'] = self.proxy.auth_token
|
||||||
self.proxy = Popen(["node", os.path.join(here, 'js', 'main.js'),
|
self.proxy = Popen(["node", os.path.join(here, 'js', 'main.js'),
|
||||||
'--port', str(self.port),
|
'--port', str(self.proxy.public_server.port),
|
||||||
'--api-port', str(self.proxy_api_port),
|
'--api-port', str(self.proxy.api_server.port),
|
||||||
'--upstream-port', str(self.multiuser_port),
|
'--upstream-port', str(self.hub.server.port),
|
||||||
], env=env)
|
], env=env)
|
||||||
|
|
||||||
def init_tornado_settings(self):
|
def init_tornado_settings(self):
|
||||||
base_url = self.multiuser_prefix
|
base_url = self.base_url
|
||||||
self.tornado_settings.update(
|
self.tornado_settings.update(
|
||||||
|
db=self.db,
|
||||||
|
hub=self.hub,
|
||||||
base_url=base_url,
|
base_url=base_url,
|
||||||
user_manager=self.user_manager,
|
cookie_secret=self.cookie_secret,
|
||||||
cookie_secret='super secret',
|
login_url=url_path_join(self.hub.server.base_url, 'login'),
|
||||||
cookie_name='multiusertest',
|
|
||||||
multiuser_prefix=base_url,
|
|
||||||
multiuser_api_url=utils.url_path_join(
|
|
||||||
'http://localhost:%i' % self.multiuser_port,
|
|
||||||
base_url,
|
|
||||||
'api',
|
|
||||||
),
|
|
||||||
login_url=utils.url_path_join(base_url, 'login'),
|
|
||||||
template_path=os.path.join(here, 'templates'),
|
template_path=os.path.join(here, 'templates'),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -112,25 +182,28 @@ class MultiUserAuthenticationApp(Application):
|
|||||||
self.tornado_application = web.Application(self.handlers, **self.tornado_settings)
|
self.tornado_application = web.Application(self.handlers, **self.tornado_settings)
|
||||||
|
|
||||||
def initialize(self, *args, **kwargs):
|
def initialize(self, *args, **kwargs):
|
||||||
super(MultiUserAuthenticationApp, self).initialize(*args, **kwargs)
|
super(MultiUserApp, self).initialize(*args, **kwargs)
|
||||||
self.init_user_manager()
|
self.init_db()
|
||||||
|
self.init_hub()
|
||||||
self.init_proxy()
|
self.init_proxy()
|
||||||
self.init_handlers()
|
self.init_handlers()
|
||||||
self.init_tornado_settings()
|
self.init_tornado_settings()
|
||||||
self.init_tornado_application()
|
self.init_tornado_application()
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
|
self.start_proxy()
|
||||||
http_server = tornado.httpserver.HTTPServer(self.tornado_application)
|
http_server = tornado.httpserver.HTTPServer(self.tornado_application)
|
||||||
http_server.listen(self.multiuser_port)
|
http_server.listen(self.hub_port)
|
||||||
try:
|
try:
|
||||||
tornado.ioloop.IOLoop.instance().start()
|
tornado.ioloop.IOLoop.instance().start()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("\nInterrupted")
|
print("\nInterrupted")
|
||||||
finally:
|
finally:
|
||||||
self.proxy.terminate()
|
pass
|
||||||
self.user_manager.cleanup()
|
# self.proxy.terminate()
|
||||||
|
# self.user_manager.cleanup()
|
||||||
|
|
||||||
main = MultiUserAuthenticationApp.launch_instance
|
main = MultiUserApp.launch_instance
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
213
multiuser/db.py
Normal file
213
multiuser/db.py
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
"""sqlalchemy ORM tools for the user database"""
|
||||||
|
|
||||||
|
# Copyright (c) Jupyter Development Team.
|
||||||
|
# Distributed under the terms of the Modified BSD License.
|
||||||
|
|
||||||
|
import json
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from sqlalchemy.types import TypeDecorator, VARCHAR
|
||||||
|
from sqlalchemy import (
|
||||||
|
Column, Integer, String, ForeignKey, Unicode, Binary,
|
||||||
|
)
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base, declared_attr
|
||||||
|
|
||||||
|
from sqlalchemy.orm import sessionmaker, relationship, backref
|
||||||
|
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
|
||||||
|
from IPython.utils.py3compat import str_to_unicode
|
||||||
|
|
||||||
|
from .utils import random_port
|
||||||
|
|
||||||
|
|
||||||
|
def new_token(*args, **kwargs):
|
||||||
|
"""generator for new random tokens
|
||||||
|
|
||||||
|
For now, just UUIDs.
|
||||||
|
"""
|
||||||
|
return str_to_unicode(uuid.uuid4().hex)
|
||||||
|
|
||||||
|
|
||||||
|
class JSONDict(TypeDecorator):
|
||||||
|
"""Represents an immutable structure as a json-encoded string.
|
||||||
|
|
||||||
|
Usage::
|
||||||
|
|
||||||
|
JSONEncodedDict(255)
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
impl = VARCHAR
|
||||||
|
|
||||||
|
def process_bind_param(self, value, dialect):
|
||||||
|
if value is not None:
|
||||||
|
value = json.dumps(value)
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
def process_result_value(self, value, dialect):
|
||||||
|
if value is not None:
|
||||||
|
value = json.loads(value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
|
||||||
|
class Server(Base):
|
||||||
|
__tablename__ = 'servers'
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
proto = Column(Unicode, default=u'http')
|
||||||
|
ip = Column(Unicode, default=u'localhost')
|
||||||
|
port = Column(Integer, default=random_port)
|
||||||
|
cookie_secret = Column(Binary)
|
||||||
|
cookie_name = Column(Unicode)
|
||||||
|
base_url = Column(Unicode, default=u'/')
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<Server(%s:%s)>" % (self.ip, self.port)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url(self):
|
||||||
|
return "{proto}://{ip}:{port}{url}".format(
|
||||||
|
proto=self.proto,
|
||||||
|
ip=self.ip,
|
||||||
|
port=self.port,
|
||||||
|
url=self.base_url,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Proxy(Base):
|
||||||
|
"""A configurable-http-proxy instance"""
|
||||||
|
__tablename__ = 'proxies'
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
auth_token = Column(Unicode, default=new_token)
|
||||||
|
_public_server_id = Column(Integer, ForeignKey('servers.id'))
|
||||||
|
public_server = relationship(Server, primaryjoin=_public_server_id == Server.id)
|
||||||
|
_api_server_id = Column(Integer, ForeignKey('servers.id'))
|
||||||
|
api_server = relationship(Server, primaryjoin=_api_server_id == Server.id)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
if self.public_server:
|
||||||
|
return "<%s %s:%s>" % (
|
||||||
|
self.__class__.__name__, self.public_server.ip, self.public_server.port,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return "<%s [unconfigured]>" % self.__class__.__name__
|
||||||
|
|
||||||
|
|
||||||
|
class Hub(Base):
|
||||||
|
"""Bring it all together at the hub"""
|
||||||
|
__tablename__ = 'hubs'
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
_server_id = Column(Integer, ForeignKey('servers.id'))
|
||||||
|
server = relationship(Server, primaryjoin=_server_id == Server.id)
|
||||||
|
api_url = Column(Unicode, default=u'/hub/api/')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def api_host_url(self):
|
||||||
|
"""return the full API url (with proto://host...)"""
|
||||||
|
return "{proto}://{ip}:{port}{url}".format(
|
||||||
|
proto=self.server.proto,
|
||||||
|
ip=self.server.ip,
|
||||||
|
port=self.server.port,
|
||||||
|
url=self.api_url,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
if self.server:
|
||||||
|
return "<%s %s:%s>" % (
|
||||||
|
self.__class__.__name__, self.server.ip, self.server.port,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return "<%s [unconfigured]>" % self.__class__.__name__
|
||||||
|
|
||||||
|
|
||||||
|
class User(Base):
|
||||||
|
"""The User table"""
|
||||||
|
__tablename__ = 'users'
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
name = Column(Unicode)
|
||||||
|
_server_id = Column(Integer, ForeignKey('servers.id'))
|
||||||
|
server = relationship(Server, primaryjoin=_server_id == Server.id)
|
||||||
|
|
||||||
|
api_tokens = relationship("APIToken", backref="user")
|
||||||
|
cookie_tokens = relationship("CookieToken", backref="user")
|
||||||
|
state = Column(JSONDict)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
if self.server:
|
||||||
|
return "<{cls}({name}@{ip}:{port})>".format(
|
||||||
|
cls=self.__class__.__name__,
|
||||||
|
name=self.name,
|
||||||
|
ip=self.server.ip,
|
||||||
|
port=self.server.port,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return "<{cls}({name} [unconfigured])>".format(
|
||||||
|
cls=self.__class__.__name__,
|
||||||
|
name=self.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _new_token(self, cls):
|
||||||
|
assert self.id is not None
|
||||||
|
return cls(token=new_token(), user_id=self.id)
|
||||||
|
|
||||||
|
def new_api_token(self):
|
||||||
|
"""Return a new API token"""
|
||||||
|
return self._new_token(APIToken)
|
||||||
|
|
||||||
|
def new_cookie_token(self):
|
||||||
|
"""Return a new cookie token"""
|
||||||
|
return self._new_token(CookieToken)
|
||||||
|
|
||||||
|
|
||||||
|
class Token(object):
|
||||||
|
"""Mixin for token tables, since we have two"""
|
||||||
|
token = Column(String, primary_key=True)
|
||||||
|
@declared_attr
|
||||||
|
def user_id(cls):
|
||||||
|
return Column(Integer, ForeignKey('users.id'))
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<{cls}('{t}', user='{u}')>".format(
|
||||||
|
cls=self.__class__.__name__,
|
||||||
|
t=self.token,
|
||||||
|
u=self.user.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class APIToken(Token, Base):
|
||||||
|
"""An API token"""
|
||||||
|
__tablename__ = 'api_tokens'
|
||||||
|
|
||||||
|
|
||||||
|
class CookieToken(Token, Base):
|
||||||
|
"""A cookie token"""
|
||||||
|
__tablename__ = 'cookie_tokens'
|
||||||
|
|
||||||
|
|
||||||
|
def new_session(url="sqlite:///:memory:", **kwargs):
|
||||||
|
"""Create a new session at url"""
|
||||||
|
engine = create_engine(url, **kwargs)
|
||||||
|
Session = sessionmaker(bind=engine)
|
||||||
|
session = Session()
|
||||||
|
Base.metadata.create_all(engine)
|
||||||
|
return session
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
engine = create_engine('sqlite:///:memory:', echo=True)
|
||||||
|
Session = sessionmaker(bind=engine)
|
||||||
|
session = Session()
|
||||||
|
Base.metadata.create_all(engine)
|
||||||
|
|
||||||
|
hub = Hub()
|
||||||
|
session.add(hub)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
minrk = User(name="minrk")
|
||||||
|
session.add(minrk)
|
||||||
|
session.commit()
|
||||||
|
|
@@ -1,35 +1,61 @@
|
|||||||
"""HTTP Handlers for the multi-user server"""
|
"""HTTP Handlers for the hub server"""
|
||||||
|
|
||||||
# Copyright (c) IPython Development Team.
|
# Copyright (c) Jupyter Development Team.
|
||||||
# Distributed under the terms of the Modified BSD License.
|
# Distributed under the terms of the Modified BSD License.
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
from tornado.log import app_log
|
from tornado.log import app_log
|
||||||
from tornado.escape import url_escape
|
from tornado.escape import url_escape
|
||||||
from tornado.httputil import url_concat
|
from tornado.httputil import url_concat
|
||||||
from tornado.web import RequestHandler
|
from tornado.web import RequestHandler
|
||||||
from tornado import web
|
from tornado import web
|
||||||
|
|
||||||
|
from IPython.html.utils import url_path_join
|
||||||
|
|
||||||
|
from . import db
|
||||||
|
from .spawner import PopenSpawner
|
||||||
|
from .utils import random_port, wait_for_server
|
||||||
|
|
||||||
|
|
||||||
class BaseHandler(RequestHandler):
|
class BaseHandler(RequestHandler):
|
||||||
"""Base Handler class with access to common methods and properties."""
|
"""Base Handler class with access to common methods and properties."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def log(self):
|
||||||
|
"""I can't seem to avoid typing self.log"""
|
||||||
|
return app_log
|
||||||
|
|
||||||
|
@property
|
||||||
|
def config(self):
|
||||||
|
return self.settings.get('config', None)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def db(self):
|
||||||
|
return self.settings['db']
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hub(self):
|
||||||
|
return self.settings['hub']
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def cookie_name(self):
|
def cookie_name(self):
|
||||||
return self.settings.get('cookie_name', 'cookie')
|
return self.settings.get('cookie_name', 'cookie')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def multiuser_url(self):
|
def hub_url(self):
|
||||||
return self.settings.get('multiuser_url', '')
|
return self.settings.get('hub_url', '')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def multiuser_prefix(self):
|
def hub_prefix(self):
|
||||||
return self.settings.get('multiuser_prefix', '/multiuser/')
|
return self.settings.get('hub_prefix', '/hub/')
|
||||||
|
|
||||||
def get_current_user(self):
|
def get_current_user(self):
|
||||||
if 'get_current_user' in self.settings:
|
if 'get_current_user' in self.settings:
|
||||||
return self.settings['get_current_user']
|
return self.settings['get_current_user']()
|
||||||
|
|
||||||
token = self.get_cookie(self.cookie_name, '')
|
token = self.get_cookie(self.cookie_name, '')
|
||||||
if token:
|
if token:
|
||||||
@@ -41,21 +67,27 @@ class BaseHandler(RequestHandler):
|
|||||||
def base_url(self):
|
def base_url(self):
|
||||||
return self.settings.setdefault('base_url', '/')
|
return self.settings.setdefault('base_url', '/')
|
||||||
|
|
||||||
@property
|
|
||||||
def user_manager(self):
|
|
||||||
return self.settings['user_manager']
|
|
||||||
|
|
||||||
def clear_login_cookie(self):
|
def clear_login_cookie(self):
|
||||||
self.clear_cookie(self.cookie_name)
|
self.clear_cookie(self.cookie_name)
|
||||||
|
|
||||||
def spawn_single_user(self, user):
|
@property
|
||||||
session = self.user_manager.get_session(user,
|
def spawner_class(self):
|
||||||
cookie_secret=self.settings['cookie_secret'],
|
return self.settings.get('spawner_class', PopenSpawner)
|
||||||
multiuser_api_url=self.settings['multiuser_api_url'],
|
|
||||||
multiuser_prefix=self.settings['multiuser_prefix'],
|
# def spawn_single_user(self, user):
|
||||||
)
|
# spawner = self.spawner_class(
|
||||||
self.user_manager.spawn(user)
|
# user=user,
|
||||||
return session
|
# cookie_secret=self.settings['cookie_secret'],
|
||||||
|
# hub_api_url=self.settings['hub_api_url'],
|
||||||
|
# hub_prefix=self.settings['hub_prefix'],
|
||||||
|
# )
|
||||||
|
# session = self.user_manager.get_session(user,
|
||||||
|
# cookie_secret=self.settings['cookie_secret'],
|
||||||
|
# hub_api_url=self.settings['hub_api_url'],
|
||||||
|
# hub_prefix=self.settings['hub_prefix'],
|
||||||
|
# )
|
||||||
|
# self.user_manager.spawn(user)
|
||||||
|
# return session
|
||||||
|
|
||||||
|
|
||||||
class RootHandler(BaseHandler):
|
class RootHandler(BaseHandler):
|
||||||
@@ -72,7 +104,7 @@ class UserHandler(BaseHandler):
|
|||||||
"""
|
"""
|
||||||
@web.authenticated
|
@web.authenticated
|
||||||
def get(self, user):
|
def get(self, user):
|
||||||
self.log.debug("multi-user at single-user url: %s", user)
|
self.log.debug("hub at single-user url: %s", user)
|
||||||
if self.get_current_user() == user:
|
if self.get_current_user() == user:
|
||||||
self.spawn_single_user(user)
|
self.spawn_single_user(user)
|
||||||
self.redirect('')
|
self.redirect('')
|
||||||
@@ -106,18 +138,82 @@ class LoginHandler(BaseHandler):
|
|||||||
user = self.get_argument('user', default='')
|
user = self.get_argument('user', default='')
|
||||||
self._render(user=user)
|
self._render(user=user)
|
||||||
|
|
||||||
|
def notify_proxy(self, user):
|
||||||
|
proxy = self.db.query(db.Proxy).first()
|
||||||
|
r = requests.post(
|
||||||
|
url_path_join(
|
||||||
|
proxy.api_server.url,
|
||||||
|
user.server.base_url,
|
||||||
|
),
|
||||||
|
data=json.dumps(dict(
|
||||||
|
target=user.server.url,
|
||||||
|
user=user.name,
|
||||||
|
)),
|
||||||
|
headers={'Authorization': "token %s" % proxy.auth_token},
|
||||||
|
)
|
||||||
|
wait_for_server(user.server.ip, user.server.port)
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
|
def spawn_single_user(self, name):
|
||||||
|
user = db.User(name=name,
|
||||||
|
server=db.Server(
|
||||||
|
cookie_name='%s-%s' % (self.hub.server.cookie_name, name),
|
||||||
|
cookie_secret=self.hub.server.cookie_secret,
|
||||||
|
base_url=url_path_join(self.base_url, 'user', name),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self.db.add(user)
|
||||||
|
self.db.commit()
|
||||||
|
|
||||||
|
api_token = user.new_api_token()
|
||||||
|
self.db.add(api_token)
|
||||||
|
self.db.commit()
|
||||||
|
|
||||||
|
spawner = self.spawner_class(
|
||||||
|
config=self.config,
|
||||||
|
user=user,
|
||||||
|
hub=self.hub,
|
||||||
|
api_token=api_token.token,
|
||||||
|
)
|
||||||
|
spawner.start()
|
||||||
|
|
||||||
|
# store state
|
||||||
|
user.state = spawner.get_state()
|
||||||
|
self.db.commit()
|
||||||
|
|
||||||
|
self.notify_proxy(user)
|
||||||
|
return user
|
||||||
|
|
||||||
def post(self):
|
def post(self):
|
||||||
user = self.get_argument('user', default='')
|
name = self.get_argument('user', default='')
|
||||||
pwd = self.get_argument('password', default=u'')
|
pwd = self.get_argument('password', default=u'')
|
||||||
next_url = self.get_argument('next', default='') or '/user/%s/' % user
|
next_url = self.get_argument('next', default='') or '/user/%s/' % name
|
||||||
if user and pwd == 'password':
|
if name and pwd == 'password':
|
||||||
if user not in self.user_manager.users:
|
import IPython
|
||||||
session = self.spawn_single_user(user)
|
# IPython.embed()
|
||||||
else:
|
user = self.db.query(db.User).filter(db.User.name == name).first()
|
||||||
session = self.user_manager.users[user]
|
if user is None:
|
||||||
cookie_token = session.cookie_token
|
user = self.spawn_single_user(name)
|
||||||
self.set_cookie(session.cookie_name, cookie_token, path=session.url_prefix)
|
|
||||||
self.set_cookie(self.cookie_name, cookie_token, path=self.base_url)
|
# create and set a new cookie token for the single-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,
|
||||||
|
path=user.server.base_url,
|
||||||
|
)
|
||||||
|
|
||||||
|
# create and set a new cookie token for the hub
|
||||||
|
cookie_token = user.new_cookie_token()
|
||||||
|
self.db.add(cookie_token)
|
||||||
|
self.db.commit()
|
||||||
|
self.set_cookie(
|
||||||
|
self.hub.server.cookie_name,
|
||||||
|
cookie_token.token,
|
||||||
|
path=self.hub.server.base_url)
|
||||||
else:
|
else:
|
||||||
self._render(
|
self._render(
|
||||||
message={'error': 'Invalid username or password'},
|
message={'error': 'Invalid username or password'},
|
||||||
@@ -142,10 +238,10 @@ def token_authorized(method):
|
|||||||
if not match:
|
if not match:
|
||||||
raise web.HTTPError(403)
|
raise web.HTTPError(403)
|
||||||
token = match.group(1)
|
token = match.group(1)
|
||||||
session = self.user_manager.user_for_api_token(token)
|
db_token = self.db.query(db.APIToken).filter(db.APIToken.token == token).first()
|
||||||
if session is None:
|
self.log.info("Token: %s: %s", token, db_token)
|
||||||
|
if db_token is None:
|
||||||
raise web.HTTPError(403)
|
raise web.HTTPError(403)
|
||||||
self.request_session = session
|
|
||||||
return method(self, *args, **kwargs)
|
return method(self, *args, **kwargs)
|
||||||
check_token.__name__ = method.__name__
|
check_token.__name__ = method.__name__
|
||||||
check_token.__doc__ = method.__doc__
|
check_token.__doc__ = method.__doc__
|
||||||
@@ -155,12 +251,14 @@ def token_authorized(method):
|
|||||||
class AuthorizationsHandler(BaseHandler):
|
class AuthorizationsHandler(BaseHandler):
|
||||||
@token_authorized
|
@token_authorized
|
||||||
def get(self, token):
|
def get(self, token):
|
||||||
session = self.user_manager.user_for_cookie_token(token)
|
db_token = self.db.query(db.CookieToken).filter(db.CookieToken.token == token).first()
|
||||||
if session is None:
|
import IPython
|
||||||
app_log.debug('cookie tokens: %r',
|
IPython.embed()
|
||||||
{ user:s.cookie_token for user,s in self.user_manager.users.items() }
|
if db_token is None:
|
||||||
)
|
# app_log.debug('cookie tokens: %r',
|
||||||
|
# { user:s.cookie_token for user,s in self.user_manager.users.items() }
|
||||||
|
# )
|
||||||
raise web.HTTPError(404)
|
raise web.HTTPError(404)
|
||||||
self.write(json.dumps({
|
self.write(json.dumps({
|
||||||
'user' : session.user,
|
'user' : db_token.user.name,
|
||||||
}))
|
}))
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
"""Extend regular notebook server to be aware of multi-user things."""
|
"""Extend regular notebook server to be aware of multiuser things."""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
@@ -24,12 +24,12 @@ def verify_token(self, token):
|
|||||||
# we've seen this token before, don't ask upstream again
|
# we've seen this token before, don't ask upstream again
|
||||||
return token_cache[token]
|
return token_cache[token]
|
||||||
|
|
||||||
multiuser_api_url = self.settings['multiuser_api_url']
|
hub_api_url = self.settings['hub_api_url']
|
||||||
multiuser_api_key = self.settings['multiuser_api_key']
|
hub_api_key = self.settings['hub_api_key']
|
||||||
r = requests.get(utils.url_path_join(
|
r = requests.get(utils.url_path_join(
|
||||||
multiuser_api_url, "authorizations", token,
|
hub_api_url, "authorizations", token,
|
||||||
),
|
),
|
||||||
headers = {'Authorization' : 'token %s' % multiuser_api_key}
|
headers = {'Authorization' : 'token %s' % hub_api_key}
|
||||||
)
|
)
|
||||||
if r.status_code == 404:
|
if r.status_code == 404:
|
||||||
data = {'user' : ''}
|
data = {'user' : ''}
|
||||||
@@ -53,48 +53,52 @@ def get_current_user(self):
|
|||||||
if user == my_user:
|
if user == my_user:
|
||||||
return user
|
return user
|
||||||
else:
|
else:
|
||||||
raise web.HTTPError(403, "User %s does not have access to %s" % (user, my_user))
|
# import IPython
|
||||||
|
# IPython.embed()
|
||||||
|
return None
|
||||||
|
# imoprt
|
||||||
|
# raise web.HTTPError(403, "User %s does not have access to %s" % (user, my_user))
|
||||||
else:
|
else:
|
||||||
self.log.debug("No token cookie")
|
self.log.debug("No token cookie")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
# register new multi-user related command-line aliases
|
# register new hub related command-line aliases
|
||||||
aliases = NotebookApp.aliases.get_default_value()
|
aliases = NotebookApp.aliases.get_default_value()
|
||||||
aliases.update({
|
aliases.update({
|
||||||
'user' : 'SingleUserNotebookApp.user',
|
'user' : 'SingleUserNotebookApp.user',
|
||||||
'cookie-name': 'SingleUserNotebookApp.cookie_name',
|
'cookie-name': 'SingleUserNotebookApp.cookie_name',
|
||||||
'multiuser-prefix': 'SingleUserNotebookApp.multiuser_prefix',
|
'hub-prefix': 'SingleUserNotebookApp.hub_prefix',
|
||||||
'multiuser-api-url': 'SingleUserNotebookApp.multiuser_api_url',
|
'hub-api-url': 'SingleUserNotebookApp.hub_api_url',
|
||||||
'base-url': 'SingleUserNotebookApp.base_url',
|
'base-url': 'SingleUserNotebookApp.base_url',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class SingleUserNotebookApp(NotebookApp):
|
class SingleUserNotebookApp(NotebookApp):
|
||||||
"""A Subclass of the regular NotebookApp that is aware of the parent multi-user context."""
|
"""A Subclass of the regular NotebookApp that is aware of the parent multiuser context."""
|
||||||
user = Unicode(config=True)
|
user = Unicode(config=True)
|
||||||
cookie_name = Unicode(config=True)
|
cookie_name = Unicode(config=True)
|
||||||
multiuser_prefix = Unicode(config=True)
|
hub_prefix = Unicode(config=True)
|
||||||
multiuser_api_url = Unicode(config=True)
|
hub_api_url = Unicode(config=True)
|
||||||
aliases = aliases
|
aliases = aliases
|
||||||
browser = False
|
browser = False
|
||||||
|
|
||||||
def init_webapp(self):
|
def init_webapp(self):
|
||||||
# monkeypatch authentication to use the multi-user
|
# monkeypatch authentication to use the hub
|
||||||
from IPython.html.base.handlers import AuthenticatedHandler
|
from IPython.html.base.handlers import AuthenticatedHandler
|
||||||
AuthenticatedHandler.verify_token = verify_token
|
AuthenticatedHandler.verify_token = verify_token
|
||||||
AuthenticatedHandler.get_current_user = get_current_user
|
AuthenticatedHandler.get_current_user = get_current_user
|
||||||
|
|
||||||
# load the multi-user related settings into the tornado settings dict
|
# load the hub related settings into the tornado settings dict
|
||||||
env = os.environ
|
env = os.environ
|
||||||
s = self.webapp_settings
|
s = self.webapp_settings
|
||||||
s['token_cache'] = {}
|
s['token_cache'] = {}
|
||||||
s['user'] = self.user
|
s['user'] = self.user
|
||||||
s['multiuser_api_key'] = env.get('IPY_API_TOKEN', '')
|
s['hub_api_key'] = env.get('IPY_API_TOKEN', '')
|
||||||
s['cookie_secret'] = env.get('IPY_COOKIE_SECRET', '')
|
s['cookie_secret'] = env.get('IPY_COOKIE_SECRET', '')
|
||||||
s['cookie_name'] = self.cookie_name
|
s['cookie_name'] = self.cookie_name
|
||||||
s['login_url'] = utils.url_path_join(self.multiuser_prefix, 'login')
|
s['login_url'] = utils.url_path_join(self.hub_prefix, 'login')
|
||||||
s['multiuser_api_url'] = self.multiuser_api_url
|
s['hub_api_url'] = self.hub_api_url
|
||||||
super(SingleUserNotebookApp, self).init_webapp()
|
super(SingleUserNotebookApp, self).init_webapp()
|
||||||
|
|
||||||
|
|
||||||
|
185
multiuser/spawner.py
Normal file
185
multiuser/spawner.py
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
"""Class for spawning single-user notebook servers."""
|
||||||
|
|
||||||
|
# Copyright (c) Jupyter Development Team.
|
||||||
|
# Distributed under the terms of the Modified BSD License.
|
||||||
|
|
||||||
|
import errno
|
||||||
|
import os
|
||||||
|
import signal
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from subprocess import Popen
|
||||||
|
|
||||||
|
from tornado import gen
|
||||||
|
|
||||||
|
from IPython.config import LoggingConfigurable
|
||||||
|
from IPython.utils.traitlets import (
|
||||||
|
Any, Dict, Instance, Integer, List, Unicode,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
from .utils import random_port, wait_for_server
|
||||||
|
|
||||||
|
|
||||||
|
class Spawner(LoggingConfigurable):
|
||||||
|
"""Base class for spawning single-user notebook servers.
|
||||||
|
|
||||||
|
Subclass this, and override the following methods:
|
||||||
|
|
||||||
|
- load_state
|
||||||
|
- get_state
|
||||||
|
- start
|
||||||
|
- stop
|
||||||
|
- poll
|
||||||
|
"""
|
||||||
|
|
||||||
|
user = Any()
|
||||||
|
hub = Any()
|
||||||
|
api_token = Unicode()
|
||||||
|
|
||||||
|
env_prefix = Unicode('IPY_')
|
||||||
|
def _env_key(self, d, key, value):
|
||||||
|
d['%s%s' % (self.env_prefix, key)] = value
|
||||||
|
|
||||||
|
env = Dict()
|
||||||
|
def _env_default(self):
|
||||||
|
env = os.environ.copy()
|
||||||
|
self._env_key(env, 'COOKIE_SECRET', self.user.server.cookie_secret)
|
||||||
|
self._env_key(env, 'API_TOKEN', self.api_token)
|
||||||
|
return env
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def fromJSON(cls, state, **kwargs):
|
||||||
|
"""Create a new instance, and load its JSON state
|
||||||
|
|
||||||
|
state will be a dict, loaded from JSON in the database.
|
||||||
|
"""
|
||||||
|
inst = cls(**kwargs)
|
||||||
|
inst.load_state(state)
|
||||||
|
return inst
|
||||||
|
|
||||||
|
def load_state(self, state):
|
||||||
|
"""load state from the database
|
||||||
|
|
||||||
|
This is the extensible part of state
|
||||||
|
|
||||||
|
Override in a subclass if there is state to load.
|
||||||
|
|
||||||
|
See Also
|
||||||
|
--------
|
||||||
|
|
||||||
|
get_state
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_state(self):
|
||||||
|
"""store the state necessary for load_state
|
||||||
|
|
||||||
|
A black box of extra state for custom spawners
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
|
||||||
|
state: dict
|
||||||
|
a JSONable dict of state
|
||||||
|
"""
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def get_args(self):
|
||||||
|
"""Return the arguments to be passed after self.cmd"""
|
||||||
|
return [
|
||||||
|
'--user=%s' % self.user.name,
|
||||||
|
'--port=%i' % self.user.server.port,
|
||||||
|
'--cookie-name=%s' % self.user.server.cookie_name,
|
||||||
|
'--base-url=%s' % self.user.server.base_url,
|
||||||
|
|
||||||
|
'--hub-prefix=%s' % self.hub.server.base_url,
|
||||||
|
'--hub-api-url=%s' % self.hub.api_host_url,
|
||||||
|
]
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
raise NotImplementedError("Override in subclass")
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
raise NotImplementedError("Override in subclass")
|
||||||
|
|
||||||
|
def poll(self):
|
||||||
|
raise NotImplementedError("Override in subclass")
|
||||||
|
|
||||||
|
|
||||||
|
class PopenSpawner(Spawner):
|
||||||
|
"""A Spawner that just uses Popen to start local processes."""
|
||||||
|
cmd = List(Unicode, config=True,
|
||||||
|
help="""The command used for starting notebooks."""
|
||||||
|
)
|
||||||
|
def _cmd_default(self):
|
||||||
|
# should have sudo -u self.user
|
||||||
|
return [sys.executable, '-m', 'multiuser.singleuser']
|
||||||
|
|
||||||
|
proc = Instance(Popen)
|
||||||
|
pid = Integer()
|
||||||
|
|
||||||
|
def load_state(self, state):
|
||||||
|
self.pid = state['pid']
|
||||||
|
|
||||||
|
def get_state(self):
|
||||||
|
return dict(pid=self.pid)
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
self.user.server.port = random_port()
|
||||||
|
cmd = self.cmd + self.get_args()
|
||||||
|
|
||||||
|
self.log.info("Spawning %r", cmd)
|
||||||
|
self.proc = Popen(cmd, env=self.env,
|
||||||
|
# don't forward signals
|
||||||
|
preexec_fn=os.setpgrp,
|
||||||
|
)
|
||||||
|
self.pid = self.proc.pid
|
||||||
|
|
||||||
|
def poll(self):
|
||||||
|
# if we started the process, poll with Popen
|
||||||
|
if self.proc is not None:
|
||||||
|
return self.proc.poll()
|
||||||
|
|
||||||
|
# if we resumed from stored state,
|
||||||
|
# we don't have the Popen handle anymore
|
||||||
|
|
||||||
|
# this doesn't work on Windows.
|
||||||
|
# multi-user doesn't support Windows.
|
||||||
|
try:
|
||||||
|
os.kill(self.pid, 0)
|
||||||
|
except OSError as e:
|
||||||
|
if e.errno == errno.ESRCH:
|
||||||
|
# no such process, return exitcode == 0, since we don't know
|
||||||
|
return 0
|
||||||
|
else:
|
||||||
|
# None indicates the process is running
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _wait_for_death(self, timeout=10):
|
||||||
|
"""wait for the process to die, up to timeout seconds"""
|
||||||
|
for i in range(int(timeout * 10)):
|
||||||
|
if self.poll() is None:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
def stop(self, now=False):
|
||||||
|
"""stop the subprocess"""
|
||||||
|
if not now:
|
||||||
|
# double-sigint to request clean shutdown
|
||||||
|
os.kill(self.pid, signal.SIGINT)
|
||||||
|
os.kill(self.pid, signal.SIGINT)
|
||||||
|
self._wait_for_death(10)
|
||||||
|
|
||||||
|
# clean shutdown failed, use TERM
|
||||||
|
if self.poll() is None:
|
||||||
|
os.kill(self.pid, signal.SIGTERM)
|
||||||
|
self._wait_for_death(5)
|
||||||
|
|
||||||
|
# TERM failed, use KILL
|
||||||
|
if self.poll() is None:
|
||||||
|
os.kill(self.pid, signal.SIGKILL)
|
||||||
|
self._wait_for_death(5)
|
||||||
|
|
||||||
|
# it all failed, zombie process
|
@@ -1,180 +0,0 @@
|
|||||||
"""Classes for managing users."""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import signal
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
import uuid
|
|
||||||
from subprocess import Popen
|
|
||||||
|
|
||||||
import requests
|
|
||||||
|
|
||||||
from tornado.log import app_log
|
|
||||||
|
|
||||||
from IPython.utils.traitlets import Any, Unicode, Integer, Dict
|
|
||||||
from IPython.config import LoggingConfigurable
|
|
||||||
|
|
||||||
from .utils import random_port, wait_for_server
|
|
||||||
|
|
||||||
class UserSession(LoggingConfigurable):
|
|
||||||
env_prefix = Unicode('IPY_')
|
|
||||||
process = Any()
|
|
||||||
port = Integer()
|
|
||||||
user = Unicode()
|
|
||||||
cookie_secret = Unicode()
|
|
||||||
cookie_name = Unicode()
|
|
||||||
def _cookie_name_default(self):
|
|
||||||
return 'ipy-multiuser-%s' % self.user
|
|
||||||
|
|
||||||
multiuser_prefix = Unicode()
|
|
||||||
multiuser_api_url = Unicode()
|
|
||||||
|
|
||||||
url_prefix = Unicode()
|
|
||||||
def _url_prefix_default(self):
|
|
||||||
return '/user/%s/' % self.user
|
|
||||||
|
|
||||||
api_token = Unicode()
|
|
||||||
def _api_token_default(self):
|
|
||||||
return str(uuid.uuid4())
|
|
||||||
|
|
||||||
cookie_token = Unicode()
|
|
||||||
def _cookie_token_default(self):
|
|
||||||
return str(uuid.uuid4())
|
|
||||||
|
|
||||||
def _env_key(self, d, key, value):
|
|
||||||
d['%s%s' % (self.env_prefix, key)] = value
|
|
||||||
|
|
||||||
env = Dict()
|
|
||||||
def _env_default(self):
|
|
||||||
env = os.environ.copy()
|
|
||||||
self._env_key(env, 'COOKIE_SECRET', self.cookie_secret)
|
|
||||||
self._env_key(env, 'API_TOKEN', self.api_token)
|
|
||||||
return env
|
|
||||||
|
|
||||||
@property
|
|
||||||
def auth_data(self):
|
|
||||||
return dict(
|
|
||||||
user=self.user,
|
|
||||||
)
|
|
||||||
|
|
||||||
def start(self):
|
|
||||||
assert self.process is None or self.process.poll() is not None
|
|
||||||
cmd = [sys.executable, '-m', 'multiuser.singleuser',
|
|
||||||
'--user=%s' % self.user, '--port=%i' % self.port,
|
|
||||||
'--cookie-name=%s' % self.cookie_name,
|
|
||||||
'--multiuser-prefix=%s' % self.multiuser_prefix,
|
|
||||||
'--multiuser-api-url=%s' % self.multiuser_api_url,
|
|
||||||
'--base-url=%s' % self.url_prefix,
|
|
||||||
]
|
|
||||||
app_log.info("Spawning: %s" % cmd)
|
|
||||||
self.process = Popen(cmd, env=self.env,
|
|
||||||
# don't forward signals:
|
|
||||||
preexec_fn=os.setpgrp,
|
|
||||||
)
|
|
||||||
|
|
||||||
def running(self):
|
|
||||||
if self.process is None:
|
|
||||||
return False
|
|
||||||
if self.process.poll() is not None:
|
|
||||||
self.process = None
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def request_stop(self):
|
|
||||||
if self.running():
|
|
||||||
self.process.send_signal(signal.SIGINT)
|
|
||||||
time.sleep(0.1)
|
|
||||||
if self.running():
|
|
||||||
self.process.send_signal(signal.SIGINT)
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
for i in range(100):
|
|
||||||
if self.running():
|
|
||||||
time.sleep(0.1)
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
if self.running():
|
|
||||||
self.process.terminate()
|
|
||||||
|
|
||||||
|
|
||||||
class UserManager(LoggingConfigurable):
|
|
||||||
|
|
||||||
users = Dict()
|
|
||||||
routes_t = Unicode('http://{ip}:{port}/api/routes{uri}')
|
|
||||||
single_user_t = Unicode('http://{ip}:{port}')
|
|
||||||
|
|
||||||
single_user_ip = Unicode('localhost')
|
|
||||||
proxy_ip = Unicode('localhost')
|
|
||||||
proxy_port = Integer(8001)
|
|
||||||
|
|
||||||
proxy_auth_token = Unicode()
|
|
||||||
def _proxy_auth_token_default(self):
|
|
||||||
return str(uuid.uuid4())
|
|
||||||
|
|
||||||
def get_session(self, user, **kwargs):
|
|
||||||
if user not in self.users:
|
|
||||||
kwargs['user'] = user
|
|
||||||
self.users[user] = UserSession(**kwargs)
|
|
||||||
return self.users[user]
|
|
||||||
|
|
||||||
def spawn(self, user):
|
|
||||||
session = self.get_session(user)
|
|
||||||
if session.running():
|
|
||||||
app_log.warn("User session %s already running", user)
|
|
||||||
return
|
|
||||||
session.port = port = random_port()
|
|
||||||
session.start()
|
|
||||||
|
|
||||||
r = requests.post(
|
|
||||||
self.routes_t.format(
|
|
||||||
ip=self.proxy_ip,
|
|
||||||
port=self.proxy_port,
|
|
||||||
uri=session.url_prefix,
|
|
||||||
),
|
|
||||||
data=json.dumps(dict(
|
|
||||||
target=self.single_user_t.format(
|
|
||||||
ip=self.single_user_ip,
|
|
||||||
port=port
|
|
||||||
),
|
|
||||||
user=user,
|
|
||||||
)),
|
|
||||||
headers={'Authorization': "token %s" % self.proxy_auth_token},
|
|
||||||
)
|
|
||||||
wait_for_server(self.single_user_ip, port)
|
|
||||||
r.raise_for_status()
|
|
||||||
|
|
||||||
def user_for_api_token(self, token):
|
|
||||||
"""Get the user session object for a given API token"""
|
|
||||||
for session in self.users.values():
|
|
||||||
if session.api_token == token:
|
|
||||||
return session
|
|
||||||
|
|
||||||
def user_for_cookie_token(self, token):
|
|
||||||
"""Get the user session object for a given cookie token"""
|
|
||||||
for session in self.users.values():
|
|
||||||
if session.cookie_token == token:
|
|
||||||
return session
|
|
||||||
|
|
||||||
def shutdown(self, user):
|
|
||||||
assert user in self.users
|
|
||||||
session = self.users.pop(user)
|
|
||||||
session.stop()
|
|
||||||
r = requests.delete(self.routes_url,
|
|
||||||
data=json.dumps(user=user, port=session.port),
|
|
||||||
)
|
|
||||||
r.raise_for_status()
|
|
||||||
|
|
||||||
def cleanup(self):
|
|
||||||
sessions = list(self.users.values())
|
|
||||||
self.users = {}
|
|
||||||
for session in sessions:
|
|
||||||
self.log.info("Cleaning up %s's server" % session.user)
|
|
||||||
session.request_stop()
|
|
||||||
for i in range(100):
|
|
||||||
if any([ session.running() for session in sessions ]):
|
|
||||||
time.sleep(0.1)
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
for session in sessions:
|
|
||||||
session.stop()
|
|
@@ -1,11 +1,13 @@
|
|||||||
"""Miscellaneous utilities"""
|
"""Miscellaneous utilities"""
|
||||||
|
|
||||||
# Copyright (c) IPython Development Team.
|
# Copyright (c) Jupyter Development Team.
|
||||||
# Distributed under the terms of the Modified BSD License.
|
# Distributed under the terms of the Modified BSD License.
|
||||||
|
|
||||||
import socket
|
import socket
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def random_port():
|
def random_port():
|
||||||
"""get a single random port"""
|
"""get a single random port"""
|
||||||
sock = socket.socket()
|
sock = socket.socket()
|
||||||
@@ -14,6 +16,7 @@ def random_port():
|
|||||||
sock.close()
|
sock.close()
|
||||||
return port
|
return port
|
||||||
|
|
||||||
|
|
||||||
def wait_for_server(ip, port, timeout=10):
|
def wait_for_server(ip, port, timeout=10):
|
||||||
"""wait for a server to show up at ip:port"""
|
"""wait for a server to show up at ip:port"""
|
||||||
tic = time.time()
|
tic = time.time()
|
||||||
|
Reference in New Issue
Block a user