diff --git a/jupyterhub/orm.py b/jupyterhub/orm.py index 0f1fb437..45bfb25c 100644 --- a/jupyterhub/orm.py +++ b/jupyterhub/orm.py @@ -12,7 +12,7 @@ from tornado import gen from tornado.log import app_log from tornado.httpclient import HTTPRequest, AsyncHTTPClient -from sqlalchemy.types import TypeDecorator, VARCHAR +from sqlalchemy.types import TypeDecorator, TEXT from sqlalchemy import ( inspect, Column, Integer, ForeignKey, Unicode, Boolean, @@ -39,7 +39,7 @@ class JSONDict(TypeDecorator): """ - impl = VARCHAR + impl = TEXT def process_bind_param(self, value, dialect): if value is not None: @@ -59,20 +59,20 @@ Base.log = app_log class Server(Base): """The basic state of a server - + connection and cookie info """ __tablename__ = 'servers' id = Column(Integer, primary_key=True) - proto = Column(Unicode, default='http') - ip = Column(Unicode, default='') + proto = Column(Unicode(15), default='http') + ip = Column(Unicode(255), default='') # could also be a DNS name port = Column(Integer, default=random_port) - base_url = Column(Unicode, default='/') - cookie_name = Column(Unicode, default='cookie') - + base_url = Column(Unicode(255), default='/') + cookie_name = Column(Unicode(255), default='cookie') + def __repr__(self): return "" % (self.ip, self.port) - + @property def host(self): ip = self.ip @@ -91,18 +91,18 @@ class Server(Base): host=self.host, uri=self.base_url, ) - + @property def bind_url(self): """representation of URL used for binding - + Never used in APIs, only logging, since it can be non-connectable value, such as '', meaning all interfaces. """ if self.ip in {'', '0.0.0.0'}: return self.url.replace('127.0.0.1', self.ip or '*', 1) return self.url - + @gen.coroutine def wait_up(self, timeout=10, http=False): """Wait for this server to come up""" @@ -110,7 +110,7 @@ class Server(Base): yield wait_for_http_server(self.url, timeout=timeout) else: yield wait_for_server(self.ip or '127.0.0.1', self.port, timeout=timeout) - + def is_up(self): """Is the server accepting connections?""" try: @@ -136,7 +136,7 @@ class Server(Base): class Proxy(Base): """A configurable-http-proxy instance. - + A proxy consists of the API server info and the public-facing server info, plus an auth token for configuring the proxy table. """ @@ -147,7 +147,7 @@ class Proxy(Base): 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>" % ( @@ -155,7 +155,7 @@ class Proxy(Base): ) else: return "<%s [unconfigured]>" % self.__class__.__name__ - + def api_request(self, path, method='GET', body=None, client=None): """Make an authenticated API request of the proxy""" client = client or AsyncHTTPClient() @@ -178,10 +178,11 @@ class Proxy(Base): self.log.info("Adding user %s to proxy %s => %s", user.name, user.proxy_path, user.server.host, ) + if user.spawn_pending: raise RuntimeError( "User %s's spawn is pending, shouldn't be added to the proxy yet!", user.name) - + yield self.api_request(user.proxy_path, method='POST', body=dict( @@ -190,7 +191,7 @@ class Proxy(Base): ), client=client, ) - + @gen.coroutine def delete_user(self, user, client=None): """Remove a user's server to the proxy table.""" @@ -199,7 +200,7 @@ class Proxy(Base): method='DELETE', client=client, ) - + @gen.coroutine def get_routes(self, client=None): """Fetch the proxy's routes""" @@ -209,7 +210,7 @@ class Proxy(Base): @gen.coroutine def add_all_users(self, user_dict): """Update the proxy table from the database. - + Used when loading up a new proxy. """ db = inspect(self).session @@ -251,9 +252,9 @@ class Proxy(Base): class Hub(Base): """Bring it all together at the hub. - + The Hub is a server, plus its API path suffix - + the api_url is the full URL plus the api_path suffix on the end of the server base_url. """ @@ -262,12 +263,12 @@ class Hub(Base): _server_id = Column(Integer, ForeignKey('servers.id')) server = relationship(Server, primaryjoin=_server_id == Server.id) host = '' - + @property def api_url(self): """return the full API url (with proto://host...)""" return url_path_join(self.server.url, 'api') - + def __repr__(self): if self.server: return "<%s %s:%s>" % ( @@ -279,31 +280,31 @@ class Hub(Base): class User(Base): """The User table - + Each user has a single server, and multiple tokens used for authorization. - + API tokens grant access to the Hub's REST API. These are used by single-user servers to authenticate requests, and external services to manipulate the Hub. - + Cookies are set with a single ID. Resetting the Cookie ID invalidates all cookies, forcing user to login again. - + A `state` column contains a JSON dict, used for restoring state of a Spawner. """ __tablename__ = 'users' id = Column(Integer, primary_key=True, autoincrement=True) - name = Column(Unicode) + name = Column(Unicode(1023)) # 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) last_activity = Column(DateTime, default=datetime.utcnow) - + api_tokens = relationship("APIToken", backref="user") - cookie_id = Column(Unicode, default=new_token) + cookie_id = Column(Unicode(1023), default=new_token) state = Column(JSONDict) other_user_cookies = set([]) @@ -321,7 +322,7 @@ class User(Base): cls=self.__class__.__name__, name=self.name, ) - + def new_api_token(self): """Create a new API token""" assert self.id is not None @@ -344,29 +345,29 @@ class User(Base): class APIToken(Base): """An API token""" __tablename__ = 'api_tokens' - + @declared_attr def user_id(cls): return Column(Integer, ForeignKey('users.id')) id = Column(Integer, primary_key=True) - hashed = Column(Unicode) - prefix = Column(Unicode) + hashed = Column(Unicode(1023)) + prefix = Column(Unicode(1023)) prefix_length = 4 algorithm = "sha512" rounds = 16384 salt_bytes = 8 - + @property def token(self): raise AttributeError("token is write-only") - + @token.setter def token(self, token): """Store the hashed value and prefix for a token""" self.prefix = token[:self.prefix_length] self.hashed = hash_token(token, rounds=self.rounds, salt=self.salt_bytes, algorithm=self.algorithm) - + def __repr__(self): return "<{cls}('{pre}...', user='{u}')>".format( cls=self.__class__.__name__, @@ -387,7 +388,7 @@ class APIToken(Base): for orm_token in prefix_match: if orm_token.match(token): return orm_token - + def match(self, token): """Is this my token?""" return compare_token(self.hashed, token)