Skip to content

Commit

Permalink
Require --feeling-safe for local provisioning (teemtee#3103)
Browse files Browse the repository at this point in the history
  • Loading branch information
martinhoyer authored and The-Mule committed Oct 14, 2024
1 parent 26a265b commit 0fc9c26
Show file tree
Hide file tree
Showing 6 changed files with 89 additions and 14 deletions.
2 changes: 1 addition & 1 deletion tests/login/step.sh
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ rlJournalStart
rlAssertGrep "interactive" "output"

if [ "$step" = "provision" ]; then
rlRun "grep '^ $step$' -A12 output | grep -i interactive"
rlRun "grep '^ $step$' -A13 output | grep -i interactive"
elif [ "$step" = "prepare" ]; then
rlRun "grep '^ $step$' -A17 output | grep -i interactive"
elif [ "$step" = "execute" ]; then
Expand Down
46 changes: 46 additions & 0 deletions tests/unit/test_steps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from unittest.mock import MagicMock, patch

import pytest

import tmt
from tmt.steps import Phase
from tmt.utils import GeneralError


class TestPhaseAssertFeelingSafe:

def setup_method(self):
self.mock_logger = MagicMock()
self.phase = Phase(logger=self.mock_logger)

@pytest.mark.parametrize(
("tmt_version", "deprecated_version", "expect_warn", "expect_exception"), [
('1.30', '1.38', True, False), # warn for older version
('1.40', '1.38', False, True), # raise exception for newer version
('1.38', '1.38', False, True) # raise exception for same version
])
def test_assert_feeling_safe(
self,
tmt_version,
deprecated_version,
expect_warn,
expect_exception):
with patch.object(self.phase, 'warn') as mock_warn:
tmt.__version__ = tmt_version

if expect_exception:
with pytest.raises(GeneralError):
self.phase.assert_feeling_safe(deprecated_version, 'Local provision plugin')
else:
self.phase.assert_feeling_safe(deprecated_version, 'Local provision plugin')

assert mock_warn.called == expect_warn

def test_assert_feeling_safe_feeling_safe(self):
with (patch.object(Phase, 'is_feeling_safe', True),
patch.object(self.phase, 'warn') as mock_warn):
tmt.__version__ = '1.40'
self.phase.assert_feeling_safe('1.38', 'Local provision plugin')

# Check that warn is not called when feeling safe
assert not mock_warn.called
12 changes: 2 additions & 10 deletions tmt/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ def write_dl(
force_dry_options = create_options_decorator(tmt.options.FORCE_DRY_OPTIONS)
again_option = create_options_decorator(tmt.options.AGAIN_OPTION)
fix_options = create_options_decorator(tmt.options.FIX_OPTIONS)
feeling_safe_option = create_options_decorator(tmt.options.FEELING_SAFE_OPTION)
workdir_root_options = create_options_decorator(tmt.options.WORKDIR_ROOT_OPTIONS)
filtering_options = create_options_decorator(tmt.options.FILTERING_OPTIONS)
filtering_options_long = create_options_decorator(tmt.options.FILTERING_OPTIONS_LONG)
Expand All @@ -281,17 +282,8 @@ def write_dl(
dimensions or the @FILE notation to load data from provided yaml file. Can be specified
multiple times.
""")
@option(
'--feeling-safe',
is_flag=True,
help="""
WARNING: with this option, tmt would be allowed to make
potentially dangerous actions. For example, some metadata
keys may cause scripts being executed on the runner.
Do not use this option unless you trust metadata consumed
by tmt, or unless you know what you are doing.
""")
@verbosity_options
@feeling_safe_option
@option(
'--show-time',
is_flag=True,
Expand Down
15 changes: 14 additions & 1 deletion tmt/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ def option(
help='If specified, --debug and --verbose would emit logs also for these topics.')
]

# Force, dry and run again actions
# Force, dry, feeling-safe and run again actions
DRY_OPTIONS: list[ClickOptionDecoratorType] = [
option(
'-n', '--dry', is_flag=True, default=False,
Expand All @@ -163,6 +163,19 @@ def option(
help='Run again, even if already done before.'),
]

FEELING_SAFE_OPTION: list[ClickOptionDecoratorType] = [
option(
'--feeling-safe', metavar='FEELING_SAFE', envvar='TMT_FEELING_SAFE',
is_flag=True, default=False,
help="""
WARNING: with this option, tmt would be allowed to make
potentially dangerous actions. For example, some metadata
keys may cause scripts being executed on the runner.
Do not use this option unless you trust metadata consumed
by tmt, or unless you know what you are doing.
""")
]

# Fix action
FIX_OPTIONS: list[ClickOptionDecoratorType] = [
option('-F', '--fix', is_flag=True, help='Attempt to fix all discovered issues.')
Expand Down
20 changes: 19 additions & 1 deletion tmt/steps/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,23 @@ def is_in_standalone_mode(self) -> bool:
"""
return False

def assert_feeling_safe(self, deprecated_in_version: str, subject: str) -> None:
"""
Raises a tmt.utils.ProvisionError if feeling-safe is required, but not set.
Warns when feeling-safe will be required in a future version.
:param deprecated_in_version: Version from which feeling-safe is required, e.g. '1.38'.
:param subject: Subject requiring feeling-safe, e.g. 'Local provision plugin'.
"""
if self.is_feeling_safe:
return

if tmt.__version__ < deprecated_in_version:
self.warn(f"{subject} will require '--feeling-safe' option "
f"from version {deprecated_in_version}.")

else:
raise tmt.utils.GeneralError(f"{subject} requires '--feeling-safe' option")


# A variable used to describe a generic type for all classes derived from Phase
PhaseT = TypeVar('PhaseT', bound=Phase)
Expand Down Expand Up @@ -1264,7 +1281,8 @@ def options(cls, how: Optional[str] = None) -> list[tmt.options.ClickOptionDecor
] + (
tmt.options.VERBOSITY_OPTIONS +
tmt.options.FORCE_DRY_OPTIONS +
tmt.options.AGAIN_OPTION
tmt.options.AGAIN_OPTION +
tmt.options.FEELING_SAFE_OPTION
)

@classmethod
Expand Down
8 changes: 7 additions & 1 deletion tmt/steps/provision/local.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,14 @@ class ProvisionLocal(tmt.steps.provision.ProvisionPlugin[ProvisionLocalData]):
.. warning::
In general it is not recommended to run tests on your local machine
In general, it is not recommended to run tests on your local machine
as there might be security risks. Run only those tests which you
know are safe so that you don't destroy your laptop ;-)
From tmt version 1.38, ``--feeling-safe`` option or
``TMT_FEELING_SAFE=True`` environment variable will
be required in order to use local provision plugin.
Example config:
.. code-block:: yaml
Expand Down Expand Up @@ -183,6 +187,8 @@ def go(self, *, logger: Optional[tmt.log.Logger] = None) -> None:

data.show(verbose=self.verbosity_level, logger=self._logger)

self.assert_feeling_safe("1.38", "Local provision plugin")

if data.hardware and data.hardware.constraint:
self.warn("The 'local' provision plugin does not support hardware requirements.")

Expand Down

0 comments on commit 0fc9c26

Please sign in to comment.