Merge pull request #37 from jupyterhub/authenticator-support

Support using arbitrary set of installed authenticators
This commit is contained in:
Yuvi Panda
2018-07-16 16:24:24 -07:00
committed by GitHub
4 changed files with 195 additions and 31 deletions

145
tests/test_configurer.py Normal file
View File

@@ -0,0 +1,145 @@
"""
Test
"""
from tljh import configurer
class MockConfigurer:
"""
Mock a Traitlet Configurable object.
Equivalent to the `c` in `c.JupyterHub.some_property` method of setting
traitlet properties. If an accessed attribute doesn't exist, a new instance
of EmtpyObject is returned. This lets us set arbitrary attributes two
levels deep.
>>> c = MockConfigurer()
>>> c.FirstLevel.second_level = 'hi'
>>> c.FirstLevel.second_level == 'hi'
True
>>> hasattr(c.FirstLevel, 'does_not_exist')
False
"""
class _EmptyObject:
"""
Empty class for putting attributes in.
"""
pass
def __getattr__(self, k):
if k not in self.__dict__:
self.__dict__[k] = MockConfigurer._EmptyObject()
return self.__dict__[k]
def test_mock_configurer():
"""
Test the MockConfigurer's mocking ability
"""
m = MockConfigurer()
m.SomethingSomething = 'hi'
m.FirstLevel.second_level = 'boo'
assert m.SomethingSomething == 'hi'
assert m.FirstLevel.second_level == 'boo'
assert not hasattr(m.FirstLevel, 'non_existent')
def apply_mock_config(overrides):
"""
Configure a mock configurer with given overrides.
overrides should be a dict that matches what you parse from a config.yaml
"""
c = MockConfigurer()
configurer.apply_config(overrides, c)
return c
def test_app_default():
"""
Test default application with no config overrides.
"""
c = apply_mock_config({})
# default_url is not set, so JupyterHub will pick default.
assert not hasattr(c.Spawner, 'default_url')
def test_app_jupyterlab():
"""
Test setting JupyterLab as default application
"""
c = apply_mock_config({'user_environment': {'default_app': 'jupyterlab'}})
assert c.Spawner.default_url == '/lab'
def test_app_nteract():
"""
Test setting nteract as default application
"""
c = apply_mock_config({'user_environment': {'default_app': 'nteract'}})
assert c.Spawner.default_url == '/nteract'
def test_auth_default():
"""
Test default authentication settings with no overrides
"""
c = apply_mock_config({})
assert c.JupyterHub.authenticator_class == 'firstuseauthenticator.FirstUseAuthenticator'
# Do not auto create users who haven't been manually added by default
assert not c.FirstUseAuthenticator.create_users
def test_auth_dummy():
"""
Test setting Dummy Authenticator & password
"""
c = apply_mock_config({
'auth': {
'type': 'dummyauthenticator.DummyAuthenticator',
'DummyAuthenticator': {
'password': 'test'
}
}
})
assert c.JupyterHub.authenticator_class == 'dummyauthenticator.DummyAuthenticator'
assert c.DummyAuthenticator.password == 'test'
def test_auth_firstuse():
"""
Test setting FirstUse Authenticator options
"""
c = apply_mock_config({
'auth': {
'type': 'firstuseauthenticator.FirstUseAuthenticator',
'FirstUseAuthenticator': {
'create_users': True
}
}
})
assert c.JupyterHub.authenticator_class == 'firstuseauthenticator.FirstUseAuthenticator'
assert c.FirstUseAuthenticator.create_users
def test_auth_github():
"""
Test using GitHub authenticator
"""
c = apply_mock_config({
'auth': {
'type': 'oauthenticator.github.GitHubOAuthenticator',
'GitHubOAuthenticator': {
'client_id': 'something',
'client_secret': 'something-else'
}
}
})
assert c.JupyterHub.authenticator_class == 'oauthenticator.github.GitHubOAuthenticator'
assert c.GitHubOAuthenticator.client_id == 'something'
assert c.GitHubOAuthenticator.client_secret == 'something-else'

View File

