Skip to content

Commit

Permalink
preliminary privlege escalation unification + pbrun
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
bcoca authored and jimi-c committed Mar 10, 2015
1 parent f4329c8 commit bce4bb2
Show file tree
Hide file tree
Showing 45 changed files with 845 additions and 476 deletions.
53 changes: 21 additions & 32 deletions bin/ansible
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,12 @@ class Cli(object):
''' create an options parser for bin/ansible '''

parser = utils.base_parser(
constants=C,
runas_opts=True,
subset_opts=True,
constants=C,
runas_opts=True,
subset_opts=True,
async_opts=True,
output_opts=True,
connect_opts=True,
output_opts=True,
connect_opts=True,
check_opts=True,
diff_opts=False,
usage='%prog <host-pattern> [options]'
Expand All @@ -82,12 +82,8 @@ class Cli(object):
parser.print_help()
sys.exit(1)

# su and sudo 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):
parser.error("Sudo arguments ('--sudo', '--sudo-user', and '--ask-sudo-pass') "
"and su arguments ('-su', '--su-user', and '--ask-su-pass') are "
"mutually exclusive")
# privlege escalation command line arguments need to be mutually exclusive
utils.check_mutually_exclusive_privilege(options, parser)

if (options.ask_vault_pass and options.vault_password_file):
parser.error("--ask-vault-pass and --vault-password-file are mutually exclusive")
Expand All @@ -101,20 +97,20 @@ class Cli(object):

pattern = args[0]

sshpass = None
sudopass = None
su_pass = None
vault_pass = None
sshpass = becomepass = vault_pass = become_method = None

options.ask_pass = options.ask_pass or C.DEFAULT_ASK_PASS
# Never ask for an SSH password when we run with local connection
if options.connection == "local":
options.ask_pass = False
options.ask_sudo_pass = options.ask_sudo_pass or C.DEFAULT_ASK_SUDO_PASS
options.ask_su_pass = options.ask_su_pass or C.DEFAULT_ASK_SU_PASS
else:
options.ask_pass = options.ask_pass or C.DEFAULT_ASK_PASS

options.ask_vault_pass = options.ask_vault_pass or C.DEFAULT_ASK_VAULT_PASS

(sshpass, sudopass, su_pass, vault_pass) = utils.ask_passwords(ask_pass=options.ask_pass, ask_sudo_pass=options.ask_sudo_pass, ask_su_pass=options.ask_su_pass, ask_vault_pass=options.ask_vault_pass)
# become
utils.normalize_become_options(options)
prompt_method = utils.choose_pass_prompt(options)
(sshpass, becomepass, vault_pass) = utils.ask_passwords(ask_pass=options.ask_pass, become_ask_pass=options.become_ask_pass, ask_vault_pass=options.ask_vault_pass, become_method=prompt_method)

# read vault_pass from a file
if not options.ask_vault_pass and options.vault_password_file:
Expand All @@ -126,6 +122,7 @@ class Cli(object):
if options.subset:
inventory_manager.subset(options.subset)
hosts = inventory_manager.list_hosts(pattern)

if len(hosts) == 0:
callbacks.display("No hosts matched", stderr=True)
sys.exit(0)
Expand All @@ -135,16 +132,10 @@ class Cli(object):
callbacks.display(' %s' % host)
sys.exit(0)

if ((options.module_name == 'command' or options.module_name == 'shell')
and not options.module_args):
if options.module_name in ['command','shell'] and not options.module_args:
callbacks.display("No argument passed to %s module" % options.module_name, color='red', stderr=True)
sys.exit(1)


if options.su_user or options.ask_su_pass:
options.su = True
options.sudo_user = options.sudo_user or C.DEFAULT_SUDO_USER
options.su_user = options.su_user or C.DEFAULT_SU_USER
if options.tree:
utils.prepare_writeable_dir(options.tree)

Expand All @@ -160,17 +151,15 @@ class Cli(object):
forks=options.forks,
pattern=pattern,
callbacks=self.callbacks,
sudo=options.sudo,
sudo_pass=sudopass,
sudo_user=options.sudo_user,
transport=options.connection,
subset=options.subset,
check=options.check,
diff=options.check,
su=options.su,
su_pass=su_pass,
su_user=options.su_user,
vault_pass=vault_pass,
become=options.become,
become_method=options.become_method,
become_pass=becomepass,
become_user=options.become_user,
extra_vars=extra_vars,
)

Expand Down
36 changes: 16 additions & 20 deletions bin/ansible-playbook
Original file line number Diff line number Diff line change
Expand Up @@ -108,19 +108,14 @@ def main(args):
parser.print_help(file=sys.stderr)
return 1

