diff --git a/docs/topic/tljh-config.rst b/docs/topic/tljh-config.rst index 5060d29..e4eeab6 100644 --- a/docs/topic/tljh-config.rst +++ b/docs/topic/tljh-config.rst @@ -148,6 +148,34 @@ User Environment sudo tljh-config set user_environment.default_app jupyterlab +.. _tljh-set-extra-user-groups: + +Extra User Groups +================= + + +``users.extra_user_groups`` is a configuration option that can be used +to automatically add a user to a specific group. By default, there are +no extra groups defined. + +Users can be "paired" with the desired, **existing** groups using: + +* ``tljh-config set``, if only one user is to be added to the + desired group: + +.. code-block:: bash + + tljh-config set users.extra_user_groups.group1 user1 + +* ``tljh-config add-item``, if multiple users are to be added to + the group: + +.. code-block:: bash + + tljh-config add-item users.extra_user_groups.group1 user1 + tljh-config add-item users.extra_user_groups.group1 user2 + + .. _tljh-view-conf: View current configuration diff --git a/integration-tests/test_hub.py b/integration-tests/test_hub.py index 550e1ab..d47893d 100644 --- a/integration-tests/test_hub.py +++ b/integration-tests/test_hub.py @@ -10,6 +10,7 @@ import pwd import grp import sys import subprocess +from os import system from tljh.normalize import generate_system_username @@ -141,6 +142,48 @@ async def test_long_username(): raise +@pytest.mark.asyncio +async def test_user_group_adding(): + """ + User logs in, and we check if they are added to the specified group. + """ + # This *must* be localhost, not an IP + # aiohttp throws away cookies if we are connecting to an IP! + hub_url = 'http://localhost' + username = secrets.token_hex(8) + groups = {"somegroup": [username]} + # Create the group we want to add the user to + system('groupadd somegroup') + + assert 0 == await (await asyncio.create_subprocess_exec(*TLJH_CONFIG_PATH, 'set', 'auth.type', 'dummyauthenticator.DummyAuthenticator')).wait() + assert 0 == await (await asyncio.create_subprocess_exec(*TLJH_CONFIG_PATH, 'add-item', 'users.extra_user_groups.somegroup', username)).wait() + assert 0 == await (await asyncio.create_subprocess_exec(*TLJH_CONFIG_PATH, 'reload')).wait() + + try: + async with User(username, hub_url, partial(login_dummy, password='')) as u: + await u.login() + await u.ensure_server() + + # Assert that the user exists + system_username = generate_system_username(f'jupyter-{username}') + assert pwd.getpwnam(system_username) is not None + + # Assert that the user was added to the specified group + assert f'jupyter-{username}' in grp.getgrnam('somegroup').gr_mem + + await u.stop_server() + # Delete the group + system('groupdel somegroup') + except: + # If we have any errors, print jupyterhub logs before exiting + subprocess.check_call([ + 'journalctl', + '-u', 'jupyterhub', + '--no-pager' + ]) + raise + + @pytest.mark.asyncio async def test_idle_server_culled(): """ diff --git a/tests/test_configurer.py b/tests/test_configurer.py index ab5f0d2..0481585 100644 --- a/tests/test_configurer.py +++ b/tests/test_configurer.py @@ -129,6 +129,24 @@ def test_auth_dummy(): assert c.JupyterHub.authenticator_class == 'dummyauthenticator.DummyAuthenticator' assert c.DummyAuthenticator.password == 'test' +from traitlets import Dict +def test_user_groups(): + """ + Test setting user groups + """ + c = apply_mock_config({ + 'users': { + 'extra_user_groups': { + "g1": ["u1", "u2"], + "g2": ["u3", "u4"] + }, + } + }) + assert c.UserCreatingSpawner.user_groups == { + "g1": ["u1", "u2"], + "g2": ["u3", "u4"] + } + def test_auth_firstuse(): """ diff --git a/tljh/configurer.py b/tljh/configurer.py index 9da4e0a..3a9ec05 100644 --- a/tljh/configurer.py +++ b/tljh/configurer.py @@ -27,6 +27,7 @@ default = { 'allowed': [], 'banned': [], 'admin': [], + 'extra_user_groups': {} }, 'limits': { 'memory': None, @@ -93,6 +94,7 @@ def apply_config(config_overrides, c): update_auth(c, tljh_config) update_userlists(c, tljh_config) + update_usergroups(c, tljh_config) update_limits(c, tljh_config) update_user_environment(c, tljh_config) update_user_account_config(c, tljh_config) @@ -168,6 +170,14 @@ def update_userlists(c, config): c.Authenticator.admin_users = set(users['admin']) +def update_usergroups(c, config): + """ + Set user groups + """ + users = config['users'] + c.UserCreatingSpawner.user_groups = users['extra_user_groups'] + + def update_limits(c, config): """ Set user server limits diff --git a/tljh/jupyterhub_config.py b/tljh/jupyterhub_config.py index 7f11bfa..51455dc 100644 --- a/tljh/jupyterhub_config.py +++ b/tljh/jupyterhub_config.py @@ -13,12 +13,16 @@ from tljh.normalize import generate_system_username from tljh.yaml import yaml from jupyterhub_traefik_proxy import TraefikTomlProxy +from traitlets import Dict, Unicode, List + class UserCreatingSpawner(SystemdSpawner): """ SystemdSpawner with user creation on spawn. FIXME: Remove this somehow? """ + user_groups = Dict(key_trait=Unicode(), value_trait=List(Unicode()), config=True) + def start(self): """ Perform system user activities before starting server @@ -34,6 +38,10 @@ class UserCreatingSpawner(SystemdSpawner): user.ensure_user_group(system_username, 'jupyterhub-admins') else: user.remove_user_group(system_username, 'jupyterhub-admins') + if self.user_groups: + for group, users in self.user_groups.items(): + if self.user.name in users: + user.ensure_user_group(system_username, group) return super().start() c.JupyterHub.spawner_class = UserCreatingSpawner