"""Implementation of partial-become rule.""" # Copyright (c) 2016 Will Thames # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. from __future__ import annotations import sys from functools import reduce from typing import TYPE_CHECKING, Any from ansiblelint.constants import LINE_NUMBER_KEY from ansiblelint.rules import AnsibleLintRule if TYPE_CHECKING: from ansiblelint.errors import MatchError from ansiblelint.file_utils import Lintable def _get_subtasks(data: dict[str, Any]) -> list[Any]: result: list[Any] = [] block_names = [ "tasks", "pre_tasks", "post_tasks", "handlers", "block", "always", "rescue", ] for name in block_names: if data and name in data: result += data[name] or [] return result def _nested_search(term: str, data: dict[str, Any]) -> Any: if data and term in data: return True return reduce( (lambda x, y: x or _nested_search(term, y)), _get_subtasks(data), False, ) def _become_user_without_become(becomeuserabove: bool, data: dict[str, Any]) -> Any: if "become" in data: # If become is in lineage of tree then correct return False if "become_user" in data and _nested_search("become", data): # If 'become_user' on tree and become somewhere below # we must check for a case of a second 'become_user' without a # 'become' in its lineage subtasks = _get_subtasks(data) return reduce( (lambda x, y: x or _become_user_without_become(False, y)), subtasks, False, ) if _nested_search("become_user", data): # Keep searching down if 'become_user' exists in the tree below current task subtasks = _get_subtasks(data) return len(subtasks) == 0 or reduce( ( lambda x, y: x or _become_user_without_become( becomeuserabove or "become_user" in data, y, ) ), subtasks, False, ) # If at bottom of tree, flag up if 'become_user' existed in the lineage of the tree and # 'become' was not. This is an error if any lineage has a 'become_user' but no become return becomeuserabove class BecomeUserWithoutBecomeRule(AnsibleLintRule): """become_user requires become to work as expected.""" id = "partial-become" description = "``become_user`` without ``become`` will not actually change user" severity = "VERY_HIGH" tags = ["unpredictability"] version_added = "historic" def matchplay(self, file: Lintable, data: dict[str, Any]) -> list[MatchError]: if file.kind == "playbook": result = _become_user_without_become(False, data) if result: return [ self.create_matcherror( message=self.shortdesc, filename=file, lineno=data[LINE_NUMBER_KEY], ), ] return [] # testing code to be loaded only with pytest or when executed the rule file if "pytest" in sys.modules: from ansiblelint.rules import RulesCollection # pylint: disable=ungrouped-imports from ansiblelint.runner import Runner # pylint: disable=ungrouped-imports def test_partial_become_positive() -> None: """Positive test for partial-become.""" collection = RulesCollection() collection.register(BecomeUserWithoutBecomeRule()) success = "examples/playbooks/rule-partial-become-without-become-pass.yml" good_runner = Runner(success, rules=collection) assert [] == good_runner.run() def test_partial_become_negative() -> None: """Negative test for partial-become.""" collection = RulesCollection() collection.register(BecomeUserWithoutBecomeRule()) failure = "examples/playbooks/rule-partial-become-without-become-fail.yml" bad_runner = Runner(failure, rules=collection) errs = bad_runner.run() assert len(errs) == 3