Provide way to "initialise" oslo.privsep

Specifically, the goal here is to provide a default that can use
rootwrap.

This change implements a `priv_context.init` function that allows
oslo.privsep to hook into the startup of programs using oslo.privsep.
The intention is to call this function near the top of main() - after
oslo.config is available but before anything "interesting" is performed.

In this change, this init function just allows you to set the default
"run as root" prefix for helper_command to include something like
rootwrap.

In the future, it is expected to use this same call point to do other
"early" tasks like immediately forking privileged helpers and dropping
root if already running as root.

Change-Id: I3ea73e16b07a870629e7d69e897f2524d7068ae8
Partial-Bug: #1592043
This commit is contained in:
Angus Lees 2016-06-15 15:15:40 +10:00
parent 8e981daaf3
commit 9bf606327d
4 changed files with 109 additions and 86 deletions

View File

@ -49,7 +49,6 @@ import io
import logging as pylogging
import os
import platform
import shlex
import socket
import subprocess
import sys
@ -280,7 +279,7 @@ class RootwrapClientChannel(_ClientChannel):
listen_sock.bind(sockpath)
listen_sock.listen(1)
cmd = self._helper_command(context, sockpath)
cmd = context.helper_command(sockpath)
LOG.info(_LI('Running privsep helper: %s'), cmd)
proc = subprocess.Popen(cmd, shell=False, stderr=_fd_logger())
if proc.wait() != 0:
@ -305,51 +304,6 @@ class RootwrapClientChannel(_ClientChannel):
super(RootwrapClientChannel, self).__init__(sock)
@staticmethod
def _helper_command(context, sockpath):
# We need to be able to reconstruct the context object in the new
# python process we'll get after rootwrap/sudo. This means we
# need to construct the context object and store it somewhere
# globally accessible, and then use that python name to find it
# again in the new python interpreter. Yes, it's all a bit
# clumsy, and none of it is required when using the fork-based
# alternative above.
# These asserts here are just attempts to catch errors earlier.
# TODO(gus): Consider replacing with setuptools entry_points.
assert context.pypath is not None, (
'RootwrapClientChannel requires priv_context '
'pypath to be specified')
assert importutils.import_class(context.pypath) is context, (
'RootwrapClientChannel requires priv_context pypath '
'for context object')
# Note order is important here. Deployments will (hopefully)
# have the exact arguments in sudoers/rootwrap configs and
# reordering args will break configs!
if context.conf.helper_command:
cmd = shlex.split(context.conf.helper_command)
else:
cmd = ['sudo', 'privsep-helper']
try:
for cfg_file in cfg.CONF.config_file:
cmd.extend(['--config-file', cfg_file])
except cfg.NoSuchOptError:
pass
try:
if cfg.CONF.config_dir is not None:
cmd.extend(['--config-dir', cfg.CONF.config_dir])
except cfg.NoSuchOptError:
pass
cmd.extend(
['--privsep_context', context.pypath,
'--privsep_sock_path', sockpath])
return cmd
class Daemon(object):
"""NB: This doesn't fork() - do that yourself before calling run()"""

View File

