295 lines
12 KiB
Python
295 lines
12 KiB
Python
|
# Copyright 2019, David Wilson
|
||
|
#
|
||
|
# Redistribution and use in source and binary forms, with or without
|
||
|
# modification, are permitted provided that the following conditions are met:
|
||
|
#
|
||
|
# 1. Redistributions of source code must retain the above copyright notice,
|
||
|
# this list of conditions and the following disclaimer.
|
||
|
#
|
||
|
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
||
|
# this list of conditions and the following disclaimer in the documentation
|
||
|
# and/or other materials provided with the distribution.
|
||
|
#
|
||
|
# 3. Neither the name of the copyright holder nor the names of its contributors
|
||
|
# may be used to endorse or promote products derived from this software without
|
||
|
# specific prior written permission.
|
||
|
#
|
||
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||
|
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||
|
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||
|
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||
|
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||
|
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||
|
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||
|
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||
|
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||
|
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||
|
# POSSIBILITY OF SUCH DAMAGE.
|
||
|
|
||
|
# !mitogen: minify_safe
|
||
|
|
||
|
import base64
|
||
|
import logging
|
||
|
import optparse
|
||
|
import re
|
||
|
|
||
|
import mitogen.core
|
||
|
import mitogen.parent
|
||
|
|
||
|
|
||
|
LOG = logging.getLogger(__name__)
|
||
|
|
||
|
password_incorrect_msg = 'sudo password is incorrect'
|
||
|
password_required_msg = 'sudo password is required'
|
||
|
|
||
|
# These are base64-encoded UTF-8 as our existing minifier/module server
|
||
|
# struggles with Unicode Python source in some (forgotten) circumstances.
|
||
|
PASSWORD_PROMPTS = [
|
||
|
'cGFzc3dvcmQ=', # english
|
||
|
'bG96aW5rYQ==', # sr@latin.po
|
||
|
'44OR44K544Ov44O844OJ', # ja.po
|
||
|
'4Kaq4Ka+4Ka44KaT4Kef4Ka+4Kaw4KeN4Kah', # bn.po
|
||
|
'2YPZhNmF2Kkg2KfZhNiz2LE=', # ar.po
|
||
|
'cGFzYWhpdHph', # eu.po
|
||
|
'0L/QsNGA0L7Qu9GM', # uk.po
|
||
|
'cGFyb29s', # et.po
|
||
|
'c2FsYXNhbmE=', # fi.po
|
||
|
'4Kiq4Ki+4Ki44Ki14Kiw4Kih', # pa.po
|
||
|
'Y29udHJhc2lnbm8=', # ia.po
|
||
|
'Zm9jYWwgZmFpcmU=', # ga.po
|
||
|
'16HXodee15Q=', # he.po
|
||
|
'4Kqq4Kq+4Kq44Kq14Kqw4KuN4Kqh', # gu.po
|
||
|
'0L/QsNGA0L7Qu9Cw', # bg.po
|
||
|
'4Kyq4K2N4Kyw4Kys4K2H4Ky2IOCsuOCsmeCtjeCsleCth+CspA==', # or.po
|
||
|
'4K6V4K6f4K614K+B4K6a4K+N4K6a4K+K4K6y4K+N', # ta.po
|
||
|
'cGFzc3dvcnQ=', # de.po
|
||
|
'7JWU7Zi4', # ko.po
|
||
|
'0LvQvtC30LjQvdC60LA=', # sr.po
|
||
|
'beG6rXQga2jhuql1', # vi.po
|
||
|
'c2VuaGE=', # pt_BR.po
|
||
|
'cGFzc3dvcmQ=', # it.po
|
||
|
'aGVzbG8=', # cs.po
|
||
|
'5a+G56K877ya', # zh_TW.po
|
||
|
'aGVzbG8=', # sk.po
|
||
|
'4LC44LCC4LCV4LGH4LCk4LCq4LCm4LCu4LGB', # te.po
|
||
|
'0L/QsNGA0L7Qu9GM', # kk.po
|
||
|
'aGFzxYJv', # pl.po
|
||
|
'Y29udHJhc2VueWE=', # ca.po
|
||
|
'Y29udHJhc2XDsWE=', # es.po
|
||
|
'4LSF4LSf4LSv4LS+4LSz4LS14LS+4LSV4LWN4LSV4LWN', # ml.po
|
||
|
'c2VuaGE=', # pt.po
|
||
|
'5a+G56CB77ya', # zh_CN.po
|
||
|
'4KSX4KWB4KSq4KWN4KSk4KS24KSs4KWN4KSm', # mr.po
|
||
|
'bMO2c2Vub3Jk', # sv.po
|
||
|
'4YOe4YOQ4YOg4YOd4YOa4YOY', # ka.po
|
||
|
'4KS24KSs4KWN4KSm4KSV4KWC4KSf', # hi.po
|
||
|
'YWRnYW5nc2tvZGU=', # da.po
|
||
|
'4La74LeE4LeD4LeK4La04Lav4La6', # si.po
|
||
|
'cGFzc29yZA==', # nb.po
|
||
|
'd2FjaHR3b29yZA==', # nl.po
|
||
|
'4Kaq4Ka+4Ka44KaT4Kef4Ka+4Kaw4KeN4Kah', # bn_IN.po
|
||
|
'cGFyb2xh', # tr.po
|
||
|
'4LKX4LOB4LKq4LON4LKk4LKq4LKm', # kn.po
|
||
|
'c2FuZGk=', # id.po
|
||
|
'0L/QsNGA0L7Qu9GM', # ru.po
|
||
|
'amVsc3rDsw==', # hu.po
|
||
|
'bW90IGRlIHBhc3Nl', # fr.po
|
||
|
'aXBoYXNpd2VkaQ==', # zu.po
|
||
|
'4Z6W4Z624Z6A4Z+S4Z6Z4Z6f4Z6Y4Z+S4Z6E4Z624Z6P4Z+LwqDhn5Y=', # km.po
|
||
|
'4KaX4KeB4Kaq4KeN4Kak4Ka24Kas4KeN4Kam', # as.po
|
||
|
]
|
||
|
|
||
|
|
||
|
PASSWORD_PROMPT_RE = re.compile(
|
||
|
mitogen.core.b('|').join(
|
||
|
base64.b64decode(s)
|
||
|
for s in PASSWORD_PROMPTS
|
||
|
),
|
||
|
re.I
|
||
|
)
|
||
|
|
||
|
SUDO_OPTIONS = [
|
||
|
#(False, 'bool', '--askpass', '-A')
|
||
|
#(False, 'str', '--auth-type', '-a')
|
||
|
#(False, 'bool', '--background', '-b')
|
||
|
#(False, 'str', '--close-from', '-C')
|
||
|
#(False, 'str', '--login-class', 'c')
|
||
|
(True, 'bool', '--preserve-env', '-E'),
|
||
|
#(False, 'bool', '--edit', '-e')
|
||
|
#(False, 'str', '--group', '-g')
|
||
|
(True, 'bool', '--set-home', '-H'),
|
||
|
#(False, 'str', '--host', '-h')
|
||
|
(False, 'bool', '--login', '-i'),
|
||
|
#(False, 'bool', '--remove-timestamp', '-K')
|
||
|
#(False, 'bool', '--reset-timestamp', '-k')
|
||
|
#(False, 'bool', '--list', '-l')
|
||
|
#(False, 'bool', '--preserve-groups', '-P')
|
||
|
#(False, 'str', '--prompt', '-p')
|
||
|
|
||
|
# SELinux options. Passed through as-is.
|
||
|
(False, 'str', '--role', '-r'),
|
||
|
(False, 'str', '--type', '-t'),
|
||
|
|
||
|
# These options are supplied by default by Ansible, but are ignored, as
|
||
|
# sudo always runs under a TTY with Mitogen.
|
||
|
(True, 'bool', '--stdin', '-S'),
|
||
|
(True, 'bool', '--non-interactive', '-n'),
|
||
|
|
||
|
#(False, 'str', '--shell', '-s')
|
||
|
#(False, 'str', '--other-user', '-U')
|
||
|
(False, 'str', '--user', '-u'),
|
||
|
#(False, 'bool', '--version', '-V')
|
||
|
#(False, 'bool', '--validate', '-v')
|
||
|
]
|
||
|
|
||
|
|
||
|
class OptionParser(optparse.OptionParser):
|
||
|
def help(self):
|
||
|
self.exit()
|
||
|
def error(self, msg):
|
||
|
self.exit(msg=msg)
|
||
|
def exit(self, status=0, msg=None):
|
||
|
msg = 'sudo: ' + (msg or 'unsupported option')
|
||
|
raise mitogen.core.StreamError(msg)
|
||
|
|
||
|
|
||
|
def make_sudo_parser():
|
||
|
parser = OptionParser()
|
||
|
for supported, kind, longopt, shortopt in SUDO_OPTIONS:
|
||
|
if kind == 'bool':
|
||
|
parser.add_option(longopt, shortopt, action='store_true')
|
||
|
else:
|
||
|
parser.add_option(longopt, shortopt)
|
||
|
return parser
|
||
|
|
||
|
|
||
|
def parse_sudo_flags(args):
|
||
|
parser = make_sudo_parser()
|
||
|
opts, args = parser.parse_args(args)
|
||
|
if len(args):
|
||
|
raise mitogen.core.StreamError('unsupported sudo arguments:'+str(args))
|
||
|
return opts
|
||
|
|
||
|
|
||
|
class PasswordError(mitogen.core.StreamError):
|
||
|
pass
|
||
|
|
||
|
|
||
|
def option(default, *args):
|
||
|
for arg in args:
|
||
|
if arg is not None:
|
||
|
return arg
|
||
|
return default
|
||
|
|
||
|
|
||
|
class Options(mitogen.parent.Options):
|
||
|
sudo_path = 'sudo'
|
||
|
username = 'root'
|
||
|
password = None
|
||
|
preserve_env = False
|
||
|
set_home = False
|
||
|
login = False
|
||
|
|
||
|
selinux_role = None
|
||
|
selinux_type = None
|
||
|
|
||
|
def __init__(self, username=None, sudo_path=None, password=None,
|
||
|
preserve_env=None, set_home=None, sudo_args=None,
|
||
|
login=None, selinux_role=None, selinux_type=None, **kwargs):
|
||
|
super(Options, self).__init__(**kwargs)
|
||
|
opts = parse_sudo_flags(sudo_args or [])
|
||
|
|
||
|
self.username = option(self.username, username, opts.user)
|
||
|
self.sudo_path = option(self.sudo_path, sudo_path)
|
||
|
if password:
|
||
|
self.password = mitogen.core.to_text(password)
|
||
|
self.preserve_env = option(self.preserve_env,
|
||
|
preserve_env, opts.preserve_env)
|
||
|
self.set_home = option(self.set_home, set_home, opts.set_home)
|
||
|
self.login = option(self.login, login, opts.login)
|
||
|
self.selinux_role = option(self.selinux_role, selinux_role, opts.role)
|
||
|
self.selinux_type = option(self.selinux_type, selinux_type, opts.type)
|
||
|
|
||
|
|
||
|
class SetupProtocol(mitogen.parent.RegexProtocol):
|
||
|
password_sent = False
|
||
|
|
||
|
def _on_password_prompt(self, line, match):
|
||
|
LOG.debug('%s: (password prompt): %s',
|
||
|
self.stream.name, line.decode('utf-8', 'replace'))
|
||
|
|
||
|
if self.stream.conn.options.password is None:
|
||
|
self.stream.conn._fail_connection(
|
||
|
PasswordError(password_required_msg)
|
||
|
)
|
||
|
return
|
||
|
|
||
|
if self.password_sent:
|
||
|
self.stream.conn._fail_connection(
|
||
|
PasswordError(password_incorrect_msg)
|
||
|
)
|
||
|
return
|
||
|
|
||
|
self.stream.transmit_side.write(
|
||
|
(self.stream.conn.options.password + '\n').encode('utf-8')
|
||
|
)
|
||
|
self.password_sent = True
|
||
|
|
||
|
PARTIAL_PATTERNS = [
|
||
|
(PASSWORD_PROMPT_RE, _on_password_prompt),
|
||
|
]
|
||
|
|
||
|
|
||
|
class Connection(mitogen.parent.Connection):
|
||
|
diag_protocol_class = SetupProtocol
|
||
|
options_class = Options
|
||
|
create_child = staticmethod(mitogen.parent.hybrid_tty_create_child)
|
||
|
create_child_args = {
|
||
|
'escalates_privilege': True,
|
||
|
}
|
||
|
child_is_immediate_subprocess = False
|
||
|
|
||
|
def _get_name(self):
|
||
|
return u'sudo.' + mitogen.core.to_text(self.options.username)
|
||
|
|
||
|
def get_boot_command(self):
|
||
|
# Note: sudo did not introduce long-format option processing until July
|
||
|
# 2013, so even though we parse long-format options, supply short-form
|
||
|
# to the sudo command.
|
||
|
boot_cmd = super(Connection, self).get_boot_command()
|
||
|
|
||
|
bits = [self.options.sudo_path, '-u', self.options.username]
|
||
|
if self.options.preserve_env:
|
||
|
bits += ['-E']
|
||
|
if self.options.set_home:
|
||
|
bits += ['-H']
|
||
|
if self.options.login:
|
||
|
bits += ['-i']
|
||
|
if self.options.selinux_role:
|
||
|
bits += ['-r', self.options.selinux_role]
|
||
|
if self.options.selinux_type:
|
||
|
bits += ['-t', self.options.selinux_type]
|
||
|
|
||
|
# special handling for bash builtins
|
||
|
# TODO: more efficient way of doing this, at least
|
||
|
# it's only 1 iteration of boot_cmd to go through
|
||
|
source_found = False
|
||
|
for cmd in boot_cmd[:]:
|
||
|
# rip `source` from boot_cmd if it exists; sudo.py can't run this
|
||
|
# even with -i or -s options
|
||
|
# since we've already got our ssh command working we shouldn't
|
||
|
# need to source anymore
|
||
|
# couldn't figure out how to get this to work using sudo flags
|
||
|
if 'source' == cmd:
|
||
|
boot_cmd.remove(cmd)
|
||
|
source_found = True
|
||
|
continue
|
||
|
if source_found:
|
||
|
# remove words until we hit the python interpreter call
|
||
|
if not cmd.endswith('python'):
|
||
|
boot_cmd.remove(cmd)
|
||
|
else:
|
||
|
break
|
||
|
|
||
|
return bits + ['--'] + boot_cmd
|