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 errno
import os import os
import pipes
import pwd import pwd
import re import re
import signal import signal
@@ -262,20 +263,6 @@ def set_user_setuid(username):
return preexec 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): class LocalProcessSpawner(Spawner):
"""A Spawner that just uses Popen to start local processes.""" """A Spawner that just uses Popen to start local processes."""
@@ -291,29 +278,9 @@ class LocalProcessSpawner(Spawner):
proc = Instance(Popen) proc = Instance(Popen)
pid = Integer(0) pid = Integer(0)
sudo_args = List(['-n'], config=True,
help="""arguments to be passed to sudo (in addition to -u [username]) def make_preexec_fn(self, name):
return set_user_setuid(name)
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 load_state(self, state): def load_state(self, state):
"""load pid from state""" """load pid from state"""
@@ -333,66 +300,30 @@ class LocalProcessSpawner(Spawner):
super(LocalProcessSpawner, self).clear_state() super(LocalProcessSpawner, self).clear_state()
self.pid = 0 self.pid = 0
def sudo_cmd(self, user):
return ['sudo', '-u', user.name] + self.sudo_args
def user_env(self, env): def user_env(self, env):
if self.set_user == 'setuid': env['USER'] = self.user.name
env['USER'] = self.user.name env['HOME'] = pwd.getpwnam(self.user.name).pw_dir
env['HOME'] = pwd.getpwnam(self.user.name).pw_dir
return env return env
def _get_pg_pids(self, ppid): def _env_default(self):
"""get pids in group excluding the group id itself env = super()._env_default()
return self.user_env(env)
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
@gen.coroutine @gen.coroutine
def start(self): def start(self):
"""Start the process""" """Start the process"""
self.user.server.port = random_port() self.user.server.port = random_port()
cmd = [] cmd = []
env = self.user_env(self.env) env = self.env.copy()
if self.set_user == 'sudo':
cmd = self.sudo_cmd(self.user)
cmd.extend(self.cmd) cmd.extend(self.cmd)
cmd.extend(self.get_args()) 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, self.proc = Popen(cmd, env=env,
preexec_fn=self.make_preexec_fn(self.user.name), preexec_fn=self.make_preexec_fn(self.user.name),
) )
if self.set_user == 'sudo': self.pid = self.proc.pid
self.pid = yield self.get_sudo_pid()
self.proc = None
else:
self.pid = self.proc.pid
@gen.coroutine @gen.coroutine
def poll(self): def poll(self):
@@ -424,17 +355,6 @@ class LocalProcessSpawner(Spawner):
@gen.coroutine @gen.coroutine
def _signal(self, sig): 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)""" """simple implementation of signal, which we can use when we are using setuid (we are root)"""
try: try:
os.kill(self.pid, sig) os.kill(self.pid, sig)
@@ -444,29 +364,6 @@ class LocalProcessSpawner(Spawner):
else: else:
raise raise
return True # process exists 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 @gen.coroutine
def stop(self, now=False): def stop(self, now=False):

View File

@@ -54,15 +54,6 @@ def test_spawner(db, io_loop):
assert status == -signal.SIGINT 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): def test_stop_spawner_sigint_fails(db, io_loop):
spawner = new_spawner(db, cmd=[sys.executable, '-c', _uninterruptible]) spawner = new_spawner(db, cmd=[sys.executable, '-c', _uninterruptible])
io_loop.run_sync(spawner.start) io_loop.run_sync(spawner.start)