Merge pull request #517 from minrk/load-tokens

allow pre-loading API tokens from config
This commit is contained in:
Carol Willing
2016-04-15 06:49:40 -07:00
4 changed files with 101 additions and 3 deletions

View File

@@ -352,6 +352,13 @@ class JupyterHub(Application):
cookie_secret_file = Unicode('jupyterhub_cookie_secret', cookie_secret_file = Unicode('jupyterhub_cookie_secret',
help="""File in which to store the cookie secret.""" help="""File in which to store the cookie secret."""
).tag(config=True) ).tag(config=True)
api_tokens = Dict(Unicode(),
help="""Dict of token:username to be loaded into the database.
Allows ahead-of-time generation of API tokens for use by services.
"""
).tag(config=True)
authenticator_class = Type(PAMAuthenticator, Authenticator, authenticator_class = Type(PAMAuthenticator, Authenticator,
help="""Class for authenticating users. help="""Class for authenticating users.
@@ -794,6 +801,37 @@ class JupyterHub(Application):
# From this point on, any user changes should be done simultaneously # From this point on, any user changes should be done simultaneously
# to the whitelist set and user db, unless the whitelist is empty (all users allowed). # to the whitelist set and user db, unless the whitelist is empty (all users allowed).
def init_api_tokens(self):
"""Load predefined API tokens (for services) into database"""
db = self.db
for token, username in self.api_tokens.items():
username = self.authenticator.normalize_username(username)
if not self.authenticator.check_whitelist(username):
raise ValueError("Token username %r is not in whitelist" % username)
if not self.authenticator.validate_username(username):
raise ValueError("Token username %r is not valid" % username)
orm_token = orm.APIToken.find(db, token)
if orm_token is None:
user = orm.User.find(db, username)
user_created = False
if user is None:
user_created = True
self.log.debug("Adding user %r to database", username)
user = orm.User(name=username)
db.add(user)
db.commit()
self.log.info("Adding API token for %s", username)
try:
user.new_api_token(token)
except Exception:
if user_created:
# don't allow bad tokens to create users
db.delete(user)
db.commit()
raise
else:
self.log.debug("Not duplicating token %s", orm_token)
db.commit()
@gen.coroutine @gen.coroutine
def init_spawners(self): def init_spawners(self):
@@ -1055,6 +1093,7 @@ class JupyterHub(Application):
self.init_hub() self.init_hub()
self.init_proxy() self.init_proxy()
yield self.init_users() yield self.init_users()
self.init_api_tokens()
self.init_tornado_settings() self.init_tornado_settings()
yield self.init_spawners() yield self.init_spawners()
self.init_handlers() self.init_handlers()

View File

@@ -303,11 +303,21 @@ class User(Base):
name=self.name, name=self.name,
) )
def new_api_token(self): def new_api_token(self, token=None):
"""Create a new API token""" """Create a new API token
If `token` is given, load that token.
"""
assert self.id is not None assert self.id is not None
db = inspect(self).session db = inspect(self).session
token = new_token() if token is None:
token = new_token()
else:
if len(token) < 8:
raise ValueError("Tokens must be at least 8 characters, got %r" % token)
found = APIToken.find(db, token)
if found:
raise ValueError("Collision on token: %s..." % token[:4])
orm_token = APIToken(user_id=self.id) orm_token = APIToken(user_id=self.id)
orm_token.token = token orm_token.token = token
db.add(orm_token) db.add(orm_token)

View File

@@ -5,7 +5,11 @@ import re
import sys import sys
from subprocess import check_output, Popen, PIPE from subprocess import check_output, Popen, PIPE
from tempfile import NamedTemporaryFile, TemporaryDirectory from tempfile import NamedTemporaryFile, TemporaryDirectory
import pytest
from .mocking import MockHub from .mocking import MockHub
from .. import orm
def test_help_all(): def test_help_all():
out = check_output([sys.executable, '-m', 'jupyterhub', '--help-all']).decode('utf8', 'replace') out = check_output([sys.executable, '-m', 'jupyterhub', '--help-all']).decode('utf8', 'replace')
@@ -48,3 +52,38 @@ def test_generate_config():
assert cfg_file in out assert cfg_file in out
assert 'Spawner.cmd' in cfg_text assert 'Spawner.cmd' in cfg_text
assert 'Authenticator.whitelist' in cfg_text assert 'Authenticator.whitelist' in cfg_text
def test_init_tokens():
with TemporaryDirectory() as td:
db_file = os.path.join(td, 'jupyterhub.sqlite')
tokens = {
'super-secret-token': 'alyx',
'also-super-secret': 'gordon',
'boagasdfasdf': 'chell',
}
app = MockHub(db_file=db_file, api_tokens=tokens)
app.initialize([])
db = app.db
for token, username in tokens.items():
api_token = orm.APIToken.find(db, token)
assert api_token is not None
user = api_token.user
assert user.name == username
# simulate second startup, reloading same tokens:
app = MockHub(db_file=db_file, api_tokens=tokens)
app.initialize([])
db = app.db
for token, username in tokens.items():
api_token = orm.APIToken.find(db, token)
assert api_token is not None
user = api_token.user
assert user.name == username
# don't allow failed token insertion to create users:
tokens['short'] = 'gman'
app = MockHub(db_file=db_file, api_tokens=tokens)
# with pytest.raises(ValueError):
app.initialize([])
assert orm.User.find(app.db, 'gman') is None

View File

@@ -93,6 +93,16 @@ def test_tokens(db):
found = orm.APIToken.find(db, 'something else') found = orm.APIToken.find(db, 'something else')
assert found is None assert found is None
secret = 'super-secret-preload-token'
token = user.new_api_token(secret)
assert token == secret
assert len(user.api_tokens) == 3
# raise ValueError on collision
with pytest.raises(ValueError):
user.new_api_token(token)
assert len(user.api_tokens) == 3
def test_spawn_fails(db, io_loop): def test_spawn_fails(db, io_loop):
orm_user = orm.User(name='aeofel') orm_user = orm.User(name='aeofel')