---
# Copyright (c) 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

# Initialise environment

- name: Register variables
  set_fact:
    sudoers_path: /etc/sudoers.d
    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
  ansible.builtin.file:
    path: "{{ sudoers_path }}"
    state: directory
    recurse: true

- name: Ensure alternative sudoers directory exists
  ansible.builtin.file:
    path: "{{ alt_sudoers_path }}"
    state: directory
    recurse: true


# Run module and collect data

- name: Create first rule
  community.general.sudoers:
    name: my-sudo-rule-1
    state: present
    user: alice
    commands: /usr/local/bin/command
  register: rule_1

- name: Stat my-sudo-rule-1 file
  ansible.builtin.stat:
    path: "{{ sudoers_path }}/my-sudo-rule-1"
  register: rule_1_stat

- name: Grab contents of my-sudo-rule-1
  ansible.builtin.slurp:
    src: "{{ sudoers_path }}/my-sudo-rule-1"
  register: rule_1_contents

- name: Create first rule again
  community.general.sudoers:
    name: my-sudo-rule-1
    state: present
    user: alice
    commands: /usr/local/bin/command
  register: rule_1_again


- name: Create second rule with two commands
  community.general.sudoers:
    name: my-sudo-rule-2
    state: present
    user: alice
    commands:
      - /usr/local/bin/command1
      - /usr/local/bin/command2
  register: rule_2

- name: Grab contents of my-sudo-rule-2
  ansible.builtin.slurp:
    src: "{{ sudoers_path }}/my-sudo-rule-2"
  register: rule_2_contents


- name: Create rule requiring a password
  community.general.sudoers:
    name: my-sudo-rule-3
    state: present
    user: alice
    commands: /usr/local/bin/command
    nopassword: false
  register: rule_3

- name: Grab contents of my-sudo-rule-3
  ansible.builtin.slurp:
    src: "{{ sudoers_path }}/my-sudo-rule-3"
  register: rule_3_contents


- name: Create rule using a group
  community.general.sudoers:
    name: my-sudo-rule-4
    state: present
    group: students
    commands: /usr/local/bin/command
  register: rule_4

- name: Grab contents of my-sudo-rule-4
  ansible.builtin.slurp:
    src: "{{ sudoers_path }}/my-sudo-rule-4"
  register: rule_4_contents


- name: Create rule in a alternative directory
  community.general.sudoers:
    name: my-sudo-rule-5
    state: present
    user: alice
    commands: /usr/local/bin/command
    sudoers_path: "{{ alt_sudoers_path }}"
  register: rule_5

- name: Grab contents of my-sudo-rule-5 (in alternative directory)
  ansible.builtin.slurp:
    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:
    name: my-sudo-rule-1
    state: absent
  register: revoke_rule_1

- name: Stat rule 1
  ansible.builtin.stat:
    path: "{{ sudoers_path }}/my-sudo-rule-1"
  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
  community.general.sudoers:
    name: non-existing-rule
    state: absent
  register: revoke_non_existing_rule

- name: Stat non-existing rule
  ansible.builtin.stat:
    path: "{{ sudoers_path }}/non-existing-rule"
  register: revoke_non_existing_rule_stat


# Run assertions

- name: Check rule 1 file stat
  ansible.builtin.assert:
    that:
      - rule_1_stat.stat.exists
      - rule_1_stat.stat.isreg
      - rule_1_stat.stat.mode == '0440'

- name: Check changed status
  ansible.builtin.assert:
    that:
      - rule_1 is changed
      - rule_1_again is not changed
      - rule_5 is changed
      - revoke_rule_1 is changed
      - revoke_non_existing_rule is not changed

- name: Check contents
  ansible.builtin.assert:
    that:
      - "rule_1_contents['content'] | b64decode == 'alice ALL=NOPASSWD: /usr/local/bin/command\n'"
      - "rule_2_contents['content'] | b64decode == 'alice ALL=NOPASSWD: /usr/local/bin/command1, /usr/local/bin/command2\n'"
      - "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 revocation stat
  ansible.builtin.assert:
    that:
      - not revoke_rule_1_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'