mirror of https://github.com/ansible/ansible.git
Fix local connection and become issues (#84700)
* Fixed various become-related issues in `local` connection plugin. * Fixed various issues in `sudo` and `su` become plugins. * Added unit and integration test coverage. Co-authored-by: Matt Clay <matt@mystile.com> Co-authored-by: Matt Davis <nitzmahone@redhat.com>pull/84727/head
parent
a4d4315d37
commit
5d7b8288f8
@ -0,0 +1,22 @@
|
|||||||
|
minor_changes:
|
||||||
|
- local connection plugin - A new ``become_success_timeout`` operation-wide timeout config (default 10s) was added for ``become``.
|
||||||
|
- local connection plugin - A new ``become_strip_preamble`` config option (default True) was added; disable to preserve diagnostic ``become`` output in task results.
|
||||||
|
- local connection plugin - When a ``become`` plugin's ``prompt`` value is a non-string after the ``check_password_prompt`` callback has completed, no prompt stripping will occur on stderr.
|
||||||
|
|
||||||
|
bugfixes:
|
||||||
|
- local connection plugin - Fixed silent ignore of ``become`` failures and loss of task output when data arrived concurrently on stdout and stderr during ``become`` operation validation.
|
||||||
|
- local connection plugin - Fixed hang or spurious failure when data arrived concurrently on stdout and stderr during a successful ``become`` operation validation.
|
||||||
|
- local connection plugin - Fixed task output header truncation when post-become data arrived before ``become`` operation validation had completed.
|
||||||
|
- local connection plugin - Ensure ``become`` success validation always occurs, even when an active plugin does not set ``prompt``.
|
||||||
|
- local connection plugin - Fixed cases where the internal ``BECOME-SUCCESS`` message appeared in task output.
|
||||||
|
- local connection plugin - Fixed long timeout/hang for ``become`` plugins that repeat their prompt on failure (e.g., ``sudo``, some ``su`` implementations).
|
||||||
|
- local connection plugin - Fixed hang when an active become plugin incorrectly signals lack of prompt.
|
||||||
|
- local connection plugin - Fixed hang when a become plugin expects a prompt but a password was not provided.
|
||||||
|
- local connection plugin - Fixed hang when an internal become read timeout expired before the password prompt was written.
|
||||||
|
- local connection plugin - Fixed hang when only one of stdout or stderr was closed by the ``become_exe`` subprocess.
|
||||||
|
- local connection plugin - Become timeout errors now include all received data. Previously, the most recently-received data was discarded.
|
||||||
|
- sudo become plugin - The `sudo_chdir` config option allows the current directory to be set to the specified value before executing sudo to avoid permission errors when dropping privileges.
|
||||||
|
- su become plugin - Ensure generated regex from ``prompt_l10n`` config values is properly escaped.
|
||||||
|
- su become plugin - Ensure that password prompts are correctly detected in the presence of leading output. Previously, this case resulted in a timeout or hang.
|
||||||
|
- su become plugin - Ensure that trailing colon is expected on all ``prompt_l10n`` config values.
|
||||||
|
- ansible-test - Managed macOS instances now use the ``sudo_chdir`` option for the ``sudo`` become plugin to avoid permission errors when dropping privileges.
|
||||||
@ -1,3 +1,9 @@
|
|||||||
destructive
|
destructive
|
||||||
shippable/posix/group3
|
shippable/posix/group1
|
||||||
context/controller
|
context/target
|
||||||
|
gather_facts/no
|
||||||
|
needs/target/setup_become_user_pair
|
||||||
|
needs/target/setup_test_user
|
||||||
|
setup/always/setup_passlib_controller # required for setup_test_user
|
||||||
|
skip/macos # requires a TTY
|
||||||
|
skip/freebsd # appears to require a TTY (ignores password input from stdin)
|
||||||
|
|||||||
@ -0,0 +1,22 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# A command wrapper that delegates to su after lowering privilege through an intermediate user (via sudo).
|
||||||
|
# This allows forcing an environment that always requires a password prompt for su.
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
args=("${@}")
|
||||||
|
|
||||||
|
for i in "${!args[@]}"; do
|
||||||
|
case "${args[$i]}" in
|
||||||
|
"--intermediate-user")
|
||||||
|
intermediate_user_idx="${i}"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
intermediate_user_name="${args[intermediate_user_idx+1]}"
|
||||||
|
|
||||||
|
unset "args[intermediate_user_idx]"
|
||||||
|
unset "args[intermediate_user_idx+1]"
|
||||||
|
|
||||||
|
exec sudo -n -u "${intermediate_user_name}" su "${args[@]}"
|
||||||
@ -1,6 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
set -eux
|
|
||||||
|
|
||||||
# ensure we execute su with a pseudo terminal
|
|
||||||
[ "$(ansible -a whoami --become-method=su localhost --become)" != "su: requires a terminal to execute" ]
|
|
||||||
@ -0,0 +1,63 @@
|
|||||||
|
- name: create unprivileged user pair
|
||||||
|
include_role:
|
||||||
|
name: setup_become_user_pair
|
||||||
|
public: true # this exports target_user_name, target_user_password, intermediate_user_name
|
||||||
|
vars:
|
||||||
|
intermediate_user_groups: "{{ 'staff,admin' if ansible_os_family == 'Darwin' else omit }}" # this works, but requires a TTY; disabled MacOS su testing in CI for now via aliases
|
||||||
|
|
||||||
|
- name: deploy su shim
|
||||||
|
copy:
|
||||||
|
src: sushim.sh
|
||||||
|
dest: /tmp/sushim.sh
|
||||||
|
mode: a+rx
|
||||||
|
|
||||||
|
- name: ensure su is setuid on Alpine
|
||||||
|
file:
|
||||||
|
path: /bin/su
|
||||||
|
mode: +s
|
||||||
|
when: ansible_os_family == 'Alpine'
|
||||||
|
|
||||||
|
- name: test su scenarios where a password prompt must be encountered
|
||||||
|
vars:
|
||||||
|
ansible_become: yes
|
||||||
|
ansible_become_method: su
|
||||||
|
ansible_become_exe: /tmp/sushim.sh
|
||||||
|
ansible_become_flags: --intermediate-user {{ intermediate_user_name | quote }} # the default plugin flags are empty
|
||||||
|
ansible_become_user: "{{ target_user_name }}"
|
||||||
|
ansible_become_password: "{{ target_user_password }}"
|
||||||
|
block:
|
||||||
|
- name: basic success check
|
||||||
|
raw: whoami
|
||||||
|
register: success
|
||||||
|
# NOTE: The ssh connection plugin does not properly strip noise from raw stdout, unlike the local connection plugin.
|
||||||
|
# Once that is fixed, this can be changed to a comparison against stdout, not stdout_lines[-1].
|
||||||
|
failed_when: success.stdout_lines[-1] != target_user_name
|
||||||
|
|
||||||
|
- name: validate that a password prompt is being used
|
||||||
|
vars:
|
||||||
|
ansible_become_password: BOGUSPASS
|
||||||
|
raw: exit 99
|
||||||
|
ignore_errors: yes
|
||||||
|
register: bogus_password
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- | # account for different failure behavior between local and ssh
|
||||||
|
bogus_password.msg is contains "Incorrect su password" or
|
||||||
|
bogus_password.msg is contains "Premature end of stream waiting for become success." or
|
||||||
|
(bogus_password.stdout | default('')) is contains "Sorry"
|
||||||
|
|
||||||
|
- name: test wrong su prompt expected
|
||||||
|
raw: echo hi mom from $(whoami)
|
||||||
|
register: wrong_su_prompt
|
||||||
|
vars:
|
||||||
|
ansible_su_prompt_l10n: NOT_A_VALID_PROMPT
|
||||||
|
ansible_local_become_success_timeout: 3 # actual become success timeout
|
||||||
|
ansible_ssh_timeout: 3 # connection timeout, which results in an N+2 second select timeout
|
||||||
|
ignore_errors: yes
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- wrong_su_prompt is failed
|
||||||
|
- ansible_connection != "local" or wrong_su_prompt.msg is contains "Timed out waiting for become success or become password prompt"
|
||||||
|
- ansible_connection != "ssh" or wrong_su_prompt.msg is contains "waiting for privilege escalation prompt"
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
destructive
|
||||||
|
shippable/posix/group1
|
||||||
|
context/target
|
||||||
|
gather_facts/no
|
||||||
|
needs/target/setup_become_user_pair
|
||||||
|
needs/target/setup_test_user
|
||||||
|
setup/always/setup_passlib_controller # required for setup_test_user
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# A command wrapper that delegates to sudo after lowering privilege through an intermediate user (via sudo).
|
||||||
|
# This allows forcing an environment that always requires a password prompt for sudo.
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
args=("${@}")
|
||||||
|
|
||||||
|
for i in "${!args[@]}"; do
|
||||||
|
case "${args[$i]}" in
|
||||||
|
"--intermediate-user")
|
||||||
|
intermediate_user_idx="${i}"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
intermediate_user_name="${args[intermediate_user_idx+1]}"
|
||||||
|
|
||||||
|
unset "args[intermediate_user_idx]"
|
||||||
|
unset "args[intermediate_user_idx+1]"
|
||||||
|
|
||||||
|
exec sudo -n -u "${intermediate_user_name}" sudo -k "${args[@]}"
|
||||||
@ -0,0 +1,84 @@
|
|||||||
|
- name: create unprivileged become user pair
|
||||||
|
include_role:
|
||||||
|
name: setup_become_user_pair
|
||||||
|
public: true
|
||||||
|
|
||||||
|
- name: capture config values
|
||||||
|
set_fact:
|
||||||
|
# this needs to be looked up and stored before setting ansible_become_flags
|
||||||
|
default_become_flags: "{{ lookup('config', 'become_flags', plugin_type='become', plugin_name='sudo') }}"
|
||||||
|
intermediate_flags: --intermediate-user {{ intermediate_user_name | quote }}
|
||||||
|
|
||||||
|
- name: deploy sudo shim
|
||||||
|
copy:
|
||||||
|
src: sudoshim.sh
|
||||||
|
dest: /tmp/sudoshim.sh
|
||||||
|
mode: a+rx
|
||||||
|
|
||||||
|
- name: apply shared become vars to all tasks that use the sudo test shim
|
||||||
|
vars:
|
||||||
|
ansible_become: yes
|
||||||
|
ansible_become_method: sudo
|
||||||
|
ansible_become_user: '{{ target_user_name }}'
|
||||||
|
ansible_become_password: '{{ intermediate_user_password }}'
|
||||||
|
ansible_become_exe: /tmp/sudoshim.sh
|
||||||
|
ansible_become_flags: '{{ default_become_flags }} {{ intermediate_flags }}'
|
||||||
|
ansible_local_become_strip_preamble: true
|
||||||
|
block:
|
||||||
|
- name: basic success check
|
||||||
|
raw: whoami
|
||||||
|
register: success
|
||||||
|
# NOTE: The ssh connection plugin does not properly strip noise from raw stdout, unlike the local connection plugin.
|
||||||
|
# Once that is fixed, this can be changed to a comparison against stdout, not stdout_lines[-1].
|
||||||
|
failed_when: success.stdout_lines[-1] != target_user_name
|
||||||
|
|
||||||
|
- name: validate that a password prompt is being used and that the shim is invalidating the sudo timestamp
|
||||||
|
vars:
|
||||||
|
ansible_become_password: BOGUSPASS
|
||||||
|
raw: exit 99
|
||||||
|
ignore_errors: true
|
||||||
|
register: bogus_password
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- bogus_password.msg is contains "Incorrect sudo password" or bogus_password.msg is contains "Duplicate become password prompt encountered"
|
||||||
|
|
||||||
|
- name: request sudo chdir to a nonexistent root dir; expected failure
|
||||||
|
raw: echo himom
|
||||||
|
vars:
|
||||||
|
ansible_sudo_chdir: /nonexistent_dir
|
||||||
|
ignore_errors: true
|
||||||
|
register: nonexistent_chdir
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- nonexistent_chdir is failed
|
||||||
|
# deal with inconsistent failure behavior across different connection plugins
|
||||||
|
- (nonexistent_chdir.msg ~ (nonexistent_chdir.stdout | default('')) ~ (nonexistent_chdir.stderr | default(''))) is search "cd.*/nonexistent_dir"
|
||||||
|
|
||||||
|
- name: request sudo chdir to /; cwd should successfully be / before sudo runs
|
||||||
|
raw: echo "CWD IS <$(pwd)>"
|
||||||
|
vars:
|
||||||
|
ansible_sudo_chdir: /
|
||||||
|
register: chdir_root
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- chdir_root.stdout is contains 'CWD IS </>'
|
||||||
|
|
||||||
|
- name: become with custom sudo `--` flags (similar to defaults)
|
||||||
|
vars:
|
||||||
|
ansible_become_flags: --set-home --stdin --non-interactive {{ intermediate_flags }}
|
||||||
|
raw: whoami
|
||||||
|
register: custom_flags
|
||||||
|
|
||||||
|
- name: become with no user
|
||||||
|
vars:
|
||||||
|
ansible_become_user: ""
|
||||||
|
raw: whoami
|
||||||
|
register: no_user
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- custom_flags.stdout.strip() == test_user_name
|
||||||
|
- no_user.stdout.strip() == "root"
|
||||||
@ -1,2 +1,6 @@
|
|||||||
shippable/posix/group5
|
shippable/posix/group5
|
||||||
needs/target/connection
|
needs/target/connection
|
||||||
|
needs/target/setup_become_user_pair
|
||||||
|
needs/target/setup_test_user
|
||||||
|
setup/once/setup_passlib_controller
|
||||||
|
destructive
|
||||||
|
|||||||
@ -0,0 +1,88 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# A wrapper around `sudo` that replaces the expected password prompt string (if given) with a bogus value.
|
||||||
|
# This allows testing situations where the expected password prompt is not found.
|
||||||
|
# This wrapper also supports becoming an intermediate user before executing sudo, to support testing as root.
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
args=("${@}")
|
||||||
|
intermediate_user_idx=''
|
||||||
|
original_prompt=''
|
||||||
|
shell_executable=''
|
||||||
|
shell_command=''
|
||||||
|
original_prompt_idx=''
|
||||||
|
|
||||||
|
# some args show up after others, but we need them before processing args that came before them
|
||||||
|
for i in "${!args[@]}"; do
|
||||||
|
case "${args[$i]}" in
|
||||||
|
"-p")
|
||||||
|
original_prompt="${args[i+1]}"
|
||||||
|
original_prompt_idx="${i}"
|
||||||
|
;;
|
||||||
|
"-c")
|
||||||
|
shell_executable="${args[i-1]}"
|
||||||
|
shell_command="${args[i+1]}"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
for i in "${!args[@]}"; do
|
||||||
|
case "${args[$i]}" in
|
||||||
|
"--inject-stdout-noise")
|
||||||
|
echo "stdout noise"
|
||||||
|
unset "args[i]"
|
||||||
|
;;
|
||||||
|
"--inject-stderr-noise")
|
||||||
|
echo >&2 "stderr noise"
|
||||||
|
unset "args[i]"
|
||||||
|
;;
|
||||||
|
"--bogus-prompt")
|
||||||
|
args[original_prompt_idx+1]="BOGUSPROMPT"
|
||||||
|
unset "args[i]"
|
||||||
|
;;
|
||||||
|
"--intermediate-user")
|
||||||
|
intermediate_user_idx="${i}"
|
||||||
|
;;
|
||||||
|
"--close-stderr")
|
||||||
|
>&2 echo "some injected stderr, EOF now"
|
||||||
|
exec 2>&- # close stderr, doesn't seem to work on Ubuntu 24.04 (either not closed or not seen in Python?)
|
||||||
|
unset "args[i]"
|
||||||
|
;;
|
||||||
|
"--sleep-before-sudo")
|
||||||
|
sleep 3
|
||||||
|
unset "args[i]"
|
||||||
|
;;
|
||||||
|
"--pretend-to-be-broken-passwordless-sudo")
|
||||||
|
echo '{"hello":"not a module response"}'
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
"--pretend-to-be-broken-sudo")
|
||||||
|
echo -n "${original_prompt}"
|
||||||
|
read -rs
|
||||||
|
echo
|
||||||
|
echo "success, but not invoking given command"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
"--pretend-to-be-sudo")
|
||||||
|
echo -n "${original_prompt}"
|
||||||
|
read -rs
|
||||||
|
echo
|
||||||
|
echo "success, invoking given command"
|
||||||
|
"${shell_executable}" -c "${shell_command}"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "${intermediate_user_idx}" ]]; then
|
||||||
|
# The current user can sudo without a password prompt, so delegate to an intermediate user first.
|
||||||
|
intermediate_user_name="${args[intermediate_user_idx+1]}"
|
||||||
|
|
||||||
|
unset "args[intermediate_user_idx]"
|
||||||
|
unset "args[intermediate_user_idx+1]"
|
||||||
|
|
||||||
|
exec sudo -n -u "${intermediate_user_name}" sudo -k "${args[@]}"
|
||||||
|
else
|
||||||
|
# The current user requires a password to sudo, so sudo can be used directly.
|
||||||
|
exec sudo -k "${args[@]}"
|
||||||
|
fi
|
||||||
@ -0,0 +1,199 @@
|
|||||||
|
- hosts: testhost
|
||||||
|
gather_facts: no
|
||||||
|
tasks:
|
||||||
|
- name: skip this test for non-root users
|
||||||
|
block:
|
||||||
|
- debug:
|
||||||
|
msg: Skipping sudo test for non-root user.
|
||||||
|
- meta: end_play
|
||||||
|
when: lookup('pipe', 'whoami') != 'root'
|
||||||
|
|
||||||
|
- name: attempt root-to-root sudo become (no-op, should succeed)
|
||||||
|
vars:
|
||||||
|
ansible_become: yes
|
||||||
|
ansible_become_user: root
|
||||||
|
ansible_become_method: sudo
|
||||||
|
raw: whoami
|
||||||
|
register: root_noop_sudo
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- root_noop_sudo.stdout is contains "root"
|
||||||
|
|
||||||
|
- name: create an unprivileged become user pair
|
||||||
|
include_role:
|
||||||
|
name: setup_become_user_pair
|
||||||
|
public: true
|
||||||
|
|
||||||
|
- name: deploy sudo shim
|
||||||
|
copy:
|
||||||
|
src: sudoshim.sh
|
||||||
|
dest: /tmp/sudoshim.sh
|
||||||
|
mode: u+x
|
||||||
|
|
||||||
|
- set_fact:
|
||||||
|
default_sudo_flags: "{{ lookup('config', 'become_flags', plugin_type='become', plugin_name='sudo') }}"
|
||||||
|
|
||||||
|
- name: apply shared become vars to all tasks that use the sudo test shim
|
||||||
|
vars:
|
||||||
|
ansible_become: yes
|
||||||
|
ansible_become_method: sudo
|
||||||
|
ansible_become_user: '{{ target_user_name }}'
|
||||||
|
ansible_become_password: '{{ target_user_password }}'
|
||||||
|
ansible_become_exe: /tmp/sudoshim.sh
|
||||||
|
ansible_local_become_strip_preamble: true
|
||||||
|
intermediate: "{{ ((' --intermediate-user ' + intermediate_user_name) if intermediate_user_name is defined else '') }}"
|
||||||
|
block:
|
||||||
|
- name: verify stdout/stderr noise is present with preamble strip disabled
|
||||||
|
raw: echo $(whoami) ran
|
||||||
|
vars:
|
||||||
|
ansible_become_flags: "{{ default_sudo_flags ~ intermediate ~ ' --inject-stdout-noise --inject-stderr-noise' }}"
|
||||||
|
ansible_become_password: "{{ intermediate_user_password }}"
|
||||||
|
ansible_local_become_strip_preamble: false
|
||||||
|
ansible_pipelining: true
|
||||||
|
register: preamble_visible
|
||||||
|
|
||||||
|
- name: verify stdout/stderr noise is stripped with preamble strip enabled
|
||||||
|
raw: echo $(whoami) ran
|
||||||
|
vars:
|
||||||
|
ansible_become_flags: "{{ default_sudo_flags ~ intermediate ~ ' --inject-stdout-noise --inject-stderr-noise' }}"
|
||||||
|
ansible_become_password: "{{ intermediate_user_password }}"
|
||||||
|
ansible_pipelining: true
|
||||||
|
register: preamble_stripped
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- preamble_visible.stdout is contains(target_user_name ~ " ran")
|
||||||
|
- preamble_stripped.stdout is contains(target_user_name ~ " ran")
|
||||||
|
- preamble_visible.stdout is contains "stdout noise"
|
||||||
|
- preamble_stripped.stdout is not contains "stdout noise"
|
||||||
|
- preamble_visible.stderr is contains "stderr noise"
|
||||||
|
- preamble_stripped.stderr is not contains "stderr noise"
|
||||||
|
|
||||||
|
- name: verify sudo succeeds with a password (no PTY/pipelined)
|
||||||
|
raw: echo $(whoami) ran
|
||||||
|
vars:
|
||||||
|
ansible_become_flags: "{{ default_sudo_flags ~ intermediate }}"
|
||||||
|
ansible_become_password: "{{ intermediate_user_password }}"
|
||||||
|
ansible_local_become_strip_preamble: false # allow prompt sniffing from the output
|
||||||
|
ansible_pipelining: true
|
||||||
|
register: success_pipelined
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- success_pipelined.stdout is contains(target_user_name ~ " ran")
|
||||||
|
- success_pipelined.stderr is search 'sudo via ansible.*password\:'
|
||||||
|
|
||||||
|
- name: verify sudo works with a PTY allocated (pipelining disabled)
|
||||||
|
raw: echo $(whoami) ran without pipelining
|
||||||
|
vars:
|
||||||
|
ansible_become_flags: "{{ default_sudo_flags ~ intermediate }}"
|
||||||
|
ansible_become_password: "{{ intermediate_user_password }}"
|
||||||
|
ansible_local_become_strip_preamble: false
|
||||||
|
ansible_pipelining: no # a PTY is allocated by the local plugin only when pipelining is disabled
|
||||||
|
register: pty_non_pipelined
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- pty_non_pipelined.stdout is contains(test_user_name ~ " ran without pipelining")
|
||||||
|
|
||||||
|
- name: verify early-closed stderr still sees success
|
||||||
|
# this test triggers early EOF (which unregisters the associated selector) on most OSs, but not on Ubuntu 24.04
|
||||||
|
vars:
|
||||||
|
ansible_become_flags: --close-stderr --sleep-before-sudo --pretend-to-be-sudo
|
||||||
|
ansible_local_become_success_timeout: 5
|
||||||
|
raw: echo ran_ok
|
||||||
|
register: stderr_closed
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- stderr_closed.stderr is contains "some injected stderr, EOF now"
|
||||||
|
- stderr_closed.stdout is contains "ran_ok"
|
||||||
|
|
||||||
|
- name: verify timeout handling by setting a sudo prompt that won't trigger password send
|
||||||
|
vars:
|
||||||
|
ansible_become_flags: "{{ default_sudo_flags ~ intermediate }} --bogus-prompt"
|
||||||
|
ansible_local_become_success_timeout: 2
|
||||||
|
raw: exit 99
|
||||||
|
ignore_errors: true
|
||||||
|
register: prompt_timeout
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- prompt_timeout is failed
|
||||||
|
- prompt_timeout.msg is contains "Timed out waiting for become success or become password prompt"
|
||||||
|
- prompt_timeout.msg is contains "BOGUSPROMPT"
|
||||||
|
- prompt_timeout.rc is not defined
|
||||||
|
|
||||||
|
- name: verify sub 1s timeout is always increased
|
||||||
|
vars:
|
||||||
|
ansible_become_flags: "{{ default_sudo_flags ~ ' --sleep-before-sudo' }}"
|
||||||
|
ansible_local_become_success_timeout: 0 # a 0s timeout would always cause select to be skipped in the current impl, but added a 2s sleep in the shim in case that changes
|
||||||
|
raw: whoami
|
||||||
|
register: timeout_increased
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- timeout_increased.stdout is contains target_user_name
|
||||||
|
|
||||||
|
- name: verify handling of premature exit/stream closure
|
||||||
|
vars:
|
||||||
|
ansible_become_exe: /bogus
|
||||||
|
raw: exit 99
|
||||||
|
ignore_errors: true
|
||||||
|
register: early_close
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- early_close is failed
|
||||||
|
- early_close.msg is contains "Premature end of stream"
|
||||||
|
|
||||||
|
- name: verify lack of required password fails as expected
|
||||||
|
raw: exit 99
|
||||||
|
vars:
|
||||||
|
ansible_become_flags: "{{ default_sudo_flags ~ intermediate }}"
|
||||||
|
ansible_become_password: ~
|
||||||
|
ignore_errors: true
|
||||||
|
register: missing_required_password
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- missing_required_password is failed
|
||||||
|
- missing_required_password.msg is contains "password is required"
|
||||||
|
|
||||||
|
- name: verify duplicate password prompts are handled (due to incorrect password)
|
||||||
|
raw: echo hi mom
|
||||||
|
vars:
|
||||||
|
ansible_become_flags: "{{ default_sudo_flags ~ intermediate }}"
|
||||||
|
ansible_become_password: not_the_correct_password
|
||||||
|
ignore_errors: yes
|
||||||
|
register: incorrect_password
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- incorrect_password is failed
|
||||||
|
- incorrect_password.msg is contains "Duplicate become password prompt encountered"
|
||||||
|
|
||||||
|
- name: no error, but no become success message
|
||||||
|
vars:
|
||||||
|
ansible_become_flags: --pretend-to-be-broken-sudo # handle password prompt, but return no output
|
||||||
|
raw: exit 99 # should never actually run anyway
|
||||||
|
ignore_errors: true
|
||||||
|
register: no_become_success
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- no_become_success is failed
|
||||||
|
- no_become_success.msg is contains "Premature end of stream waiting for become success"
|
||||||
|
|
||||||
|
- name: test broken passwordless sudo
|
||||||
|
raw: echo hi mom
|
||||||
|
vars:
|
||||||
|
ansible_become_flags: --pretend-to-be-broken-passwordless-sudo
|
||||||
|
ignore_errors: yes
|
||||||
|
register: broken_passwordless_sudo
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- broken_passwordless_sudo is failed
|
||||||
|
- broken_passwordless_sudo.msg is contains "not a module response"
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
target_user_name: ansibletest0 # target unprivileged user
|
||||||
|
intermediate_user_name: ansibletest1 # an intermediate user
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
- name: create an unprivileged user on target
|
||||||
|
include_role:
|
||||||
|
name: setup_test_user
|
||||||
|
public: true
|
||||||
|
vars:
|
||||||
|
test_user_name: '{{ target_user_name }}'
|
||||||
|
test_user_groups: '{{ target_user_groups | default(omit) }}'
|
||||||
|
|
||||||
|
- name: capture target user password
|
||||||
|
set_fact:
|
||||||
|
target_user_password: '{{ test_user_plaintext_password }}'
|
||||||
|
|
||||||
|
- name: create an intermediate user on target with password-required sudo ability
|
||||||
|
include_role:
|
||||||
|
name: setup_test_user
|
||||||
|
public: true
|
||||||
|
vars:
|
||||||
|
test_user_name: "{{ intermediate_user_name }}"
|
||||||
|
test_user_groups: '{{ intermediate_user_groups | default(omit) }}'
|
||||||
|
test_user_allow_sudo: true
|
||||||
|
|
||||||
|
- name: capture config values, intermediate user password from role
|
||||||
|
set_fact:
|
||||||
|
intermediate_user_password: "{{ test_user_plaintext_password }}"
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
# true/false/nopasswd
|
||||||
|
test_user_allow_sudo: false
|
||||||
|
test_user_name: ansibletest0
|
||||||
|
test_user_group: ~
|
||||||
|
test_user_groups: ~
|
||||||
@ -1,6 +1,7 @@
|
|||||||
- name: delete test user
|
- name: delete test user
|
||||||
user:
|
user:
|
||||||
name: "{{ test_user_name }}"
|
name: "{{ item }}"
|
||||||
state: absent
|
state: absent
|
||||||
remove: yes
|
remove: yes
|
||||||
force: yes
|
force: yes
|
||||||
|
loop: "{{ delete_users }}"
|
||||||
|
|||||||
@ -0,0 +1,14 @@
|
|||||||
|
- name: probe for sudoers config path
|
||||||
|
shell: visudo -c
|
||||||
|
ignore_errors: true
|
||||||
|
register: visudo_result
|
||||||
|
|
||||||
|
- set_fact:
|
||||||
|
sudoers_path: '{{ ((visudo_result.stdout ~ visudo_result.stderr) | regex_search("(/.*sudoers).*:", "\1"))[0] }}'
|
||||||
|
|
||||||
|
- name: allow the user to use sudo {{"with no password" if test_user_allow_sudo == "nopasswd" else "with a password"}}
|
||||||
|
copy:
|
||||||
|
content: |
|
||||||
|
{{ test_user_name }} ALL=(ALL) {{"NOPASSWD: " if test_user_allow_sudo == "nopasswd" else ""}} ALL
|
||||||
|
mode: '0440'
|
||||||
|
dest: '{{ sudoers_path ~ ".d/" ~ test_user_name }}'
|
||||||
Loading…
Reference in New Issue