From c4913ffc9635b10bc567a3ceb201bdeb6380a850 Mon Sep 17 00:00:00 2001 From: Min RK Date: Tue, 25 Nov 2014 19:25:39 -0800 Subject: [PATCH] remove sudo add SudoSpawner in separate repo, which works much better than this ever did. --- examples/sudo/Dockerfile | 30 ------- examples/sudo/jupyterhub_config.py | 7 -- examples/sudo/sudoers | 15 ---- jupyterhub/spawner.py | 127 +++-------------------------- jupyterhub/tests/test_spawner.py | 9 -- 5 files changed, 12 insertions(+), 176 deletions(-) delete mode 100644 examples/sudo/Dockerfile delete mode 100644 examples/sudo/jupyterhub_config.py delete mode 100644 examples/sudo/sudoers diff --git a/examples/sudo/Dockerfile b/examples/sudo/Dockerfile deleted file mode 100644 index a5a62f04..00000000 --- a/examples/sudo/Dockerfile +++ /dev/null @@ -1,30 +0,0 @@ -# example showing sudo config -# docker run -it -p 9000:8000 jupyter/jupyterhub-sudo - -FROM jupyter/jupyterhub - -MAINTAINER Jupyter Project - -# 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 diff --git a/examples/sudo/jupyterhub_config.py b/examples/sudo/jupyterhub_config.py deleted file mode 100644 index 00837509..00000000 --- a/examples/sudo/jupyterhub_config.py +++ /dev/null @@ -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'} diff --git a/examples/sudo/sudoers b/examples/sudo/sudoers deleted file mode 100644 index 6518c0b9..00000000 --- a/examples/sudo/sudoers +++ /dev/null @@ -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 diff --git a/jupyterhub/spawner.py b/jupyterhub/spawner.py index 09caa4f0..d6bffc53 100644 --- a/jupyterhub/spawner.py +++ b/jupyterhub/spawner.py @@ -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): diff --git a/jupyterhub/tests/test_spawner.py b/jupyterhub/tests/test_spawner.py index 3e973114..6fce6739 100644 --- a/jupyterhub/tests/test_spawner.py +++ b/jupyterhub/tests/test_spawner.py @@ -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)