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 <yasufum.o@gmail.com>
Change-Id: Id8de4c2bae91718d6ba45ed523edc103f0b21718
This commit is contained in:
Yasufumi Ogawa 2022-03-01 18:33:59 +00:00
parent 4d11475f3a
commit 3d6d849faa
10 changed files with 161 additions and 101 deletions

View File

@ -7,4 +7,8 @@
# cmd-name: filter-name, raw-command, user, args # cmd-name: filter-name, raw-command, user, args
[Filters] [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

View File

@ -24,7 +24,7 @@ docutils==0.14
dogpile.cache==0.6.5 dogpile.cache==0.6.5
dulwich==0.19.0 dulwich==0.19.0
enum-compat==0.0.2 enum-compat==0.0.2
eventlet==0.18.2 eventlet==0.30.1
extras==1.0.0 extras==1.0.0
fasteners==0.14.1 fasteners==0.14.1
fixtures==3.0.0 fixtures==3.0.0
@ -52,7 +52,7 @@ Mako==1.0.7
MarkupSafe==1.1 MarkupSafe==1.1
monotonic==1.4 monotonic==1.4
mox3==0.25.0 mox3==0.25.0
msgpack==0.5.6 msgpack==0.6.0
munch==2.2.0 munch==2.2.0
netaddr==0.7.18 netaddr==0.7.18
netifaces==0.10.6 netifaces==0.10.6
@ -72,10 +72,11 @@ oslo.log==3.36.0
oslo.messaging==9.3.0 oslo.messaging==9.3.0
oslo.middleware==3.31.0 oslo.middleware==3.31.0
oslo.policy==3.6.0 oslo.policy==3.6.0
oslo.privsep==2.4.0
oslo.reports==1.18.0 oslo.reports==1.18.0
oslo.rootwrap==5.8.0 oslo.rootwrap==5.8.0
oslo.serialization==2.18.0 oslo.serialization==2.18.0
oslo.service==1.24.0 oslo.service==2.5.0
oslo.upgradecheck==1.3.0 oslo.upgradecheck==1.3.0
oslo.utils==4.8.0 oslo.utils==4.8.0
oslo.versionedobjects==1.33.3 oslo.versionedobjects==1.33.3

View File

@ -7,7 +7,7 @@ Paste>=2.0.2 # MIT
PasteDeploy>=1.5.0 # MIT PasteDeploy>=1.5.0 # MIT
Routes>=2.3.1 # MIT Routes>=2.3.1 # MIT
amqp>=2.4.0 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 requests>=2.25.1 # Apache-2.0
jsonschema>=3.2.0 # MIT jsonschema>=3.2.0 # MIT
keystonemiddleware>=4.17.0 # Apache-2.0 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.messaging>=9.3.0 # Apache-2.0
oslo.middleware>=3.31.0 # Apache-2.0 oslo.middleware>=3.31.0 # Apache-2.0
oslo.policy>=3.6.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.reports>=1.18.0 # Apache-2.0
oslo.rootwrap>=5.8.0 # Apache-2.0 oslo.rootwrap>=5.8.0 # Apache-2.0
oslo.serialization!=2.19.1,>=2.18.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.upgradecheck>=1.3.0 # Apache-2.0
oslo.utils>=4.8.0 # Apache-2.0 oslo.utils>=4.8.0 # Apache-2.0
oslo.versionedobjects>=1.33.3 # Apache-2.0 oslo.versionedobjects>=1.33.3 # Apache-2.0

View File

@ -43,7 +43,7 @@ console_scripts =
tacker-db-manage = tacker.db.migration.cli:main tacker-db-manage = tacker.db.migration.cli:main
tacker-server = tacker.cmd.eventlet.tacker_server:main tacker-server = tacker.cmd.eventlet.tacker_server:main
tacker-conductor = tacker.cmd.eventlet.conductor: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-status = tacker.cmd.status:main
tacker.service_plugins = tacker.service_plugins =
dummy = tacker.tests.unit.dummy_plugin:DummyServicePlugin dummy = tacker.tests.unit.dummy_plugin:DummyServicePlugin

View File

@ -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],
)

View File

@ -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

View File

@ -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

View File

@ -21,4 +21,3 @@ lock_path = $state_path/lock
[database] [database]
connection = 'sqlite://' connection = 'sqlite://'

View File

@ -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()

View File

@ -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)