mirror of
https://github.com/jupyterhub/the-littlest-jupyterhub.git
synced 2025-12-18 21:54:05 +08:00
Merge pull request #37 from jupyterhub/authenticator-support
Support using arbitrary set of installed authenticators
This commit is contained in:
145
tests/test_configurer.py
Normal file
145
tests/test_configurer.py
Normal 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'
|
||||
@@ -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'
|
||||
|
||||
|
||||
|
||||
@@ -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',
|
||||
])
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user