oslo.rootwrap-1.2.0/0000775000175300017540000000000012314054302015471 5ustar jenkinsjenkins00000000000000oslo.rootwrap-1.2.0/etc/0000775000175300017540000000000012314054302016244 5ustar jenkinsjenkins00000000000000oslo.rootwrap-1.2.0/etc/rootwrap.conf.sample0000664000175300017540000000222012314054252022250 0ustar jenkinsjenkins00000000000000# Configuration for rootwrap # This file should be owned by (and only-writeable by) the root user [DEFAULT] # List of directories to load filter definitions from (separated by ','). # These directories MUST all be only writeable by root ! filters_path=/etc/oslo-rootwrap/filters.d,/usr/share/oslo-rootwrap # List of directories to search executables in, in case filters do not # explicitly specify a full path (separated by ',') # If not specified, defaults to system PATH environment variable. # These directories MUST all be only writeable by root ! exec_dirs=/sbin,/usr/sbin,/bin,/usr/bin # Enable logging to syslog # Default value is False use_syslog=False # Enable RFC5424 compliant format for syslog (add APP-NAME before MSG part) # Default value is False - no format changes # TODO(bogdando) remove or use True after existing syslog format deprecation in J use_syslog_rfc_format=False # Which syslog facility to use. # Valid values include auth, authpriv, syslog, user0, user1... # Default value is 'syslog' syslog_log_facility=syslog # Which messages to log. # INFO means log all usage # ERROR means only log unsuccessful attempts syslog_log_level=ERROR oslo.rootwrap-1.2.0/README.rst0000664000175300017540000002344112314054252017170 0ustar jenkinsjenkins00000000000000------------- Oslo Rootwrap ------------- The Oslo Rootwrap allows fine filtering of shell commands to run as `root` from OpenStack services. Rootwrap should be used as a separate Python process calling the oslo.rootwrap.cmd:main function. You can set up a specific console_script calling into oslo.rootwrap.cmd:main, called for example `nova-rootwrap`. To keep things simple, this document will consider that your console_script is called `/usr/bin/nova-rootwrap`. The rootwrap command line should be called under `sudo`. It's first parameter is the configuration file to use, and the remainder of the parameters are the command line to execute: `sudo nova-rootwrap ROOTWRAP_CONFIG COMMAND_LINE` How rootwrap works ================== OpenStack services generally run under a specific, unprivileged user. However, sometimes they need to run a command as `root`. Instead of just calling `sudo make me a sandwich` and have a blanket `sudoers` permission to always escalate rights from their unprivileged users to `root`, those services can call `sudo nova-rootwrap /etc/nova/rootwrap.conf make me a sandwich`. A sudoers entry lets the unprivileged user run `nova-rootwrap` as `root`. `nova-rootwrap` looks for filter definition directories in its configuration file, and loads command filters from them. Then it checks if the command requested by the OpenStack service matches one of those filters, in which case it executes the command (as `root`). If no filter matches, it denies the request. This allows for complex filtering of allowed commands, as well as shipping filter definitions together with the OpenStack code that needs them. Security model ============== The escalation path is fully controlled by the `root` user. A `sudoers` entry (owned by `root`) allows the unprivileged user to run (as `root`) a specific rootwrap executable, and only with a specific configuration file (which should be owned by `root`) as its first parameter. `nova-rootwrap` imports the Python modules it needs from a cleaned (and system-default) `PYTHONPATH`. The configuration file points to root-owned filter definition directories, which contain root-owned filters definition files. This chain ensures that the unprivileged user itself is never in control of the configuration or modules used by the `nova-rootwrap` executable. Installation ============ All nodes wishing to run `nova-rootwrap` should contain a `sudoers` entry that lets the unprivileged user run `nova-rootwrap` as `root`, pointing to the root-owned `rootwrap.conf` configuration file and allowing any parameter after that. For example, Nova nodes should have this line in their `sudoers` file, to allow the `nova` user to call `sudo nova-rootwrap`: ``nova ALL = (root) NOPASSWD: /usr/bin/nova-rootwrap /etc/nova/rootwrap.conf *`` Then the node also should ship the filter definitions corresponding to its usage of `nova-rootwrap`. You should not install any other filters file on that node, otherwise you would allow extra unneeded commands to be run as `root`. The filter file(s) corresponding to the node must be installed in one of the filters_path directories. For example, on Nova compute nodes, you should only have `compute.filters` installed. The file should be owned and writeable only by the `root` user. Rootwrap configuration ====================== The `rootwrap.conf` file is used to influence how `nova-rootwrap` works. Since it's in the trusted security path, it needs to be owned and writeable only by the `root` user. Its location is specified in the `sudoers` entry, and must be provided on `nova-rootwrap` command line as its first argument. `rootwrap.conf` uses an *INI* file format with the following sections and parameters: [DEFAULT] section ----------------- filters_path Comma-separated list of directories containing filter definition files. All directories listed must be owned and only writeable by `root`. This is the only mandatory parameter. Example: ``filters_path=/etc/nova/rootwrap.d,/usr/share/nova/rootwrap`` exec_dirs Comma-separated list of directories to search executables in, in case filters do not explicitely specify a full path. If not specified, defaults to the system `PATH` environment variable. All directories listed must be owned and only writeable by `root`. Example: ``exec_dirs=/sbin,/usr/sbin,/bin,/usr/bin`` use_syslog Enable logging to syslog. Default value is False. Example: ``use_syslog=True`` use_syslog_rfc_format Enable RFC5424 compliant format for syslog (add APP-NAME before MSG part). Default value is False. Example: ``use_syslog_rfc_format=True`` syslog_log_facility Which syslog facility to use for syslog logging. Valid values include `auth`, `authpriv`, `syslog`, `user0`, `user1`... Default value is `syslog`. Example: ``syslog_log_facility=syslog`` syslog_log_level Which messages to log. `INFO` means log all usage, `ERROR` means only log unsuccessful attempts. Example: ``syslog_log_level=ERROR`` .filters files ============== Filters definition files contain lists of filters that `nova-rootwrap` will use to allow or deny a specific command. They are generally suffixed by `.filters`. Since they are in the trusted security path, they need to be owned and writeable only by the `root` user. Their location is specified in the `rootwrap.conf` file. It uses an *INI* file format with a `[Filters]` section and several lines, each with a unique parameter name (different for each filter you define): [Filters] section ----------------- filter_name (different for each filter) Comma-separated list containing first the Filter class to use, followed by that Filter arguments (which vary depending on the Filter class selected). Example: ``kpartx: CommandFilter, /sbin/kpartx, root`` Available filter classes ======================== CommandFilter ------------- Basic filter that only checks the executable called. Parameters are: 1. Executable allowed 2. User to run the command under Example: allow to run kpartx as the root user, with any parameters: ``kpartx: CommandFilter, kpartx, root`` RegExpFilter ------------ Generic filter that checks the executable called, then uses a list of regular expressions to check all subsequent arguments. Parameters are: 1. Executable allowed 2. User to run the command under 3. (and following) Regular expressions to use to match first (and subsequent) command arguments Example: allow to run `/usr/sbin/tunctl`, but only with three parameters with the first two being -b and -t: ``tunctl: /usr/sbin/tunctl, root, tunctl, -b, -t, .*`` PathFilter ---------- Generic filter that lets you check that paths provided as parameters fall under a given directory. Parameters are: 1. Executable allowed 2. User to run the command under 3. (and following) Command arguments. There are three types of command arguments: `pass` will accept any parameter value, a string will only accept the corresponding string as a parameter, except if the string starts with '/' in which case it will accept any path that resolves under the corresponding directory. Example: allow to chown to the 'nova' user any file under /var/lib/images: ``chown: PathFilter, /bin/chown, root, nova, /var/lib/images`` EnvFilter --------- Filter allowing extra environment variables to be set by the calling code. Parameters are: 1. `env` 2. User to run the command under 3. (and following) name of the environment variables that can be set, suffixed by `=` 4. Executable allowed Example: allow to run `CONFIG_FILE=foo NETWORK_ID=bar dnsmasq ...` as root: ``dnsmasq: EnvFilter, env, root, CONFIG_FILE=, NETWORK_ID=, dnsmasq`` ReadFileFilter -------------- Specific filter that lets you read files as `root` using `cat`. Parameters are: 1. Path to the file that you want to read as the `root` user. Example: allow to run `cat /etc/iscsi/initiatorname.iscsi` as `root`: ``read_initiator: ReadFileFilter, /etc/iscsi/initiatorname.iscsi`` KillFilter ---------- Kill-specific filter that checks the affected process and the signal sent before allowing the command. Parameters are: 1. User to run `kill` under 2. Only affect processes running that executable 3. (and following) Signals you're allowed to send Example: allow to send `-9` or `-HUP` signals to `/usr/sbin/dnsmasq` processes: ``kill_dnsmasq: KillFilter, root, /usr/sbin/dnsmasq, -9, -HUP`` IpFilter -------- ip-specific filter that allows to run any `ip` command, except for `ip netns` (in which case it only allows the list, add and delete subcommands). Parameters are: 1. `ip` 2. User to run `ip` under Example: allow to run any `ip` command except `ip netns exec` and `ip netns monitor`: ``ip: IpFilter, ip, root`` IpNetnsExecFilter ----------------- ip-specific filter that allows to run any otherwise-allowed command under `ip netns exec`. The command specified to `ip netns exec` must match another filter for this filter to accept it. Parameters are: 1. `ip` 2. User to run `ip` under Example: allow to run `ip netns exec ` as long as `` matches another filter: ``ip: IpNetnsExecFilter, ip, root`` Calling rootwrap from OpenStack services ============================================= The `oslo.processutils` library ships with a convenience `execute()` function that can be used to call shell commands as `root`, if you call it with the following parameters: ``run_as_root=True`` ``root_helper='sudo nova-rootwrap /etc/nova/rootwrap.conf`` NB: Some services ship with a `utils.execute()` convenience function that automatically sets `root_helper` based on the value of a `rootwrap_config` parameter, so only `run_as_root=True` needs to be set. If you want to call as `root` a previously-unauthorized command, you will also need to modify the filters (generally shipped in the source tree under `etc/rootwrap.d` so that the command you want to run as `root` will actually be allowed by `nova-rootwrap`. oslo.rootwrap-1.2.0/tox.ini0000664000175300017540000000107312314054252017011 0ustar jenkinsjenkins00000000000000[tox] minversion = 1.6 envlist = py26,py27,py33,pep8 skipsdist = True [testenv] usedevelop = True install_command = pip install -U {opts} {packages} setenv = VIRTUAL_ENV={envdir} deps = -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt commands = python setup.py testr --slowest --testr-args='{posargs}' [testenv:pep8] commands = flake8 [testenv:cover] setenv = VIRTUAL_ENV={envdir} commands = python setup.py testr --coverage [testenv:venv] commands = {posargs} [flake8] show-source = True exclude = .tox,dist,doc,*.egg,build builtins = _ oslo.rootwrap-1.2.0/PKG-INFO0000664000175300017540000003141612314054302016573 0ustar jenkinsjenkins00000000000000Metadata-Version: 1.1 Name: oslo.rootwrap Version: 1.2.0 Summary: Oslo Rootwrap Home-page: https://launchpad.net/oslo Author: OpenStack Author-email: openstack-dev@lists.openstack.org License: UNKNOWN Description: ------------- Oslo Rootwrap ------------- The Oslo Rootwrap allows fine filtering of shell commands to run as `root` from OpenStack services. Rootwrap should be used as a separate Python process calling the oslo.rootwrap.cmd:main function. You can set up a specific console_script calling into oslo.rootwrap.cmd:main, called for example `nova-rootwrap`. To keep things simple, this document will consider that your console_script is called `/usr/bin/nova-rootwrap`. The rootwrap command line should be called under `sudo`. It's first parameter is the configuration file to use, and the remainder of the parameters are the command line to execute: `sudo nova-rootwrap ROOTWRAP_CONFIG COMMAND_LINE` How rootwrap works ================== OpenStack services generally run under a specific, unprivileged user. However, sometimes they need to run a command as `root`. Instead of just calling `sudo make me a sandwich` and have a blanket `sudoers` permission to always escalate rights from their unprivileged users to `root`, those services can call `sudo nova-rootwrap /etc/nova/rootwrap.conf make me a sandwich`. A sudoers entry lets the unprivileged user run `nova-rootwrap` as `root`. `nova-rootwrap` looks for filter definition directories in its configuration file, and loads command filters from them. Then it checks if the command requested by the OpenStack service matches one of those filters, in which case it executes the command (as `root`). If no filter matches, it denies the request. This allows for complex filtering of allowed commands, as well as shipping filter definitions together with the OpenStack code that needs them. Security model ============== The escalation path is fully controlled by the `root` user. A `sudoers` entry (owned by `root`) allows the unprivileged user to run (as `root`) a specific rootwrap executable, and only with a specific configuration file (which should be owned by `root`) as its first parameter. `nova-rootwrap` imports the Python modules it needs from a cleaned (and system-default) `PYTHONPATH`. The configuration file points to root-owned filter definition directories, which contain root-owned filters definition files. This chain ensures that the unprivileged user itself is never in control of the configuration or modules used by the `nova-rootwrap` executable. Installation ============ All nodes wishing to run `nova-rootwrap` should contain a `sudoers` entry that lets the unprivileged user run `nova-rootwrap` as `root`, pointing to the root-owned `rootwrap.conf` configuration file and allowing any parameter after that. For example, Nova nodes should have this line in their `sudoers` file, to allow the `nova` user to call `sudo nova-rootwrap`: ``nova ALL = (root) NOPASSWD: /usr/bin/nova-rootwrap /etc/nova/rootwrap.conf *`` Then the node also should ship the filter definitions corresponding to its usage of `nova-rootwrap`. You should not install any other filters file on that node, otherwise you would allow extra unneeded commands to be run as `root`. The filter file(s) corresponding to the node must be installed in one of the filters_path directories. For example, on Nova compute nodes, you should only have `compute.filters` installed. The file should be owned and writeable only by the `root` user. Rootwrap configuration ====================== The `rootwrap.conf` file is used to influence how `nova-rootwrap` works. Since it's in the trusted security path, it needs to be owned and writeable only by the `root` user. Its location is specified in the `sudoers` entry, and must be provided on `nova-rootwrap` command line as its first argument. `rootwrap.conf` uses an *INI* file format with the following sections and parameters: [DEFAULT] section ----------------- filters_path Comma-separated list of directories containing filter definition files. All directories listed must be owned and only writeable by `root`. This is the only mandatory parameter. Example: ``filters_path=/etc/nova/rootwrap.d,/usr/share/nova/rootwrap`` exec_dirs Comma-separated list of directories to search executables in, in case filters do not explicitely specify a full path. If not specified, defaults to the system `PATH` environment variable. All directories listed must be owned and only writeable by `root`. Example: ``exec_dirs=/sbin,/usr/sbin,/bin,/usr/bin`` use_syslog Enable logging to syslog. Default value is False. Example: ``use_syslog=True`` use_syslog_rfc_format Enable RFC5424 compliant format for syslog (add APP-NAME before MSG part). Default value is False. Example: ``use_syslog_rfc_format=True`` syslog_log_facility Which syslog facility to use for syslog logging. Valid values include `auth`, `authpriv`, `syslog`, `user0`, `user1`... Default value is `syslog`. Example: ``syslog_log_facility=syslog`` syslog_log_level Which messages to log. `INFO` means log all usage, `ERROR` means only log unsuccessful attempts. Example: ``syslog_log_level=ERROR`` .filters files ============== Filters definition files contain lists of filters that `nova-rootwrap` will use to allow or deny a specific command. They are generally suffixed by `.filters`. Since they are in the trusted security path, they need to be owned and writeable only by the `root` user. Their location is specified in the `rootwrap.conf` file. It uses an *INI* file format with a `[Filters]` section and several lines, each with a unique parameter name (different for each filter you define): [Filters] section ----------------- filter_name (different for each filter) Comma-separated list containing first the Filter class to use, followed by that Filter arguments (which vary depending on the Filter class selected). Example: ``kpartx: CommandFilter, /sbin/kpartx, root`` Available filter classes ======================== CommandFilter ------------- Basic filter that only checks the executable called. Parameters are: 1. Executable allowed 2. User to run the command under Example: allow to run kpartx as the root user, with any parameters: ``kpartx: CommandFilter, kpartx, root`` RegExpFilter ------------ Generic filter that checks the executable called, then uses a list of regular expressions to check all subsequent arguments. Parameters are: 1. Executable allowed 2. User to run the command under 3. (and following) Regular expressions to use to match first (and subsequent) command arguments Example: allow to run `/usr/sbin/tunctl`, but only with three parameters with the first two being -b and -t: ``tunctl: /usr/sbin/tunctl, root, tunctl, -b, -t, .*`` PathFilter ---------- Generic filter that lets you check that paths provided as parameters fall under a given directory. Parameters are: 1. Executable allowed 2. User to run the command under 3. (and following) Command arguments. There are three types of command arguments: `pass` will accept any parameter value, a string will only accept the corresponding string as a parameter, except if the string starts with '/' in which case it will accept any path that resolves under the corresponding directory. Example: allow to chown to the 'nova' user any file under /var/lib/images: ``chown: PathFilter, /bin/chown, root, nova, /var/lib/images`` EnvFilter --------- Filter allowing extra environment variables to be set by the calling code. Parameters are: 1. `env` 2. User to run the command under 3. (and following) name of the environment variables that can be set, suffixed by `=` 4. Executable allowed Example: allow to run `CONFIG_FILE=foo NETWORK_ID=bar dnsmasq ...` as root: ``dnsmasq: EnvFilter, env, root, CONFIG_FILE=, NETWORK_ID=, dnsmasq`` ReadFileFilter -------------- Specific filter that lets you read files as `root` using `cat`. Parameters are: 1. Path to the file that you want to read as the `root` user. Example: allow to run `cat /etc/iscsi/initiatorname.iscsi` as `root`: ``read_initiator: ReadFileFilter, /etc/iscsi/initiatorname.iscsi`` KillFilter ---------- Kill-specific filter that checks the affected process and the signal sent before allowing the command. Parameters are: 1. User to run `kill` under 2. Only affect processes running that executable 3. (and following) Signals you're allowed to send Example: allow to send `-9` or `-HUP` signals to `/usr/sbin/dnsmasq` processes: ``kill_dnsmasq: KillFilter, root, /usr/sbin/dnsmasq, -9, -HUP`` IpFilter -------- ip-specific filter that allows to run any `ip` command, except for `ip netns` (in which case it only allows the list, add and delete subcommands). Parameters are: 1. `ip` 2. User to run `ip` under Example: allow to run any `ip` command except `ip netns exec` and `ip netns monitor`: ``ip: IpFilter, ip, root`` IpNetnsExecFilter ----------------- ip-specific filter that allows to run any otherwise-allowed command under `ip netns exec`. The command specified to `ip netns exec` must match another filter for this filter to accept it. Parameters are: 1. `ip` 2. User to run `ip` under Example: allow to run `ip netns exec ` as long as `` matches another filter: ``ip: IpNetnsExecFilter, ip, root`` Calling rootwrap from OpenStack services ============================================= The `oslo.processutils` library ships with a convenience `execute()` function that can be used to call shell commands as `root`, if you call it with the following parameters: ``run_as_root=True`` ``root_helper='sudo nova-rootwrap /etc/nova/rootwrap.conf`` NB: Some services ship with a `utils.execute()` convenience function that automatically sets `root_helper` based on the value of a `rootwrap_config` parameter, so only `run_as_root=True` needs to be set. If you want to call as `root` a previously-unauthorized command, you will also need to modify the filters (generally shipped in the source tree under `etc/rootwrap.d` so that the command you want to run as `root` will actually be allowed by `nova-rootwrap`. Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Environment :: OpenStack Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Information Technology Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.3 oslo.rootwrap-1.2.0/ChangeLog0000664000175300017540000000351412314054302017246 0ustar jenkinsjenkins00000000000000CHANGES ======= 1.2.0 ----- * Avoid matching ip -s netns exec in IpFilter * Don't use system pip things in tox * Add Python 3 trove classifiers * To honor RFC5424 add use_syslog_rfc_format config option * Trivial changes from oslo-incubator 1.1.0 ----- * Discontinue usage of oslo-rootwrap * Add missing oslo/__init__.py * Fix spelling errors in comments 1.0.0 ----- * Use oslo-rootwrap in config directory names * Ship with etc/oslo.rootwrap instead of etc/oslo * Add a complete README.rst * Add .gitreview for oslo.rootwrap * Add standalone project packaging support files * Make Rootwrap python3-compatible * Make tests not depend on openstack.common stuff * Move files to new locations for oslo-config * Skip hidden files while traversion rootwrap filters * Fix os.getlogin() problem with no tty * Send rootwrap exit error message to stderr * rootwrap: improve Python 3 compatibility * Replace using tests.utils part2 * Fixes files with wrong bitmode * Remove DnsmasqFilter and DeprecatedDnsmasqFilter * Handle empty arglists in Filters * Handle empty PATH environment variable * Add IpFilter, IPNetnsExecFilter and EnvFilter * Handle relative path arguments in Killfilter * Enable hacking H404 test * Enable hacking H402 test * Update KillFilter to stop at '\0' for readlink() function * Stylistic improvements from quantum-rootwrap * Use print_function __future__ import * Revert common logging use in rootwrap * Improve Python 3.x compatibility * Replaces standard logging with common logging * Move bin/ scripts to entrypoints * Add PathFilter to rootwrap * update OpenStack, LLC to OpenStack Foundation * Fix Copyright Headers - Rename LLC to Foundation * Replaced direct usage of stubout with BaseTestCase * Use testtools as test base class * Remove unused etc/openstack-common.conf.test * Fix pep8 E125 errors * Move rootwrap code to openstack.common oslo.rootwrap-1.2.0/MANIFEST.in0000664000175300017540000000027012314054252017232 0ustar jenkinsjenkins00000000000000include AUTHORS include ChangeLog include LICENSE include README.rst include tox.ini .testr.conf recursive-include tests * exclude .gitignore exclude .gitreview global-exclude *.pyc oslo.rootwrap-1.2.0/.testr.conf0000664000175300017540000000032212314054252017560 0ustar jenkinsjenkins00000000000000[DEFAULT] test_command=OS_STDOUT_CAPTURE=1 OS_STDERR_CAPTURE=1 OS_TEST_TIMEOUT=60 ${PYTHON:-python} -m subunit.run discover -t ./ . $LISTOPT $IDOPTION test_id_option=--load-list $IDFILE test_list_option=--list oslo.rootwrap-1.2.0/setup.py0000664000175300017540000000141512314054252017210 0ustar jenkinsjenkins00000000000000#!/usr/bin/env python # Copyright (c) 2013 Hewlett-Packard Development Company, L.P. # # 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. # THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT import setuptools setuptools.setup( setup_requires=['pbr'], pbr=True) oslo.rootwrap-1.2.0/oslo.rootwrap.egg-info/0000775000175300017540000000000012314054302022013 5ustar jenkinsjenkins00000000000000oslo.rootwrap-1.2.0/oslo.rootwrap.egg-info/SOURCES.txt0000664000175300017540000000111512314054302023675 0ustar jenkinsjenkins00000000000000.testr.conf AUTHORS CONTRIBUTING.rst ChangeLog LICENSE MANIFEST.in README.rst requirements.txt setup.cfg setup.py test-requirements.txt tox.ini etc/rootwrap.conf.sample oslo/__init__.py oslo.rootwrap.egg-info/PKG-INFO oslo.rootwrap.egg-info/SOURCES.txt oslo.rootwrap.egg-info/dependency_links.txt oslo.rootwrap.egg-info/namespace_packages.txt oslo.rootwrap.egg-info/not-zip-safe oslo.rootwrap.egg-info/requires.txt oslo.rootwrap.egg-info/top_level.txt oslo/rootwrap/__init__.py oslo/rootwrap/cmd.py oslo/rootwrap/filters.py oslo/rootwrap/wrapper.py tests/__init__.py tests/test_rootwrap.pyoslo.rootwrap-1.2.0/oslo.rootwrap.egg-info/not-zip-safe0000664000175300017540000000000112314054301024240 0ustar jenkinsjenkins00000000000000 oslo.rootwrap-1.2.0/oslo.rootwrap.egg-info/PKG-INFO0000664000175300017540000003141612314054302023115 0ustar jenkinsjenkins00000000000000Metadata-Version: 1.1 Name: oslo.rootwrap Version: 1.2.0 Summary: Oslo Rootwrap Home-page: https://launchpad.net/oslo Author: OpenStack Author-email: openstack-dev@lists.openstack.org License: UNKNOWN Description: ------------- Oslo Rootwrap ------------- The Oslo Rootwrap allows fine filtering of shell commands to run as `root` from OpenStack services. Rootwrap should be used as a separate Python process calling the oslo.rootwrap.cmd:main function. You can set up a specific console_script calling into oslo.rootwrap.cmd:main, called for example `nova-rootwrap`. To keep things simple, this document will consider that your console_script is called `/usr/bin/nova-rootwrap`. The rootwrap command line should be called under `sudo`. It's first parameter is the configuration file to use, and the remainder of the parameters are the command line to execute: `sudo nova-rootwrap ROOTWRAP_CONFIG COMMAND_LINE` How rootwrap works ================== OpenStack services generally run under a specific, unprivileged user. However, sometimes they need to run a command as `root`. Instead of just calling `sudo make me a sandwich` and have a blanket `sudoers` permission to always escalate rights from their unprivileged users to `root`, those services can call `sudo nova-rootwrap /etc/nova/rootwrap.conf make me a sandwich`. A sudoers entry lets the unprivileged user run `nova-rootwrap` as `root`. `nova-rootwrap` looks for filter definition directories in its configuration file, and loads command filters from them. Then it checks if the command requested by the OpenStack service matches one of those filters, in which case it executes the command (as `root`). If no filter matches, it denies the request. This allows for complex filtering of allowed commands, as well as shipping filter definitions together with the OpenStack code that needs them. Security model ============== The escalation path is fully controlled by the `root` user. A `sudoers` entry (owned by `root`) allows the unprivileged user to run (as `root`) a specific rootwrap executable, and only with a specific configuration file (which should be owned by `root`) as its first parameter. `nova-rootwrap` imports the Python modules it needs from a cleaned (and system-default) `PYTHONPATH`. The configuration file points to root-owned filter definition directories, which contain root-owned filters definition files. This chain ensures that the unprivileged user itself is never in control of the configuration or modules used by the `nova-rootwrap` executable. Installation ============ All nodes wishing to run `nova-rootwrap` should contain a `sudoers` entry that lets the unprivileged user run `nova-rootwrap` as `root`, pointing to the root-owned `rootwrap.conf` configuration file and allowing any parameter after that. For example, Nova nodes should have this line in their `sudoers` file, to allow the `nova` user to call `sudo nova-rootwrap`: ``nova ALL = (root) NOPASSWD: /usr/bin/nova-rootwrap /etc/nova/rootwrap.conf *`` Then the node also should ship the filter definitions corresponding to its usage of `nova-rootwrap`. You should not install any other filters file on that node, otherwise you would allow extra unneeded commands to be run as `root`. The filter file(s) corresponding to the node must be installed in one of the filters_path directories. For example, on Nova compute nodes, you should only have `compute.filters` installed. The file should be owned and writeable only by the `root` user. Rootwrap configuration ====================== The `rootwrap.conf` file is used to influence how `nova-rootwrap` works. Since it's in the trusted security path, it needs to be owned and writeable only by the `root` user. Its location is specified in the `sudoers` entry, and must be provided on `nova-rootwrap` command line as its first argument. `rootwrap.conf` uses an *INI* file format with the following sections and parameters: [DEFAULT] section ----------------- filters_path Comma-separated list of directories containing filter definition files. All directories listed must be owned and only writeable by `root`. This is the only mandatory parameter. Example: ``filters_path=/etc/nova/rootwrap.d,/usr/share/nova/rootwrap`` exec_dirs Comma-separated list of directories to search executables in, in case filters do not explicitely specify a full path. If not specified, defaults to the system `PATH` environment variable. All directories listed must be owned and only writeable by `root`. Example: ``exec_dirs=/sbin,/usr/sbin,/bin,/usr/bin`` use_syslog Enable logging to syslog. Default value is False. Example: ``use_syslog=True`` use_syslog_rfc_format Enable RFC5424 compliant format for syslog (add APP-NAME before MSG part). Default value is False. Example: ``use_syslog_rfc_format=True`` syslog_log_facility Which syslog facility to use for syslog logging. Valid values include `auth`, `authpriv`, `syslog`, `user0`, `user1`... Default value is `syslog`. Example: ``syslog_log_facility=syslog`` syslog_log_level Which messages to log. `INFO` means log all usage, `ERROR` means only log unsuccessful attempts. Example: ``syslog_log_level=ERROR`` .filters files ============== Filters definition files contain lists of filters that `nova-rootwrap` will use to allow or deny a specific command. They are generally suffixed by `.filters`. Since they are in the trusted security path, they need to be owned and writeable only by the `root` user. Their location is specified in the `rootwrap.conf` file. It uses an *INI* file format with a `[Filters]` section and several lines, each with a unique parameter name (different for each filter you define): [Filters] section ----------------- filter_name (different for each filter) Comma-separated list containing first the Filter class to use, followed by that Filter arguments (which vary depending on the Filter class selected). Example: ``kpartx: CommandFilter, /sbin/kpartx, root`` Available filter classes ======================== CommandFilter ------------- Basic filter that only checks the executable called. Parameters are: 1. Executable allowed 2. User to run the command under Example: allow to run kpartx as the root user, with any parameters: ``kpartx: CommandFilter, kpartx, root`` RegExpFilter ------------ Generic filter that checks the executable called, then uses a list of regular expressions to check all subsequent arguments. Parameters are: 1. Executable allowed 2. User to run the command under 3. (and following) Regular expressions to use to match first (and subsequent) command arguments Example: allow to run `/usr/sbin/tunctl`, but only with three parameters with the first two being -b and -t: ``tunctl: /usr/sbin/tunctl, root, tunctl, -b, -t, .*`` PathFilter ---------- Generic filter that lets you check that paths provided as parameters fall under a given directory. Parameters are: 1. Executable allowed 2. User to run the command under 3. (and following) Command arguments. There are three types of command arguments: `pass` will accept any parameter value, a string will only accept the corresponding string as a parameter, except if the string starts with '/' in which case it will accept any path that resolves under the corresponding directory. Example: allow to chown to the 'nova' user any file under /var/lib/images: ``chown: PathFilter, /bin/chown, root, nova, /var/lib/images`` EnvFilter --------- Filter allowing extra environment variables to be set by the calling code. Parameters are: 1. `env` 2. User to run the command under 3. (and following) name of the environment variables that can be set, suffixed by `=` 4. Executable allowed Example: allow to run `CONFIG_FILE=foo NETWORK_ID=bar dnsmasq ...` as root: ``dnsmasq: EnvFilter, env, root, CONFIG_FILE=, NETWORK_ID=, dnsmasq`` ReadFileFilter -------------- Specific filter that lets you read files as `root` using `cat`. Parameters are: 1. Path to the file that you want to read as the `root` user. Example: allow to run `cat /etc/iscsi/initiatorname.iscsi` as `root`: ``read_initiator: ReadFileFilter, /etc/iscsi/initiatorname.iscsi`` KillFilter ---------- Kill-specific filter that checks the affected process and the signal sent before allowing the command. Parameters are: 1. User to run `kill` under 2. Only affect processes running that executable 3. (and following) Signals you're allowed to send Example: allow to send `-9` or `-HUP` signals to `/usr/sbin/dnsmasq` processes: ``kill_dnsmasq: KillFilter, root, /usr/sbin/dnsmasq, -9, -HUP`` IpFilter -------- ip-specific filter that allows to run any `ip` command, except for `ip netns` (in which case it only allows the list, add and delete subcommands). Parameters are: 1. `ip` 2. User to run `ip` under Example: allow to run any `ip` command except `ip netns exec` and `ip netns monitor`: ``ip: IpFilter, ip, root`` IpNetnsExecFilter ----------------- ip-specific filter that allows to run any otherwise-allowed command under `ip netns exec`. The command specified to `ip netns exec` must match another filter for this filter to accept it. Parameters are: 1. `ip` 2. User to run `ip` under Example: allow to run `ip netns exec ` as long as `` matches another filter: ``ip: IpNetnsExecFilter, ip, root`` Calling rootwrap from OpenStack services ============================================= The `oslo.processutils` library ships with a convenience `execute()` function that can be used to call shell commands as `root`, if you call it with the following parameters: ``run_as_root=True`` ``root_helper='sudo nova-rootwrap /etc/nova/rootwrap.conf`` NB: Some services ship with a `utils.execute()` convenience function that automatically sets `root_helper` based on the value of a `rootwrap_config` parameter, so only `run_as_root=True` needs to be set. If you want to call as `root` a previously-unauthorized command, you will also need to modify the filters (generally shipped in the source tree under `etc/rootwrap.d` so that the command you want to run as `root` will actually be allowed by `nova-rootwrap`. Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Environment :: OpenStack Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Information Technology Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.3 oslo.rootwrap-1.2.0/oslo.rootwrap.egg-info/namespace_packages.txt0000664000175300017540000000000512314054302026341 0ustar jenkinsjenkins00000000000000oslo oslo.rootwrap-1.2.0/oslo.rootwrap.egg-info/top_level.txt0000664000175300017540000000000512314054302024540 0ustar jenkinsjenkins00000000000000oslo oslo.rootwrap-1.2.0/oslo.rootwrap.egg-info/dependency_links.txt0000664000175300017540000000000112314054302026061 0ustar jenkinsjenkins00000000000000 oslo.rootwrap-1.2.0/oslo.rootwrap.egg-info/requires.txt0000664000175300017540000000001212314054302024404 0ustar jenkinsjenkins00000000000000six>=1.4.1oslo.rootwrap-1.2.0/requirements.txt0000664000175300017540000000001312314054252020753 0ustar jenkinsjenkins00000000000000six>=1.4.1 oslo.rootwrap-1.2.0/AUTHORS0000664000175300017540000000000112314054302016530 0ustar jenkinsjenkins00000000000000 oslo.rootwrap-1.2.0/oslo/0000775000175300017540000000000012314054302016445 5ustar jenkinsjenkins00000000000000oslo.rootwrap-1.2.0/oslo/__init__.py0000664000175300017540000000116512314054252020565 0ustar jenkinsjenkins00000000000000# 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__('pkg_resources').declare_namespace(__name__) oslo.rootwrap-1.2.0/oslo/rootwrap/0000775000175300017540000000000012314054302020322 5ustar jenkinsjenkins00000000000000oslo.rootwrap-1.2.0/oslo/rootwrap/__init__.py0000664000175300017540000000000012314054252022425 0ustar jenkinsjenkins00000000000000oslo.rootwrap-1.2.0/oslo/rootwrap/cmd.py0000664000175300017540000001127712314054252021453 0ustar jenkinsjenkins00000000000000# Copyright (c) 2011 OpenStack Foundation. # 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 OpenStack services Filters which commands a service is allowed to run as another user. To use this with oslo, you should set the following in oslo.conf: rootwrap_config=/etc/oslo/rootwrap.conf You also need to let the oslo user run oslo-rootwrap as root in sudoers: oslo ALL = (root) NOPASSWD: /usr/bin/oslo-rootwrap /etc/oslo/rootwrap.conf * Service packaging should deploy .filters files only on nodes where they are needed, to avoid allowing more than is necessary. """ from __future__ import print_function import logging import os import pwd import signal import subprocess import sys from six import moves RC_UNAUTHORIZED = 99 RC_NOCOMMAND = 98 RC_BADCONFIG = 97 RC_NOEXECFOUND = 96 def _subprocess_setup(): # Python installs a SIGPIPE handler by default. This is usually not what # non-Python subprocesses expect. signal.signal(signal.SIGPIPE, signal.SIG_DFL) def _exit_error(execname, message, errorcode, log=True): print("%s: %s" % (execname, message), file=sys.stderr) if log: logging.error(message) sys.exit(errorcode) def _getlogin(): try: return os.getlogin() except OSError: return (os.getenv('USER') or os.getenv('USERNAME') or os.getenv('LOGNAME')) def main(): # Split arguments, require at least a command execname = sys.argv.pop(0) if len(sys.argv) < 2: _exit_error(execname, "No command specified", RC_NOCOMMAND, log=False) configfile = sys.argv.pop(0) 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, "oslo", "__init__.py")): sys.path.insert(0, possible_topdir) from oslo.rootwrap import wrapper # Load configuration try: rawconfig = moves.configparser.RawConfigParser() rawconfig.read(configfile) config = wrapper.RootwrapConfig(rawconfig) except ValueError as exc: msg = "Incorrect value in %s: %s" % (configfile, exc.message) _exit_error(execname, msg, RC_BADCONFIG, log=False) except moves.configparser.Error: _exit_error(execname, "Incorrect configuration file: %s" % configfile, RC_BADCONFIG, log=False) if config.use_syslog: wrapper.setup_syslog(execname, config.syslog_log_facility, config.syslog_log_level) # Execute command if it matches any of the loaded filters filters = wrapper.load_filters(config.filters_path) try: filtermatch = wrapper.match_filter(filters, userargs, exec_dirs=config.exec_dirs) if filtermatch: command = filtermatch.get_command(userargs, exec_dirs=config.exec_dirs) if config.use_syslog: logging.info("(%s > %s) Executing %s (filter match = %s)" % ( _getlogin(), pwd.getpwuid(os.getuid())[0], command, filtermatch.name)) obj = subprocess.Popen(command, stdin=sys.stdin, stdout=sys.stdout, stderr=sys.stderr, preexec_fn=_subprocess_setup, env=filtermatch.get_environment(userargs)) obj.wait() sys.exit(obj.returncode) except wrapper.FilterMatchNotExecutable as exc: msg = ("Executable not found: %s (filter match = %s)" % (exc.match.exec_path, exc.match.name)) _exit_error(execname, msg, RC_NOEXECFOUND, log=config.use_syslog) except wrapper.NoFilterMatched: msg = ("Unauthorized command: %s (no filter matched)" % ' '.join(userargs)) _exit_error(execname, msg, RC_UNAUTHORIZED, log=config.use_syslog) oslo.rootwrap-1.2.0/oslo/rootwrap/wrapper.py0000664000175300017540000001451112314054252022362 0ustar jenkinsjenkins00000000000000# Copyright (c) 2011 OpenStack Foundation. # 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 logging import logging.handlers import os import string from six import moves from oslo.rootwrap import filters class NoFilterMatched(Exception): """This exception is raised when no filter matched.""" pass class FilterMatchNotExecutable(Exception): """Raised when a filter matched but no executable was found.""" def __init__(self, match=None, **kwargs): self.match = match class RootwrapConfig(object): def __init__(self, config): # filters_path self.filters_path = config.get("DEFAULT", "filters_path").split(",") # exec_dirs if config.has_option("DEFAULT", "exec_dirs"): self.exec_dirs = config.get("DEFAULT", "exec_dirs").split(",") else: self.exec_dirs = [] # Use system PATH if exec_dirs is not specified if "PATH" in os.environ: self.exec_dirs = os.environ['PATH'].split(':') # syslog_log_facility if config.has_option("DEFAULT", "syslog_log_facility"): v = config.get("DEFAULT", "syslog_log_facility") facility_names = logging.handlers.SysLogHandler.facility_names self.syslog_log_facility = getattr(logging.handlers.SysLogHandler, v, None) if self.syslog_log_facility is None and v in facility_names: self.syslog_log_facility = facility_names.get(v) if self.syslog_log_facility is None: raise ValueError('Unexpected syslog_log_facility: %s' % v) else: default_facility = logging.handlers.SysLogHandler.LOG_SYSLOG self.syslog_log_facility = default_facility # syslog_log_level if config.has_option("DEFAULT", "syslog_log_level"): v = config.get("DEFAULT", "syslog_log_level") self.syslog_log_level = logging.getLevelName(v.upper()) if (self.syslog_log_level == "Level %s" % v.upper()): raise ValueError('Unexpected syslog_log_level: %s' % v) else: self.syslog_log_level = logging.ERROR # use_syslog if config.has_option("DEFAULT", "use_syslog"): self.use_syslog = config.getboolean("DEFAULT", "use_syslog") else: self.use_syslog = False # use_syslog_rfc_format if config.has_option("DEFAULT", "use_syslog_rfc_format"): self.use_syslog_rfc_format = config.getboolean( "DEFAULT", "use_syslog_rfc_format") else: self.use_syslog_rfc_format = False def setup_syslog(execname, facility, level): rootwrap_logger = logging.getLogger() rootwrap_logger.setLevel(level) handler = logging.handlers.SysLogHandler(address='/dev/log', facility=facility) handler.setFormatter(logging.Formatter( os.path.basename(execname) + ': %(message)s')) rootwrap_logger.addHandler(handler) def build_filter(class_name, *args): """Returns a filter object of class class_name.""" if not hasattr(filters, class_name): logging.warning("Skipping unknown filter class (%s) specified " "in filter definitions" % class_name) return None filterclass = getattr(filters, class_name) return filterclass(*args) def load_filters(filters_path): """Load filters from a list of directories.""" filterlist = [] for filterdir in filters_path: if not os.path.isdir(filterdir): continue for filterfile in filter(lambda f: not f.startswith('.'), os.listdir(filterdir)): filterconfig = moves.configparser.RawConfigParser() filterconfig.read(os.path.join(filterdir, filterfile)) for (name, value) in filterconfig.items("Filters"): filterdefinition = [string.strip(s) for s in value.split(',')] newfilter = build_filter(*filterdefinition) if newfilter is None: continue newfilter.name = name filterlist.append(newfilter) return filterlist def match_filter(filter_list, userargs, exec_dirs=[]): """Checks user command and arguments through command filters. Returns the first matching filter. Raises NoFilterMatched if no filter matched. Raises FilterMatchNotExecutable if no executable was found for the best filter match. """ first_not_executable_filter = None for f in filter_list: if f.match(userargs): if isinstance(f, filters.ChainingFilter): # This command calls exec verify that remaining args # matches another filter. def non_chain_filter(fltr): return (fltr.run_as == f.run_as and not isinstance(fltr, filters.ChainingFilter)) leaf_filters = [fltr for fltr in filter_list if non_chain_filter(fltr)] args = f.exec_args(userargs) if (not args or not match_filter(leaf_filters, args, exec_dirs=exec_dirs)): continue # Try other filters if executable is absent if not f.get_exec(exec_dirs=exec_dirs): if not first_not_executable_filter: first_not_executable_filter = f continue # Otherwise return matching filter for execution return f if first_not_executable_filter: # A filter matched, but no executable was found for it raise FilterMatchNotExecutable(match=first_not_executable_filter) # No filter matched raise NoFilterMatched() oslo.rootwrap-1.2.0/oslo/rootwrap/filters.py0000664000175300017540000002540112314054252022352 0ustar jenkinsjenkins00000000000000# Copyright (c) 2011 OpenStack Foundation. # 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.name = '' self.exec_path = exec_path self.run_as = run_as self.args = args self.real_exec = None def get_exec(self, exec_dirs=[]): """Returns existing executable, or empty string if none found.""" if self.real_exec is not None: return self.real_exec self.real_exec = "" if os.path.isabs(self.exec_path): if os.access(self.exec_path, os.X_OK): self.real_exec = self.exec_path else: for binary_path in exec_dirs: expanded_path = os.path.join(binary_path, self.exec_path) if os.access(expanded_path, os.X_OK): self.real_exec = expanded_path break return self.real_exec def match(self, userargs): """Only check that the first argument (command) matches exec_path.""" return userargs and os.path.basename(self.exec_path) == userargs[0] def get_command(self, userargs, exec_dirs=[]): """Returns command to execute (with sudo -u if run_as != root).""" to_exec = self.get_exec(exec_dirs=exec_dirs) or self.exec_path if (self.run_as != 'root'): # Used to run commands at lesser privileges return ['sudo', '-u', self.run_as, to_exec] + userargs[1:] return [to_exec] + userargs[1:] def get_environment(self, userargs): """Returns specific environment to set, None if none.""" return None 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 (not userargs or 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 PathFilter(CommandFilter): """Command filter checking that path arguments are within given dirs One can specify the following constraints for command arguments: 1) pass - pass an argument as is to the resulting command 2) some_str - check if an argument is equal to the given string 3) abs path - check if a path argument is within the given base dir A typical rootwrapper filter entry looks like this: # cmdname: filter name, raw command, user, arg_i_constraint [, ...] chown: PathFilter, /bin/chown, root, nova, /var/lib/images """ def match(self, userargs): if not userargs or len(userargs) < 2: return False command, arguments = userargs[0], userargs[1:] equal_args_num = len(self.args) == len(arguments) exec_is_valid = super(PathFilter, self).match(userargs) args_equal_or_pass = all( arg == 'pass' or arg == value for arg, value in zip(self.args, arguments) if not os.path.isabs(arg) # arguments not specifying abs paths ) paths_are_within_base_dirs = all( os.path.commonprefix([arg, os.path.realpath(value)]) == arg for arg, value in zip(self.args, arguments) if os.path.isabs(arg) # arguments specifying abs paths ) return (equal_args_num and exec_is_valid and args_equal_or_pass and paths_are_within_base_dirs) def get_command(self, userargs, exec_dirs=[]): command, arguments = userargs[0], userargs[1:] # convert path values to canonical ones; copy other args as is args = [os.path.realpath(value) if os.path.isabs(arg) else value for arg, value in zip(self.args, arguments)] return super(PathFilter, self).get_command([command] + args, exec_dirs) class KillFilter(CommandFilter): """Specific filter for the kill calls. 1st argument is the user to run /bin/kill under 2nd argument is the location of the affected executable if the argument is not absolute, it is checked against $PATH Subsequent arguments list the accepted signals (if any) This filter relies on /proc to accurately determine affected executable, so it will only work on procfs-capable systems (not OSX). """ def __init__(self, *args): super(KillFilter, self).__init__("/bin/kill", *args) def match(self, userargs): if not userargs or userargs[0] != "kill": return False args = list(userargs) if len(args) == 3: # A specific signal is requested signal = args.pop(1) if signal not in self.args[1:]: # Requested signal not in accepted list return False else: if len(args) != 2: # Incorrect number of arguments return False if len(self.args) > 1: # No signal requested, but filter requires specific signal return False try: command = os.readlink("/proc/%d/exe" % int(args[1])) except (ValueError, OSError): # Incorrect PID return False # NOTE(yufang521247): /proc/PID/exe may have '\0' on the # end, because python doesn't stop at '\0' when read the # target path. command = command.partition('\0')[0] # NOTE(dprince): /proc/PID/exe may have ' (deleted)' on # the end if an executable is updated or deleted if command.endswith(" (deleted)"): command = command[:-len(" (deleted)")] kill_command = self.args[0] if os.path.isabs(kill_command): return kill_command == command return (os.path.isabs(command) and kill_command == os.path.basename(command) and os.path.dirname(command) in os.environ.get('PATH', '' ).split(':')) class ReadFileFilter(CommandFilter): """Specific filter for the utils.read_file_as_root call.""" def __init__(self, file_path, *args): self.file_path = file_path super(ReadFileFilter, self).__init__("/bin/cat", "root", *args) def match(self, userargs): return (userargs == ['cat', self.file_path]) class IpFilter(CommandFilter): """Specific filter for the ip utility to that does not match exec.""" def match(self, userargs): if userargs[0] == 'ip': # Avoid the 'netns exec' command here for a, b in zip(userargs[1:], userargs[2:]): if a == 'netns': return (b != 'exec') else: return True class EnvFilter(CommandFilter): """Specific filter for the env utility. Behaves like CommandFilter, except that it handles leading env A=B.. strings appropriately. """ def _extract_env(self, arglist): """Extract all leading NAME=VALUE arguments from arglist.""" envs = set() for arg in arglist: if '=' not in arg: break envs.add(arg.partition('=')[0]) return envs def __init__(self, exec_path, run_as, *args): super(EnvFilter, self).__init__(exec_path, run_as, *args) env_list = self._extract_env(self.args) # Set exec_path to X when args are in the form of # env A=a B=b C=c X Y Z if "env" in exec_path and len(env_list) < len(self.args): self.exec_path = self.args[len(env_list)] def match(self, userargs): # ignore leading 'env' if userargs[0] == 'env': userargs.pop(0) # require one additional argument after configured ones if len(userargs) < len(self.args): return False # extract all env args user_envs = self._extract_env(userargs) filter_envs = self._extract_env(self.args) user_command = userargs[len(user_envs):len(user_envs) + 1] # match first non-env argument with CommandFilter return (super(EnvFilter, self).match(user_command) and len(filter_envs) and user_envs == filter_envs) def exec_args(self, userargs): args = userargs[:] # ignore leading 'env' if args[0] == 'env': args.pop(0) # Throw away leading NAME=VALUE arguments while args and '=' in args[0]: args.pop(0) return args def get_command(self, userargs, exec_dirs=[]): to_exec = self.get_exec(exec_dirs=exec_dirs) or self.exec_path return [to_exec] + self.exec_args(userargs)[1:] def get_environment(self, userargs): env = os.environ.copy() # ignore leading 'env' if userargs[0] == 'env': userargs.pop(0) # Handle leading NAME=VALUE pairs for a in userargs: env_name, equals, env_value = a.partition('=') if not equals: break if env_name and env_value: env[env_name] = env_value return env class ChainingFilter(CommandFilter): def exec_args(self, userargs): return [] class IpNetnsExecFilter(ChainingFilter): """Specific filter for the ip utility to that does match exec.""" def match(self, userargs): # Network namespaces currently require root # require argument if self.run_as != "root" or len(userargs) < 4: return False return (userargs[:3] == ['ip', 'netns', 'exec']) def exec_args(self, userargs): args = userargs[4:] if args: args[0] = os.path.basename(args[0]) return args oslo.rootwrap-1.2.0/LICENSE0000664000175300017540000002665212314054252016515 0ustar jenkinsjenkins00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. --- License for python-keystoneclient versions prior to 2.1 --- All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of this project nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. oslo.rootwrap-1.2.0/test-requirements.txt0000664000175300017540000000043112314054252021734 0ustar jenkinsjenkins00000000000000hacking>=0.8.0,<0.9 discover fixtures>=0.3.14 python-subunit testrepository>=0.0.17 testscenarios>=0.4 testtools>=0.9.32 # when we can require tox>= 1.4, this can go into tox.ini: # [testenv:cover] # deps = {[testenv]deps} coverage coverage>=3.6 # mocking framework mock>=1.0 oslo.rootwrap-1.2.0/CONTRIBUTING.rst0000664000175300017540000000103212314054252020132 0ustar jenkinsjenkins00000000000000If you would like to contribute to the development of OpenStack, you must follow the steps in the "If you're a developer, start here" section of this page: http://wiki.openstack.org/HowToContribute Once those steps have been completed, changes to OpenStack should be submitted for review via the Gerrit tool, following the workflow documented at: http://wiki.openstack.org/GerritWorkflow Pull requests submitted through GitHub will be ignored. Bugs should be filed on Launchpad, not GitHub: https://bugs.launchpad.net/oslo oslo.rootwrap-1.2.0/setup.cfg0000664000175300017540000000152712314054302017317 0ustar jenkinsjenkins00000000000000[metadata] name = oslo.rootwrap author = OpenStack author-email = openstack-dev@lists.openstack.org summary = Oslo Rootwrap description-file = README.rst home-page = https://launchpad.net/oslo classifier = Development Status :: 4 - Beta Environment :: OpenStack Intended Audience :: Developers Intended Audience :: Information Technology License :: OSI Approved :: Apache Software License Operating System :: OS Independent Programming Language :: Python Programming Language :: Python :: 2.6 Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 Programming Language :: Python :: 3.3 [files] packages = oslo namespace_packages = oslo [build_sphinx] source-dir = doc/source build-dir = doc/build all_files = 1 [upload_sphinx] upload-dir = doc/build/html [egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 oslo.rootwrap-1.2.0/tests/0000775000175300017540000000000012314054302016633 5ustar jenkinsjenkins00000000000000oslo.rootwrap-1.2.0/tests/__init__.py0000664000175300017540000000000012314054252020736 0ustar jenkinsjenkins00000000000000oslo.rootwrap-1.2.0/tests/test_rootwrap.py0000664000175300017540000005315712314054252022140 0ustar jenkinsjenkins00000000000000# Copyright 2011 OpenStack Foundation # # 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 logging import logging.handlers import os import subprocess import testtools import uuid import fixtures import mock from six import moves from oslo.rootwrap import cmd from oslo.rootwrap import filters from oslo.rootwrap import wrapper class RootwrapTestCase(testtools.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_CommandFilter(self): f = filters.CommandFilter("sleep", 'root', '10') self.assertFalse(f.match(["sleep2"])) # verify that any arguments are accepted self.assertTrue(f.match(["sleep"])) self.assertTrue(f.match(["sleep", "anything"])) self.assertTrue(f.match(["sleep", "10"])) f = filters.CommandFilter("sleep", 'root') self.assertTrue(f.match(["sleep", "10"])) def test_empty_commandfilter(self): f = filters.CommandFilter("sleep", "root") self.assertFalse(f.match([])) self.assertFalse(f.match(None)) def test_empty_regexpfilter(self): f = filters.RegExpFilter("sleep", "root", "sleep") self.assertFalse(f.match([])) self.assertFalse(f.match(None)) def test_empty_invalid_regexpfilter(self): f = filters.RegExpFilter("sleep", "root") self.assertFalse(f.match(["anything"])) self.assertFalse(f.match([])) 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_EnvFilter_as_DnsMasq(self, config_file_arg): usercmd = ['env', config_file_arg + '=A', 'NETWORK_ID=foobar', 'dnsmasq', 'foo'] f = filters.EnvFilter("env", "root", config_file_arg + '=A', 'NETWORK_ID=', "/usr/bin/dnsmasq") 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_EnvFilter(self): envset = ['A=/some/thing', 'B=somethingelse'] envcmd = ['env'] + envset realcmd = ['sleep', '10'] usercmd = envcmd + realcmd f = filters.EnvFilter("env", "root", "A=", "B=ignored", "sleep") # accept with leading env self.assertTrue(f.match(envcmd + ["sleep"])) # accept without leading env self.assertTrue(f.match(envset + ["sleep"])) # any other command does not match self.assertFalse(f.match(envcmd + ["sleep2"])) self.assertFalse(f.match(envset + ["sleep2"])) # accept any trailing arguments self.assertTrue(f.match(usercmd)) # require given environment variables to match self.assertFalse(f.match([envcmd, 'C=ELSE'])) self.assertFalse(f.match(['env', 'C=xx'])) self.assertFalse(f.match(['env', 'A=xx'])) # require env command to be given # (otherwise CommandFilters should match self.assertFalse(f.match(realcmd)) # require command to match self.assertFalse(f.match(envcmd)) self.assertFalse(f.match(envcmd[1:])) # ensure that the env command is stripped when executing self.assertEqual(f.exec_args(usercmd), realcmd) env = f.get_environment(usercmd) # check that environment variables are set self.assertEqual(env.get('A'), '/some/thing') self.assertEqual(env.get('B'), 'somethingelse') self.assertFalse('sleep' in env.keys()) def test_EnvFilter_without_leading_env(self): envset = ['A=/some/thing', 'B=somethingelse'] envcmd = ['env'] + envset realcmd = ['sleep', '10'] f = filters.EnvFilter("sleep", "root", "A=", "B=ignored") # accept without leading env self.assertTrue(f.match(envset + ["sleep"])) self.assertEqual(f.get_command(envcmd + realcmd), realcmd) self.assertEqual(f.get_command(envset + realcmd), realcmd) env = f.get_environment(envset + realcmd) # check that environment variables are set self.assertEqual(env.get('A'), '/some/thing') self.assertEqual(env.get('B'), 'somethingelse') self.assertFalse('sleep' in env.keys()) 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)) # verify that relative paths are matched against $PATH f = filters.KillFilter("root", "cat") # Our own PID does not match so it should fail usercmd = ['kill', os.getpid()] self.assertFalse(f.match(usercmd)) # Filter should find cat in /bin or /usr/bin usercmd = ['kill', p.pid] self.assertTrue(f.match(usercmd)) # Filter shouldn't be able to find binary in $PATH, so fail with fixtures.EnvironmentVariable("PATH", "/foo:/bar"): self.assertFalse(f.match(usercmd)) # ensure that unset $PATH is not causing an exception with fixtures.EnvironmentVariable("PATH"): self.assertFalse(f.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)) # no arguments should also be fine self.assertFalse(f.match([])) self.assertFalse(f.match(None)) def test_KillFilter_deleted_exe(self): """Makes sure deleted exe's are killed correctly.""" f = filters.KillFilter("root", "/bin/commandddddd") usercmd = ['kill', 1234] # Providing no signal should work with mock.patch('os.readlink') as readlink: readlink.return_value = '/bin/commandddddd (deleted)' self.assertTrue(f.match(usercmd)) def test_KillFilter_upgraded_exe(self): """Makes sure upgraded exe's are killed correctly.""" f = filters.KillFilter("root", "/bin/commandddddd") usercmd = ['kill', 1234] with mock.patch('os.readlink') as readlink: readlink.return_value = '/bin/commandddddd\0\05190bfb2 (deleted)' 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_IpFilter_non_netns(self): f = filters.IpFilter('/sbin/ip', 'root') self.assertTrue(f.match(['ip', 'link', 'list'])) self.assertTrue(f.match(['ip', '-s', 'link', 'list'])) self.assertTrue(f.match(['ip', '-s', '-v', 'netns', 'add'])) self.assertTrue(f.match(['ip', 'link', 'set', 'interface', 'netns', 'somens'])) def test_IpFilter_netns(self): f = filters.IpFilter('/sbin/ip', 'root') self.assertFalse(f.match(['ip', 'netns', 'exec', 'foo'])) self.assertFalse(f.match(['ip', 'netns', 'exec'])) self.assertFalse(f.match(['ip', '-s', 'netns', 'exec'])) self.assertFalse(f.match(['ip', '-l', '42', 'netns', 'exec'])) def _test_IpFilter_netns_helper(self, action): f = filters.IpFilter('/sbin/ip', 'root') self.assertTrue(f.match(['ip', 'link', action])) def test_IpFilter_netns_add(self): self._test_IpFilter_netns_helper('add') def test_IpFilter_netns_delete(self): self._test_IpFilter_netns_helper('delete') def test_IpFilter_netns_list(self): self._test_IpFilter_netns_helper('list') def test_IpNetnsExecFilter_match(self): f = filters.IpNetnsExecFilter('/sbin/ip', 'root') self.assertTrue( f.match(['ip', 'netns', 'exec', 'foo', 'ip', 'link', 'list'])) def test_IpNetnsExecFilter_nomatch(self): f = filters.IpNetnsExecFilter('/sbin/ip', 'root') self.assertFalse(f.match(['ip', 'link', 'list'])) # verify that at least a NS is given self.assertFalse(f.match(['ip', 'netns', 'exec'])) def test_IpNetnsExecFilter_nomatch_nonroot(self): f = filters.IpNetnsExecFilter('/sbin/ip', 'user') self.assertFalse( f.match(['ip', 'netns', 'exec', 'foo', 'ip', 'link', 'list'])) def test_match_filter_recurses_exec_command_filter_matches(self): filter_list = [filters.IpNetnsExecFilter('/sbin/ip', 'root'), filters.IpFilter('/sbin/ip', 'root')] args = ['ip', 'netns', 'exec', 'foo', 'ip', 'link', 'list'] self.assertIsNotNone(wrapper.match_filter(filter_list, args)) def test_match_filter_recurses_exec_command_matches_user(self): filter_list = [filters.IpNetnsExecFilter('/sbin/ip', 'root'), filters.IpFilter('/sbin/ip', 'user')] args = ['ip', 'netns', 'exec', 'foo', 'ip', 'link', 'list'] # Currently ip netns exec requires root, so verify that # no non-root filter is matched, as that would escalate privileges self.assertRaises(wrapper.NoFilterMatched, wrapper.match_filter, filter_list, args) def test_match_filter_recurses_exec_command_filter_does_not_match(self): filter_list = [filters.IpNetnsExecFilter('/sbin/ip', 'root'), filters.IpFilter('/sbin/ip', 'root')] args = ['ip', 'netns', 'exec', 'foo', 'ip', 'netns', 'exec', 'bar', 'ip', 'link', 'list'] self.assertRaises(wrapper.NoFilterMatched, wrapper.match_filter, filter_list, args) def test_ReadFileFilter_empty_args(self): goodfn = '/good/file.name' f = filters.ReadFileFilter(goodfn) self.assertFalse(f.match([])) self.assertFalse(f.match(None)) 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 = moves.configparser.RawConfigParser() # Empty config should raise configparser.Error self.assertRaises(moves.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(':')) with fixtures.EnvironmentVariable("PATH"): c = wrapper.RootwrapConfig(raw) self.assertEqual(c.exec_dirs, []) self.assertFalse(config.use_syslog) self.assertFalse(config.use_syslog_rfc_format) 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', 'use_syslog_rfc_format', 'oui') self.assertRaises(ValueError, wrapper.RootwrapConfig, raw) raw.set('DEFAULT', 'use_syslog_rfc_format', 'true') config = wrapper.RootwrapConfig(raw) self.assertTrue(config.use_syslog_rfc_format) 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) def test_getlogin(self): with mock.patch('os.getlogin') as os_getlogin: os_getlogin.return_value = 'foo' self.assertEqual(cmd._getlogin(), 'foo') def test_getlogin_bad(self): with mock.patch('os.getenv') as os_getenv: with mock.patch('os.getlogin') as os_getlogin: os_getenv.side_effect = [None, None, 'bar'] os_getlogin.side_effect = OSError( '[Errno 22] Invalid argument') self.assertEqual(cmd._getlogin(), 'bar') os_getlogin.assert_called_once_with() self.assertEqual(os_getenv.call_count, 3) class PathFilterTestCase(testtools.TestCase): def setUp(self): super(PathFilterTestCase, self).setUp() tmpdir = fixtures.TempDir('/tmp') self.useFixture(tmpdir) self.f = filters.PathFilter('/bin/chown', 'root', 'nova', tmpdir.path) gen_name = lambda: str(uuid.uuid4()) self.SIMPLE_FILE_WITHIN_DIR = os.path.join(tmpdir.path, 'some') self.SIMPLE_FILE_OUTSIDE_DIR = os.path.join('/tmp', 'some') self.TRAVERSAL_WITHIN_DIR = os.path.join(tmpdir.path, 'a', '..', 'some') self.TRAVERSAL_OUTSIDE_DIR = os.path.join(tmpdir.path, '..', 'some') self.TRAVERSAL_SYMLINK_WITHIN_DIR = os.path.join(tmpdir.path, gen_name()) os.symlink(os.path.join(tmpdir.path, 'a', '..', 'a'), self.TRAVERSAL_SYMLINK_WITHIN_DIR) self.TRAVERSAL_SYMLINK_OUTSIDE_DIR = os.path.join(tmpdir.path, gen_name()) os.symlink(os.path.join(tmpdir.path, 'a', '..', '..', '..', 'etc'), self.TRAVERSAL_SYMLINK_OUTSIDE_DIR) self.SYMLINK_WITHIN_DIR = os.path.join(tmpdir.path, gen_name()) os.symlink(os.path.join(tmpdir.path, 'a'), self.SYMLINK_WITHIN_DIR) self.SYMLINK_OUTSIDE_DIR = os.path.join(tmpdir.path, gen_name()) os.symlink(os.path.join('/tmp', 'some_file'), self.SYMLINK_OUTSIDE_DIR) def test_empty_args(self): self.assertFalse(self.f.match([])) self.assertFalse(self.f.match(None)) def test_argument_pass_constraint(self): f = filters.PathFilter('/bin/chown', 'root', 'pass', 'pass') args = ['chown', 'something', self.SIMPLE_FILE_OUTSIDE_DIR] self.assertTrue(f.match(args)) def test_argument_equality_constraint(self): f = filters.PathFilter('/bin/chown', 'root', 'nova', '/tmp/spam/eggs') args = ['chown', 'nova', '/tmp/spam/eggs'] self.assertTrue(f.match(args)) args = ['chown', 'quantum', '/tmp/spam/eggs'] self.assertFalse(f.match(args)) def test_wrong_arguments_number(self): args = ['chown', '-c', 'nova', self.SIMPLE_FILE_WITHIN_DIR] self.assertFalse(self.f.match(args)) def test_wrong_exec_command(self): args = ['wrong_exec', self.SIMPLE_FILE_WITHIN_DIR] self.assertFalse(self.f.match(args)) def test_match(self): args = ['chown', 'nova', self.SIMPLE_FILE_WITHIN_DIR] self.assertTrue(self.f.match(args)) def test_match_traversal(self): args = ['chown', 'nova', self.TRAVERSAL_WITHIN_DIR] self.assertTrue(self.f.match(args)) def test_match_symlink(self): args = ['chown', 'nova', self.SYMLINK_WITHIN_DIR] self.assertTrue(self.f.match(args)) def test_match_traversal_symlink(self): args = ['chown', 'nova', self.TRAVERSAL_SYMLINK_WITHIN_DIR] self.assertTrue(self.f.match(args)) def test_reject(self): args = ['chown', 'nova', self.SIMPLE_FILE_OUTSIDE_DIR] self.assertFalse(self.f.match(args)) def test_reject_traversal(self): args = ['chown', 'nova', self.TRAVERSAL_OUTSIDE_DIR] self.assertFalse(self.f.match(args)) def test_reject_symlink(self): args = ['chown', 'nova', self.SYMLINK_OUTSIDE_DIR] self.assertFalse(self.f.match(args)) def test_reject_traversal_symlink(self): args = ['chown', 'nova', self.TRAVERSAL_SYMLINK_OUTSIDE_DIR] self.assertFalse(self.f.match(args)) def test_get_command(self): args = ['chown', 'nova', self.SIMPLE_FILE_WITHIN_DIR] expected = ['/bin/chown', 'nova', self.SIMPLE_FILE_WITHIN_DIR] self.assertEqual(expected, self.f.get_command(args)) def test_get_command_traversal(self): args = ['chown', 'nova', self.TRAVERSAL_WITHIN_DIR] expected = ['/bin/chown', 'nova', os.path.realpath(self.TRAVERSAL_WITHIN_DIR)] self.assertEqual(expected, self.f.get_command(args)) def test_get_command_symlink(self): args = ['chown', 'nova', self.SYMLINK_WITHIN_DIR] expected = ['/bin/chown', 'nova', os.path.realpath(self.SYMLINK_WITHIN_DIR)] self.assertEqual(expected, self.f.get_command(args)) def test_get_command_traversal_symlink(self): args = ['chown', 'nova', self.TRAVERSAL_SYMLINK_WITHIN_DIR] expected = ['/bin/chown', 'nova', os.path.realpath(self.TRAVERSAL_SYMLINK_WITHIN_DIR)] self.assertEqual(expected, self.f.get_command(args))