From 3d6d849faa2752a8248655d16badaf64b6024cd0 Mon Sep 17 00:00:00 2001 From: Yasufumi Ogawa Date: Tue, 1 Mar 2022 18:33:59 +0000 Subject: [PATCH] Migrate rootwrap to privsep As a part of the community goal [1], we should replace rootwrap in favor of privsep. Although the latest codes don't have a dependency on rootwrap for now, but it might be happened to introduce a task with root privilege after. In addition, there are some complex mechanism embeded in Tacker such as in setuptools or configs under `etc/`. It's hard to drop and restore them again. So, keep the mechanism active. In this update, two methods used for tests are implemented in `tacker/privileged/linux_cmd.py`, but can be used for general purpose. For the test, it's also including rootwrap for backward compatibility which will be removed in a future update. It also updates required libs as bellow for oslo.privsep 2.4.0. - eventlet>=0.30.1 - msgpack>=0.6.0 - oslo.service>=2.5.0 [1] https://governance.openstack.org/tc/goals/selected/migrate-to-privsep.html Partially-Implements: bp privsep-migration Signed-off-by: Yasufumi Ogawa Change-Id: Id8de4c2bae91718d6ba45ed523edc103f0b21718 --- etc/tacker/rootwrap.d/tacker.filters | 4 + lower-constraints.txt | 7 +- requirements.txt | 5 +- setup.cfg | 2 +- tacker/privileged/__init__.py | 31 +++++++ tacker/privileged/linux_cmd.py | 33 +++++++ .../tests/etc/rootwrap.d/tacker.test.filters | 12 --- tacker/tests/etc/tacker.conf.test | 1 - tacker/tests/unit/_test_rootwrap_exec.py | 82 ------------------ tacker/tests/unit/test_privsep.py | 85 +++++++++++++++++++ 10 files changed, 161 insertions(+), 101 deletions(-) create mode 100644 tacker/privileged/__init__.py create mode 100644 tacker/privileged/linux_cmd.py delete mode 100644 tacker/tests/etc/rootwrap.d/tacker.test.filters delete mode 100644 tacker/tests/unit/_test_rootwrap_exec.py create mode 100644 tacker/tests/unit/test_privsep.py diff --git a/etc/tacker/rootwrap.d/tacker.filters b/etc/tacker/rootwrap.d/tacker.filters index 6c99f21e2..4e0a680c5 100644 --- a/etc/tacker/rootwrap.d/tacker.filters +++ b/etc/tacker/rootwrap.d/tacker.filters @@ -7,4 +7,8 @@ # cmd-name: filter-name, raw-command, user, args [Filters] +privsep-rootwrap-sys_admin: RegExpFilter, privsep-helper, root, privsep-helper, --config-file, /etc/(?!\.\.).*, --privsep_context, tacker.privileged.default, --privsep_sock_path, /tmp/.* +# This definition is for test purpose. It's used in +# 'tacker.tests.unit.test_rootwrap_exec.RootwrapTestExec.test_rootwrap' +pwd: CommandFilter, pwd, root diff --git a/lower-constraints.txt b/lower-constraints.txt index a2d3c4d3b..796cc770d 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -24,7 +24,7 @@ docutils==0.14 dogpile.cache==0.6.5 dulwich==0.19.0 enum-compat==0.0.2 -eventlet==0.18.2 +eventlet==0.30.1 extras==1.0.0 fasteners==0.14.1 fixtures==3.0.0 @@ -52,7 +52,7 @@ Mako==1.0.7 MarkupSafe==1.1 monotonic==1.4 mox3==0.25.0 -msgpack==0.5.6 +msgpack==0.6.0 munch==2.2.0 netaddr==0.7.18 netifaces==0.10.6 @@ -72,10 +72,11 @@ oslo.log==3.36.0 oslo.messaging==9.3.0 oslo.middleware==3.31.0 oslo.policy==3.6.0 +oslo.privsep==2.4.0 oslo.reports==1.18.0 oslo.rootwrap==5.8.0 oslo.serialization==2.18.0 -oslo.service==1.24.0 +oslo.service==2.5.0 oslo.upgradecheck==1.3.0 oslo.utils==4.8.0 oslo.versionedobjects==1.33.3 diff --git a/requirements.txt b/requirements.txt index 87c2149cc..80daa4d27 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ Paste>=2.0.2 # MIT PasteDeploy>=1.5.0 # MIT Routes>=2.3.1 # MIT amqp>=2.4.0 -eventlet!=0.18.3,!=0.20.1,>=0.18.2 # MIT +eventlet>=0.30.1 # MIT requests>=2.25.1 # Apache-2.0 jsonschema>=3.2.0 # MIT keystonemiddleware>=4.17.0 # Apache-2.0 @@ -27,10 +27,11 @@ oslo.log>=3.36.0 # Apache-2.0 oslo.messaging>=9.3.0 # Apache-2.0 oslo.middleware>=3.31.0 # Apache-2.0 oslo.policy>=3.6.0 # Apache-2.0 +oslo.privsep>=2.4.0 # Apache-2.0 oslo.reports>=1.18.0 # Apache-2.0 oslo.rootwrap>=5.8.0 # Apache-2.0 oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0 -oslo.service!=1.28.1,>=1.24.0 # Apache-2.0 +oslo.service>=2.5.0 # Apache-2.0 oslo.upgradecheck>=1.3.0 # Apache-2.0 oslo.utils>=4.8.0 # Apache-2.0 oslo.versionedobjects>=1.33.3 # Apache-2.0 diff --git a/setup.cfg b/setup.cfg index b8042a042..2254c6f1c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -43,7 +43,7 @@ console_scripts = tacker-db-manage = tacker.db.migration.cli:main tacker-server = tacker.cmd.eventlet.tacker_server:main tacker-conductor = tacker.cmd.eventlet.conductor:main - tacker-rootwrap = oslo.rootwrap.cmd:main + tacker-rootwrap = oslo_rootwrap.cmd:main tacker-status = tacker.cmd.status:main tacker.service_plugins = dummy = tacker.tests.unit.dummy_plugin:DummyServicePlugin diff --git a/tacker/privileged/__init__.py b/tacker/privileged/__init__.py new file mode 100644 index 000000000..6dc64b1a8 --- /dev/null +++ b/tacker/privileged/__init__.py @@ -0,0 +1,31 @@ +# Copyright (C) 2022 Nippon Telegraph and Telephone Corporation +# 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. + + +"""Setup privsep decorator.""" + +from oslo_privsep import capabilities as caps +from oslo_privsep import priv_context + +default = priv_context.PrivContext( + __name__, + cfg_section='privsep', + pypath="f{__name__}.default", + capabilities=[caps.CAP_SYS_ADMIN, + caps.CAP_NET_ADMIN, + caps.CAP_DAC_OVERRIDE, + caps.CAP_DAC_READ_SEARCH, + caps.CAP_SYS_PTRACE], +) diff --git a/tacker/privileged/linux_cmd.py b/tacker/privileged/linux_cmd.py new file mode 100644 index 000000000..141b5b2da --- /dev/null +++ b/tacker/privileged/linux_cmd.py @@ -0,0 +1,33 @@ +# Copyright (C) 2022 Nippon Telegraph and Telephone Corporation +# 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. + + +"""Basic Linux commands intented to be used in unittests""" + +from oslo_concurrency import processutils + +import tacker.privileged + + +@tacker.privileged.default.entrypoint +def pwd(): + pwd = processutils.execute('pwd') + return pwd + + +@tacker.privileged.default.entrypoint +def ls(): + ls = processutils.execute('ls') + return ls diff --git a/tacker/tests/etc/rootwrap.d/tacker.test.filters b/tacker/tests/etc/rootwrap.d/tacker.test.filters deleted file mode 100644 index 4e56828bd..000000000 --- a/tacker/tests/etc/rootwrap.d/tacker.test.filters +++ /dev/null @@ -1,12 +0,0 @@ -# tacker-rootwrap command filters for the unit test - -# this file goes with tacker/tests/unit/_test_rootwrap_exec.py. -# See the comments there about how to run that unit tests - -# format seems to be -# cmd-name: filter-name, raw-command, user, args - -[Filters] - -# a test filter for the RootwrapTest unit test -bash: CommandFilter, /usr/bin/bash, root diff --git a/tacker/tests/etc/tacker.conf.test b/tacker/tests/etc/tacker.conf.test index ec37f4a23..3017ce072 100644 --- a/tacker/tests/etc/tacker.conf.test +++ b/tacker/tests/etc/tacker.conf.test @@ -21,4 +21,3 @@ lock_path = $state_path/lock [database] connection = 'sqlite://' - diff --git a/tacker/tests/unit/_test_rootwrap_exec.py b/tacker/tests/unit/_test_rootwrap_exec.py deleted file mode 100644 index dbbc5d8ae..000000000 --- a/tacker/tests/unit/_test_rootwrap_exec.py +++ /dev/null @@ -1,82 +0,0 @@ -# Copyright (c) 2012 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 fixtures -from oslo_log import log as logging - -from tacker.agent.linux import utils -from tacker.tests import base - - -LOG = logging.getLogger(__name__) - - -class RootwrapTestExec(base.BaseTestCase): - """Simple unit test to test the basic rootwrap mechanism - - Essentially hello-world. Just run a command as root and check that - it actually *did* run as root, and generated the right output. - - NB that this is named _test_rootwrap so as not to get run by default - from scripts like tox. That's because it actually executes a sudo'ed - command, and that won't work in the automated test environment, at - least as it stands today. To run this, rename it to - test_rootwrap.py, or run it by hand. - """ - - def setUp(self): - super(RootwrapTestExec, self).setUp() - self.cwd = os.getcwd() + "/../../.." - # stuff a stupid bash script into /tmp, so that the next - # method can execute it. - self.test_file = self.useFixture( - fixtures.TempDir()).join("rootwrap-test.sh") - with open(self.test_file, 'w') as f: - f.write('#!/bin/bash\n') - f.write('ID=`id | sed \'s/uid=//\' | sed \'s/(.*//\' `\n') - f.write("echo $ID $1\ -\" Now is the time for all good men to come \ -to the aid of their party.\"\n") - # we need a temporary conf file, pointing into pwd for the filter - # specs. there's probably a better way to do this, but I couldn't - # figure it out. 08/15/12 -- jrd - self.conf_file = self.useFixture( - fixtures.TempDir()).join("rootwrap.conf") - with open(self.conf_file, 'w') as f: - f.write("# temporary conf file for rootwrap-test, " + - "generated by test_rootwrap.py\n") - f.write("[DEFAULT]\n") - f.write("filters_path=" + self.cwd + - "/tacker/tests/etc/rootwrap.d/") - # now set the root helper to sudo our rootwrap script, - # with the new conf - self.root_helper = "sudo " + self.cwd + "/bin/tacker-rootwrap " - self.root_helper += self.conf_file - - def runTest(self): - try: - result = utils.execute(["bash", self.test_file, 'arg'], - self.root_helper) - self.assertEqual("0 arg Now is the time for all good men to \ -come to the aid of their party.", result) - except Exception: - LOG.exception("Losing in rootwrap test") - - def tearDown(self): - os.remove(self.test_file) - os.remove(self.conf_file) - super(RootwrapTestExec, self).tearDown() diff --git a/tacker/tests/unit/test_privsep.py b/tacker/tests/unit/test_privsep.py new file mode 100644 index 000000000..f169ff6a8 --- /dev/null +++ b/tacker/tests/unit/test_privsep.py @@ -0,0 +1,85 @@ +# Copyright (c) 2012 OpenStack Foundation +# Copyright (C) 2022 Nippon Telegraph and Telephone Corporation +# 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 +from oslo_log import log as logging +import unittest + +from tacker.agent.linux import utils +import tacker.privileged.linux_cmd +from tacker.tests import base + + +LOG = logging.getLogger(__name__) + +# Use env 'PWD' to check if tests are run on zuul because we cannot run the +# tests require root privileges. Skip them on zuul, but still run on +# localhost to test privsep features. +_PWD = os.environ['PWD'] +_PWD_ZUUL = "/home/zuul/src/opendev.org/openstack/tacker" + + +class PrivsepTest(base.BaseTestCase): + """Simple unit test to test the basic privsep mechanism + + Essentially hello-world. Just run a command as root and check that + it actually *did* run as root. + """ + + def setUp(self): + super(PrivsepTest, self).setUp() + + @unittest.skipIf(_PWD == _PWD_ZUUL, "Failed on zuul for root priv") + def test_privsep_ls(self): + """Run ls with root privilege + + This ls command is expected to be run on `/`. + """ + + ls = tacker.privileged.linux_cmd.ls() + # The result is a series of dirs on '/' and separated with '\n' like + # as 'bin\nboot\ndev\netc\n...'. + res = ls[0].split('\n') + + # 'boot' dir must be under '/'. + self.assertIn('boot', res) + + @unittest.skipIf(_PWD == _PWD_ZUUL, "Failed on zuul for root priv") + def test_privsep_pwd(self): + """Run pwd with root privilege + + This ls command is expected to be run on `/`. + """ + res = tacker.privileged.linux_cmd.pwd()[0] + self.assertEqual('/\n', res) + + @unittest.skipIf(_PWD == _PWD_ZUUL, "Failed on zuul for root priv") + def test_rootwrap(self): + """Confirm a command can be run with tacker-rootwrap + + pwd is used as a harmless command in this test and defined in + '/etc/tacker/rootwrap.d/tacker.filters' as a CommandFilter. + """ + + root_helper = ["sudo", "tacker-rootwrap", + "/etc/tacker/rootwrap.conf"] + cmd = "pwd" + + actual = utils.execute(root_helper + [cmd]) + expected = utils.execute([cmd]) + + self.assertEqual(expected, actual)