From a75250e51205d65253105dac84a052683f81c2c3 Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Wed, 10 Jul 2019 15:07:40 +0300 Subject: [PATCH 1/6] Add the option to set admin passwords during install --- setup.py | 1 + tljh/installer.py | 27 +++++++++++++++++++++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) 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/tljh/installer.py b/tljh/installer.py index 01057db..b53f221 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, @@ -271,7 +274,7 @@ 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(admins, passwords): """ Setup given list of users as admins. """ @@ -290,6 +293,13 @@ def ensure_admins(admins): config['users']['admin'] = [admin for admin_sublist in admins for admin in admin_sublist] + if passwords: + for i in range(len(passwords)): + passwords[i] = bcrypt.hashpw(passwords[i].encode(), bcrypt.gensalt()) + db_passw = os.path.join(STATE_DIR, 'passwords.dbm') + with dbm.open(db_passw, 'c', 0o600) as db: + db[admins[i]] = passwords[i] + with open(config_path, 'w+') as f: yaml.dump(config, f) @@ -454,13 +464,26 @@ def main(): nargs='*', help='Plugin pip-specs to install' ) + argparser.add_argument( + '--password', + action='store_true', + help='List of admin passwords' + ) args = argparser.parse_args() pm = setup_plugins(args.plugin) ensure_config_yaml(pm) - ensure_admins(args.admin) + + # Set a password for each admin user + passwords = [] + if args.password: + for admin_user in args.admin: + passw = getpass(prompt='Set password for %s: ' % admin_user) + passwords.append(passw) + ensure_admins(args.admin, passwords) + ensure_usergroups() ensure_user_environment(args.user_requirements_txt_url) From 657280fdfd462f92012e4605fb3c3ed4e88f6337 Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Wed, 10 Jul 2019 15:11:47 +0300 Subject: [PATCH 2/6] Changed help msg for password --- tljh/installer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tljh/installer.py b/tljh/installer.py index b53f221..27e343d 100644 --- a/tljh/installer.py +++ b/tljh/installer.py @@ -467,7 +467,7 @@ def main(): argparser.add_argument( '--password', action='store_true', - help='List of admin passwords' + help='Whether or not to set admin passwords during install' ) args = argparser.parse_args() From f653d48b87a6b43e32244d214d8cdbed0afd00ee Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Tue, 16 Jul 2019 11:49:15 +0300 Subject: [PATCH 3/6] Pass admin:password to the installer --- tljh/installer.py | 41 ++++++++++++++++++----------------------- 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/tljh/installer.py b/tljh/installer.py index 27e343d..48be4d0 100644 --- a/tljh/installer.py +++ b/tljh/installer.py @@ -274,11 +274,11 @@ def ensure_user_environment(user_requirements_txt_file): conda.ensure_pip_requirements(USER_ENV_PREFIX, user_requirements_txt_file) -def ensure_admins(admins, passwords): +def ensure_admins(admin_password_list): """ Setup given list of users as admins. """ - if not admins: + if not admin_password_list: return logger.info("Setting up admin users") config_path = CONFIG_FILE @@ -289,16 +289,22 @@ def ensure_admins(admins, passwords): config = {} config['users'] = config.get('users', {}) - # Flatten admin lists - config['users']['admin'] = [admin for admin_sublist in admins - for admin in admin_sublist] - if passwords: - for i in range(len(passwords)): - passwords[i] = bcrypt.hashpw(passwords[i].encode(), bcrypt.gensalt()) - db_passw = os.path.join(STATE_DIR, 'passwords.dbm') - with dbm.open(db_passw, 'c', 0o600) as db: - db[admins[i]] = passwords[i] + db_passw = os.path.join(STATE_DIR, 'passwords.dbm') + + admins = [] + for i in range(len(admin_password_list)): + for admin_password_pair in admin_password_list[i]: + 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) @@ -464,11 +470,6 @@ def main(): nargs='*', help='Plugin pip-specs to install' ) - argparser.add_argument( - '--password', - action='store_true', - help='Whether or not to set admin passwords during install' - ) args = argparser.parse_args() @@ -476,13 +477,7 @@ def main(): ensure_config_yaml(pm) - # Set a password for each admin user - passwords = [] - if args.password: - for admin_user in args.admin: - passw = getpass(prompt='Set password for %s: ' % admin_user) - passwords.append(passw) - ensure_admins(args.admin, passwords) + ensure_admins(args.admin) ensure_usergroups() ensure_user_environment(args.user_requirements_txt_url) From b4b37d84cc1de4a1b642a7de4bb9d46fcb72ed91 Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Tue, 16 Jul 2019 20:18:45 +0300 Subject: [PATCH 4/6] Added tests --- .circleci/config.yml | 6 +++ integration-tests/test_admin_installer.py | 45 +++++++++++++++++++++++ tests/test_installer.py | 14 +++++-- tljh/installer.py | 8 ++-- 4 files changed, 66 insertions(+), 7 deletions(-) create mode 100644 integration-tests/test_admin_installer.py 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/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/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 48be4d0..322ee45 100644 --- a/tljh/installer.py +++ b/tljh/installer.py @@ -129,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() @@ -278,6 +276,8 @@ def ensure_admins(admin_password_list): """ Setup given list of users as admins. """ + os.makedirs(STATE_DIR, mode=0o700, exist_ok=True) + if not admin_password_list: return logger.info("Setting up admin users") @@ -293,8 +293,8 @@ def ensure_admins(admin_password_list): db_passw = os.path.join(STATE_DIR, 'passwords.dbm') admins = [] - for i in range(len(admin_password_list)): - for admin_password_pair in admin_password_list[i]: + 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) From 4730ec35d32e570d8113f7c0a274348ecba944db Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Wed, 17 Jul 2019 15:29:13 +0300 Subject: [PATCH 5/6] Update docs with admin password --- docs/contributing/dev-setup.rst | 8 ++++++++ docs/howto/admin/admin-users.rst | 3 ++- docs/topic/customizing-installer.rst | 29 ++++++++++++++++++++++++---- 3 files changed, 35 insertions(+), 5 deletions(-) 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 ================================================== From e1c0c911cb5c30a4497df759fce7293bde515cc9 Mon Sep 17 00:00:00 2001 From: GeorgianaElena Date: Tue, 14 Jan 2020 14:07:52 +0200 Subject: [PATCH 6/6] Try to trigger test report --- tljh/installer.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tljh/installer.py b/tljh/installer.py index 322ee45..c02d198 100644 --- a/tljh/installer.py +++ b/tljh/installer.py @@ -476,9 +476,7 @@ def main(): pm = setup_plugins(args.plugin) ensure_config_yaml(pm) - ensure_admins(args.admin) - ensure_usergroups() ensure_user_environment(args.user_requirements_txt_url)