Merge pull request #179 from yuvipanda/normalize-systemuser

Normalize systemuser
This commit is contained in:
Yuvi Panda
2018-09-14 16:27:43 -07:00
committed by GitHub
5 changed files with 104 additions and 7 deletions

View File

@@ -18,7 +18,10 @@ permissions.
#. The unix user account created for a JupyterHub user named ``<username>`` is #. The unix user account created for a JupyterHub user named ``<username>`` is
``jupyter-<username>``. This prefix helps prevent clashes with users that ``jupyter-<username>``. This prefix helps prevent clashes with users that
already exist - otherwise a user named ``root`` can trivially gain full root already exist - otherwise a user named ``root`` can trivially gain full root
access to your server. access to your server. If the username (including the ``jupyter-`` prefix)
is longer than 26 characters, it is truncated at 26 characters & a 5 charcter
hash is appeneded to it. This keeps usernames under the linux username limit
of 32 characters while also reducing chances of collision.
#. A home directory is created for the user under ``/home/jupyter-<username>``. #. A home directory is created for the user under ``/home/jupyter-<username>``.

View File

@@ -8,6 +8,8 @@ import asyncio
import pwd import pwd
import grp import grp
import sys import sys
import subprocess
from tljh.normalize import generate_system_username
# Use sudo to invoke it, since this is how users invoke it. # Use sudo to invoke it, since this is how users invoke it.
@@ -112,4 +114,40 @@ async def test_user_admin_remove():
await u.ensure_server() await u.ensure_server()
# Assert that the user does *not* have admin rights # Assert that the user does *not* have admin rights
assert f'jupyter-{username}' in grp.getgrnam('jupyterhub-admins').gr_mem assert f'jupyter-{username}' in grp.getgrnam('jupyterhub-admins').gr_mem
@pytest.mark.asyncio
async def test_long_username():
"""
User with a long name logs in, and we check if their name is properly truncated.
"""
# 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(32)
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, 'reload')).wait()
# FIXME: wait for reload to finish & hub to come up
# Should be part of tljh-config reload
await asyncio.sleep(1)
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
await u.stop_server()
except:
# If we have any errors, print jupyterhub logs before exiting
subprocess.check_call([
'journalctl',
'-u', 'jupyterhub',
'--no-pager'
])
raise

24
tests/test_normalize.py Normal file
View File

@@ -0,0 +1,24 @@
"""
Test functions for normalizing various kinds of values
"""
from tljh.normalize import generate_system_username
def test_generate_username():
"""
Test generating system usernames from hub usernames
"""
usernames = {
# Very short
'jupyter-test': 'jupyter-test',
# Very long
'jupyter-aelie9sohjeequ9iemeipuimuoshahz4aitugiuteeg4ohioh5yuiha6aei7te5z': 'jupyter-aelie9sohjeequ9iem-4b726',
# 26 characters, just below our cutoff for hashing
'jupyter-abcdefghijklmnopq': 'jupyter-abcdefghijklmnopq',
# 27 characters, just above our cutoff for hashing
'jupyter-abcdefghijklmnopqr': 'jupyter-abcdefghijklmnopqr-e375e',
}
for hub_user, system_user in usernames.items():
assert generate_system_username(hub_user) == system_user
assert len(system_user) <= 32

View File

@@ -7,17 +7,26 @@ import yaml
from glob import glob from glob import glob
from systemdspawner import SystemdSpawner from systemdspawner import SystemdSpawner
from tljh import user, configurer from tljh import configurer, user
from tljh.config import INSTALL_PREFIX, USER_ENV_PREFIX, CONFIG_DIR from tljh.config import INSTALL_PREFIX, USER_ENV_PREFIX, CONFIG_DIR
from tljh.normalize import generate_system_username
class CustomSpawner(SystemdSpawner): class UserCreatingSpawner(SystemdSpawner):
"""
SystemdSpawner with user creation on spawn.
FIXME: Remove this somehow?
"""
def start(self): def start(self):
""" """
Perform system user activities before starting server Perform system user activities before starting server
""" """
# FIXME: Move this elsewhere? Into the Authenticator? # FIXME: Move this elsewhere? Into the Authenticator?
system_username = 'jupyter-' + self.user.name system_username = generate_system_username('jupyter-' + self.user.name)
# FIXME: This is a hack. Allow setting username directly instead
self.username_template = system_username
user.ensure_user(system_username) user.ensure_user(system_username)
user.ensure_user_group(system_username, 'jupyterhub-users') user.ensure_user_group(system_username, 'jupyterhub-users')
if self.user.admin: if self.user.admin:
@@ -26,8 +35,7 @@ class CustomSpawner(SystemdSpawner):
user.remove_user_group(system_username, 'jupyterhub-admins') user.remove_user_group(system_username, 'jupyterhub-admins')
return super().start() return super().start()
c.JupyterHub.spawner_class = UserCreatingSpawner
c.JupyterHub.spawner_class = CustomSpawner
# leave users running when the Hub restarts # leave users running when the Hub restarts
c.JupyterHub.cleanup_servers = False c.JupyterHub.cleanup_servers = False

24
tljh/normalize.py Normal file
View File

@@ -0,0 +1,24 @@
"""
Functions to normalize various inputs
"""
import hashlib
def generate_system_username(username):
"""
Generate a posix username from given username.
If username < 26 char, we just return it.
Else, we hash the username, truncate username at
26 char, append a '-' and first add 5char of hash.
This makes sure our usernames are always under 32char.
"""
if len(username) < 26:
return username
userhash = hashlib.sha256(username.encode('utf-8')).hexdigest()
return '{username_trunc}-{hash}'.format(
username_trunc=username[:26],
hash=userhash[:5]
)