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

View File

@@ -73,7 +73,9 @@ def ensure_jupyterhub_package(prefix):
'jupyterhub==0.9.0', 'jupyterhub==0.9.0',
'jupyterhub-dummyauthenticator==0.3.1', 'jupyterhub-dummyauthenticator==0.3.1',
'jupyterhub-systemdspawner==0.11', '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 import os
from systemdspawner import SystemdSpawner from systemdspawner import SystemdSpawner
from tljh import user, configurer from tljh import user, configurer
import yaml
import copy
INSTALL_PREFIX = os.environ.get('TLJH_INSTALL_PREFIX') INSTALL_PREFIX = os.environ.get('TLJH_INSTALL_PREFIX')
USER_ENV_PREFIX = os.path.join(INSTALL_PREFIX, 'user') 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 # Drop the '-singleuser' suffix present in the default template
c.SystemdSpawner.unit_name_template = 'jupyter-{USERNAME}' 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)