# su and sudo 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):
parser.error("Sudo arguments ('--sudo', '--sudo-user', and '--ask-sudo-pass') "
"and su arguments ('-su', '--su-user', and '--ask-su-pass') are "
"mutually exclusive")
# privlege escalation command line arguments need to be mutually exclusive
utils.check_mutually_exclusive_privilege(options, parser)

if (options.ask_vault_pass and options.vault_password_file):
parser.error("--ask-vault-pass and --vault-password-file are mutually exclusive")

sshpass = None
sudopass = None
su_pass = None
becomepass = None
vault_pass = None

options.ask_vault_pass = options.ask_vault_pass or C.DEFAULT_ASK_VAULT_PASS
Expand All @@ -132,11 +127,14 @@ def main(args):
# Never ask for an SSH password when we run with local connection
if options.connection == "local":
options.ask_pass = False
options.ask_sudo_pass = options.ask_sudo_pass or C.DEFAULT_ASK_SUDO_PASS
options.ask_su_pass = options.ask_su_pass or C.DEFAULT_ASK_SU_PASS
(sshpass, sudopass, su_pass, vault_pass) = utils.ask_passwords(ask_pass=options.ask_pass, ask_sudo_pass=options.ask_sudo_pass, ask_su_pass=options.ask_su_pass, ask_vault_pass=options.ask_vault_pass)
options.sudo_user = options.sudo_user or C.DEFAULT_SUDO_USER
options.su_user = options.su_user or C.DEFAULT_SU_USER

# set pe options
utils.normalize_become_options(options)
prompt_method = utils.choose_pass_prompt(options)
(sshpass, becomepass, vault_pass) = utils.ask_passwords(ask_pass=options.ask_pass,
become_ask_pass=options.become_ask_pass,
ask_vault_pass=options.ask_vault_pass,
become_method=prompt_method)

# read vault_pass from a file
if not options.ask_vault_pass and options.vault_password_file:
Expand Down Expand Up @@ -197,20 +195,18 @@ def main(args):
stats=stats,
timeout=options.timeout,
transport=options.connection,
sudo=options.sudo,
sudo_user=options.sudo_user,
sudo_pass=sudopass,
become=options.become,
become_method=options.become_method,
become_user=options.become_user,
become_pass=becomepass,
extra_vars=extra_vars,
private_key_file=options.private_key_file,
only_tags=only_tags,
skip_tags=skip_tags,
check=options.check,
diff=options.diff,
su=options.su,
su_pass=su_pass,
su_user=options.su_user,
vault_password=vault_pass,
force_handlers=options.force_handlers
force_handlers=options.force_handlers,
)

if options.flush_cache:
Expand Down
83 changes: 83 additions & 0 deletions docsite/rst/become.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
Ansible Privilege Escalation
++++++++++++++++++++++++++++

Ansible can use existing privilege escalation systems to allow a user to execute tasks as another.

.. contents:: Topics

Become
``````
Before 1.9 Ansible mostly allowed the use of sudo and a limited use of su to allow a login/remote user to become a different user
and execute tasks, create resources with the 2nd user's permissions. As of 1.9 'become' supersedes the old sudo/su, while still
being backwards compatible. This new system also makes it easier to add other privilege escalation tools like pbrun (Powerbroker),
pfexec and others.


New directives
--------------

become
equivalent to adding sudo: or su: to a play or task, set to true/yes to activate privilege escalation

become_user
equivalent to adding sudo_user: or su_user: to a play or task

become_method
at play or task level overrides the default method set in ansibile.cfg


New ansible_ variables
----------------------
Each allows you to set an option per group and/or host

ansible_become
equivalent to ansible_sudo or ansbile_su, allows to force privilege escalation

ansible_become_method
allows to set privilege escalation method

ansible_become_user
equivalent to ansible_sudo_user or ansbile_su_user, allows to set the user you become through privilege escalation

ansible_become_pass
equivalent to ansible_sudo_pass or ansbile_su_pass, allows you to set the privilege escalation password


New command line options
-----------------------

--ask-become-pass
ask for privilege escalation password

-b, --become
run operations with become (no passorwd implied)

--become-method=BECOME_METHOD
privilege escalation method to use (default=sudo),
valid choices: [ sudo | su | pbrun | pfexec ]

--become-user=BECOME_USER
run operations as this user (default=root)


sudo and su still work!
-----------------------

Old playbooks will not need to be changed, even though they are deprecated, sudo and su directives will continue to work though it
is recommended to move to become as they may be retired at one point. You cannot mix directives on the same object though, ansible
will complain if you try to.

Become will default to using the old sudo/su configs and variables if they exist, but will override them if you specify any of the
new ones.



