Use seteuid instead of su to control privileges

Control the privileges by setting the effective UID before running the
command. Earlier we used to run command using su -c "USER".

Original EUID is restored after running the command. This is required to
run multiple commands in succession with different run-as users.

Change-Id: I414fc6a802f11deb320b43c6d011f802a42c40c9
Partial-Bug: #1312246
This commit is contained in:
Anant Patil 2015-09-10 09:46:22 +05:30
parent 090a14dd63
commit f427a69443
2 changed files with 147 additions and 39 deletions

View File

@ -1,4 +1,4 @@
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
@ -19,6 +19,7 @@ Not implemented yet:
- placeholders are ignored
"""
import atexit
import contextlib
import errno
import functools
import grp
@ -40,6 +41,7 @@ import six.moves.configparser as ConfigParser
import subprocess
import tempfile
# Override BOTO_CONFIG, which makes boto look only at the specified
# config file, instead of the default locations
os.environ['BOTO_CONFIG'] = '/var/lib/heat-cfntools/cfn-boto-cfg'
@ -152,6 +154,33 @@ class Hook(object):
self.action)
class ControlledPrivilegesFailureException(Exception):
pass
@contextlib.contextmanager
def controlled_privileges(user):
orig_euid = None
try:
real = pwd.getpwnam(user)
if os.geteuid() != real.pw_uid:
orig_euid = os.geteuid()
os.seteuid(real.pw_uid)
LOG.debug("Privileges set for user %s" % user)
except Exception as e:
raise ControlledPrivilegesFailureException(e)
try:
yield
finally:
if orig_euid is not None:
try:
os.seteuid(orig_euid)
LOG.debug("Original privileges restored.")
except Exception as e:
LOG.error("Error restoring privileges %s" % e)
class CommandRunner(object):
"""Helper class to run a command and store the output."""
@ -180,19 +209,34 @@ class CommandRunner(object):
self
"""
LOG.debug("Running command: %s" % self._command)
cmd = ['su', user, '-c', self._command]
subproc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, cwd=cwd, env=env)
output = subproc.communicate()
self._status = subproc.returncode
self._stdout = output[0]
self._stderr = output[1]
cmd = self._command
# Ensure commands are passed as strings only as we run them
# using shell
assert isinstance(cmd, six.string_types)
try:
with controlled_privileges(user):
subproc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, cwd=cwd,
env=env, shell=True)
output = subproc.communicate()
self._status = subproc.returncode
self._stdout = output[0]
self._stderr = output[1]
except ControlledPrivilegesFailureException as e:
LOG.error("Error setting privileges for user '%s': %s"
% (user, e))
self._status = 126
self._stderr = six.text_type(e)
if self._status:
LOG.debug("Return code of %d after executing: '%s'\n"
"stdout: '%s'\n"
"stderr: '%s'" % (self._status, cmd, self._stdout,
self._stderr))
if self._next:
self._next.run()
return self

View File

