Files
puppet-openstacklib/lib/puppet/provider/openstack.rb
Tobias Urdin 16ce2f30de Prevent --password from leaking in failed command output
There is cases when a command times out or when it fails
that we and Puppet [1] will output the raw command that
was executed.

For a user create command that output contains the
--password argument passed down to openstack CLI which
causes sensitive passwords to be leaked into log files
of the system executing Puppet, these can then be shipped
of from the system into a remote syslog and still be in
plain text.

This tries to use Ruby gsub with a regular expression
matching the two cases and instead output [redacted secret]
the same way we do with config provider.

[1] https://github.com/puppetlabs/puppet/blob/main/lib/puppet/util/execution.rb#L286

Change-Id: I4cad8f88fc7b67bb7aa4330832fc47bac41ae9df
2021-09-23 17:03:59 +00:00

164 lines
5.4 KiB
Ruby

require 'csv'
require 'puppet'
require 'timeout'
class Puppet::Error::OpenstackAuthInputError < Puppet::Error
end
class Puppet::Error::OpenstackUnauthorizedError < Puppet::Error
end
class Puppet::Provider::Openstack < Puppet::Provider
initvars # so commands will work
commands :openstack_command => 'openstack'
@@no_retry_actions = %w(create remove delete)
@@command_timeout = 40
# Fails on the 5th retry for a max of 212s (~3.5min) before total
# failure.
@@request_timeout = 170
@@retry_sleep = 3
class << self
[:no_retry_actions, :request_timeout, :retry_sleep].each do |m|
define_method m do
self.class_variable_get("@@#{m}")
end
define_method :"#{m}=" do |value|
self.class_variable_set("@@#{m}", value)
end
end
end
# timeout the openstack command
# after this number of seconds
# retry the command until the request_timeout,
# unless it's a no_retry_actions call
def self.command_timeout(action=nil)
# give no_retry actions the full time limit to finish
return self.request_timeout() if no_retry_actions.include? action
self.class_variable_get("@@command_timeout")
end
# redact sensitive information in exception and raise
def self.redact_and_raise(e)
new_message = e.message.gsub(/\-\-password\ [\w]+/, "--password [redacted secret]")
raise e.class, new_message
end
# with command_timeout
def self.openstack(*args)
begin
action = args[1]
Timeout.timeout(command_timeout(action)) do
execute([command(:openstack_command)] + args, override_locale: false, failonfail: true, combine: true)
end
rescue Timeout::Error
e = Puppet::ExecutionFailure.new "Command: 'openstack #{args.inspect}' has been running for more than #{command_timeout(action)} seconds"
redact_and_raise(e)
rescue Puppet::ExecutionFailure => e
redact_and_raise(e)
end
end
# get the current timestamp
def self.current_time
Time.now.to_i
end
def self.request_without_retry(&block)
previous_timeout = self.request_timeout
rc = nil
if block_given?
self.request_timeout = 0
rc = yield
end
ensure
self.request_timeout = previous_timeout
rc
end
# Returns an array of hashes, where the keys are the downcased CSV headers
# with underscores instead of spaces
#
# @param options [Hash] Other options
# @options :no_retry_exception_msgs [Array<Regexp>,Regexp] exception without retries
def self.request(service, action, properties, credentials=nil, options={})
env = credentials ? credentials.to_env : {}
no_retry = options[:no_retry_exception_msgs]
Puppet::Util.withenv(env) do
rv = nil
end_time = current_time + request_timeout
start_time = current_time
retry_count = 0
loop do
begin
if action == 'list'
# shell output is:
# ID,Name,Description,Enabled
response = openstack(service, action, '--quiet', '--format', 'csv', properties)
response = parse_csv(response)
keys = response.delete_at(0)
rv = response.collect do |line|
hash = {}
keys.each_index do |index|
key = keys[index].downcase.gsub(/ /, '_').to_sym
hash[key] = line[index]
end
hash
end
elsif action == 'show' or action == 'create'
rv = {}
# shell output is:
# name="value1"
# id="value2"
# description="value3"
openstack(service, action, '--format', 'shell', properties).split("\n").each do |line|
# key is everything before the first "="
key, val = line.split('=', 2)
next unless val # Ignore warnings
# value is everything after the first "=", with leading and trailing double quotes stripped
val = val.gsub(/\A"|"\Z/, '')
rv[key.downcase.to_sym] = val
end
else
rv = openstack(service, action, properties)
end
break
rescue Puppet::ExecutionFailure => exception
raise Puppet::Error::OpenstackUnauthorizedError, 'Could not authenticate' if exception.message =~ /HTTP 40[13]/
raise Puppet::Error::OpenstackUnauthorizedError, 'Could not authenticate' if exception.message.match(/Missing value \S* required for auth plugin/)
if current_time > end_time
error_message = exception.message
error_message += " (tried #{retry_count}, for a total of #{end_time - start_time } seconds)"
raise(Puppet::ExecutionFailure, error_message)
end
raise exception if no_retry_actions.include? action
if no_retry
no_retry = [no_retry] unless no_retry.is_a?(Array)
no_retry.each do |nr|
raise exception if exception.message.match(nr)
end
end
debug "Non-fatal error: '#{exception.message}'. Retrying for #{end_time - current_time} more seconds"
sleep retry_sleep
retry_count += 1
retry
end
end
return rv
end
end
private
def self.parse_csv(text)
# Ignore warnings - assume legitimate output starts with a double quoted
# string. Errors will be caught and raised prior to this
text = text.split("\n").drop_while { |line| line !~ /^\".*\"/ }.join("\n")
return CSV.parse(text + "\n")
end
end