mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-15 14:03:02 +00:00
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:
@@ -7,7 +7,6 @@ import errno
|
|||||||
import os
|
import os
|
||||||
import pwd
|
import pwd
|
||||||
import signal
|
import signal
|
||||||
import sys
|
|
||||||
import time
|
import time
|
||||||
from subprocess import Popen
|
from subprocess import Popen
|
||||||
|
|
||||||
@@ -16,7 +15,7 @@ from tornado.ioloop import IOLoop
|
|||||||
|
|
||||||
from IPython.config import LoggingConfigurable
|
from IPython.config import LoggingConfigurable
|
||||||
from IPython.utils.traitlets import (
|
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."""
|
help="""The command used for starting notebooks."""
|
||||||
)
|
)
|
||||||
def _cmd_default(self):
|
def _cmd_default(self):
|
||||||
here = os.path.abspath(os.path.dirname(__file__))
|
return ['jupyterhub-singleuser']
|
||||||
singleuser_py = os.path.join(here, 'singleuserapp.py')
|
|
||||||
return [sys.executable, singleuser_py]
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def fromJSON(cls, state, **kwargs):
|
def fromJSON(cls, state, **kwargs):
|
||||||
@@ -109,19 +106,25 @@ class Spawner(LoggingConfigurable):
|
|||||||
|
|
||||||
@gen.coroutine
|
@gen.coroutine
|
||||||
def start(self):
|
def start(self):
|
||||||
|
"""Start the single-user process"""
|
||||||
raise NotImplementedError("Override in subclass. Must be a Tornado gen.coroutine.")
|
raise NotImplementedError("Override in subclass. Must be a Tornado gen.coroutine.")
|
||||||
|
|
||||||
@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.")
|
raise NotImplementedError("Override in subclass. Must be a Tornado gen.coroutine.")
|
||||||
|
|
||||||
@gen.coroutine
|
@gen.coroutine
|
||||||
def poll(self):
|
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.")
|
raise NotImplementedError("Override in subclass. Must be a Tornado gen.coroutine.")
|
||||||
|
|
||||||
|
|
||||||
def set_user(username):
|
def set_user_setuid(username):
|
||||||
"""return a preexec_fn for setting the user of a spawned process"""
|
"""return a preexec_fn for setting the user (via setuid) of a spawned process"""
|
||||||
user = pwd.getpwnam(username)
|
user = pwd.getpwnam(username)
|
||||||
uid = user.pw_uid
|
uid = user.pw_uid
|
||||||
gid = user.pw_gid
|
gid = user.pw_gid
|
||||||
@@ -139,10 +142,47 @@ def set_user(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
|
||||||
|
os.chdir(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."""
|
||||||
proc = Instance(Popen)
|
proc = Instance(Popen)
|
||||||
pid = Integer()
|
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):
|
def load_state(self, state):
|
||||||
self.pid = state['pid']
|
self.pid = state['pid']
|
||||||
@@ -150,20 +190,28 @@ class LocalProcessSpawner(Spawner):
|
|||||||
def get_state(self):
|
def get_state(self):
|
||||||
return dict(pid=self.pid)
|
return dict(pid=self.pid)
|
||||||
|
|
||||||
|
def sudo_cmd(self, user):
|
||||||
|
return ['sudo', '-u', user.name] + self.sudo_args
|
||||||
|
|
||||||
@gen.coroutine
|
@gen.coroutine
|
||||||
def start(self):
|
def start(self):
|
||||||
|
"""Start the process"""
|
||||||
self.user.server.port = random_port()
|
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.log.info("Spawning %r", cmd)
|
||||||
self.proc = Popen(cmd, env=self.env,
|
self.proc = Popen(cmd, env=self.env,
|
||||||
# spawn the process as the correct user
|
preexec_fn=self.make_preexec_fn(self.user.name),
|
||||||
preexec_fn=set_user(self.user.name),
|
|
||||||
)
|
)
|
||||||
self.pid = self.proc.pid
|
self.pid = self.proc.pid
|
||||||
|
|
||||||
@gen.coroutine
|
@gen.coroutine
|
||||||
def poll(self):
|
def poll(self):
|
||||||
|
"""Poll the process"""
|
||||||
# if we started the process, poll with Popen
|
# if we started the process, poll with Popen
|
||||||
if self.proc is not None:
|
if self.proc is not None:
|
||||||
raise gen.Return(self.proc.poll())
|
raise gen.Return(self.proc.poll())
|
||||||
|
Reference in New Issue
Block a user