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 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):
|
||||||
|
@@ -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)
|
||||||
|
Reference in New Issue
Block a user