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.
|
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'
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user