diff --git a/.github/BOTMETA.yml b/.github/BOTMETA.yml index 41a4824d26..60d68a2833 100644 --- a/.github/BOTMETA.yml +++ b/.github/BOTMETA.yml @@ -33,6 +33,8 @@ files: maintainers: $team_ansible_core $becomes/pmrun.py: maintainers: $team_ansible_core + $becomes/run0.py: + maintainers: konstruktoid $becomes/sesu.py: maintainers: nekonyuu $becomes/sudosu.py: diff --git a/plugins/become/run0.py b/plugins/become/run0.py new file mode 100644 index 0000000000..1d6d7cb754 --- /dev/null +++ b/plugins/become/run0.py @@ -0,0 +1,128 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2024, Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +DOCUMENTATION = """ + name: run0 + short_description: Systemd's run0 + description: + - This become plugins allows your remote/login user to execute commands as another user via the C(run0) utility. + author: + - Thomas Sjögren (@konstruktoid) + version_added: '9.0.0' + options: + become_user: + description: User you 'become' to execute the task. + default: root + ini: + - section: privilege_escalation + key: become_user + - section: run0_become_plugin + key: user + vars: + - name: ansible_become_user + - name: ansible_run0_user + env: + - name: ANSIBLE_BECOME_USER + - name: ANSIBLE_RUN0_USER + type: string + become_exe: + description: The C(run0) executable. + default: run0 + ini: + - section: privilege_escalation + key: become_exe + - section: run0_become_plugin + key: executable + vars: + - name: ansible_become_exe + - name: ansible_run0_exe + env: + - name: ANSIBLE_BECOME_EXE + - name: ANSIBLE_RUN0_EXE + type: string + become_flags: + description: Options to pass to run0. + default: '' + ini: + - section: privilege_escalation + key: become_flags + - section: run0_become_plugin + key: flags + vars: + - name: ansible_become_flags + - name: ansible_run0_flags + env: + - name: ANSIBLE_BECOME_FLAGS + - name: ANSIBLE_RUN0_FLAGS + type: string + notes: + - This plugin will only work when a polkit rule is in place. +""" + +EXAMPLES = r""" +# An example polkit rule that allows the user 'ansible' in the 'wheel' group +# to execute commands using run0 without authentication. +/etc/polkit-1/rules.d/60-run0-fast-user-auth.rules: | + polkit.addRule(function(action, subject) { + if(action.id == "org.freedesktop.systemd1.manage-units" && + subject.isInGroup("wheel") + subject.user == "ansible") { + return polkit.Result.YES; + } + }); +""" + +from re import compile as re_compile + +from ansible.plugins.become import BecomeBase +from ansible.module_utils._text import to_bytes + +ansi_color_codes = re_compile(to_bytes(r"\x1B\[[0-9;]+m")) + + +class BecomeModule(BecomeBase): + + name = "community.general.run0" + + prompt = "Password: " + fail = ("==== AUTHENTICATION FAILED ====",) + success = ("==== AUTHENTICATION COMPLETE ====",) + require_tty = ( + True # see https://github.com/ansible-collections/community.general/issues/6932 + ) + + @staticmethod + def remove_ansi_codes(line): + return ansi_color_codes.sub(b"", line) + + def build_become_command(self, cmd, shell): + super().build_become_command(cmd, shell) + + if not cmd: + return cmd + + become = self.get_option("become_exe") + flags = self.get_option("become_flags") + user = self.get_option("become_user") + + return ( + f"{become} --user={user} {flags} {self._build_success_command(cmd, shell)}" + ) + + def check_success(self, b_output): + b_output = self.remove_ansi_codes(b_output) + return super().check_success(b_output) + + def check_incorrect_password(self, b_output): + b_output = self.remove_ansi_codes(b_output) + return super().check_incorrect_password(b_output) + + def check_missing_password(self, b_output): + b_output = self.remove_ansi_codes(b_output) + return super().check_missing_password(b_output) diff --git a/tests/unit/plugins/become/test_run0.py b/tests/unit/plugins/become/test_run0.py new file mode 100644 index 0000000000..7507c556e8 --- /dev/null +++ b/tests/unit/plugins/become/test_run0.py @@ -0,0 +1,64 @@ +# Copyright (c) 2024 Ansible Project +# +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import re + +from ansible import context + +from .helper import call_become_plugin + + +def test_run0_basic(mocker, parser, reset_cli_args): + options = parser.parse_args([]) + context._init_global_context(options) + + default_cmd = "/bin/foo" + default_exe = "/bin/sh" + run0_exe = "run0" + + success = "BECOME-SUCCESS-.+?" + + task = { + "become_method": "community.general.run0", + } + var_options = {} + cmd = call_become_plugin(task, var_options, cmd=default_cmd, executable=default_exe) + assert ( + re.match( + f"{run0_exe} --user=root {default_exe} -c 'echo {success}; {default_cmd}'", + cmd, + ) + is not None + ) + + +def test_run0_flags(mocker, parser, reset_cli_args): + options = parser.parse_args([]) + context._init_global_context(options) + + default_cmd = "/bin/foo" + default_exe = "/bin/sh" + run0_exe = "run0" + run0_flags = "--nice=15" + + success = "BECOME-SUCCESS-.+?" + + task = { + "become_method": "community.general.run0", + "become_flags": run0_flags, + } + var_options = {} + cmd = call_become_plugin(task, var_options, cmd=default_cmd, executable=default_exe) + assert ( + re.match( + f"{run0_exe} --user=root --nice=15 {default_exe} -c 'echo {success}; {default_cmd}'", + cmd, + ) + is not None + )