A more secure root-wrapper alternative
Alternative to using a sudoers file to limit which commands can be run as root in Nova. This one makes use of command filters defined in Nova code itself, which can be customized to deeply inspect command arguments before allowing a command to be executed. This change puts the infrastructure in place, together with command filters that replicate the level of filtering provided by a sudoers file (no deep argument inspection yet). An example of an advanced filter (RegExpFilter) is also provided. This new root wrapper is not active by default (root_helper still defaults to "sudo"). Implements blueprint nova-rootwrap. Change-Id: I7ad723b55e9446758876f21b4fbb09374a910425
This commit is contained in:
parent
6fdb709302
commit
1273f2c1fd
|
@ -0,0 +1,72 @@
|
|||
#!/usr/bin/env python
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright (c) 2011 Openstack, LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Root wrapper for Nova
|
||||
|
||||
Uses modules in nova.rootwrap containing filters for commands
|
||||
that nova is allowed to run as another user.
|
||||
|
||||
To switch to using this, you should:
|
||||
* Set "--root_helper=sudo nova-rootwrap" in nova.conf
|
||||
* Allow nova to run nova-rootwrap as root in nova_sudoers:
|
||||
nova ALL = (root) NOPASSWD: /usr/bin/nova-rootwrap
|
||||
(all other commands can be removed from this file)
|
||||
|
||||
To make allowed commands node-specific, your packaging should only
|
||||
install nova/rootwrap/{compute,network,volume}.py respectively on
|
||||
compute, network and volume nodes (i.e. nova-api nodes should not
|
||||
have any of those files installed).
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
RC_UNAUTHORIZED = 99
|
||||
RC_NOCOMMAND = 98
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Split arguments, require at least a command
|
||||
execname = sys.argv.pop(0)
|
||||
if len(sys.argv) == 0:
|
||||
print "%s: %s" % (execname, "No command specified")
|
||||
sys.exit(RC_NOCOMMAND)
|
||||
|
||||
userargs = sys.argv[:]
|
||||
|
||||
# Add ../ to sys.path to allow running from branch
|
||||
possible_topdir = os.path.normpath(os.path.join(os.path.abspath(execname),
|
||||
os.pardir, os.pardir))
|
||||
if os.path.exists(os.path.join(possible_topdir, "nova", "__init__.py")):
|
||||
sys.path.insert(0, possible_topdir)
|
||||
|
||||
from nova.rootwrap import wrapper
|
||||
|
||||
# Execute command if it matches any of the loaded filters
|
||||
filters = wrapper.load_filters()
|
||||
filtermatch = wrapper.match_filter(filters, userargs)
|
||||
if filtermatch:
|
||||
obj = subprocess.Popen(filtermatch.get_command(userargs),
|
||||
stdin=sys.stdin,
|
||||
stdout=sys.stdout,
|
||||
stderr=sys.stderr)
|
||||
sys.exit(obj.returncode)
|
||||
|
||||
print "Unauthorized command: %s" % ' '.join(userargs)
|
||||
sys.exit(RC_UNAUTHORIZED)
|
|
@ -0,0 +1,16 @@
|
|||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright (c) 2011 Openstack, LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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.
|
|
@ -0,0 +1,159 @@
|
|||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright (c) 2011 Openstack, LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
|
||||
from nova.rootwrap.filters import CommandFilter, DnsmasqFilter
|
||||
|
||||
filters = [
|
||||
# nova/virt/disk.py: 'kpartx', '-a', device
|
||||
# nova/virt/disk.py: 'kpartx', '-d', device
|
||||
CommandFilter("/sbin/kpartx", "root"),
|
||||
|
||||
# nova/virt/disk.py: 'tune2fs', '-c', 0, '-i', 0, mapped_device
|
||||
# nova/virt/xenapi/vm_utils.py: "tune2fs", "-O ^has_journal", part_path
|
||||
# nova/virt/xenapi/vm_utils.py: "tune2fs", "-j", partition_path
|
||||
CommandFilter("/sbin/tune2fs", "root"),
|
||||
|
||||
# nova/virt/disk.py: 'mount', mapped_device, tmpdir
|
||||
# nova/virt/disk.py: 'mount', device, container_dir
|
||||
# nova/virt/disk.py: 'mount'
|
||||
# nova/virt/xenapi/vm_utils.py: 'mount', '-t', 'ext2,ext3,ext4,reiserfs'..
|
||||
CommandFilter("/bin/mount", "root"),
|
||||
|
||||
# nova/virt/disk.py: 'umount', mapped_device
|
||||
# nova/virt/disk.py: 'umount', container_dir
|
||||
# nova/virt/xenapi/vm_utils.py: 'umount', dev_path
|
||||
CommandFilter("/bin/umount", "root"),
|
||||
|
||||
# nova/virt/disk.py: 'qemu-nbd', '-c', device, image
|
||||
# nova/virt/disk.py: 'qemu-nbd', '-d', device
|
||||
CommandFilter("/usr/bin/qemu-nbd", "root"),
|
||||
|
||||
# nova/virt/disk.py: 'losetup', '--find', '--show', image
|
||||
# nova/virt/disk.py: 'losetup', '--detach', device
|
||||
CommandFilter("/sbin/losetup", "root"),
|
||||
|
||||
# nova/virt/disk.py: 'tee', metadata_path
|
||||
# nova/virt/disk.py: 'tee', '-a', keyfile
|
||||
# nova/virt/disk.py: 'tee', netfile
|
||||
CommandFilter("/usr/bin/tee", "root"),
|
||||
|
||||
# nova/virt/disk.py: 'mkdir', '-p', sshdir
|
||||
# nova/virt/disk.py: 'mkdir', '-p', netdir
|
||||
CommandFilter("/bin/mkdir", "root"),
|
||||
|
||||
# nova/virt/disk.py: 'chown', 'root', sshdir
|
||||
# nova/virt/disk.py: 'chown', 'root:root', netdir
|
||||
# nova/virt/libvirt/connection.py: 'chown', os.getuid(), console_log
|
||||
# nova/virt/libvirt/connection.py: 'chown', os.getuid(), console_log
|
||||
# nova/virt/libvirt/connection.py: 'chown', 'root', basepath('disk')
|
||||
# nova/virt/xenapi/vm_utils.py: 'chown', os.getuid(), dev_path
|
||||
CommandFilter("/bin/chown", "root"),
|
||||
|
||||
# nova/virt/disk.py: 'chmod', '700', sshdir
|
||||
# nova/virt/disk.py: 'chmod', 755, netdir
|
||||
CommandFilter("/bin/chmod", "root"),
|
||||
|
||||
# nova/virt/libvirt/vif.py: 'ip', 'tuntap', 'add', dev, 'mode', 'tap'
|
||||
# nova/virt/libvirt/vif.py: 'ip', 'link', 'set', dev, 'up'
|
||||
# nova/virt/libvirt/vif.py: 'ip', 'link', 'delete', dev
|
||||
# nova/network/linux_net.py: 'ip', 'addr', 'add', str(floating_ip)+'/32'i..
|
||||
# nova/network/linux_net.py: 'ip', 'addr', 'del', str(floating_ip)+'/32'..
|
||||
# nova/network/linux_net.py: 'ip', 'addr', 'add', '169.254.169.254/32',..
|
||||
# nova/network/linux_net.py: 'ip', 'addr', 'show', 'dev', dev, 'scope',..
|
||||
# nova/network/linux_net.py: 'ip', 'addr', 'del/add', ip_params, dev)
|
||||
# nova/network/linux_net.py: 'ip', 'addr', 'del', params, fields[-1]
|
||||
# nova/network/linux_net.py: 'ip', 'addr', 'add', params, bridge
|
||||
# nova/network/linux_net.py: 'ip', '-f', 'inet6', 'addr', 'change', ..
|
||||
# nova/network/linux_net.py: 'ip', 'link', 'set', 'dev', dev, 'promisc',..
|
||||
# nova/network/linux_net.py: 'ip', 'link', 'add', 'link', bridge_if ...
|
||||
# nova/network/linux_net.py: 'ip', 'link', 'set', interface, "address",..
|
||||
# nova/network/linux_net.py: 'ip', 'link', 'set', interface, 'up'
|
||||
# nova/network/linux_net.py: 'ip', 'link', 'set', bridge, 'up'
|
||||
# nova/network/linux_net.py: 'ip', 'addr', 'show', 'dev', interface, ..
|
||||
# nova/network/linux_net.py: 'ip', 'link', 'set', dev, "address", ..
|
||||
# nova/network/linux_net.py: 'ip', 'link', 'set', dev, 'up'
|
||||
CommandFilter("/sbin/ip", "root"),
|
||||
|
||||
# nova/virt/libvirt/vif.py: 'tunctl', '-b', '-t', dev
|
||||
CommandFilter("/usr/sbin/tunctl", "root"),
|
||||
|
||||
# nova/virt/libvirt/vif.py: 'ovs-vsctl', ...
|
||||
# nova/virt/libvirt/vif.py: 'ovs-vsctl', 'del-port', ...
|
||||
# nova/network/linux_net.py: 'ovs-vsctl', ....
|
||||
CommandFilter("/usr/bin/ovs-vsctl", "root"),
|
||||
|
||||
# nova/virt/libvirt/connection.py: 'dd', "if=%s" % virsh_output, ...
|
||||
CommandFilter("/bin/dd", "root"),
|
||||
|
||||
# nova/virt/xenapi/volume_utils.py: 'iscsiadm', '-m', ...
|
||||
CommandFilter("/sbin/iscsiadm", "root"),
|
||||
|
||||
# nova/virt/xenapi/vm_utils.py: "parted", "--script", ...
|
||||
# nova/virt/xenapi/vm_utils.py: 'parted', '--script', dev_path, ..*.
|
||||
CommandFilter("/sbin/parted", "root"),
|
||||
|
||||
# nova/virt/xenapi/vm_utils.py: fdisk %(dev_path)s
|
||||
CommandFilter("/sbin/fdisk", "root"),
|
||||
|
||||
# nova/virt/xenapi/vm_utils.py: "e2fsck", "-f", "-p", partition_path
|
||||
CommandFilter("/sbin/e2fsck", "root"),
|
||||
|
||||
# nova/virt/xenapi/vm_utils.py: "resize2fs", partition_path
|
||||
CommandFilter("/sbin/resize2fs", "root"),
|
||||
|
||||
# nova/network/linux_net.py: 'ip[6]tables-save' % (cmd,), '-t', ...
|
||||
CommandFilter("/sbin/iptables-save", "root"),
|
||||
CommandFilter("/sbin/ip6tables-save", "root"),
|
||||
|
||||
# nova/network/linux_net.py: 'ip[6]tables-restore' % (cmd,)
|
||||
CommandFilter("/sbin/iptables-restore", "root"),
|
||||
CommandFilter("/sbin/ip6tables-restore", "root"),
|
||||
|
||||
# nova/network/linux_net.py: 'arping', '-U', floating_ip, '-A', '-I', ...
|
||||
# nova/network/linux_net.py: 'arping', '-U', network_ref['dhcp_server'],..
|
||||
CommandFilter("/usr/bin/arping", "root"),
|
||||
|
||||
# nova/network/linux_net.py: 'route', '-n'
|
||||
# nova/network/linux_net.py: 'route', 'del', 'default', 'gw'
|
||||
# nova/network/linux_net.py: 'route', 'add', 'default', 'gw'
|
||||
# nova/network/linux_net.py: 'route', '-n'
|
||||
# nova/network/linux_net.py: 'route', 'del', 'default', 'gw', old_gw, ..
|
||||
# nova/network/linux_net.py: 'route', 'add', 'default', 'gw', old_gateway
|
||||
CommandFilter("/sbin/route", "root"),
|
||||
|
||||
# nova/network/linux_net.py: 'dhcp_release', dev, address, mac_address
|
||||
CommandFilter("/usr/bin/dhcp_release", "root"),
|
||||
|
||||
# nova/network/linux_net.py: 'kill', '-9', pid
|
||||
# nova/network/linux_net.py: 'kill', '-HUP', pid
|
||||
# nova/network/linux_net.py: 'kill', pid
|
||||
CommandFilter("/bin/kill", "root"),
|
||||
|
||||
# nova/network/linux_net.py: dnsmasq call
|
||||
DnsmasqFilter("/usr/sbin/dnsmasq", "root"),
|
||||
|
||||
# nova/network/linux_net.py: 'radvd', '-C', '%s' % _ra_file(dev, 'conf'),..
|
||||
CommandFilter("/usr/sbin/radvd", "root"),
|
||||
|
||||
# nova/network/linux_net.py: 'brctl', 'addbr', bridge
|
||||
# nova/network/linux_net.py: 'brctl', 'setfd', bridge, 0
|
||||
# nova/network/linux_net.py: 'brctl', 'stp', bridge, 'off'
|
||||
# nova/network/linux_net.py: 'brctl', 'addif', bridge, interface
|
||||
CommandFilter("/sbin/brctl", "root"),
|
||||
CommandFilter("/usr/sbin/brctl", "root"),
|
||||
]
|
|
@ -0,0 +1,80 @@
|
|||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright (c) 2011 Openstack, LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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 os
|
||||
import re
|
||||
|
||||
|
||||
class CommandFilter(object):
|
||||
"""Command filter only checking that the 1st argument matches exec_path"""
|
||||
|
||||
def __init__(self, exec_path, run_as, *args):
|
||||
self.exec_path = exec_path
|
||||
self.run_as = run_as
|
||||
self.args = args
|
||||
|
||||
def match(self, userargs):
|
||||
"""Only check that the first argument (command) matches exec_path"""
|
||||
if (os.path.basename(self.exec_path) == userargs[0]):
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_command(self, userargs):
|
||||
"""Returns command to execute (with sudo -u if run_as != root)."""
|
||||
if (self.run_as != 'root'):
|
||||
# Used to run commands at lesser privileges
|
||||
return ['sudo', '-u', self.run_as, self.exec_path] + userargs[1:]
|
||||
return [self.exec_path] + userargs[1:]
|
||||
|
||||
|
||||
class RegExpFilter(CommandFilter):
|
||||
"""Command filter doing regexp matching for every argument"""
|
||||
|
||||
def match(self, userargs):
|
||||
# Early skip if command or number of args don't match
|
||||
if (len(self.args) != len(userargs)):
|
||||
# DENY: argument numbers don't match
|
||||
return False
|
||||
# Compare each arg (anchoring pattern explicitly at end of string)
|
||||
for (pattern, arg) in zip(self.args, userargs):
|
||||
try:
|
||||
if not re.match(pattern + '$', arg):
|
||||
break
|
||||
except re.error:
|
||||
# DENY: Badly-formed filter
|
||||
return False
|
||||
else:
|
||||
# ALLOW: All arguments matched
|
||||
return True
|
||||
|
||||
# DENY: Some arguments did not match
|
||||
return False
|
||||
|
||||
|
||||
class DnsmasqFilter(CommandFilter):
|
||||
"""Specific filter for the dnsmasq call (which includes env)"""
|
||||
|
||||
def match(self, userargs):
|
||||
if (userargs[0].startswith("FLAGFILE=") and
|
||||
userargs[1].startswith("NETWORK_ID=") and
|
||||
userargs[2] == "dnsmasq"):
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_command(self, userargs):
|
||||
return userargs[0:2] + [self.exec_path] + userargs[3:]
|
|
@ -0,0 +1,83 @@
|
|||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright (c) 2011 Openstack, LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
|
||||
from nova.rootwrap.filters import CommandFilter, DnsmasqFilter
|
||||
|
||||
filters = [
|
||||
# nova/network/linux_net.py: 'ip', 'addr', 'add', str(floating_ip)+'/32'i..
|
||||
# nova/network/linux_net.py: 'ip', 'addr', 'del', str(floating_ip)+'/32'..
|
||||
# nova/network/linux_net.py: 'ip', 'addr', 'add', '169.254.169.254/32',..
|
||||
# nova/network/linux_net.py: 'ip', 'addr', 'show', 'dev', dev, 'scope',..
|
||||
# nova/network/linux_net.py: 'ip', 'addr', 'del/add', ip_params, dev)
|
||||
# nova/network/linux_net.py: 'ip', 'addr', 'del', params, fields[-1]
|
||||
# nova/network/linux_net.py: 'ip', 'addr', 'add', params, bridge
|
||||
# nova/network/linux_net.py: 'ip', '-f', 'inet6', 'addr', 'change', ..
|
||||
# nova/network/linux_net.py: 'ip', 'link', 'set', 'dev', dev, 'promisc',..
|
||||
# nova/network/linux_net.py: 'ip', 'link', 'add', 'link', bridge_if ...
|
||||
# nova/network/linux_net.py: 'ip', 'link', 'set', interface, "address",..
|
||||
# nova/network/linux_net.py: 'ip', 'link', 'set', interface, 'up'
|
||||
# nova/network/linux_net.py: 'ip', 'link', 'set', bridge, 'up'
|
||||
# nova/network/linux_net.py: 'ip', 'addr', 'show', 'dev', interface, ..
|
||||
# nova/network/linux_net.py: 'ip', 'link', 'set', dev, "address", ..
|
||||
# nova/network/linux_net.py: 'ip', 'link', 'set', dev, 'up'
|
||||
CommandFilter("/sbin/ip", "root"),
|
||||
|
||||
# nova/network/linux_net.py: 'ip[6]tables-save' % (cmd,), '-t', ...
|
||||
CommandFilter("/sbin/iptables-save", "root"),
|
||||
CommandFilter("/sbin/ip6tables-save", "root"),
|
||||
|
||||
# nova/network/linux_net.py: 'ip[6]tables-restore' % (cmd,)
|
||||
CommandFilter("/sbin/iptables-restore", "root"),
|
||||
CommandFilter("/sbin/ip6tables-restore", "root"),
|
||||
|
||||
# nova/network/linux_net.py: 'arping', '-U', floating_ip, '-A', '-I', ...
|
||||
# nova/network/linux_net.py: 'arping', '-U', network_ref['dhcp_server'],..
|
||||
CommandFilter("/usr/bin/arping", "root"),
|
||||
|
||||
# nova/network/linux_net.py: 'route', '-n'
|
||||
# nova/network/linux_net.py: 'route', 'del', 'default', 'gw'
|
||||
# nova/network/linux_net.py: 'route', 'add', 'default', 'gw'
|
||||
# nova/network/linux_net.py: 'route', '-n'
|
||||
# nova/network/linux_net.py: 'route', 'del', 'default', 'gw', old_gw, ..
|
||||
# nova/network/linux_net.py: 'route', 'add', 'default', 'gw', old_gateway
|
||||
CommandFilter("/sbin/route", "root"),
|
||||
|
||||
# nova/network/linux_net.py: 'dhcp_release', dev, address, mac_address
|
||||
CommandFilter("/usr/bin/dhcp_release", "root"),
|
||||
|
||||
# nova/network/linux_net.py: 'kill', '-9', pid
|
||||
# nova/network/linux_net.py: 'kill', '-HUP', pid
|
||||
# nova/network/linux_net.py: 'kill', pid
|
||||
CommandFilter("/bin/kill", "root"),
|
||||
|
||||
# nova/network/linux_net.py: dnsmasq call
|
||||
DnsmasqFilter("/usr/sbin/dnsmasq", "root"),
|
||||
|
||||
# nova/network/linux_net.py: 'radvd', '-C', '%s' % _ra_file(dev, 'conf'),..
|
||||
CommandFilter("/usr/sbin/radvd", "root"),
|
||||
|
||||
# nova/network/linux_net.py: 'brctl', 'addbr', bridge
|
||||
# nova/network/linux_net.py: 'brctl', 'setfd', bridge, 0
|
||||
# nova/network/linux_net.py: 'brctl', 'stp', bridge, 'off'
|
||||
# nova/network/linux_net.py: 'brctl', 'addif', bridge, interface
|
||||
CommandFilter("/sbin/brctl", "root"),
|
||||
CommandFilter("/usr/sbin/brctl", "root"),
|
||||
|
||||
# nova/network/linux_net.py: 'ovs-vsctl', ....
|
||||
CommandFilter("/usr/bin/ovs-vsctl", "root"),
|
||||
]
|
|
@ -0,0 +1,48 @@
|
|||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright (c) 2011 Openstack, LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
|
||||
from nova.rootwrap.filters import CommandFilter
|
||||
|
||||
filters = [
|
||||
# nova/volume/iscsi.py: iscsi_helper '--op' ...
|
||||
CommandFilter("/usr/sbin/ietadm", "root"),
|
||||
CommandFilter("/usr/sbin/tgtadm", "root"),
|
||||
|
||||
# nova/volume/driver.py: 'vgs', '--noheadings', '-o', 'name'
|
||||
CommandFilter("/sbin/vgs", "root"),
|
||||
|
||||
# nova/volume/driver.py: 'lvcreate', '-L', sizestr, '-n', volume_name,..
|
||||
# nova/volume/driver.py: 'lvcreate', '-L', ...
|
||||
CommandFilter("/sbin/lvcreate", "root"),
|
||||
|
||||
# nova/volume/driver.py: 'dd', 'if=%s' % srcstr, 'of=%s' % deststr,...
|
||||
CommandFilter("/bin/dd", "root"),
|
||||
|
||||
# nova/volume/driver.py: 'lvremove', '-f', "%s/%s" % ...
|
||||
CommandFilter("/sbin/lvremove", "root"),
|
||||
|
||||
# nova/volume/driver.py: 'lvdisplay', '--noheading', '-C', '-o', 'Attr',..
|
||||
CommandFilter("/sbin/lvdisplay", "root"),
|
||||
|
||||
# nova/volume/driver.py: 'iscsiadm', '-m', 'discovery', '-t',...
|
||||
# nova/volume/driver.py: 'iscsiadm', '-m', 'node', '-T', ...
|
||||
CommandFilter("/sbin/iscsiadm", "root"),
|
||||
|
||||
# nova/volume/driver.py:'/var/lib/zadara/bin/zadara_sncfg', *
|
||||
# sudoers does not allow zadara_sncfg yet
|
||||
]
|
|
@ -0,0 +1,59 @@
|
|||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright (c) 2011 Openstack, LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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 os
|
||||
import sys
|
||||
|
||||
|
||||
FILTERS_MODULES = ['nova.rootwrap.compute',
|
||||
'nova.rootwrap.network',
|
||||
'nova.rootwrap.volume',
|
||||
]
|
||||
|
||||
|
||||
def load_filters():
|
||||
"""Load filters from modules present in nova.rootwrap."""
|
||||
filters = []
|
||||
for modulename in FILTERS_MODULES:
|
||||
try:
|
||||
__import__(modulename)
|
||||
module = sys.modules[modulename]
|
||||
filters = filters + module.filters
|
||||
except ImportError:
|
||||
# It's OK to have missing filters, since filter modules are
|
||||
# shipped with specific nodes rather than with python-nova
|
||||
pass
|
||||
return filters
|
||||
|
||||
|
||||
def match_filter(filters, userargs):
|
||||
"""
|
||||
Checks user command and arguments through command filters and
|
||||
returns the first matching filter, or None is none matched.
|
||||
"""
|
||||
|
||||
for f in filters:
|
||||
if f.match(userargs):
|
||||
# Skip if executable is absent
|
||||
if not os.access(f.exec_path, os.X_OK):
|
||||
continue
|
||||
# Otherwise return matching filter for execution
|
||||
return f
|
||||
|
||||
# No filter matched
|
||||
return None
|
|
@ -0,0 +1,65 @@
|
|||
# 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.
|
||||
|
||||
from nova.rootwrap.filters import CommandFilter, RegExpFilter, DnsmasqFilter
|
||||
from nova.rootwrap.wrapper import match_filter
|
||||
from nova import test
|
||||
|
||||
|
||||
class RootwrapTestCase(test.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(RootwrapTestCase, self).setUp()
|
||||
self.filters = [
|
||||
RegExpFilter("/bin/ls", "root", 'ls', '/[a-z]+'),
|
||||
CommandFilter("/usr/bin/foo_bar_not_exist", "root"),
|
||||
RegExpFilter("/bin/cat", "root", 'cat', '/[a-z]+'),
|
||||
CommandFilter("/nonexistant/cat", "root"),
|
||||
CommandFilter("/bin/cat", "root") # Keep this one last
|
||||
]
|
||||
|
||||
def tearDown(self):
|
||||
super(RootwrapTestCase, self).tearDown()
|
||||
|
||||
def test_RegExpFilter_match(self):
|
||||
usercmd = ["ls", "/root"]
|
||||
filtermatch = 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"]
|
||||
filtermatch = match_filter(self.filters, usercmd)
|
||||
self.assertTrue(filtermatch is None)
|
||||
|
||||
def test_missing_command(self):
|
||||
usercmd = ["foo_bar_not_exist"]
|
||||
filtermatch = match_filter(self.filters, usercmd)
|
||||
self.assertTrue(filtermatch is None)
|
||||
|
||||
def test_dnsmasq_filter(self):
|
||||
usercmd = ['FLAGFILE=A', 'NETWORK_ID="foo bar"', 'dnsmasq', 'foo']
|
||||
f = DnsmasqFilter("/usr/bin/dnsmasq", "root")
|
||||
self.assertTrue(f.match(usercmd))
|
||||
self.assertEqual(f.get_command(usercmd),
|
||||
['FLAGFILE=A', 'NETWORK_ID="foo bar"', '/usr/bin/dnsmasq', 'foo'])
|
||||
|
||||
def test_skips(self):
|
||||
# Check that all filters are skipped and that the last matches
|
||||
usercmd = ["cat", "/"]
|
||||
filtermatch = match_filter(self.filters, usercmd)
|
||||
self.assertTrue(filtermatch is self.filters[-1])
|
Loading…
Reference in New Issue