mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-13 13:03:01 +00:00
remove sudo
add SudoSpawner in separate repo, which works much better than this ever did.
This commit is contained in:
@@ -1,30 +0,0 @@
|
||||
# example showing sudo config
|
||||
# docker run -it -p 9000:8000 jupyter/jupyterhub-sudo
|
||||
|
||||
FROM jupyter/jupyterhub
|
||||
|
||||
MAINTAINER Jupyter Project <jupyter@googlegroups.com>
|
||||
|
||||
# fix permissions on sudo executable (how did this get messed up?)
|
||||
RUN chmod 4755 /usr/bin/sudo
|
||||
|
||||
# add the rhea user, who will run the server
|
||||
# she needs to be in the shadow group in order to access the PAM service
|
||||
RUN useradd -m -G shadow -p $(openssl passwd -1 rhea) rhea
|
||||
|
||||
# Give rhea passwordless sudo access to run the single-user servers on behalf of users:
|
||||
ADD sudoers /tmp/sudoers
|
||||
RUN cat /tmp/sudoers >> /etc/sudoers
|
||||
RUN rm /tmp/sudoers
|
||||
|
||||
# add the regular users
|
||||
RUN for name in io ganymede; do useradd -m -p $(openssl passwd -1 $name) $name; done
|
||||
|
||||
# make home directories private
|
||||
RUN chmod o-rwx /home/*
|
||||
|
||||
# make the working dir owned by rhea, so she can create the state database
|
||||
RUN chown rhea .
|
||||
|
||||
# run the server as rhea instead of root
|
||||
USER rhea
|
@@ -1,7 +0,0 @@
|
||||
# Configuration file for jupyterhub
|
||||
|
||||
c = get_config()
|
||||
|
||||
c.JupyterHub.admin_users = {'rhea'}
|
||||
c.LocalProcessSpawner.set_user = 'sudo'
|
||||
c.Authenticator.whitelist = {'ganymede', 'io', 'rhea'}
|
@@ -1,15 +0,0 @@
|
||||
# whitelist of users that can spawn single-user servers
|
||||
Runas_Alias JUPYTER_USERS = io, europa, ganymede, callisto, rhea
|
||||
|
||||
# the command(s) jupyterhub can run on behalf of the above users without needing a password
|
||||
Cmnd_Alias JUPYTER_CMD = /usr/local/bin/jupyterhub-singleuser
|
||||
|
||||
# single-user servers need some JPY_ environment variables
|
||||
Defaults!JUPYTER_CMD env_keep = JPY_*
|
||||
|
||||
# actually give hub user permission to run the above command on behalf
|
||||
# of the above users without a password
|
||||
rhea ALL=(JUPYTER_USERS) NOPASSWD:JUPYTER_CMD
|
||||
|
||||
# allow rhea to send signals to her subprocesses (required for polling and process cleanup):
|
||||
rhea ALL=(JUPYTER_USERS) NOPASSWD:/bin/kill
|
@@ -5,6 +5,7 @@
|
||||
|
||||
import errno
|
||||
import os
|
||||
import pipes
|
||||
import pwd
|
||||
import re
|
||||
import signal
|
||||
@@ -262,20 +263,6 @@ def set_user_setuid(username):
|
||||
return preexec
|
||||
|
||||
|
||||
def set_user_sudo(username):
|
||||
"""return a preexec_fn for setting the user (assuming sudo is used for setting the user)"""
|
||||
user = pwd.getpwnam(username)
|
||||
home = user.pw_dir
|
||||
|
||||
def preexec():
|
||||
# don't forward signals
|
||||
os.setpgrp()
|
||||
|
||||
# start in the user's home dir
|
||||
_try_setcwd(home)
|
||||
return preexec
|
||||
|
||||
|
||||
class LocalProcessSpawner(Spawner):
|
||||
"""A Spawner that just uses Popen to start local processes."""
|
||||
|
||||
@@ -291,29 +278,9 @@ class LocalProcessSpawner(Spawner):
|
||||
|
||||
proc = Instance(Popen)
|
||||
pid = Integer(0)
|
||||
sudo_args = List(['-n'], config=True,
|
||||
help="""arguments to be passed to sudo (in addition to -u [username])
|
||||
|
||||
only used if set_user = sudo
|
||||
"""
|
||||
)
|
||||
|
||||
make_preexec_fn = Any(set_user_setuid)
|
||||
|
||||
set_user = Enum(['sudo', 'setuid'], default_value='setuid', config=True,
|
||||
help="""scheme for setting the user of the spawned process
|
||||
|
||||
'sudo' can be more prudently restricted,
|
||||
but 'setuid' is simpler for a server run as root
|
||||
"""
|
||||
)
|
||||
def _set_user_changed(self, name, old, new):
|
||||
if new == 'sudo':
|
||||
self.make_preexec_fn = set_user_sudo
|
||||
elif new == 'setuid':
|
||||
self.make_preexec_fn = set_user_setuid
|
||||
else:
|
||||
raise ValueError("This should be impossible")
|
||||
|
||||
def make_preexec_fn(self, name):
|
||||
return set_user_setuid(name)
|
||||
|
||||
def load_state(self, state):
|
||||
"""load pid from state"""
|
||||
@@ -333,66 +300,30 @@ class LocalProcessSpawner(Spawner):
|
||||
super(LocalProcessSpawner, self).clear_state()
|
||||
self.pid = 0
|
||||
|
||||
def sudo_cmd(self, user):
|
||||
return ['sudo', '-u', user.name] + self.sudo_args
|
||||
|
||||
def user_env(self, env):
|
||||
if self.set_user == 'setuid':
|
||||
env['USER'] = self.user.name
|
||||
env['HOME'] = pwd.getpwnam(self.user.name).pw_dir
|
||||
env['USER'] = self.user.name
|
||||
env['HOME'] = pwd.getpwnam(self.user.name).pw_dir
|
||||
return env
|
||||
|
||||
def _get_pg_pids(self, ppid):
|
||||
"""get pids in group excluding the group id itself
|
||||
|
||||
used for getting actual process started by `sudo`
|
||||
"""
|
||||
out = check_output(['pgrep', '-g', str(ppid)]).decode('utf8', 'replace')
|
||||
self.log.debug("pgrep output: %r", out)
|
||||
return [ int(ns) for ns in NUM_PAT.findall(out) if int(ns) != ppid ]
|
||||
|
||||
@gen.coroutine
|
||||
def get_sudo_pid(self):
|
||||
"""Get the actual process started with sudo
|
||||
|
||||
use the output of `pgrep -g PPID` to get the child process ID
|
||||
"""
|
||||
ppid = self.proc.pid
|
||||
loop = IOLoop.current()
|
||||
for i in range(100):
|
||||
if self.proc.poll() is not None:
|
||||
break
|
||||
pids = self._get_pg_pids(ppid)
|
||||
if pids:
|
||||
return pids[0]
|
||||
else:
|
||||
yield gen.Task(loop.add_timeout, loop.time() + 0.1)
|
||||
self.log.error("Failed to get single-user PID")
|
||||
# return sudo pid if we can't get the real PID
|
||||
# this shouldn't happen
|
||||
return ppid
|
||||
def _env_default(self):
|
||||
env = super()._env_default()
|
||||
return self.user_env(env)
|
||||
|
||||
@gen.coroutine
|
||||
def start(self):
|
||||
"""Start the process"""
|
||||
self.user.server.port = random_port()
|
||||
cmd = []
|
||||
env = self.user_env(self.env)
|
||||
if self.set_user == 'sudo':
|
||||
cmd = self.sudo_cmd(self.user)
|
||||
env = self.env.copy()
|
||||
|
||||
cmd.extend(self.cmd)
|
||||
cmd.extend(self.get_args())
|
||||
|
||||
self.log.info("Spawning %r", cmd)
|
||||
self.log.info("Spawning %s", ' '.join(pipes.quote(s) for s in cmd))
|
||||
self.proc = Popen(cmd, env=env,
|
||||
preexec_fn=self.make_preexec_fn(self.user.name),
|
||||
)
|
||||
if self.set_user == 'sudo':
|
||||
self.pid = yield self.get_sudo_pid()
|
||||
self.proc = None
|
||||
else:
|
||||
self.pid = self.proc.pid
|
||||
self.pid = self.proc.pid
|
||||
|
||||
@gen.coroutine
|
||||
def poll(self):
|
||||
@@ -424,17 +355,6 @@ class LocalProcessSpawner(Spawner):
|
||||
|
||||
@gen.coroutine
|
||||
def _signal(self, sig):
|
||||
"""send a signal, and ignore ERSCH because it just means it already died
|
||||
|
||||
returns bool for whether the process existed to receive the signal.
|
||||
"""
|
||||
if self.set_user == 'sudo':
|
||||
rc = yield self._signal_sudo(sig)
|
||||
return rc
|
||||
else:
|
||||
return self._signal_setuid(sig)
|
||||
|
||||
def _signal_setuid(self, sig):
|
||||
"""simple implementation of signal, which we can use when we are using setuid (we are root)"""
|
||||
try:
|
||||
os.kill(self.pid, sig)
|
||||
@@ -444,29 +364,6 @@ class LocalProcessSpawner(Spawner):
|
||||
else:
|
||||
raise
|
||||
return True # process exists
|
||||
|
||||
@gen.coroutine
|
||||
def _signal_sudo(self, sig):
|
||||
"""use `sudo kill` to send signals"""
|
||||
# check for existence with `ps -p` instead of `kill -0`
|
||||
try:
|
||||
check_output(['ps', '-p', str(self.pid)], stderr=PIPE)
|
||||
except CalledProcessError:
|
||||
return False # process is gone
|
||||
else:
|
||||
if sig == 0:
|
||||
return True # process exists
|
||||
|
||||
# build sudo -u user kill -SIG PID
|
||||
cmd = self.sudo_cmd(self.user)
|
||||
cmd.extend([
|
||||
'kill', '-%i' % sig, str(self.pid),
|
||||
])
|
||||
self.log.debug("Signaling: %s", cmd)
|
||||
check_output(cmd,
|
||||
preexec_fn=self.make_preexec_fn(self.user.name),
|
||||
)
|
||||
return True # process exists
|
||||
|
||||
@gen.coroutine
|
||||
def stop(self, now=False):
|
||||
|
@@ -54,15 +54,6 @@ def test_spawner(db, io_loop):
|
||||
assert status == -signal.SIGINT
|
||||
|
||||
|
||||
def test_preexec_switch(db):
|
||||
spawner = new_spawner(db)
|
||||
assert spawner.make_preexec_fn is spawnermod.set_user_setuid
|
||||
spawner.set_user = 'sudo'
|
||||
assert spawner.make_preexec_fn is spawnermod.set_user_sudo
|
||||
spawner.set_user = 'setuid'
|
||||
assert spawner.make_preexec_fn is spawnermod.set_user_setuid
|
||||
|
||||
|
||||
def test_stop_spawner_sigint_fails(db, io_loop):
|
||||
spawner = new_spawner(db, cmd=[sys.executable, '-c', _uninterruptible])
|
||||
io_loop.run_sync(spawner.start)
|
||||
|
Reference in New Issue
Block a user