Use common rootwrap from oslo-incubator

Make Nova use common rootwrap code from oslo-incubator.
Implements bp nova-common-rootwrap

Change-Id: I3282d65940375589fceb8485829097380d84d946
This commit is contained in:
Thierry Carrez 2013-01-17 11:36:22 +01:00
parent d806266d23
commit 476f15d610
6 changed files with 21 additions and 221 deletions

View File

@ -16,20 +16,18 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
"""Root wrapper for Nova """Root wrapper for OpenStack services
Filters which commands nova is allowed to run as another user. Filters which commands a service is allowed to run as another user.
To use this, you should set the following in nova.conf: To use this with nova, you should set the following in nova.conf:
rootwrap_config=/etc/nova/rootwrap.conf rootwrap_config=/etc/nova/rootwrap.conf
You also need to let the nova user run nova-rootwrap as root in sudoers: You also need to let the nova user run nova-rootwrap as root in sudoers:
nova ALL = (root) NOPASSWD: /usr/bin/nova-rootwrap /etc/nova/rootwrap.conf * nova ALL = (root) NOPASSWD: /usr/bin/nova-rootwrap /etc/nova/rootwrap.conf *
To make allowed commands node-specific, your packaging should only Service packaging should deploy .filters files only on nodes where they are
install {compute,network,volume}.filters respectively on compute, network needed, to avoid allowing more than is necessary.
and volume nodes (i.e. nova-api nodes should not have any of those files
installed).
""" """
import ConfigParser import ConfigParser
@ -75,7 +73,7 @@ if __name__ == '__main__':
if os.path.exists(os.path.join(possible_topdir, "nova", "__init__.py")): if os.path.exists(os.path.join(possible_topdir, "nova", "__init__.py")):
sys.path.insert(0, possible_topdir) sys.path.insert(0, possible_topdir)
from nova.rootwrap import wrapper from nova.openstack.common.rootwrap import wrapper
# Load configuration # Load configuration
try: try:

View File

@ -20,7 +20,7 @@ import re
class CommandFilter(object): class CommandFilter(object):
"""Command filter only checking that the 1st argument matches exec_path.""" """Command filter only checking that the 1st argument matches exec_path"""
def __init__(self, exec_path, run_as, *args): def __init__(self, exec_path, run_as, *args):
self.name = '' self.name = ''
@ -30,7 +30,7 @@ class CommandFilter(object):
self.real_exec = None self.real_exec = None
def get_exec(self, exec_dirs=[]): def get_exec(self, exec_dirs=[]):
"""Returns existing executable, or empty string if none found.""" """Returns existing executable, or empty string if none found"""
if self.real_exec is not None: if self.real_exec is not None:
return self.real_exec return self.real_exec
self.real_exec = "" self.real_exec = ""
@ -46,7 +46,7 @@ class CommandFilter(object):
return self.real_exec return self.real_exec
def match(self, userargs): def match(self, userargs):
"""Only check that the first argument (command) matches exec_path.""" """Only check that the first argument (command) matches exec_path"""
if (os.path.basename(self.exec_path) == userargs[0]): if (os.path.basename(self.exec_path) == userargs[0]):
return True return True
return False return False
@ -60,12 +60,12 @@ class CommandFilter(object):
return [to_exec] + userargs[1:] return [to_exec] + userargs[1:]
def get_environment(self, userargs): def get_environment(self, userargs):
"""Returns specific environment to set, None if none.""" """Returns specific environment to set, None if none"""
return None return None
class RegExpFilter(CommandFilter): class RegExpFilter(CommandFilter):
"""Command filter doing regexp matching for every argument.""" """Command filter doing regexp matching for every argument"""
def match(self, userargs): def match(self, userargs):
# Early skip if command or number of args don't match # Early skip if command or number of args don't match
@ -89,15 +89,15 @@ class RegExpFilter(CommandFilter):
class DnsmasqFilter(CommandFilter): class DnsmasqFilter(CommandFilter):
"""Specific filter for the dnsmasq call (which includes env).""" """Specific filter for the dnsmasq call (which includes env)"""
CONFIG_FILE_ARG = 'CONFIG_FILE' CONFIG_FILE_ARG = 'CONFIG_FILE'
def match(self, userargs): def match(self, userargs):
if (userargs[0] == 'env' and if (userargs[0] == 'env' and
userargs[1].startswith(self.CONFIG_FILE_ARG) and userargs[1].startswith(self.CONFIG_FILE_ARG) and
userargs[2].startswith('NETWORK_ID=') and userargs[2].startswith('NETWORK_ID=') and
userargs[3] == 'dnsmasq'): userargs[3] == 'dnsmasq'):
return True return True
return False return False
@ -114,7 +114,7 @@ class DnsmasqFilter(CommandFilter):
class DeprecatedDnsmasqFilter(DnsmasqFilter): class DeprecatedDnsmasqFilter(DnsmasqFilter):
"""Variant of dnsmasq filter to support old-style FLAGFILE.""" """Variant of dnsmasq filter to support old-style FLAGFILE"""
CONFIG_FILE_ARG = 'FLAGFILE' CONFIG_FILE_ARG = 'FLAGFILE'
@ -164,7 +164,7 @@ class KillFilter(CommandFilter):
class ReadFileFilter(CommandFilter): class ReadFileFilter(CommandFilter):
"""Specific filter for the utils.read_file_as_root call.""" """Specific filter for the utils.read_file_as_root call"""
def __init__(self, file_path, *args): def __init__(self, file_path, *args):
self.file_path = file_path self.file_path = file_path