.. note:: Privilege escalation methods must also be supported by the connection plugin used, most will warn if they do not, some will just ignore it as they always run as root (jail, chroot, etc).

.. seealso::

`Mailing List <http://groups.google.com/group/ansible-project>`_
Questions? Help? Ideas? Stop by the list on Google Groups
`irc.freenode.net <http://irc.freenode.net>`_
#ansible IRC chat channel

6 changes: 6 additions & 0 deletions examples/ansible.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,12 @@ fact_caching = memory
#retry_files_enabled = False
#retry_files_save_path = ~/.ansible-retry

[privilege_escalation]
#become=True
#become_method='sudo'
#become_user='root'
#become_ask_pass=False

[paramiko_connection]

# uncomment this line to cause the paramiko connection plugin to not record new host
Expand Down
34 changes: 22 additions & 12 deletions lib/ansible/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,6 @@ def shell_expand_path(path):
path = os.path.expanduser(os.path.expandvars(path))
return path

def get_plugin_paths(path):
return ':'.join([os.path.join(x, path) for x in [os.path.expanduser('~/.ansible/plugins/'), '/usr/share/ansible_plugins/']])

p = load_config_file()

active_user = pwd.getpwuid(os.geteuid())[0]
Expand Down Expand Up @@ -137,16 +134,28 @@ def get_plugin_paths(path):
DEFAULT_SU_USER = get_config(p, DEFAULTS, 'su_user', 'ANSIBLE_SU_USER', 'root')
DEFAULT_ASK_SU_PASS = get_config(p, DEFAULTS, 'ask_su_pass', 'ANSIBLE_ASK_SU_PASS', False, boolean=True)
DEFAULT_GATHERING = get_config(p, DEFAULTS, 'gathering', 'ANSIBLE_GATHERING', 'implicit').lower()

DEFAULT_ACTION_PLUGIN_PATH = get_config(p, DEFAULTS, 'action_plugins', 'ANSIBLE_ACTION_PLUGINS', get_plugin_paths('action_plugins'))
DEFAULT_CACHE_PLUGIN_PATH = get_config(p, DEFAULTS, 'cache_plugins', 'ANSIBLE_CACHE_PLUGINS', get_plugin_paths('cache_plugins'))
DEFAULT_CALLBACK_PLUGIN_PATH = get_config(p, DEFAULTS, 'callback_plugins', 'ANSIBLE_CALLBACK_PLUGINS', get_plugin_paths('callback_plugins'))
DEFAULT_CONNECTION_PLUGIN_PATH = get_config(p, DEFAULTS, 'connection_plugins', 'ANSIBLE_CONNECTION_PLUGINS', get_plugin_paths('connection_plugins'))
DEFAULT_LOOKUP_PLUGIN_PATH = get_config(p, DEFAULTS, 'lookup_plugins', 'ANSIBLE_LOOKUP_PLUGINS', get_plugin_paths('lookup_plugins'))
DEFAULT_VARS_PLUGIN_PATH = get_config(p, DEFAULTS, 'vars_plugins', 'ANSIBLE_VARS_PLUGINS', get_plugin_paths('vars_plugins'))
DEFAULT_FILTER_PLUGIN_PATH = get_config(p, DEFAULTS, 'filter_plugins', 'ANSIBLE_FILTER_PLUGINS', get_plugin_paths('filter_plugins'))
DEFAULT_LOG_PATH = shell_expand_path(get_config(p, DEFAULTS, 'log_path', 'ANSIBLE_LOG_PATH', ''))

#TODO: get rid of ternary chain mess
BECOME_METHODS = ['sudo','su','pbrun','runas','pfexec']
DEFAULT_BECOME = get_config(p, 'privilege_escalation', 'become', 'ANSIBLE_BECOME',True if DEFAULT_SUDO or DEFAULT_SU else False, boolean=True)
DEFAULT_BECOME_METHOD = get_config(p, 'privilege_escalation', 'become_method', 'ANSIBLE_BECOME_METHOD','sudo' if DEFAULT_SUDO else 'su' if DEFAULT_SU else 'sudo' ).lower()
DEFAULT_BECOME_USER = get_config(p, 'privilege_escalation', 'become_user', 'ANSIBLE_BECOME_USER',DEFAULT_SUDO_USER if DEFAULT_SUDO else DEFAULT_SU_USER if DEFAULT_SU else 'root')
DEFAULT_BECOME_ASK_PASS = get_config(p, 'privilege_escalation', 'become_ask_pass', 'ANSIBLE_BECOME_ASK_PASS',True if DEFAULT_ASK_SUDO_PASS else False, boolean=True)
# need to rethink impementing these 2
DEFAULT_BECOME_EXE = None
#DEFAULT_BECOME_EXE = get_config(p, DEFAULTS, 'become_exe', 'ANSIBLE_BECOME_EXE','sudo' if DEFAULT_SUDO else 'su' if DEFAULT_SU else 'sudo')
#DEFAULT_BECOME_FLAGS = get_config(p, DEFAULTS, 'become_flags', 'ANSIBLE_BECOME_FLAGS',DEFAULT_SUDO_FLAGS if DEFAULT_SUDO else DEFAULT_SU_FLAGS if DEFAULT_SU else '-H')


