diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ea492ae..b4f47e8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,47 +1,75 @@ name: CI on: pull_request jobs: setup_matrix: name: 'Setup Test Matrix' runs-on: ubuntu-latest outputs: beaker_setfiles: ${{ steps.get-outputs.outputs.beaker_setfiles }} puppet_major_versions: ${{ steps.get-outputs.outputs.puppet_major_versions }} puppet_unit_test_matrix: ${{ steps.get-outputs.outputs.puppet_unit_test_matrix }} env: BUNDLE_WITHOUT: development:test:release steps: - uses: actions/checkout@v2 - name: Setup ruby uses: ruby/setup-ruby@v1 with: ruby-version: '2.7' bundler-cache: true - name: Run rake validate run: bundle exec rake validate - name: Setup Test Matrix id: get-outputs run: bundle exec metadata2gha --use-fqdn --pidfile-workaround false unit: needs: setup_matrix runs-on: ubuntu-latest strategy: fail-fast: false matrix: include: ${{fromJson(needs.setup_matrix.outputs.puppet_unit_test_matrix)}} env: BUNDLE_WITHOUT: development:system_tests:release PUPPET_VERSION: "~> ${{ matrix.puppet }}.0" name: Puppet ${{ matrix.puppet }} (Ruby ${{ matrix.ruby }}) steps: - uses: actions/checkout@v2 - name: Setup ruby uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} bundler-cache: true - name: Run tests run: bundle exec rake + + acceptance: + needs: setup_matrix + runs-on: ubuntu-latest + env: + BUNDLE_WITHOUT: development:test:release + strategy: + fail-fast: false + matrix: + setfile: ${{fromJson(needs.setup_matrix.outputs.beaker_setfiles)}} + puppet: ${{fromJson(needs.setup_matrix.outputs.puppet_major_versions)}} + name: ${{ matrix.puppet.name }} - ${{ matrix.setfile.name }} + steps: + - name: Enable IPv6 on docker + run: | + echo '{"ipv6":true,"fixed-cidr-v6":"2001:db8:1::/64"}' | sudo tee /etc/docker/daemon.json + sudo service docker restart + - uses: actions/checkout@v2 + - name: Setup ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '2.7' + bundler-cache: true + - name: Run tests + run: bundle exec rake beaker + env: + BEAKER_PUPPET_COLLECTION: ${{ matrix.puppet.collection }} + BEAKER_setfile: ${{ matrix.setfile.value }} diff --git a/.sync.yml b/.sync.yml index ff7cdcb..3f0028e 100644 --- a/.sync.yml +++ b/.sync.yml @@ -1,5 +1,5 @@ --- -.travis.yml: - secure: "S8pxtUQSCxezeHuMTOa2nmbOC1Ln37evcDBd6rcAPse4DhrwzdJ7Ijme2tE+cPcdyYgO66jL0wWUBNia+10vOoYs1mCMj3mUq2bgySRhML0CQxGIvKYu8R6PbijljSK7LYplD1THQRgFLyHQ+f4Mh7+8yFpjXSmxuAQMdCPfPv8=" spec/spec_helper.rb: unmanaged: true +spec/spec_helper_acceptance.rb: + unmanaged: false diff --git a/lib/puppet/provider/archive/curl.rb b/lib/puppet/provider/archive/curl.rb index 9f2f0fd..707fcca 100644 --- a/lib/puppet/provider/archive/curl.rb +++ b/lib/puppet/provider/archive/curl.rb @@ -1,71 +1,77 @@ require 'uri' require 'tempfile' Puppet::Type.type(:archive).provide(:curl, parent: :ruby) do commands curl: 'curl' defaultfor feature: :posix def curl_params(params) if resource[:username] - create_netrcfile - params += ['--netrc-file', @netrc_file.path] + if resource[:username] =~ %r{\s} || resource[:password] =~ %r{\s} + Puppet.warning('Username or password contains a space. Unable to use netrc file to hide credentials') + account = [resource[:username], resource[:password]].compact.join(':') + params += optional_switch(account, ['--user', '%s']) + else + create_netrcfile + params += ['--netrc-file', @netrc_file.path] + end end params += optional_switch(resource[:proxy_server], ['--proxy', '%s']) params += ['--insecure'] if resource[:allow_insecure] params += resource[:download_options] if resource[:download_options] params += optional_switch(resource[:cookie], ['--cookie', '%s']) params += optional_switch(resource[:cacert_file], ['--cacert', '%s']) params end def create_netrcfile @netrc_file = Tempfile.new('.puppet_archive_curl') machine = URI.parse(resource[:source]).host @netrc_file.write("machine #{machine}\nlogin #{resource[:username]}\npassword #{resource[:password]}\n") @netrc_file.close end def delete_netrcfile return if @netrc_file.nil? @netrc_file.unlink @netrc_file = nil end def download(filepath) params = curl_params( [ resource[:source], '-o', filepath, '-fsSLg', '--max-redirs', 5 ] ) begin curl(params) ensure delete_netrcfile end end def remote_checksum params = curl_params( [ resource[:checksum_url], '-fsSLg', '--max-redirs', 5 ] ) begin curl(params)[%r{\b[\da-f]{32,128}\b}i] ensure delete_netrcfile end end end diff --git a/lib/puppet/provider/archive/wget.rb b/lib/puppet/provider/archive/wget.rb index cf274e8..6de34af 100644 --- a/lib/puppet/provider/archive/wget.rb +++ b/lib/puppet/provider/archive/wget.rb @@ -1,44 +1,46 @@ Puppet::Type.type(:archive).provide(:wget, parent: :ruby) do commands wget: 'wget' def wget_params(params) - params += optional_switch(resource[:username], ['--user=%s']) - params += optional_switch(resource[:password], ['--password=%s']) + username = Shellwords.shellescape(resource[:username]) if resource[:username] + password = Shellwords.shellescape(resource[:password]) if resource[:password] + params += optional_switch(username, ['--user=%s']) + params += optional_switch(password, ['--password=%s']) params += optional_switch(resource[:cookie], ['--header="Cookie: %s"']) params += optional_switch(resource[:proxy_server], ['-e use_proxy=yes', "-e #{resource[:proxy_type]}_proxy=#{resource[:proxy_server]}"]) params += ['--no-check-certificate'] if resource[:allow_insecure] params += resource[:download_options] if resource[:download_options] params += optional_switch(resource[:cacert_file], ['--ca-certificate=%s']) params end def download(filepath) params = wget_params( [ Shellwords.shellescape(resource[:source]), '-O', filepath, '--max-redirect=5' ] ) # NOTE: # Do NOT use wget(params) until https://tickets.puppetlabs.com/browse/PUP-6066 is resolved. command = "wget #{params.join(' ')}" Puppet::Util::Execution.execute(command) end def remote_checksum params = wget_params( [ '-qO-', Shellwords.shellescape(resource[:checksum_url]), '--max-redirect=5' ] ) command = "wget #{params.join(' ')}" Puppet::Util::Execution.execute(command)[%r{\b[\da-f]{32,128}\b}i] end end diff --git a/spec/acceptance/authentication_spec.rb b/spec/acceptance/authentication_spec.rb new file mode 100644 index 0000000..cf7754f --- /dev/null +++ b/spec/acceptance/authentication_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper_acceptance' +require 'uri' + +context 'authenticated download' do + let(:source) do + URI.escape("http://httpbin.org/basic-auth/user/#{password}") + end + let(:pp) do + <<-EOS + archive { '/tmp/testfile': + source => '#{source.gsub("'") { "\\'" }}', + username => 'user', + password => '#{password.gsub("'") { "\\'" }}', + provider => #{provider}, + } + EOS + end + + %w[curl wget ruby].each do |provider| + context "with provider #{provider}" do + let(:provider) { provider } + + [ + 'hunter2', + 'pass word with spaces', + 'y^%88_', + "passwordwithsinglequote'!", + ].each do |password| + context "with password '#{password}'" do + let(:password) { password } + + it 'applies idempotently with no errors' do + shell('/bin/rm -f /tmp/testfile') + apply_manifest(pp, catch_failures: true) + apply_manifest(pp, catch_changes: true) + end + + describe file('/tmp/testfile') do + it { is_expected.to be_file } + its(:content_as_json) { is_expected.to include('authenticated' => true, 'user' => 'user') } + end + end + end + end + end +end diff --git a/spec/spec_helper_acceptance.rb b/spec/spec_helper_acceptance.rb new file mode 100644 index 0000000..bec34fd --- /dev/null +++ b/spec/spec_helper_acceptance.rb @@ -0,0 +1,6 @@ +# This file is completely managed via modulesync +require 'voxpupuli/acceptance/spec_helper_acceptance' + +configure_beaker + +Dir['./spec/support/acceptance/**/*.rb'].sort.each { |f| require f } diff --git a/spec/unit/puppet/provider/archive/curl_spec.rb b/spec/unit/puppet/provider/archive/curl_spec.rb index d7754c0..d6d01d2 100644 --- a/spec/unit/puppet/provider/archive/curl_spec.rb +++ b/spec/unit/puppet/provider/archive/curl_spec.rb @@ -1,188 +1,204 @@ require 'spec_helper' curl_provider = Puppet::Type.type(:archive).provider(:curl) RSpec.describe curl_provider do it_behaves_like 'an archive provider', curl_provider describe '#download' do let(:name) { '/tmp/example.zip' } let(:resource) { Puppet::Type::Archive.new(resource_properties) } let(:provider) { curl_provider.new(resource) } let(:tempfile) { Tempfile.new('mock') } let(:default_options) do [ 'http://home.lan/example.zip', '-o', String, '-fsSLg', '--max-redirs', 5 ] end before do allow(FileUtils).to receive(:mv) allow(provider).to receive(:curl) allow(Tempfile).to receive(:new).with('.puppet_archive_curl').and_return(tempfile) end context 'no extra properties specified' do let(:resource_properties) do { name: name, source: 'http://home.lan/example.zip' } end it 'calls curl with input, output and --max-redirects=5' do provider.download(name) expect(provider).to have_received(:curl).with(default_options) end end context 'username and password specified' do let(:resource_properties) do { name: name, source: 'http://home.lan/example.zip', username: 'foo', password: 'bar' } end it 'populates temp netrc file with credentials' do allow(provider).to receive(:delete_netrcfile) # Don't delete the file or we won't be able to examine its contents. provider.download(name) nettc_content = File.open(tempfile.path).read expect(nettc_content).to eq("machine home.lan\nlogin foo\npassword bar\n") end it 'calls curl with default options and path to netrc file' do netrc_filepath = tempfile.path provider.download(name) expect(provider).to have_received(:curl).with(default_options << '--netrc-file' << netrc_filepath) end it 'deletes netrc file' do netrc_filepath = tempfile.path provider.download(name) expect(File.exist?(netrc_filepath)).to eq(false) end + + context 'with password containing space' do + let(:resource_properties) do + { + name: name, + source: 'http://home.lan/example.zip', + username: 'foo', + password: 'b ar' + } + end + + it 'calls curl with default options and username and password on command line' do + provider.download(name) + expect(provider).to have_received(:curl).with(default_options << '--user' << 'foo:b ar') + end + end end context 'allow_insecure true' do let(:resource_properties) do { name: name, source: 'http://home.lan/example.zip', allow_insecure: true } end it 'calls curl with default options and --insecure' do provider.download(name) expect(provider).to have_received(:curl).with(default_options << '--insecure') end end context 'cookie specified' do let(:resource_properties) do { name: name, source: 'http://home.lan/example.zip', cookie: 'foo=bar' } end it 'calls curl with default options cookie' do provider.download(name) expect(provider).to have_received(:curl).with(default_options << '--cookie' << 'foo=bar') end end context 'using proxy' do let(:resource_properties) do { name: name, source: 'http://home.lan/example.zip', proxy_server: 'https://home.lan:8080' } end it 'calls curl with proxy' do provider.download(name) expect(provider).to have_received(:curl).with(default_options << '--proxy' << 'https://home.lan:8080') end end describe '#checksum' do subject { provider.checksum } let(:url) { nil } let(:resource_properties) do { name: name, source: 'http://home.lan/example.zip' } end before do resource[:checksum_url] = url if url end context 'with a url' do let(:curl_params) do [ 'http://example.com/checksum', '-fsSLg', '--max-redirs', 5 ] end let(:url) { 'http://example.com/checksum' } context 'responds with hash' do let(:remote_hash) { 'a0c38e1aeb175201b0dacd65e2f37e187657050a' } it 'parses checksum value' do allow(provider).to receive(:curl).with(curl_params).and_return("a0c38e1aeb175201b0dacd65e2f37e187657050a README.md\n") expect(provider.checksum).to eq('a0c38e1aeb175201b0dacd65e2f37e187657050a') end end end end describe 'custom options' do let(:resource_properties) do { name: name, source: 'http://home.lan/example.zip', download_options: ['--tlsv1'] } end it 'calls curl with custom tls options' do provider.download(name) expect(provider).to have_received(:curl).with(default_options << '--tlsv1') end end context 'using cacert_file' do let(:resource_properties) do { name: name, source: 'http://home.lan/example.zip', cacert_file: '/custom-ca-bundle.pem' } end it 'calls curl with --cacert' do provider.download(name) expect(provider).to have_received(:curl).with(default_options << '--cacert' << '/custom-ca-bundle.pem') end end end end