@ -16,10 +16,12 @@
import enum
import functools
import logging
import shlex
import sys
from oslo_config import cfg
from oslo_config import types
from oslo_utils import importutils
from oslo_privsep import capabilities
from oslo_privsep import daemon
@ -57,6 +59,7 @@ OPTS = [
]
_ENTRYPOINT_ATTR = 'privsep_entrypoint'
_HELPER_COMMAND_PREFIX = ['sudo']
@enum.unique
@ -65,6 +68,24 @@ class Method(enum.Enum):
ROOTWRAP = 2
def init(root_helper=None):
"""Initialise oslo.privsep library.
This function should be called at the top of main(), after the
command line is parsed, oslo.config is initialised and logging is
set up, but before calling any privileged entrypoint, changing
user id, forking, or anything else "odd".
:param root_helper: List of command and arguments to prefix
privsep-helper with, in order to run helper as root. Note,
ignored if context's helper_command config option is set.
"""
if root_helper:
global _HELPER_COMMAND_PREFIX
_HELPER_COMMAND_PREFIX = root_helper
class PrivContext(object):
def __init__(self, prefix, cfg_section='privsep', pypath=None,
capabilities=None):
@ -103,6 +124,50 @@ class PrivContext(object):
def __repr__(self):
return 'PrivContext(cfg_section=%s)' % self.cfg_section
def helper_command(self, sockpath):
# We need to be able to reconstruct the context object in the new
# python process we'll get after rootwrap/sudo. This means we
# need to construct the context object and store it somewhere
# globally accessible, and then use that python name to find it
# again in the new python interpreter. Yes, it's all a bit
# clumsy, and none of it is required when using the fork-based
# alternative above.
# These asserts here are just attempts to catch errors earlier.
# TODO(gus): Consider replacing with setuptools entry_points.
assert self.pypath is not None, (
'helper_command requires priv_context '
'pypath to be specified')
assert importutils.import_class(self.pypath) is self, (
'helper_command requires priv_context pypath '
'for context object')
# Note order is important here. Deployments will (hopefully)
# have the exact arguments in sudoers/rootwrap configs and
# reordering args will break configs!
if self.conf.helper_command:
cmd = shlex.split(self.conf.helper_command)
else:
cmd = _HELPER_COMMAND_PREFIX + ['privsep-helper']
try:
for cfg_file in cfg.CONF.config_file:
cmd.extend(['--config-file', cfg_file])
except cfg.NoSuchOptError:
pass
try:
if cfg.CONF.config_dir is not None:
cmd.extend(['--config-dir', cfg.CONF.config_dir])
except cfg.NoSuchOptError:
pass
cmd.extend(
['--privsep_context', self.pypath,
'--privsep_sock_path', sockpath])
return cmd
def set_client_mode(self, enabled):
if enabled and sys.platform == 'win32':
raise RuntimeError(

View File

@ -104,42 +104,3 @@ class TestWithContext(testctx.TestContextTestCase):
self.assertRaisesRegexp(
NameError, 'undecorated not exported',
testctx.context._wrap, undecorated)
def test_helper_command(self):
self.privsep_conf.privsep.helper_command = 'foo --bar'
cmd = daemon.RootwrapClientChannel._helper_command(
testctx.context, '/tmp/sockpath')
expected = [
'foo', '--bar',
'--privsep_context', testctx.context.pypath,
'--privsep_sock_path', '/tmp/sockpath',
]
self.assertEqual(expected, cmd)
def test_helper_command_default(self):
self.privsep_conf.config_file = ['/bar.conf']
cmd = daemon.RootwrapClientChannel._helper_command(
testctx.context, '/tmp/sockpath')
expected = [
'sudo', 'privsep-helper',
'--config-file', '/bar.conf',
# --config-dir arg should be skipped
'--privsep_context', testctx.context.pypath,
'--privsep_sock_path', '/tmp/sockpath',
]
self.assertEqual(expected, cmd)
def test_helper_command_default_dirtoo(self):
self.privsep_conf.config_file = ['/bar.conf', '/baz.conf']
self.privsep_conf.config_dir = '/foo.d'
cmd = daemon.RootwrapClientChannel._helper_command(
testctx.context, '/tmp/sockpath')
expected = [
'sudo', 'privsep-helper',
'--config-file', '/bar.conf',
'--config-file', '/baz.conf',
'--config-dir', '/foo.d',
'--privsep_context', testctx.context.pypath,
'--privsep_sock_path', '/tmp/sockpath',
]
self.assertEqual(expected, cmd)

View File

@ -80,6 +80,49 @@ class TestPrivContext(testctx.TestContextTestCase):
mock_sys.platform = 'win32'
self.assertRaises(RuntimeError, context.set_client_mode, True)
def test_helper_command(self):
self.privsep_conf.privsep.helper_command = 'foo --bar'
cmd = testctx.context.helper_command('/tmp/sockpath')
expected = [
'foo', '--bar',
'--privsep_context', testctx.context.pypath,
'--privsep_sock_path', '/tmp/sockpath',
]
self.assertEqual(expected, cmd)
def test_helper_command_default(self):
self.privsep_conf.config_file = ['/bar.conf']
cmd = testctx.context.helper_command('/tmp/sockpath')
expected = [
'sudo', 'privsep-helper',
'--config-file', '/bar.conf',
# --config-dir arg should be skipped
'--privsep_context', testctx.context.pypath,
'--privsep_sock_path', '/tmp/sockpath',
]
self.assertEqual(expected, cmd)
def test_helper_command_default_dirtoo(self):
self.privsep_conf.config_file = ['/bar.conf', '/baz.conf']
self.privsep_conf.config_dir = '/foo.d'
cmd = testctx.context.helper_command('/tmp/sockpath')
expected = [
'sudo', 'privsep-helper',
'--config-file', '/bar.conf',
'--config-file', '/baz.conf',
'--config-dir', '/foo.d',
'--privsep_context', testctx.context.pypath,
'--privsep_sock_path', '/tmp/sockpath',
]
self.assertEqual(expected, cmd)
def test_init_known_contexts(self):
self.assertEqual(testctx.context.helper_command('/sock')[:2],
['sudo', 'privsep-helper'])
priv_context.init(root_helper=['sudo', 'rootwrap'])
self.assertEqual(testctx.context.helper_command('/sock')[:3],
['sudo', 'rootwrap', 'privsep-helper'])
@testtools.skipIf(platform.system() != 'Linux',
'works only on Linux platform.')