@@ -7,18 +7,13 @@ be called many times per lifetime of a jupyterhub.
Traitlets that modify the startup of JupyterHub should not be here.
FIXME: A strong feeling that JSON Schema should be involved somehow.
"""
import copy
import os
import yaml
# Default configuration for tljh
# User provided config is merged into this
default = {
'auth': {
'type': 'firstuse',
'dummy': {},
'firstuse': {
'createUsers': False
'type': 'firstuseauthenticator.FirstUseAuthenticator',
'FirstUseAuthenticator': {
'create_users': False
}
},
'users': {
@@ -30,20 +25,18 @@ default = {
'memory': '1G',
'cpu': None
},
'userEnvironment': {
'defaultApp': 'classic'
'user_environment': {
'default_app': 'classic'
}
}
def apply_yaml_config(path, c):
if os.path.exists(path):
with open(path) as f:
# FIXME: Figure out correct order of merging here
tljh_config = _merge_dictionaries(dict(default), yaml.safe_load(f))
else:
tljh_config = copy.deepcopy(default)
def apply_config(config_overrides, c):
"""
Merge config_overrides with config defaults & apply to JupyterHub config c
"""
tljh_config = _merge_dictionaries(dict(default), config_overrides)
update_auth(c, tljh_config)
update_userlists(c, tljh_config)
@@ -52,21 +45,37 @@ def apply_yaml_config(path, c):
update_user_account_config(c, tljh_config)
def set_if_not_none(parent, key, value):
"""
Set attribute 'key' on parent if value is not None
"""
if value is not None:
setattr(parent, key, value)
def update_auth(c, config):
"""
Set auth related configuration from YAML config file
Use auth.type to determine authenticator to use. All parameters
in the config under auth.{auth.type} will be passed straight to the
authenticators themselves.
"""
auth = config.get('auth')
if auth['type'] == 'dummy':
c.JupyterHub.authenticator_class = 'dummyauthenticator.DummyAuthenticator'
password = auth['dummy'].get('password')
if password is not None:
c.DummyAuthenticator.password = password
return
elif auth['type'] == 'firstuse':
c.JupyterHub.authenticator_class = 'firstuseauthenticator.FirstUseAuthenticator'
c.FirstUseAuthenticator.create_users = auth['firstuse']['createUsers']
# FIXME: Make sure this is something importable.
# FIXME: SECURITY: Class must inherit from Authenticator, to prevent us being
# used to set arbitrary properties on arbitrary types of objects!
authenticator_class = auth['type']
# When specifying fully qualified name, use classname as key for config
authenticator_configname = authenticator_class.split('.')[-1]
c.JupyterHub.authenticator_class = authenticator_class
# Use just class name when setting config. If authenticator is dummyauthenticator.DummyAuthenticator,
# its config will be set under c.DummyAuthenticator
authenticator_parent = getattr(c, authenticator_class.split('.')[-1])
for k, v in auth.get(authenticator_configname, {}).items():
set_if_not_none(authenticator_parent, k, v)
def update_userlists(c, config):
@@ -94,12 +103,12 @@ def update_user_environment(c, config):
"""
Set user environment configuration
"""
user_env = config['userEnvironment']
user_env = config['user_environment']
# Set default application users are launched into
if user_env['defaultApp'] == 'jupyterlab':
if user_env['default_app'] == 'jupyterlab':
c.Spawner.default_url = '/lab'
elif user_env['defaultApp'] == 'nteract':
elif user_env['default_app'] == 'nteract':
c.Spawner.default_url = '/nteract'

View File

@@ -73,7 +73,9 @@ def ensure_jupyterhub_package(prefix):
'jupyterhub==0.9.0',
'jupyterhub-dummyauthenticator==0.3.1',
'jupyterhub-systemdspawner==0.11',
'jupyterhub-firstuseauthenticator==0.10'
'jupyterhub-firstuseauthenticator==0.10',
'jupyterhub-ldapauthenticator==1.2.2',
'oauthenticator==0.7.3',
])

View File

@@ -4,6 +4,8 @@ JupyterHub config for the littlest jupyterhub.
import os
from systemdspawner import SystemdSpawner
from tljh import user, configurer
import yaml
import copy
INSTALL_PREFIX = os.environ.get('TLJH_INSTALL_PREFIX')
USER_ENV_PREFIX = os.path.join(INSTALL_PREFIX, 'user')
@@ -40,4 +42,10 @@ c.SystemdSpawner.default_shell = '/bin/bash'
# Drop the '-singleuser' suffix present in the default template
c.SystemdSpawner.unit_name_template = 'jupyter-{USERNAME}'
configurer.apply_yaml_config(os.path.join(INSTALL_PREFIX, 'config.yaml'), c)
config_overrides_path = os.path.join(INSTALL_PREFIX, 'config.yaml')
if os.path.exists(config_overrides_path):
with open(config_overrides_path) as f:
config_overrides = yaml.safe_load(f)
else:
config_overrides = {}
configurer.apply_config(config_overrides, c)