View File

@ -22,7 +22,7 @@ import logging.handlers
import os import os
import string import string
from nova.rootwrap import filters from nova.openstack.common.rootwrap import filters
class NoFilterMatched(Exception): class NoFilterMatched(Exception):
@ -93,7 +93,7 @@ def setup_syslog(execname, facility, level):
def build_filter(class_name, *args): def build_filter(class_name, *args):
"""Returns a filter object of class class_name.""" """Returns a filter object of class class_name"""
if not hasattr(filters, class_name): if not hasattr(filters, class_name):
logging.warning("Skipping unknown filter class (%s) specified " logging.warning("Skipping unknown filter class (%s) specified "
"in filter definitions" % class_name) "in filter definitions" % class_name)
@ -103,7 +103,7 @@ def build_filter(class_name, *args):
def load_filters(filters_path): def load_filters(filters_path):
"""Load filters from a list of directories.""" """Load filters from a list of directories"""
filterlist = [] filterlist = []
for filterdir in filters_path: for filterdir in filters_path:
if not os.path.isdir(filterdir): if not os.path.isdir(filterdir):

View File

@ -1,198 +0,0 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 OpenStack LLC
#
# 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
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import ConfigParser
import logging
import logging.handlers
import os
import subprocess
from nova.rootwrap import filters
from nova.rootwrap import wrapper
from nova import test
class RootwrapTestCase(test.TestCase):
def setUp(self):
super(RootwrapTestCase, self).setUp()
self.filters = [
filters.RegExpFilter("/bin/ls", "root", 'ls', '/[a-z]+'),
filters.CommandFilter("/usr/bin/foo_bar_not_exist", "root"),
filters.RegExpFilter("/bin/cat", "root", 'cat', '/[a-z]+'),
filters.CommandFilter("/nonexistent/cat", "root"),
filters.CommandFilter("/bin/cat", "root") # Keep this one last
]
def test_RegExpFilter_match(self):
usercmd = ["ls", "/root"]
filtermatch = wrapper.match_filter(self.filters, usercmd)
self.assertFalse(filtermatch is None)
self.assertEqual(filtermatch.get_command(usercmd),
["/bin/ls", "/root"])
def test_RegExpFilter_reject(self):
usercmd = ["ls", "root"]
self.assertRaises(wrapper.NoFilterMatched,
wrapper.match_filter, self.filters, usercmd)
def test_missing_command(self):
valid_but_missing = ["foo_bar_not_exist"]
invalid = ["foo_bar_not_exist_and_not_matched"]
self.assertRaises(wrapper.FilterMatchNotExecutable,
wrapper.match_filter, self.filters, valid_but_missing)
self.assertRaises(wrapper.NoFilterMatched,
wrapper.match_filter, self.filters, invalid)
def _test_DnsmasqFilter(self, filter_class, config_file_arg):
usercmd = ['env', config_file_arg + '=A', 'NETWORK_ID=foobar',
'dnsmasq', 'foo']
f = filter_class("/usr/bin/dnsmasq", "root")
self.assertTrue(f.match(usercmd))
self.assertEqual(f.get_command(usercmd), ['/usr/bin/dnsmasq', 'foo'])
env = f.get_environment(usercmd)
self.assertEqual(env.get(config_file_arg), 'A')
self.assertEqual(env.get('NETWORK_ID'), 'foobar')
def test_DnsmasqFilter(self):
self._test_DnsmasqFilter(filters.DnsmasqFilter, 'CONFIG_FILE')
def test_DeprecatedDnsmasqFilter(self):
self._test_DnsmasqFilter(filters.DeprecatedDnsmasqFilter, 'FLAGFILE')
def test_KillFilter(self):
if not os.path.exists("/proc/%d" % os.getpid()):
self.skipTest("Test requires /proc filesystem (procfs)")
p = subprocess.Popen(["cat"], stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)
try:
f = filters.KillFilter("root", "/bin/cat", "-9", "-HUP")
f2 = filters.KillFilter("root", "/usr/bin/cat", "-9", "-HUP")
usercmd = ['kill', '-ALRM', p.pid]
# Incorrect signal should fail
self.assertFalse(f.match(usercmd) or f2.match(usercmd))
usercmd = ['kill', p.pid]
# Providing no signal should fail
self.assertFalse(f.match(usercmd) or f2.match(usercmd))
# Providing matching signal should be allowed
usercmd = ['kill', '-9', p.pid]
self.assertTrue(f.match(usercmd) or f2.match(usercmd))
f = filters.KillFilter("root", "/bin/cat")
f2 = filters.KillFilter("root", "/usr/bin/cat")
usercmd = ['kill', os.getpid()]
# Our own PID does not match /bin/sleep, so it should fail
self.assertFalse(f.match(usercmd) or f2.match(usercmd))
usercmd = ['kill', 999999]
# Nonexistent PID should fail
self.assertFalse(f.match(usercmd) or f2.match(usercmd))
usercmd = ['kill', p.pid]
# Providing no signal should work
self.assertTrue(f.match(usercmd) or f2.match(usercmd))
finally:
# Terminate the "cat" process and wait for it to finish
p.terminate()
p.wait()
def test_KillFilter_no_raise(self):
# Makes sure ValueError from bug 926412 is gone.
f = filters.KillFilter("root", "")
# Providing anything other than kill should be False
usercmd = ['notkill', 999999]
self.assertFalse(f.match(usercmd))
# Providing something that is not a pid should be False
usercmd = ['kill', 'notapid']
self.assertFalse(f.match(usercmd))
def test_KillFilter_deleted_exe(self):
# Makes sure deleted exe's are killed correctly.
# See bug #967931.
def fake_readlink(blah):
return '/bin/commandddddd (deleted)'
f = filters.KillFilter("root", "/bin/commandddddd")
usercmd = ['kill', 1234]
# Providing no signal should work
self.stubs.Set(os, 'readlink', fake_readlink)
self.assertTrue(f.match(usercmd))
def test_ReadFileFilter(self):
goodfn = '/good/file.name'
f = filters.ReadFileFilter(goodfn)
usercmd = ['cat', '/bad/file']
self.assertFalse(f.match(['cat', '/bad/file']))
usercmd = ['cat', goodfn]
self.assertEqual(f.get_command(usercmd), ['/bin/cat', goodfn])
self.assertTrue(f.match(usercmd))
def test_exec_dirs_search(self):
# This test supposes you have /bin/cat or /usr/bin/cat locally
f = filters.CommandFilter("cat", "root")
usercmd = ['cat', '/f']
self.assertTrue(f.match(usercmd))
self.assertTrue(f.get_command(usercmd, exec_dirs=['/bin',
'/usr/bin']) in (['/bin/cat', '/f'], ['/usr/bin/cat', '/f']))
def test_skips(self):
# Check that all filters are skipped and that the last matches
usercmd = ["cat", "/"]
filtermatch = wrapper.match_filter(self.filters, usercmd)
self.assertTrue(filtermatch is self.filters[-1])
def test_RootwrapConfig(self):
raw = ConfigParser.RawConfigParser()
# Empty config should raise ConfigParser.Error
self.assertRaises(ConfigParser.Error, wrapper.RootwrapConfig, raw)
# Check default values
raw.set('DEFAULT', 'filters_path', '/a,/b')
config = wrapper.RootwrapConfig(raw)
self.assertEqual(config.filters_path, ['/a', '/b'])
self.assertEqual(config.exec_dirs, os.environ["PATH"].split(':'))
self.assertFalse(config.use_syslog)
self.assertEqual(config.syslog_log_facility,
logging.handlers.SysLogHandler.LOG_SYSLOG)
self.assertEqual(config.syslog_log_level, logging.ERROR)
# Check general values
raw.set('DEFAULT', 'exec_dirs', '/a,/x')
config = wrapper.RootwrapConfig(raw)
self.assertEqual(config.exec_dirs, ['/a', '/x'])
raw.set('DEFAULT', 'use_syslog', 'oui')
self.assertRaises(ValueError, wrapper.RootwrapConfig, raw)
raw.set('DEFAULT', 'use_syslog', 'true')
config = wrapper.RootwrapConfig(raw)
self.assertTrue(config.use_syslog)
raw.set('DEFAULT', 'syslog_log_facility', 'moo')
self.assertRaises(ValueError, wrapper.RootwrapConfig, raw)
raw.set('DEFAULT', 'syslog_log_facility', 'local0')
config = wrapper.RootwrapConfig(raw)
self.assertEqual(config.syslog_log_facility,
logging.handlers.SysLogHandler.LOG_LOCAL0)
raw.set('DEFAULT', 'syslog_log_facility', 'LOG_AUTH')
config = wrapper.RootwrapConfig(raw)
self.assertEqual(config.syslog_log_facility,
logging.handlers.SysLogHandler.LOG_AUTH)
raw.set('DEFAULT', 'syslog_log_level', 'bar')
self.assertRaises(ValueError, wrapper.RootwrapConfig, raw)
raw.set('DEFAULT', 'syslog_log_level', 'INFO')
config = wrapper.RootwrapConfig(raw)
self.assertEqual(config.syslog_log_level, logging.INFO)

View File

@ -1,7 +1,7 @@
[DEFAULT] [DEFAULT]
# The list of modules to copy from openstack-common # The list of modules to copy from openstack-common
modules=cfg,cliutils,context,excutils,eventlet_backdoor,fileutils,gettextutils,importutils,iniparser,jsonutils,local,lockutils,log,network_utils,notifier,plugin,policy,setup,timeutils,rpc,uuidutils modules=cfg,cliutils,context,excutils,eventlet_backdoor,fileutils,gettextutils,importutils,iniparser,jsonutils,local,lockutils,log,network_utils,notifier,plugin,policy,rootwrap,setup,timeutils,rpc,uuidutils
# The base module to hold the copy of openstack.common # The base module to hold the copy of openstack.common
base=nova base=nova