Files
puppet-openstacklib/spec/unit/provider/openstack_spec.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

214 lines
8.8 KiB
Ruby

require 'puppet'
require 'spec_helper'
require 'puppet/provider/openstack'
describe Puppet::Provider::Openstack do
before(:each) do
ENV['OS_USERNAME'] = nil
ENV['OS_PASSWORD'] = nil
ENV['OS_PROJECT_NAME'] = nil
ENV['OS_AUTH_URL'] = nil
end
let(:type) do
Puppet::Type.newtype(:test_resource) do
newparam(:name, :namevar => true)
newparam(:log_file)
end
end
let(:credentials) do
credentials = mock('credentials')
credentials.stubs(:to_env).returns({
'OS_USERNAME' => 'user',
'OS_PASSWORD' => 'password',
'OS_PROJECT_NAME' => 'project',
'OS_AUTH_URL' => 'http://url',
})
credentials
end
let(:list_data) do
<<-eos
"ID","Name","Description","Enabled"
"1cb05cfed7c24279be884ba4f6520262","test","Test tenant",True
eos
end
let(:show_data) do
<<-eos
description="Test tenant"
enabled="True"
id="1cb05cfed7c24279be884ba4f6520262"
name="test"
eos
end
describe '#request' do
let(:resource_attrs) do
{
:name => 'stubresource',
}
end
let(:provider) do
Puppet::Provider::Openstack.new(type.new(resource_attrs))
end
it 'makes a successful list request' do
provider.class.expects(:openstack)
.with('project', 'list', '--quiet', '--format', 'csv', ['--long'])
.returns list_data
response = Puppet::Provider::Openstack.request('project', 'list', ['--long'])
expect(response.first[:description]).to eq 'Test tenant'
end
it 'makes a successful show request' do
provider.class.expects(:openstack)
.with('project', 'show', '--format', 'shell', ['1cb05cfed7c24279be884ba4f6520262'])
.returns show_data
response = Puppet::Provider::Openstack.request('project', 'show', ['1cb05cfed7c24279be884ba4f6520262'])
expect(response[:description]).to eq 'Test tenant'
end
it 'makes a successful set request' do
provider.class.expects(:openstack)
.with('project', 'set', ['--name', 'new name', '1cb05cfed7c24279be884ba4f6520262'])
.returns ''
response = Puppet::Provider::Openstack.request('project', 'set', ['--name', 'new name', '1cb05cfed7c24279be884ba4f6520262'])
expect(response).to eq ''
end
it 'uses provided credentials' do
Puppet::Util.expects(:withenv).with(credentials.to_env)
Puppet::Provider::Openstack.request('project', 'list', ['--long'], credentials)
end
it 'redacts sensitive data from an exception message' do
e1 = Puppet::ExecutionFailure.new "Execution of 'openstack user create --format shell hello --password world --enable --email foo@example.com --domain Default' returned 1: command failed"
expect do
Puppet::Provider::Openstack.redact_and_raise(e1)
end.to raise_error(Puppet::ExecutionFailure, /Execution of \'openstack user create --format shell hello --password \[redacted secret\] --enable --email foo@example.com --domain Default/)
e2 = Puppet::ExecutionFailure.new "Execution of 'openstack user create --format shell hello --password world' returned 1: command failed"
expect do
Puppet::Provider::Openstack.redact_and_raise(e2)
end.to raise_error(Puppet::ExecutionFailure, /Execution of \'openstack user create --format shell hello --password \[redacted secret\]\' returned/)
end
it 'redacts password in execution output on exception' do
provider.class.stubs(:execute)
.raises(Puppet::ExecutionFailure, "Execution of '/usr/bin/openstack user create --format shell hello --password world --enable --email foo@example.com --domain Default' returned 1: command failed")
expect do
Puppet::Provider::Openstack.request('user', 'create', ['hello', '--password', 'world', '--enable', '--email', 'foo@example.com', '--domain', 'Default'])
end.to raise_error Puppet::ExecutionFailure, "Execution of '/usr/bin/openstack user create --format shell hello --password [redacted secret] --enable --email foo@example.com --domain Default' returned 1: command failed"
end
context 'on connection errors' do
it 'retries the failed command' do
provider.class.stubs(:openstack)
.with('project', 'list', '--quiet', '--format', 'csv', ['--long'])
.raises(Puppet::ExecutionFailure, 'Unable to establish connection')
.then
.returns list_data
provider.class.expects(:sleep).with(3).returns(nil)
response = Puppet::Provider::Openstack.request('project', 'list', ['--long'])
expect(response.first[:description]).to eq 'Test tenant'
end
it 'fails after the timeout and redacts' do
provider.class.expects(:execute)
.raises(Puppet::ExecutionFailure, "Execution of 'openstack user create foo --password secret' returned 1: command failed")
.times(3)
provider.class.stubs(:sleep)
provider.class.stubs(:current_time)
.returns(0, 10, 10, 20, 20, 200, 200)
expect do
Puppet::Provider::Openstack.request('project', 'list', ['--long'])
end.to raise_error Puppet::ExecutionFailure, /Execution of \'openstack user create foo --password \[redacted secret\]\' returned 1/
end
it 'fails after the timeout' do
provider.class.expects(:openstack)
.with('project', 'list', '--quiet', '--format', 'csv', ['--long'])
.raises(Puppet::ExecutionFailure, 'Unable to establish connection')
.times(3)
provider.class.stubs(:sleep)
provider.class.stubs(:current_time)
.returns(0, 10, 10, 20, 20, 200, 200)
expect do
Puppet::Provider::Openstack.request('project', 'list', ['--long'])
end.to raise_error Puppet::ExecutionFailure, /Unable to establish connection/
end
it 'does not retry non-idempotent commands' do
provider.class.expects(:openstack)
.with('project', 'create', '--format', 'shell', ['--quiet'])
.raises(Puppet::ExecutionFailure, 'Unable to establish connection')
.then
.returns list_data
provider.class.expects(:sleep).never
expect do
Puppet::Provider::Openstack.request('project', 'create', ['--quiet'])
end.to raise_error Puppet::ExecutionFailure, /Unable to establish connection/
end
end
context 'catch unauthorized errors' do
it 'should raise an error with non-existent user' do
ENV['OS_USERNAME'] = 'test'
ENV['OS_PASSWORD'] = 'abc123'
ENV['OS_PROJECT_NAME'] = 'test'
ENV['OS_AUTH_URL'] = 'http://127.0.0.1:5000'
provider.class.stubs(:openstack)
.with('project', 'list', '--quiet', '--format', 'csv', ['--long'])
.raises(Puppet::ExecutionFailure, 'Could not find user: test (HTTP 401)')
expect do
Puppet::Provider::Openstack.request('project', 'list', ['--long'])
end.to raise_error(Puppet::Error::OpenstackUnauthorizedError, /Could not authenticate/)
end
it 'should raise an error with not authorized to perform' do
provider.class.stubs(:openstack)
.with('role', 'list', '--quiet', '--format', 'csv', ['--long'])
.raises(Puppet::ExecutionFailure, 'You are not authorized to perform the requested action: identity:list_grants (HTTP 403)')
expect do
Puppet::Provider::Openstack.request('role', 'list', ['--long'])
end.to raise_error(Puppet::Error::OpenstackUnauthorizedError, /Could not authenticate/)
end
end
end
describe 'parse_csv' do
context 'with mixed stderr' do
text = "ERROR: Testing\n\"field\",\"test\",1,2,3\n"
csv = Puppet::Provider::Openstack.parse_csv(text)
it 'should ignore non-CSV text at the beginning of the input' do
expect(csv).to be_kind_of(Array)
expect(csv[0]).to match_array(%w(field test 1 2 3))
expect(csv.size).to eq(1)
end
end
context 'with \r\n line endings' do
text = "ERROR: Testing\r\n\"field\",\"test\",1,2,3\r\n"
csv = Puppet::Provider::Openstack.parse_csv(text)
it 'ignore the carriage returns' do
expect(csv).to be_kind_of(Array)
expect(csv[0]).to match_array(%w(field test 1 2 3))
expect(csv.size).to eq(1)
end
end
context 'with embedded newlines' do
text = "ERROR: Testing\n\"field\",\"te\nst\",1,2,3\n"
csv = Puppet::Provider::Openstack.parse_csv(text)
it 'should parse correctly' do
expect(csv).to be_kind_of(Array)
expect(csv[0]).to match_array(['field', "te\nst", '1', '2', '3'])
expect(csv.size).to eq(1)
end
end
end
end