Files
the-littlest-jupyterhub/integration-tests/test_install.py
2023-05-15 08:51:36 +00:00

244 lines
7.4 KiB
Python

import grp
import os
import pwd
import subprocess
import sys
from concurrent.futures import ProcessPoolExecutor
from contextlib import contextmanager
from functools import partial
import pytest
ADMIN_GROUP = "jupyterhub-admins"
USER_GROUP = "jupyterhub-users"
INSTALL_PREFIX = os.environ.get("TLJH_INSTALL_PREFIX", "/opt/tljh")
HUB_PREFIX = os.path.join(INSTALL_PREFIX, "hub")
USER_PREFIX = os.path.join(INSTALL_PREFIX, "user")
STATE_DIR = os.path.join(INSTALL_PREFIX, "state")
@contextmanager
def noop():
"""no-op context manager
for parametrized tests
"""
yield
def setgroup(group):
"""Become the user nobody:group
Only call in a subprocess because there's no turning back
"""
gid = grp.getgrnam(group).gr_gid
uid = pwd.getpwnam("nobody").pw_uid
os.setgid(gid)
os.setgroups([])
os.setuid(uid)
os.environ["HOME"] = "/tmp/test-home-%i-%i" % (uid, gid)
@pytest.mark.parametrize("group", [ADMIN_GROUP, USER_GROUP])
def test_groups_exist(group):
"""Verify that groups exist"""
grp.getgrnam(group)
def debug_uid_gid():
return subprocess.check_output("id").decode()
def permissions_test(group, path, *, readable=None, writable=None, dirs_only=False):
"""Run a permissions test on all files in a path path"""
# start a subprocess and become nobody:group in the process
pool = ProcessPoolExecutor(1)
pool.submit(setgroup, group)
def access(path, flag):
"""Run access test in subproccess as nobody:group"""
return pool.submit(os.access, path, flag).result()
total_tested = 0
failures = []
# walk the directory and check permissions
for root, dirs, files in os.walk(path):
to_test = dirs
if not dirs_only:
to_test += files
total_tested += len(to_test)
for name in to_test:
path = os.path.join(root, name)
if os.path.islink(path):
# skip links
continue
st = os.lstat(path)
try:
user = pwd.getpwuid(st.st_uid).pw_name
except KeyError:
# uid may not exist
user = st.st_uid
try:
groupname = grp.getgrgid(st.st_gid).gr_name
except KeyError:
# gid may not exist
groupname = st.st_gid
stat_str = "{perm:04o} {user} {group}".format(
perm=st.st_mode, user=user, group=groupname
)
# check if the path should be writable
if writable is not None:
if access(path, os.W_OK) != writable:
info = pool.submit(debug_uid_gid).result()
failures.append(
"{} {} should {}be writable by {} [{}]".format(
stat_str, path, "" if writable else "not ", group, info
)
)
# check if the path should be readable
if readable is not None:
if access(path, os.R_OK) != readable:
info = pool.submit(debug_uid_gid).result()
failures.append(
"{} {} should {}be readable by {} [{}]".format(
stat_str, path, "" if readable else "not ", group, info
)
)
# verify that we actually tested some files
# (path typos)
assert total_tested > 0, "No files to test in %r" % path
# raise a nice summary of the failures:
if failures:
if len(failures) > 50:
failures = failures[:32] + ["...%i total" % len(failures)]
assert False, "\n".join(failures)
@pytest.mark.xfail(reason="admin-write permissions is not implemented")
def test_admin_writable():
permissions_test(ADMIN_GROUP, sys.prefix, writable=True, dirs_only=True)
def test_installer_log_readable():
# Test that installer.log is owned by root, and not readable by anyone else
file_stat = os.stat("/opt/tljh/installer.log")
assert file_stat.st_uid == 0
assert file_stat.st_mode == 0o100500
@pytest.mark.parametrize("group", [ADMIN_GROUP, USER_GROUP])
def test_user_env_readable(group):
# every file in user env should be readable by everyone
permissions_test(group, USER_PREFIX, readable=True)
def test_nothing_user_writable():
# nothing in the install directory should be writable by users
permissions_test(USER_GROUP, INSTALL_PREFIX, writable=False)
@pytest.mark.parametrize(
"group, readwrite", [(ADMIN_GROUP, False), (USER_GROUP, False)]
)
def test_state_permissions(group, readwrite):
state_dir = os.path.abspath(os.path.join(sys.prefix, os.pardir, "state"))
permissions_test(group, state_dir, writable=readwrite, readable=readwrite)
# FIXME: admin-group should have install permissions
@pytest.mark.parametrize(
"group, allowed",
[
(USER_GROUP, False),
pytest.param(
ADMIN_GROUP,
True,
marks=pytest.mark.xfail(reason="admin-permissions not implemented"),
),
],
)
def test_pip_install(group, allowed):
if allowed:
context = noop()
else:
context = pytest.raises(subprocess.CalledProcessError)
python = os.path.join(USER_PREFIX, "bin", "python")
with context:
# we explicitly add `--no-user` here even though a real user wouldn't
# With this test we want to check that a user can't install to the
# global site-packages directory. In new versions of pip the default
# behaviour is to install to a user location when a global install
# isn't possible. By using --no-user we disable this behaviour and
# get a failure if the user can't install to the global site. Which is
# what we wanted to test for here.
subprocess.check_call(
[
python,
"-m",
"pip",
"install",
"--no-user",
"--ignore-installed",
"--no-deps",
"flit",
],
preexec_fn=partial(setgroup, group),
)
if allowed:
subprocess.check_call(
[python, "-m", "pip", "uninstall", "-y", "flit"],
preexec_fn=partial(setgroup, group),
)
@pytest.mark.parametrize(
"group, allowed",
[
(USER_GROUP, False),
pytest.param(
ADMIN_GROUP,
True,
marks=pytest.mark.xfail(reason="admin-permissions not implemented"),
),
],
)
def test_pip_upgrade(group, allowed):
if allowed:
context = noop()
pytest.skip("admin-install permissions is not implemented")
else:
context = pytest.raises(subprocess.CalledProcessError)
python = os.path.join(USER_PREFIX, "bin", "python")
with context:
subprocess.check_call(
[
python,
"-m",
"pip",
"install",
"--no-user",
"--ignore-installed",
"--no-deps",
"testpath==0.3.0",
],
preexec_fn=partial(setgroup, group),
)
if allowed:
subprocess.check_call(
[python, "-m", "pip", "install", "--upgrade", "testpath"],
preexec_fn=partial(setgroup, group),
)
def test_symlinks():
"""
Test we symlink tljh-config to /usr/local/bin
"""
assert os.path.exists("/usr/bin/tljh-config")