diff --git a/.circleci/config.yml b/.circleci/config.yml index 64db5e0..ff1f283 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -72,6 +72,12 @@ jobs: command: | .circleci/integration-test.py run-test basic-tests test_hub.py test_install.py test_extensions.py + - run: + name: Run admin tests + command: | + .circleci/integration-test.py run-test --installer-args "--admin admin:admin" basic-tests test_admin_installer.py + + - run: name: Run plugin tests command: | diff --git a/docs/contributing/dev-setup.rst b/docs/contributing/dev-setup.rst index af0a3b8..01ca6d0 100644 --- a/docs/contributing/dev-setup.rst +++ b/docs/contributing/dev-setup.rst @@ -43,6 +43,14 @@ The easiest & safest way to develop & test TLJH is with `Docker ``` + the very first time. It is recommended that you also set a password + for the admin at this step. The :ref:`--admin ` flag passed to the installer does this. If you had forgotten to do so, the easiest way to fix this is to run the installer again. diff --git a/docs/topic/customizing-installer.rst b/docs/topic/customizing-installer.rst index ea5585f..2285d6f 100644 --- a/docs/topic/customizing-installer.rst +++ b/docs/topic/customizing-installer.rst @@ -20,11 +20,24 @@ This page documents the various options you can pass as commandline parameters t Adding admin users =================== -``--admin `` adds user ```` to JupyterHub as an admin user. -This can be repeated multiple times. +``--admin :`` adds user ```` to JupyterHub as an admin user +and sets its password to be ````. +Although it is not recommended, it is possible to only set the admin username at this point +and set the admin password after the installation. -For example, to add ``admin-user1`` and ``admin-user2`` as admins when installing, you -would do: +Also, the ``--admin`` flag can be repeated multiple times. For example, to add ``admin-user1`` +and ``admin-user2`` as admins when installing, depending if you would like to set their passwords +during install you would: + +* set ``admin-user1`` with password ``password-user1`` and ``admin-user2`` with ``password-user2`` using: + +.. code-block:: bash + + curl https://raw.githubusercontent.com/jupyterhub/the-littlest-jupyterhub/master/bootstrap/bootstrap.py \ + | sudo python3 - \ + --admin admin-user1:password-user1 --admin admin-user2:password-user2 + +* set ``admin-user1`` and ``admin-user2`` to be admins, without any passwords at this stage, using: .. code-block:: bash @@ -32,6 +45,14 @@ would do: | sudo python3 - \ --admin admin-user1 --admin admin-user2 +* set ``admin-user1`` with password ``password-user1`` and ``admin-user2`` with no password at this stage using: + +.. code-block:: bash + + curl https://raw.githubusercontent.com/jupyterhub/the-littlest-jupyterhub/master/bootstrap/bootstrap.py \ + | sudo python3 - \ + --admin admin-user1:password-user1 --admin admin-user2 + Installing python packages in the user environment ================================================== diff --git a/integration-tests/test_admin_installer.py b/integration-tests/test_admin_installer.py new file mode 100644 index 0000000..3fcc169 --- /dev/null +++ b/integration-tests/test_admin_installer.py @@ -0,0 +1,45 @@ +from hubtraf.user import User +from hubtraf.auth.dummy import login_dummy +import pytest +from functools import partial +import asyncio + + +@pytest.mark.asyncio +async def test_admin_login(): + """ + Test if the admin that was added during install can login with + the password provided. + """ + hub_url = 'http://localhost' + username = "admin" + password = "admin" + + async with User(username, hub_url, partial(login_dummy, password=password)) as u: + await u.login() + await u.ensure_server() + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "username, password", + [ + ("admin", ""), + ("admin", "wrong_passw"), + ("user", "password"), + ], +) +async def test_unsuccessful_login(username, password): + """ + Ensure nobody but the admin that was added during install can login + """ + hub_url = 'http://localhost' + + try: + async with User(username, hub_url, partial(login_dummy, password="")) as u: + await u.login() + except Exception: + # This is what we except to happen + pass + else: + raise diff --git a/setup.py b/setup.py index c794859..36bcfc1 100644 --- a/setup.py +++ b/setup.py @@ -17,6 +17,7 @@ setup( 'passlib', 'backoff', 'requests', + 'bcrypt', 'jupyterhub-traefik-proxy==0.1.*' ], entry_points={ diff --git a/tests/test_installer.py b/tests/test_installer.py index 4613cb7..7d594d6 100644 --- a/tests/test_installer.py +++ b/tests/test_installer.py @@ -2,6 +2,7 @@ Unit test functions in installer.py """ import os +import pytest from tljh import installer from tljh.yaml import yaml @@ -21,10 +22,17 @@ def test_ensure_config_yaml(tljh_dir): # verify that old config doesn't exist assert not os.path.exists(os.path.join(tljh_dir, 'config.yaml')) -def test_ensure_admins(tljh_dir): + +@pytest.mark.parametrize( + "admins, expected_config", + [ + ([['a1'], ['a2'], ['a3']], ['a1', 'a2', 'a3']), + ([['a1:p1'], ['a2']], ['a1', 'a2']), + ], +) +def test_ensure_admins(tljh_dir, admins, expected_config): # --admin option called multiple times on the installer # creates a list of argument lists. - admins = [['a1'], ['a2'], ['a3']] installer.ensure_admins(admins) config_path = installer.CONFIG_FILE @@ -32,4 +40,4 @@ def test_ensure_admins(tljh_dir): config = yaml.load(f) # verify the list was flattened - assert config['users']['admin'] == ['a1', 'a2', 'a3'] + assert config['users']['admin'] == expected_config diff --git a/tljh/installer.py b/tljh/installer.py index 01057db..c02d198 100644 --- a/tljh/installer.py +++ b/tljh/installer.py @@ -1,6 +1,7 @@ """Installation logic for TLJH""" import argparse +import dbm import itertools import logging import os @@ -10,9 +11,11 @@ import sys import time import warnings +import bcrypt import pluggy import requests from requests.packages.urllib3.exceptions import InsecureRequestWarning +from getpass import getpass from tljh import ( apt, @@ -126,8 +129,6 @@ def ensure_jupyterhub_service(prefix): Ensure JupyterHub Services are set up properly """ - os.makedirs(STATE_DIR, mode=0o700, exist_ok=True) - remove_chp() systemd.reload_daemon() @@ -271,11 +272,13 @@ def ensure_user_environment(user_requirements_txt_file): conda.ensure_pip_requirements(USER_ENV_PREFIX, user_requirements_txt_file) -def ensure_admins(admins): +def ensure_admins(admin_password_list): """ Setup given list of users as admins. """ - if not admins: + os.makedirs(STATE_DIR, mode=0o700, exist_ok=True) + + if not admin_password_list: return logger.info("Setting up admin users") config_path = CONFIG_FILE @@ -286,9 +289,22 @@ def ensure_admins(admins): config = {} config['users'] = config.get('users', {}) - # Flatten admin lists - config['users']['admin'] = [admin for admin_sublist in admins - for admin in admin_sublist] + + db_passw = os.path.join(STATE_DIR, 'passwords.dbm') + + admins = [] + for admin_password_entry in admin_password_list: + for admin_password_pair in admin_password_entry: + if ":" in admin_password_pair: + admin, password = admin_password_pair.split(':') + admins.append(admin) + # Add admin:password to the db + password = bcrypt.hashpw(password.encode(), bcrypt.gensalt()) + with dbm.open(db_passw, 'c', 0o600) as db: + db[admin] = password + else: + admins.append(admin_password_pair) + config['users']['admin'] = admins with open(config_path, 'w+') as f: yaml.dump(config, f)