# frozen_string_literal: true require 'shellwords' require 'bolt/transport/base' module Bolt module Transport class Sudoable < Base def self.validate_sudo_options(options, logger) if options['sudo-password'] && options['run-as'].nil? logger.warn("--sudo-password will not be used without specifying a " \ "user to escalate to with --run-as") end run_as_cmd = options['run-as-command'] if run_as_cmd && (!run_as_cmd.is_a?(Array) || run_as_cmd.any? { |n| !n.is_a?(String) }) raise Bolt::ValidationError, "run-as-command must be an Array of Strings, received #{run_as_cmd}" end end def self.sudo_prompt '[sudo] Bolt needs to run as another user, password: ' end def run_command(target, command, options = {}) with_connection(target) do |conn| conn.running_as(options['_run_as']) do output = conn.execute(command, sudoable: true) Bolt::Result.for_command(target, output.stdout.string, output.stderr.string, output.exit_code, 'command', command) end end end def upload(target, source, destination, options = {}) with_connection(target) do |conn| conn.running_as(options['_run_as']) do conn.with_tempdir do |dir| basename = File.basename(destination) tmpfile = File.join(dir.to_s, basename) conn.copy_file(source, tmpfile) # pass over file ownership if we're using run-as to be a different user dir.chown(conn.run_as) result = conn.execute(['mv', '-f', tmpfile, destination], sudoable: true) if result.exit_code != 0 message = "Could not move temporary file '#{tmpfile}' to #{destination}: #{result.stderr.string}" raise Bolt::Node::FileError.new(message, 'MV_ERROR') end end Bolt::Result.for_upload(target, source, destination) end end end def run_script(target, script, arguments, options = {}) # unpack any Sensitive data arguments = unwrap_sensitive_args(arguments) with_connection(target) do |conn| conn.running_as(options['_run_as']) do conn.with_tempdir do |dir| path = conn.write_executable(dir.to_s, script) dir.chown(conn.run_as) output = conn.execute([path, *arguments], sudoable: true) Bolt::Result.for_command(target, output.stdout.string, output.stderr.string, output.exit_code, 'script', script) end end end end def run_task(target, task, arguments, options = {}) implementation = select_implementation(target, task) executable = implementation['path'] input_method = implementation['input_method'] extra_files = implementation['files'] with_connection(target) do |conn| conn.running_as(options['_run_as']) do stdin, output = nil command = [] execute_options = {} execute_options[:interpreter] = select_interpreter(executable, target.options['interpreters']) interpreter_debug = if execute_options[:interpreter] " using '#{execute_options[:interpreter]}' interpreter" end # log the arguments with sensitive data redacted, do NOT log unwrapped_arguments logger.debug("Running '#{executable}' with #{arguments.to_json}#{interpreter_debug}") # unpack any Sensitive data arguments = unwrap_sensitive_args(arguments) conn.with_tempdir do |dir| if extra_files.empty? task_dir = dir else # TODO: optimize upload of directories arguments['_installdir'] = dir.to_s task_dir = File.join(dir.to_s, task.tasks_dir) dir.mkdirs([task.tasks_dir] + extra_files.map { |file| File.dirname(file['name']) }) extra_files.each do |file| conn.copy_file(file['path'], File.join(dir.to_s, file['name'])) end end remote_task_path = conn.write_executable(task_dir, executable) if STDIN_METHODS.include?(input_method) stdin = JSON.dump(arguments) end if ENVIRONMENT_METHODS.include?(input_method) execute_options[:environment] = envify_params(arguments) end if conn.run_as && stdin # Inject interpreter in to wrapper script and remove from execute options wrapper = make_wrapper_stringio(remote_task_path, stdin, execute_options[:interpreter]) execute_options.delete(:interpreter) remote_wrapper_path = conn.write_executable(dir, wrapper, 'wrapper.sh') command << remote_wrapper_path else command << remote_task_path execute_options[:stdin] = stdin end dir.chown(conn.run_as) execute_options[:sudoable] = true if conn.run_as output = conn.execute(command, execute_options) end Bolt::Result.for_task(target, output.stdout.string, output.stderr.string, output.exit_code, task.name) end end end def make_wrapper_stringio(task_path, stdin, interpreter = nil) if interpreter StringIO.new(<<~SCRIPT) #!/bin/sh '#{interpreter}' '#{task_path}' <<'EOF' #{stdin} EOF SCRIPT else StringIO.new(<<~SCRIPT) #!/bin/sh '#{task_path}' <<'EOF' #{stdin} EOF SCRIPT end end end end end