1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2024-09-14 20:13:21 +02:00

Sudoers validate (#4794)

* Use visudo to validate sudoers rules before use

* Replace use of subprocess.Popen with module.run_command

* Switch out apt for package

* Check file mode when verifying file to determine whether something needs to change

* Only install sudo package for debian and redhat environments (when testing)

* Attempt to install sudo on FreeBSD too

* Try just installing sudo for non-darwin machines

* Don't validate file ownership

* Attempt to install sudo on all platforms

* Revert "Attempt to install sudo on all platforms"

This reverts commit b9562a8916.

* Remove file permissions changes from this PR

* Add changelog fragment for 4794 sudoers validation

* Add option to control when sudoers validation is used

* Update changelog fragment

Co-authored-by: Felix Fontein <felix@fontein.de>

* Add version_added to validation property

Co-authored-by: Felix Fontein <felix@fontein.de>

* Also validate failed sudoers validation error message

Co-authored-by: Felix Fontein <felix@fontein.de>

* Make visudo not executable instead of trying to delete it

* Update edge case validation

* Write invalid sudoers file to alternative path to avoid breaking sudo

* Don't try to remove or otherwise modify visudo on Darwin

* Update plugins/modules/system/sudoers.py

Co-authored-by: Felix Fontein <felix@fontein.de>

* Remove trailing extra empty line to appease sanity checker

Co-authored-by: Felix Fontein <felix@fontein.de>
This commit is contained in:
Jon Ellis 2022-06-21 11:41:24 +01:00 committed by GitHub
parent 45362d39a2
commit 97c72f88b7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 101 additions and 2 deletions

View file

@ -0,0 +1,2 @@
minor_changes:
- sudoers - will attempt to validate the proposed sudoers rule using visudo if available, optionally skipped, or required (https://github.com/ansible-collections/community.general/pull/4794, https://github.com/ansible-collections/community.general/issues/4745).

View file

@ -65,6 +65,15 @@ options:
- The name of the user for the sudoers rule. - The name of the user for the sudoers rule.
- This option cannot be used in conjunction with I(group). - This option cannot be used in conjunction with I(group).
type: str type: str
validation:
description:
- If C(absent), the sudoers rule will be added without validation.
- If C(detect) and visudo is available, then the sudoers rule will be validated by visudo.
- If C(required), visudo must be available to validate the sudoers rule.
type: str
default: detect
choices: [ absent, detect, required ]
version_added: 5.2.0
''' '''
EXAMPLES = ''' EXAMPLES = '''
@ -118,6 +127,8 @@ class Sudoers(object):
FILE_MODE = 0o440 FILE_MODE = 0o440
def __init__(self, module): def __init__(self, module):
self.module = module
self.check_mode = module.check_mode self.check_mode = module.check_mode
self.name = module.params['name'] self.name = module.params['name']
self.user = module.params['user'] self.user = module.params['user']
@ -128,6 +139,7 @@ class Sudoers(object):
self.sudoers_path = module.params['sudoers_path'] self.sudoers_path = module.params['sudoers_path']
self.file = os.path.join(self.sudoers_path, self.name) self.file = os.path.join(self.sudoers_path, self.name)
self.commands = module.params['commands'] self.commands = module.params['commands']
self.validation = module.params['validation']
def write(self): def write(self):
if self.check_mode: if self.check_mode:
@ -167,6 +179,20 @@ class Sudoers(object):
runas_str = '({runas})'.format(runas=self.runas) if self.runas is not None else '' 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) return "{owner} ALL={runas}{nopasswd} {commands}\n".format(owner=owner, runas=runas_str, nopasswd=nopasswd_str, commands=commands_str)
def validate(self):
if self.validation == 'absent':
return
visudo_path = self.module.get_bin_path('visudo', required=self.validation == 'required')
if visudo_path is None:
return
check_command = [visudo_path, '-c', '-f', '-']
rc, stdout, stderr = self.module.run_command(check_command, data=self.content())
if rc != 0:
raise Exception('Failed to validate sudoers rule:\n{stdout}'.format(stdout=stdout))
def run(self): def run(self):
if self.state == 'absent': if self.state == 'absent':
if self.exists(): if self.exists():
@ -175,6 +201,8 @@ class Sudoers(object):
else: else:
return False return False
self.validate()
if self.exists() and self.matches(): if self.exists() and self.matches():
return False return False
@ -209,6 +237,10 @@ def main():
'choices': ['present', 'absent'], 'choices': ['present', 'absent'],
}, },
'user': {}, 'user': {},
'validation': {
'default': 'detect',
'choices': ['absent', 'detect', 'required']
},
} }
module = AnsibleModule( module = AnsibleModule(

View file

@ -1,11 +1,16 @@
--- ---
# Initialise environment # Initialise environment
- name: Register sudoers.d directory - name: Register variables
set_fact: set_fact:
sudoers_path: /etc/sudoers.d sudoers_path: /etc/sudoers.d
alt_sudoers_path: /etc/sudoers_alt alt_sudoers_path: /etc/sudoers_alt
- name: Install sudo package
ansible.builtin.package:
name: sudo
when: ansible_os_family != 'Darwin'
- name: Ensure sudoers directory exists - name: Ensure sudoers directory exists
ansible.builtin.file: ansible.builtin.file:
path: "{{ sudoers_path }}" path: "{{ sudoers_path }}"
@ -135,6 +140,52 @@
register: revoke_rule_1_stat register: revoke_rule_1_stat
# Validation testing
- name: Attempt command without full path to executable
community.general.sudoers:
name: edge-case-1
state: present
user: alice
commands: systemctl
ignore_errors: true
register: edge_case_1
- name: Attempt command without full path to executable, but disabling validation
community.general.sudoers:
name: edge-case-2
state: present
user: alice
commands: systemctl
validation: absent
sudoers_path: "{{ alt_sudoers_path }}"
register: edge_case_2
- name: find visudo
command:
cmd: which visudo
register: which_visudo
when: ansible_os_family != 'Darwin'
- name: Prevent visudo being executed
file:
path: "{{ which_visudo.stdout }}"
mode: '-x'
when: ansible_os_family != 'Darwin'
- name: Attempt command without full path to executable, but enforcing validation with no visudo present
community.general.sudoers:
name: edge-case-3
state: present
user: alice
commands: systemctl
validation: required
ignore_errors: true
when: ansible_os_family != 'Darwin'
register: edge_case_3
- name: Revoke non-existing rule - name: Revoke non-existing rule
community.general.sudoers: community.general.sudoers:
name: non-existing-rule name: non-existing-rule
@ -175,8 +226,22 @@
- "rule_5_contents['content'] | b64decode == 'alice 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'" - "rule_6_contents['content'] | b64decode == 'alice ALL=(bob)NOPASSWD: /usr/local/bin/command\n'"
- name: Check stats - name: Check revocation stat
ansible.builtin.assert: ansible.builtin.assert:
that: that:
- not revoke_rule_1_stat.stat.exists - not revoke_rule_1_stat.stat.exists
- not revoke_non_existing_rule_stat.stat.exists - not revoke_non_existing_rule_stat.stat.exists
- name: Check edge case responses
ansible.builtin.assert:
that:
- edge_case_1 is failed
- "'Failed to validate sudoers rule' in edge_case_1.msg"
- edge_case_2 is not failed
- name: Check missing validation edge case
ansible.builtin.assert:
that:
- edge_case_3 is failed
- "'Failed to find required executable' in edge_case_3.msg"
when: ansible_os_family != 'Darwin'