diff --git a/changelogs/fragments/4380-sudoers-runas-parameter.yml b/changelogs/fragments/4380-sudoers-runas-parameter.yml new file mode 100644 index 0000000000..46ed481364 --- /dev/null +++ b/changelogs/fragments/4380-sudoers-runas-parameter.yml @@ -0,0 +1,4 @@ +--- +minor_changes: + - sudoers - add support for ``runas`` parameter + (https://github.com/ansible-collections/community.general/issues/4379). diff --git a/plugins/modules/system/sudoers.py b/plugins/modules/system/sudoers.py index 43362c876c..52b87c7524 100644 --- a/plugins/modules/system/sudoers.py +++ b/plugins/modules/system/sudoers.py @@ -41,6 +41,11 @@ options: - Whether a password will be required to run the sudo'd command. default: true type: bool + runas: + description: + - Specify the target user the command(s) will run as. + type: str + version_added: 4.7.0 sudoers_path: description: - The path which sudoers config files will be managed in. @@ -69,6 +74,14 @@ EXAMPLES = ''' user: backup commands: /usr/local/bin/backup +- name: Allow the bob user to run any commands as alice with sudo -u alice + community.general.sudoers: + name: bob-do-as-alice + state: present + user: bob + runas: alice + commands: ANY + - name: >- Allow the monitoring group to run sudo /usr/local/bin/gather-app-metrics without requiring a password @@ -108,6 +121,7 @@ class Sudoers(object): self.group = module.params['group'] self.state = module.params['state'] self.nopassword = module.params['nopassword'] + self.runas = module.params['runas'] self.sudoers_path = module.params['sudoers_path'] self.file = os.path.join(self.sudoers_path, self.name) self.commands = module.params['commands'] @@ -140,7 +154,8 @@ class Sudoers(object): commands_str = ', '.join(self.commands) nopasswd_str = 'NOPASSWD:' if self.nopassword else '' - return "{owner} ALL={nopasswd} {commands}\n".format(owner=owner, nopasswd=nopasswd_str, commands=commands_str) + runas_str = '({runas})'.format(runas=self.runas) if self.runas is not None else '' + return "{owner} ALL={runas}{nopasswd} {commands}\n".format(owner=owner, runas=runas_str, nopasswd=nopasswd_str, commands=commands_str) def run(self): if self.state == 'absent' and self.exists(): @@ -168,6 +183,10 @@ def main(): 'type': 'bool', 'default': True, }, + 'runas': { + 'type': 'str', + 'default': None, + }, 'sudoers_path': { 'type': 'str', 'default': '/etc/sudoers.d', diff --git a/tests/integration/targets/sudoers/tasks/main.yml b/tests/integration/targets/sudoers/tasks/main.yml index 3a4f778d75..9a632c4de4 100644 --- a/tests/integration/targets/sudoers/tasks/main.yml +++ b/tests/integration/targets/sudoers/tasks/main.yml @@ -102,6 +102,21 @@ src: "{{ alt_sudoers_path }}/my-sudo-rule-5" register: rule_5_contents +- name: Create rule to runas another user + community.general.sudoers: + name: my-sudo-rule-6 + state: present + user: alice + commands: /usr/local/bin/command + runas: bob + sudoers_path: "{{ sudoers_path }}" + register: rule_6 + +- name: Grab contents of my-sudo-rule-6 (in alternative directory) + ansible.builtin.slurp: + src: "{{ sudoers_path }}/my-sudo-rule-6" + register: rule_6_contents + - name: Revoke rule 1 community.general.sudoers: @@ -133,6 +148,7 @@ - "rule_3_contents['content'] | b64decode == 'alice ALL= /usr/local/bin/command\n'" - "rule_4_contents['content'] | b64decode == '%students ALL=NOPASSWD: /usr/local/bin/command\n'" - "rule_5_contents['content'] | b64decode == 'alice ALL=NOPASSWD: /usr/local/bin/command\n'" + - "rule_6_contents['content'] | b64decode == 'alice ALL=(bob)NOPASSWD: /usr/local/bin/command\n'" - name: Check stats ansible.builtin.assert: