remove sudo

add SudoSpawner in separate repo,
which works much better than this ever did.
This commit is contained in:
Min RK
2014-11-25 19:25:39 -08:00
parent f8f9c9e121
commit c4913ffc96
5 changed files with 12 additions and 176 deletions

View File

@@ -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

View File

@@ -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'}

View File

@@ -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

View File

@@ -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)
@@ -445,29 +365,6 @@ class LocalProcessSpawner(Spawner):
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):
"""stop the subprocess

View File

@@ -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)