
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
214 lines
8.8 KiB
Ruby
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
|