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:
parent
8e981daaf3
commit
9bf606327d
@ -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()"""
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
|
@ -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.')
|
||||
|
Loading…
x
Reference in New Issue
Block a user