DEFAULT_ACTION_PLUGIN_PATH = get_config(p, DEFAULTS, 'action_plugins', 'ANSIBLE_ACTION_PLUGINS', '~/.ansible/plugins/action_plugins:/usr/share/ansible_plugins/action_plugins')
DEFAULT_CACHE_PLUGIN_PATH = get_config(p, DEFAULTS, 'cache_plugins', 'ANSIBLE_CACHE_PLUGINS', '~/.ansible/plugins/cache_plugins:/usr/share/ansible_plugins/cache_plugins')
DEFAULT_CALLBACK_PLUGIN_PATH = get_config(p, DEFAULTS, 'callback_plugins', 'ANSIBLE_CALLBACK_PLUGINS', '~/.ansible/plugins/callback_plugins:/usr/share/ansible_plugins/callback_plugins')
DEFAULT_CONNECTION_PLUGIN_PATH = get_config(p, DEFAULTS, 'connection_plugins', 'ANSIBLE_CONNECTION_PLUGINS', '~/.ansible/plugins/connection_plugins:/usr/share/ansible_plugins/connection_plugins')
DEFAULT_LOOKUP_PLUGIN_PATH = get_config(p, DEFAULTS, 'lookup_plugins', 'ANSIBLE_LOOKUP_PLUGINS', '~/.ansible/plugins/lookup_plugins:/usr/share/ansible_plugins/lookup_plugins')
DEFAULT_VARS_PLUGIN_PATH = get_config(p, DEFAULTS, 'vars_plugins', 'ANSIBLE_VARS_PLUGINS', '~/.ansible/plugins/vars_plugins:/usr/share/ansible_plugins/vars_plugins')
DEFAULT_FILTER_PLUGIN_PATH = get_config(p, DEFAULTS, 'filter_plugins', 'ANSIBLE_FILTER_PLUGINS', '~/.ansible/plugins/filter_plugins:/usr/share/ansible_plugins/filter_plugins')

CACHE_PLUGIN = get_config(p, DEFAULTS, 'fact_caching', 'ANSIBLE_CACHE_PLUGIN', 'memory')
CACHE_PLUGIN_CONNECTION = get_config(p, DEFAULTS, 'fact_caching_connection', 'ANSIBLE_CACHE_PLUGIN_CONNECTION', None)
CACHE_PLUGIN_PREFIX = get_config(p, DEFAULTS, 'fact_caching_prefix', 'ANSIBLE_CACHE_PLUGIN_PREFIX', 'ansible_facts')
Expand All @@ -172,7 +181,7 @@ def get_plugin_paths(path):
ANSIBLE_SSH_CONTROL_PATH = get_config(p, 'ssh_connection', 'control_path', 'ANSIBLE_SSH_CONTROL_PATH', "%(directory)s/ansible-ssh-%%h-%%p-%%r")
ANSIBLE_SSH_PIPELINING = get_config(p, 'ssh_connection', 'pipelining', 'ANSIBLE_SSH_PIPELINING', False, boolean=True)
PARAMIKO_RECORD_HOST_KEYS = get_config(p, 'paramiko_connection', 'record_host_keys', 'ANSIBLE_PARAMIKO_RECORD_HOST_KEYS', True, boolean=True)
# obsolete -- will be formally removed in 1.6
# obsolete -- will be formally removed
ZEROMQ_PORT = get_config(p, 'fireball_connection', 'zeromq_port', 'ANSIBLE_ZEROMQ_PORT', 5099, integer=True)
ACCELERATE_PORT = get_config(p, 'accelerate', 'accelerate_port', 'ACCELERATE_PORT', 5099, integer=True)
ACCELERATE_TIMEOUT = get_config(p, 'accelerate', 'accelerate_timeout', 'ACCELERATE_TIMEOUT', 30, integer=True)
Expand All @@ -188,6 +197,7 @@ def get_plugin_paths(path):
DEFAULT_PASSWORD_CHARS = ascii_letters + digits + ".,:-_"

# non-configurable things
DEFAULT_BECOME_PASS = None
DEFAULT_SUDO_PASS = None
DEFAULT_REMOTE_PASS = None
DEFAULT_SUBSET = None
Expand Down

0 comments on commit bce4bb2

Please sign in to comment.