@ -27,9 +27,10 @@ from heat_cfntools.cfntools import cfn_helper
def popen_root_calls(calls):
kwargs = {'env': None, 'cwd': None, 'stderr': -1, 'stdout': -1}
kwargs = {'env': None, 'cwd': None, 'stderr': -1, 'stdout': -1,
'shell': True}
return [
mock.call(['su', 'root', '-c', call], **kwargs)
mock.call(call, **kwargs)
for call in calls
]
@ -47,13 +48,16 @@ class FakePOpen():
pass
@mock.patch.object(cfn_helper.pwd, 'getpwnam')
@mock.patch.object(cfn_helper.os, 'seteuid')
@mock.patch.object(cfn_helper.os, 'geteuid')
class TestCommandRunner(testtools.TestCase):
def test_command_runner(self):
def test_command_runner(self, mock_geteuid, mock_seteuid, mock_getpwnam):
def returns(*args, **kwargs):
if args[0][3] == '/bin/command1':
if args[0] == '/bin/command1':
return FakePOpen('All good')
elif args[0][3] == '/bin/command2':
elif args[0] == '/bin/command2':
return FakePOpen('Doing something', 'error', -1)
else:
raise Exception('This should never happen')
@ -73,13 +77,64 @@ class TestCommandRunner(testtools.TestCase):
calls = popen_root_calls(['/bin/command1', '/bin/command2'])
mock_popen.assert_has_calls(calls)
def test_privileges_are_lowered_for_non_root_user(self, mock_geteuid,
mock_seteuid,
mock_getpwnam):
pw_entry = mock.MagicMock()
pw_entry.pw_uid = 1001
mock_getpwnam.return_value = pw_entry
mock_geteuid.return_value = 0
calls = [mock.call(1001), mock.call(0)]
with mock.patch('subprocess.Popen') as mock_popen:
command = '/bin/command --option=value arg1 arg2'
cmd = cfn_helper.CommandRunner(command)
cmd.run(user='nonroot')
self.assertTrue(mock_geteuid.called)
mock_getpwnam.assert_called_once_with('nonroot')
mock_seteuid.assert_has_calls(calls)
self.assertTrue(mock_popen.called)
def test_run_returns_when_cannot_set_privileges(self, mock_geteuid,
mock_seteuid,
mock_getpwnam):
msg = '[Error 1] Permission Denied'
mock_seteuid.side_effect = Exception(msg)
with mock.patch('subprocess.Popen') as mock_popen:
command = '/bin/command2'
cmd = cfn_helper.CommandRunner(command)
cmd.run(user='nonroot')
self.assertTrue(mock_getpwnam.called)
self.assertTrue(mock_seteuid.called)
self.assertFalse(mock_popen.called)
self.assertEqual(126, cmd.status)
self.assertEqual(msg, cmd.stderr)
def test_privileges_are_restored_for_command_failure(self, mock_geteuid,
mock_seteuid,
mock_getpwnam):
pw_entry = mock.MagicMock()
pw_entry.pw_uid = 1001
mock_getpwnam.return_value = pw_entry
mock_geteuid.return_value = 0
calls = [mock.call(1001), mock.call(0)]
with mock.patch('subprocess.Popen') as mock_popen:
mock_popen.side_effect = ValueError('Something wrong')
command = '/bin/command --option=value arg1 arg2'
cmd = cfn_helper.CommandRunner(command)
self.assertRaises(ValueError, cmd.run, user='nonroot')
self.assertTrue(mock_geteuid.called)
mock_getpwnam.assert_called_once_with('nonroot')
mock_seteuid.assert_has_calls(calls)
self.assertTrue(mock_popen.called)
@mock.patch.object(cfn_helper, 'controlled_privileges')
class TestPackages(testtools.TestCase):
def test_yum_install(self):
def test_yum_install(self, mock_cp):
def returns(*args, **kwargs):
if args[0][3].startswith('rpm -q '):
if args[0].startswith('rpm -q '):
return FakePOpen(returncode=1)
else:
return FakePOpen(returncode=0)
@ -103,11 +158,11 @@ class TestPackages(testtools.TestCase):
cfn_helper.PackagesHandler(packages).apply_packages()
mock_popen.assert_has_calls(calls, any_order=True)
def test_dnf_install_yum_unavailable(self):
def test_dnf_install_yum_unavailable(self, mock_cp):
def returns(*args, **kwargs):
if args[0][3].startswith('rpm -q ') \
or args[0][3] == 'which yum':
if args[0].startswith('rpm -q ') \
or args[0] == 'which yum':
return FakePOpen(returncode=1)
else:
return FakePOpen(returncode=0)
@ -131,10 +186,10 @@ class TestPackages(testtools.TestCase):
cfn_helper.PackagesHandler(packages).apply_packages()
mock_popen.assert_has_calls(calls, any_order=True)
def test_dnf_install(self):
def test_dnf_install(self, mock_cp):
def returns(*args, **kwargs):
if args[0][3].startswith('rpm -q '):
if args[0].startswith('rpm -q '):
return FakePOpen(returncode=1)
else:
return FakePOpen(returncode=0)
@ -158,10 +213,10 @@ class TestPackages(testtools.TestCase):
cfn_helper.PackagesHandler(packages).apply_packages()
mock_popen.assert_has_calls(calls, any_order=True)
def test_zypper_install(self):
def test_zypper_install(self, mock_cp):
def returns(*args, **kwargs):
if args[0][3].startswith('rpm -q '):
if args[0].startswith('rpm -q '):
return FakePOpen(returncode=1)
else:
return FakePOpen(returncode=0)
@ -185,7 +240,7 @@ class TestPackages(testtools.TestCase):
cfn_helper.PackagesHandler(packages).apply_packages()
mock_popen.assert_has_calls(calls, any_order=True)
def test_apt_install(self):
def test_apt_install(self, mock_cp):
packages = {
"apt": {
"mysql-server": [],
@ -200,9 +255,10 @@ class TestPackages(testtools.TestCase):
self.assertTrue(mock_popen.called)
@mock.patch.object(cfn_helper, 'controlled_privileges')
class TestServicesHandler(testtools.TestCase):
def test_services_handler_systemd(self):
def test_services_handler_systemd(self, mock_cp):
calls = []
returns = []
@ -272,7 +328,7 @@ class TestServicesHandler(testtools.TestCase):
mock_popen.assert_has_calls(calls, any_order=True)
mock_exists.assert_called_with('/bin/systemctl')
def test_services_handler_systemd_disabled(self):
def test_services_handler_systemd_disabled(self, mock_cp):
calls = []
# apply_services
@ -307,7 +363,7 @@ class TestServicesHandler(testtools.TestCase):
mock_popen.assert_has_calls(calls, any_order=True)
mock_exists.assert_called_with('/bin/systemctl')
def test_services_handler_sysv_service_chkconfig(self):
def test_services_handler_sysv_service_chkconfig(self, mock_cp):
def exists(*args, **kwargs):
return args[0] != '/bin/systemctl'
@ -367,7 +423,7 @@ class TestServicesHandler(testtools.TestCase):
mock_exists.assert_any_call('/sbin/service')
mock_exists.assert_any_call('/sbin/chkconfig')
def test_services_handler_sysv_disabled_service_chkconfig(self):
def test_services_handler_sysv_disabled_service_chkconfig(self, mock_cp):
def exists(*args, **kwargs):
return args[0] != '/bin/systemctl'
@ -405,7 +461,7 @@ class TestServicesHandler(testtools.TestCase):
mock_exists.assert_any_call('/sbin/service')
mock_exists.assert_any_call('/sbin/chkconfig')
def test_services_handler_sysv_systemctl(self):
def test_services_handler_sysv_systemctl(self, mock_cp):
calls = []
returns = []
@ -459,7 +515,7 @@ class TestServicesHandler(testtools.TestCase):
mock_popen.assert_has_calls(calls)
mock_exists.assert_called_with('/bin/systemctl')
def test_services_handler_sysv_disabled_systemctl(self):
def test_services_handler_sysv_disabled_systemctl(self, mock_cp):
calls = []
# apply_services
@ -492,7 +548,7 @@ class TestServicesHandler(testtools.TestCase):
mock_popen.assert_has_calls(calls)
mock_exists.assert_called_with('/bin/systemctl')
def test_services_handler_sysv_service_updaterc(self):
def test_services_handler_sysv_service_updaterc(self, mock_cp):
calls = []
returns = []
@ -548,7 +604,7 @@ class TestServicesHandler(testtools.TestCase):
mock_exists.assert_any_call('/sbin/service')
mock_exists.assert_any_call('/sbin/chkconfig')
def test_services_handler_sysv_disabled_service_updaterc(self):
def test_services_handler_sysv_disabled_service_updaterc(self, mock_cp):
calls = []
returns = []
@ -619,7 +675,8 @@ interval=120''' % fcreds.name).encode('UTF-8'))
self.assertIn('invalid credentials file', str(e))
fcreds.close()
def test_hup_config(self):
@mock.patch.object(cfn_helper, 'controlled_privileges')
def test_hup_config(self, mock_cp):
hooks_conf = tempfile.NamedTemporaryFile()
def write_hook_conf(f, name, triggers, path, action):
@ -1030,7 +1087,8 @@ class TestMetadataRetrieve(testtools.TestCase):
self.assertEqual(meta_in, meta_out)
def test_nova_meta_curl(self):
@mock.patch.object(cfn_helper, 'controlled_privileges')
def test_nova_meta_curl(self, mock_cp):
url = 'http://169.254.169.254/openstack/2012-08-10/meta_data.json'
temp_home = tempfile.mkdtemp()
cache_path = os.path.join(temp_home, 'meta_data.json')
@ -1065,7 +1123,8 @@ class TestMetadataRetrieve(testtools.TestCase):
mock_popen.assert_has_calls(
popen_root_calls(['curl -o %s %s' % (cache_path, url)]))
def test_nova_meta_curl_corrupt(self):
@mock.patch.object(cfn_helper, 'controlled_privileges')
def test_nova_meta_curl_corrupt(self, mock_cp):
url = 'http://169.254.169.254/openstack/2012-08-10/meta_data.json'
temp_home = tempfile.mkdtemp()
cache_path = os.path.join(temp_home, 'meta_data.json')
@ -1093,7 +1152,8 @@ class TestMetadataRetrieve(testtools.TestCase):
mock_popen.assert_has_calls(
popen_root_calls(['curl -o %s %s' % (cache_path, url)]))
def test_nova_meta_curl_failed(self):
@mock.patch.object(cfn_helper, 'controlled_privileges')
def test_nova_meta_curl_failed(self, mock_cp):
url = 'http://169.254.169.254/openstack/2012-08-10/meta_data.json'
temp_home = tempfile.mkdtemp()
cache_path = os.path.join(temp_home, 'meta_data.json')
@ -1169,7 +1229,8 @@ class TestCfnInit(testtools.TestCase):
md.cfn_init()
self.assertThat(foo_file.name, ttm.FileContains('bar'))
def test_cfn_init_with_ignore_errors_false(self):
@mock.patch.object(cfn_helper, 'controlled_privileges')
def test_cfn_init_with_ignore_errors_false(self, mock_cp):
md_data = {"AWS::CloudFormation::Init": {"config": {"commands": {
"00_foo": {"command": "/bin/command1",
"ignoreErrors": "false"}}}}}
@ -1181,7 +1242,8 @@ class TestCfnInit(testtools.TestCase):
self.assertRaises(cfn_helper.CommandsHandlerRunError, md.cfn_init)
mock_popen.assert_has_calls(popen_root_calls(['/bin/command1']))
def test_cfn_init_with_ignore_errors_true(self):
@mock.patch.object(cfn_helper, 'controlled_privileges')
def test_cfn_init_with_ignore_errors_true(self, mock_cp):
calls = []
returns = []
calls.append('/bin/command1')
@ -1228,7 +1290,8 @@ class TestSourcesHandler(testtools.TestCase):
mock_popen.assert_has_calls(calls)
mock_mkdtemp.assert_called_with()
def test_apply_sources_github(self):
@mock.patch.object(cfn_helper, 'controlled_privileges')
def test_apply_sources_github(self, mock_cp):
url = "https://github.com/NoSuchProject/tarball/NoSuchTarball"
dest = tempfile.mkdtemp()
self.addCleanup(os.rmdir, dest)
@ -1241,7 +1304,8 @@ class TestSourcesHandler(testtools.TestCase):
sh.apply_sources()
mock_popen.assert_has_calls(calls)
def test_apply_sources_general(self):
@mock.patch.object(cfn_helper, 'controlled_privileges')
def test_apply_sources_general(self, mock_cp):
url = "https://website.no.existe/a/b/c/file.tar.gz"
dest = tempfile.mkdtemp()
self.addCleanup(os.rmdir, dest)