mirror of
https://github.com/ansible-collections/community.general.git
synced 2026-05-08 14:22:46 +00:00
preliminary privlege escalation unification + pbrun
- become constants inherit existing sudo/su ones - become command line options, marked sudo/su as deprecated and moved sudo/su passwords to runas group - changed method signatures as privlege escalation is collapsed to become - added tests for su and become, diabled su for lack of support in local.py - updated playbook,play and task objects to become - added become to runner - added whoami test for become/sudo/su - added home override dir for plugins - removed useless method from ask pass - forced become pass to always be string also uses to_bytes - fixed fakerunner for tests - corrected reference in synchronize action plugin - added pfexec (needs testing) - removed unused sudo/su in runner init - removed deprecated info - updated pe tests to allow to run under sudo and not need root - normalized become options into a funciton to avoid duplication and inconsistencies - pushed suppored list to connection classs property - updated all connection plugins to latest 'become' pe - includes fixes from feedback (including typos) - added draft docs - stub of become_exe, leaving for future v2 fixes
This commit is contained in:
@@ -992,14 +992,12 @@ def base_parser(constants=C, usage="", output_opts=False, runas_opts=False,
|
||||
default=constants.DEFAULT_HOST_LIST)
|
||||
parser.add_option('-e', '--extra-vars', dest="extra_vars", action="append",
|
||||
help="set additional variables as key=value or YAML/JSON", default=[])
|
||||
parser.add_option('-u', '--user', default=constants.DEFAULT_REMOTE_USER, dest='remote_user',
|
||||
help='connect as this user (default=%s)' % constants.DEFAULT_REMOTE_USER)
|
||||
parser.add_option('-k', '--ask-pass', default=False, dest='ask_pass', action='store_true',
|
||||
help='ask for SSH password')
|
||||
parser.add_option('--private-key', default=C.DEFAULT_PRIVATE_KEY_FILE, dest='private_key_file',
|
||||
parser.add_option('--private-key', default=constants.DEFAULT_PRIVATE_KEY_FILE, dest='private_key_file',
|
||||
help='use this file to authenticate the connection')
|
||||
parser.add_option('-K', '--ask-sudo-pass', default=False, dest='ask_sudo_pass', action='store_true',
|
||||
help='ask for sudo password')
|
||||
parser.add_option('--ask-su-pass', default=False, dest='ask_su_pass', action='store_true',
|
||||
help='ask for su password')
|
||||
parser.add_option('--ask-vault-pass', default=False, dest='ask_vault_pass', action='store_true',
|
||||
help='ask for vault password')
|
||||
parser.add_option('--vault-password-file', default=constants.DEFAULT_VAULT_PASSWORD_FILE,
|
||||
@@ -1025,22 +1023,35 @@ def base_parser(constants=C, usage="", output_opts=False, runas_opts=False,
|
||||
help='log output to this directory')
|
||||
|
||||
if runas_opts:
|
||||
parser.add_option("-s", "--sudo", default=constants.DEFAULT_SUDO, action="store_true",
|
||||
dest='sudo', help="run operations with sudo (nopasswd)")
|
||||
# priv user defaults to root later on to enable detecting when this option was given here
|
||||
parser.add_option('-K', '--ask-sudo-pass', default=False, dest='ask_sudo_pass', action='store_true',
|
||||
help='ask for sudo password (deprecated, use become)')
|
||||
parser.add_option('--ask-su-pass', default=False, dest='ask_su_pass', action='store_true',
|
||||
help='ask for su password (deprecated, use become)')
|
||||
parser.add_option("-s", "--sudo", default=constants.DEFAULT_SUDO, action="store_true", dest='sudo',
|
||||
help="run operations with sudo (nopasswd) (deprecated, use become)")
|
||||
parser.add_option('-U', '--sudo-user', dest='sudo_user', default=None,
|
||||
help='desired sudo user (default=root)') # Can't default to root because we need to detect when this option was given
|
||||
parser.add_option('-u', '--user', default=constants.DEFAULT_REMOTE_USER,
|
||||
dest='remote_user', help='connect as this user (default=%s)' % constants.DEFAULT_REMOTE_USER)
|
||||
help='desired sudo user (default=root) (deprecated, use become)')
|
||||
parser.add_option('-S', '--su', default=constants.DEFAULT_SU, action='store_true',
|
||||
help='run operations with su (deprecated, use become)')
|
||||
parser.add_option('-R', '--su-user', default=None,
|
||||
help='run operations with su as this user (default=%s) (deprecated, use become)' % constants.DEFAULT_SU_USER)
|
||||
|
||||
# consolidated privilege escalation (become)
|
||||
parser.add_option("-b", "--become", default=constants.DEFAULT_BECOME, action="store_true", dest='become',
|
||||
help="run operations with become (nopasswd implied)")
|
||||
parser.add_option('--become-method', dest='become_method', default=constants.DEFAULT_BECOME_METHOD, type='string',
|
||||
help="privilege escalation method to use (default=%s), valid choices: [ %s ]" % (constants.DEFAULT_BECOME_METHOD, ' | '.join(constants.BECOME_METHODS)))
|
||||
parser.add_option('--become-user', default=None, dest='become_user', type='string',
|
||||
help='run operations as this user (default=%s)' % constants.DEFAULT_BECOME_USER)
|
||||
parser.add_option('--ask-become-pass', default=False, dest='become_ask_pass', action='store_true',
|
||||
help='ask for privilege escalation password')
|
||||
|
||||
parser.add_option('-S', '--su', default=constants.DEFAULT_SU,
|
||||
action='store_true', help='run operations with su')
|
||||
parser.add_option('-R', '--su-user', help='run operations with su as this '
|
||||
'user (default=%s)' % constants.DEFAULT_SU_USER)
|
||||
|
||||
if connect_opts:
|
||||
parser.add_option('-c', '--connection', dest='connection',
|
||||
default=C.DEFAULT_TRANSPORT,
|
||||
help="connection type to use (default=%s)" % C.DEFAULT_TRANSPORT)
|
||||
default=constants.DEFAULT_TRANSPORT,
|
||||
help="connection type to use (default=%s)" % constants.DEFAULT_TRANSPORT)
|
||||
|
||||
if async_opts:
|
||||
parser.add_option('-P', '--poll', default=constants.DEFAULT_POLL_INTERVAL, type='int',
|
||||
@@ -1059,7 +1070,6 @@ def base_parser(constants=C, usage="", output_opts=False, runas_opts=False,
|
||||
help="when changing (small) files and templates, show the differences in those files; works great with --check"
|
||||
)
|
||||
|
||||
|
||||
return parser
|
||||
|
||||
def parse_extra_vars(extra_vars_opts, vault_pass):
|
||||
@@ -1106,41 +1116,58 @@ def ask_vault_passwords(ask_vault_pass=False, ask_new_vault_pass=False, confirm_
|
||||
|
||||
return vault_pass, new_vault_pass
|
||||
|
||||
def ask_passwords(ask_pass=False, ask_sudo_pass=False, ask_su_pass=False, ask_vault_pass=False):
|
||||
def ask_passwords(ask_pass=False, become_ask_pass=False, ask_vault_pass=False, become_method=C.DEFAULT_BECOME_METHOD):
|
||||
sshpass = None
|
||||
sudopass = None
|
||||
supass = None
|
||||
becomepass = None
|
||||
vaultpass = None
|
||||
sudo_prompt = "sudo password: "
|
||||
su_prompt = "su password: "
|
||||
become_prompt = ''
|
||||
|
||||
if ask_pass:
|
||||
sshpass = getpass.getpass(prompt="SSH password: ")
|
||||
become_prompt = "%s password[defaults to SSH password]: " % become_method.upper()
|
||||
if sshpass:
|
||||
sshpass = to_bytes(sshpass, errors='strict', nonstring='simplerepr')
|
||||
sudo_prompt = "sudo password [defaults to SSH password]: "
|
||||
su_prompt = "su password [defaults to SSH password]: "
|
||||
else:
|
||||
become_prompt = "%s password: " % become_method.upper()
|
||||
|
||||
if ask_sudo_pass:
|
||||
sudopass = getpass.getpass(prompt=sudo_prompt)
|
||||
if ask_pass and sudopass == '':
|
||||
sudopass = sshpass
|
||||
if sudopass:
|
||||
sudopass = to_bytes(sudopass, errors='strict', nonstring='simplerepr')
|
||||
|
||||
if ask_su_pass:
|
||||
supass = getpass.getpass(prompt=su_prompt)
|
||||
if ask_pass and supass == '':
|
||||
supass = sshpass
|
||||
if supass:
|
||||
supass = to_bytes(supass, errors='strict', nonstring='simplerepr')
|
||||
if become_ask_pass:
|
||||
becomepass = getpass.getpass(prompt=become_prompt)
|
||||
if ask_pass and becomepass == '':
|
||||
becomepass = sshpass
|
||||
if becomepass:
|
||||
becomepass = to_bytes(becomepass)
|
||||
|
||||
if ask_vault_pass:
|
||||
vaultpass = getpass.getpass(prompt="Vault password: ")
|
||||
if vaultpass:
|
||||
vaultpass = to_bytes(vaultpass, errors='strict', nonstring='simplerepr').strip()
|
||||
|
||||
return (sshpass, sudopass, supass, vaultpass)
|
||||
return (sshpass, becomepass, vaultpass)
|
||||
|
||||
|
||||
def choose_pass_prompt(options):
|
||||
|
||||
if options.ask_su_pass:
|
||||
return 'su'
|
||||
elif options.ask_sudo_pass:
|
||||
return 'sudo'
|
||||
|
||||
return options.become_method
|
||||
|
||||
def normalize_become_options(options):
|
||||
|
||||
options.become_ask_pass = options.become_ask_pass or options.ask_sudo_pass or options.ask_su_pass or C.DEFAULT_BECOME_ASK_PASS
|
||||
options.become_user = options.become_user or options.sudo_user or options.su_user or C.DEFAULT_BECOME_USER
|
||||
|
||||
if options.become:
|
||||
pass
|
||||
elif options.sudo:
|
||||
options.become = True
|
||||
options.become_method = 'sudo'
|
||||
elif options.su:
|
||||
options.become = True
|
||||
options.become_method = 'su'
|
||||
|
||||
|
||||
def do_encrypt(result, encrypt, salt_size=None, salt=None):
|
||||
if PASSLIB_AVAILABLE:
|
||||
@@ -1194,38 +1221,63 @@ def boolean(value):
|
||||
else:
|
||||
return False
|
||||
|
||||
def make_become_cmd(cmd, user, shell, method, flags=None, exe=None):
|
||||
"""
|
||||
helper function for connection plugins to create privilege escalation commands
|
||||
"""
|
||||
|
||||
randbits = ''.join(chr(random.randint(ord('a'), ord('z'))) for x in xrange(32))
|
||||
success_key = 'BECOME-SUCCESS-%s' % randbits
|
||||
prompt = None
|
||||
becomecmd = None
|
||||
|
||||
shell = shell or '$SHELL'
|
||||
|
||||
if method == 'sudo':
|
||||
# Rather than detect if sudo wants a password this time, -k makes sudo always ask for
|
||||
# a password if one is required. Passing a quoted compound command to sudo (or sudo -s)
|
||||
# directly doesn't work, so we shellquote it with pipes.quote() and pass the quoted
|
||||
# string to the user's shell. We loop reading output until we see the randomly-generated
|
||||
# sudo prompt set with the -p option.
|
||||
prompt = '[sudo via ansible, key=%s] password: ' % randbits
|
||||
exe = exe or C.DEFAULT_SUDO_EXE
|
||||
becomecmd = '%s -k && %s %s -S -p "%s" -u %s %s -c "%s"' % \
|
||||
(exe, exe, flags or C.DEFAULT_SUDO_FLAGS, prompt, user, shell, 'echo %s; %s' % (success_key, cmd))
|
||||
|
||||
elif method == 'su':
|
||||
exe = exe or C.DEFAULT_SU_EXE
|
||||
flags = flags or C.DEFAULT_SU_FLAGS
|
||||
becomecmd = '%s %s %s -c "%s -c %s"' % (exe, flags, user, shell, pipes.quote('echo %s; %s' % (success_key, cmd)))
|
||||
|
||||
elif method == 'pbrun':
|
||||
exe = exe or 'pbrun'
|
||||
flags = flags or ''
|
||||
becomecmd = '%s -b -l %s -u %s "%s"' % (exe, flags, user, 'echo %s; %s' % (success_key,cmd))
|
||||
|
||||
elif method == 'pfexec':
|
||||
exe = exe or 'pfexec'
|
||||
flags = flags or ''
|
||||
# No user as it uses it's own exec_attr to figure it out
|
||||
becomecmd = '%s %s "%s"' % (exe, flags, 'echo %s; %s' % (success_key,cmd))
|
||||
|
||||
if becomecmd is None:
|
||||
raise errors.AnsibleError("Privilege escalation method not found: %s" % method)
|
||||
|
||||
return (('%s -c ' % shell) + pipes.quote(becomecmd), prompt, success_key)
|
||||
|
||||
|
||||
def make_sudo_cmd(sudo_exe, sudo_user, executable, cmd):
|
||||
"""
|
||||
helper function for connection plugins to create sudo commands
|
||||
"""
|
||||
# Rather than detect if sudo wants a password this time, -k makes
|
||||
# sudo always ask for a password if one is required.
|
||||
# Passing a quoted compound command to sudo (or sudo -s)
|
||||
# directly doesn't work, so we shellquote it with pipes.quote()
|
||||
# and pass the quoted string to the user's shell. We loop reading
|
||||
# output until we see the randomly-generated sudo prompt set with
|
||||
# the -p option.
|
||||
randbits = ''.join(chr(random.randint(ord('a'), ord('z'))) for x in xrange(32))
|
||||
prompt = '[sudo via ansible, key=%s] password: ' % randbits
|
||||
success_key = 'SUDO-SUCCESS-%s' % randbits
|
||||
sudocmd = '%s -k && %s %s -S -p "%s" -u %s %s -c %s' % (
|
||||
sudo_exe, sudo_exe, C.DEFAULT_SUDO_FLAGS,
|
||||
prompt, sudo_user, executable or '$SHELL', pipes.quote('echo %s; %s' % (success_key, cmd)))
|
||||
return ('/bin/sh -c ' + pipes.quote(sudocmd), prompt, success_key)
|
||||
return make_become_cmd(cmd, sudo_user, executable, 'sudo', C.DEFAULT_SUDO_FLAGS, sudo_exe)
|
||||
|
||||
|
||||
def make_su_cmd(su_user, executable, cmd):
|
||||
"""
|
||||
Helper function for connection plugins to create direct su commands
|
||||
"""
|
||||
# TODO: work on this function
|
||||
randbits = ''.join(chr(random.randint(ord('a'), ord('z'))) for x in xrange(32))
|
||||
success_key = 'SUDO-SUCCESS-%s' % randbits
|
||||
sudocmd = '%s %s %s -c "%s -c %s"' % (
|
||||
C.DEFAULT_SU_EXE, C.DEFAULT_SU_FLAGS, su_user, executable or '$SHELL',
|
||||
pipes.quote('echo %s; %s' % (success_key, cmd))
|
||||
)
|
||||
return ('/bin/sh -c ' + pipes.quote(sudocmd), None, success_key)
|
||||
return make_become_cmd(cmd, su_user, executable, 'su', C.DEFAULT_SU_FLAGS, C.DEFAULT_SU_EXE)
|
||||
|
||||
def get_diff(diff):
|
||||
# called by --diff usage in playbook and runner via callbacks
|
||||
@@ -1577,9 +1629,9 @@ def update_hash(hash, key, new_value):
|
||||
hash[key] = value
|
||||
|
||||
def censor_unlogged_data(data):
|
||||
'''
|
||||
'''
|
||||
used when the no_log: True attribute is passed to a task to keep data from a callback.
|
||||
NOT intended to prevent variable registration, but only things from showing up on
|
||||
NOT intended to prevent variable registration, but only things from showing up on
|
||||
screen
|
||||
'''
|
||||
new_data = {}
|
||||
@@ -1589,5 +1641,19 @@ def censor_unlogged_data(data):
|
||||
new_data['censored'] = 'results hidden due to no_log parameter'
|
||||
return new_data
|
||||
|
||||
def check_mutually_exclusive_privilege(options, parser):
|
||||
|
||||
# privilege escalation command line arguments need to be mutually exclusive
|
||||
if (options.su or options.su_user or options.ask_su_pass) and \
|
||||
(options.sudo or options.sudo_user or options.ask_sudo_pass) or \
|
||||
(options.su or options.su_user or options.ask_su_pass) and \
|
||||
(options.become or options.become_user or options.become_ask_pass) or \
|
||||
(options.sudo or options.sudo_user or options.ask_sudo_pass) and \
|
||||
(options.become or options.become_user or options.become_ask_pass):
|
||||
|
||||
parser.error("Sudo arguments ('--sudo', '--sudo-user', and '--ask-sudo-pass') "
|
||||
"and su arguments ('-su', '--su-user', and '--ask-su-pass') "
|
||||
"and become arguments ('--become', '--become-user', and '--ask-become-pass')"
|
||||
" are exclusive of each other")
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user