diff --git a/oslo_privsep/daemon.py b/oslo_privsep/daemon.py index 75131eb..27bc427 100644 --- a/oslo_privsep/daemon.py +++ b/oslo_privsep/daemon.py @@ -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()""" diff --git a/oslo_privsep/priv_context.py b/oslo_privsep/priv_context.py index 4ed5e37..737f3b0 100644 --- a/oslo_privsep/priv_context.py +++ b/oslo_privsep/priv_context.py @@ -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( diff --git a/oslo_privsep/tests/test_daemon.py b/oslo_privsep/tests/test_daemon.py index 15b13b2..47e81a1 100644 --- a/oslo_privsep/tests/test_daemon.py +++ b/oslo_privsep/tests/test_daemon.py @@ -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) diff --git a/oslo_privsep/tests/test_priv_context.py b/oslo_privsep/tests/test_priv_context.py index d362b78..ebf1110 100644 --- a/oslo_privsep/tests/test_priv_context.py +++ b/oslo_privsep/tests/test_priv_context.py @@ -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.')