allow spawning with sudo

alternative to setuid

allows better restricted access, and doesn't require the server to run as root.

Enable with `--LocalProcessSpawner.set_user=sudo`

Since spawning with sudo is complicated, leave setuid as the default.
This commit is contained in:
MinRK
2014-08-22 10:20:05 -07:00
parent c40b5699d7
commit c0b83d2fa0

View File

@@ -7,7 +7,6 @@ import errno
import os
import pwd
import signal
import sys
import time
from subprocess import Popen
@@ -16,7 +15,7 @@ from tornado.ioloop import IOLoop
from IPython.config import LoggingConfigurable
from IPython.utils.traitlets import (
Any, Dict, Instance, Integer, List, Unicode,
Any, Dict, Enum, Instance, Integer, List, Unicode,
)
@@ -54,9 +53,7 @@ class Spawner(LoggingConfigurable):
help="""The command used for starting notebooks."""
)
def _cmd_default(self):
here = os.path.abspath(os.path.dirname(__file__))
singleuser_py = os.path.join(here, 'singleuserapp.py')
return [sys.executable, singleuser_py]
return ['jupyterhub-singleuser']
@classmethod
def fromJSON(cls, state, **kwargs):
@@ -109,19 +106,25 @@ class Spawner(LoggingConfigurable):
@gen.coroutine
def start(self):
"""Start the single-user process"""
raise NotImplementedError("Override in subclass. Must be a Tornado gen.coroutine.")
@gen.coroutine
def stop(self):
def stop(self, now=False):
"""Stop the single-user process"""
raise NotImplementedError("Override in subclass. Must be a Tornado gen.coroutine.")
@gen.coroutine
def poll(self):
"""Check if the single-user process is running
return None if it is, an exit status (0 if unknown) if it is not.
"""
raise NotImplementedError("Override in subclass. Must be a Tornado gen.coroutine.")
def set_user(username):
"""return a preexec_fn for setting the user of a spawned process"""
def set_user_setuid(username):
"""return a preexec_fn for setting the user (via setuid) of a spawned process"""
user = pwd.getpwnam(username)
uid = user.pw_uid
gid = user.pw_gid
@@ -139,10 +142,47 @@ def set_user(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
os.chdir(home)
return preexec
class LocalProcessSpawner(Spawner):
"""A Spawner that just uses Popen to start local processes."""
proc = Instance(Popen)
pid = Integer()
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 load_state(self, state):
self.pid = state['pid']
@@ -150,20 +190,28 @@ class LocalProcessSpawner(Spawner):
def get_state(self):
return dict(pid=self.pid)
def sudo_cmd(self, user):
return ['sudo', '-u', user.name] + self.sudo_args
@gen.coroutine
def start(self):
"""Start the process"""
self.user.server.port = random_port()
cmd = self.cmd + self.get_args()
cmd = []
if self.set_user == 'sudo':
cmd = self.sudo_cmd(self.user)
cmd.extend(self.cmd)
cmd.extend(self.get_args())
self.log.info("Spawning %r", cmd)
self.proc = Popen(cmd, env=self.env,
# spawn the process as the correct user
preexec_fn=set_user(self.user.name),
preexec_fn=self.make_preexec_fn(self.user.name),
)
self.pid = self.proc.pid
@gen.coroutine
def poll(self):
"""Poll the process"""
# if we started the process, poll with Popen
if self.proc is not None:
raise gen.Return(self.proc.poll())