diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3ef21f46..5e5f1ef7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -54,6 +54,24 @@ jobs: run: docker build . --file Dockerfile --tag octocatalog-diff:ruby${{matrix.ruby-version}} --build-arg RUBY_VERSION=${{matrix.ruby-version}}-stretch - name: Tests run: docker run -e PUPPET_VERSION -e PUPPET_VERSIONS -e RSPEC_TEST -e RUBOCOP_TEST -e ENFORCE_COVERAGE octocatalog-diff:ruby${{matrix.ruby-version}} /app/script/cibuild + + puppet-6-18-0: + env: + PUPPET_VERSIONS: "6.18.0" + PUPPET_VERSION: "6.18.0" + RUBOCOP_TEST: false + RSPEC_TEST: true + runs-on: ubuntu-latest + strategy: + matrix: + ruby-version: ["2.5", "2.6"] + steps: + - name: Checkout code + uses: actions/checkout@v1 + - name: Build container + run: docker build . --file Dockerfile --tag octocatalog-diff:ruby${{matrix.ruby-version}} --build-arg RUBY_VERSION=${{matrix.ruby-version}}-stretch + - name: Tests + run: docker run -e PUPPET_VERSION -e PUPPET_VERSIONS -e RSPEC_TEST -e RUBOCOP_TEST -e ENFORCE_COVERAGE octocatalog-diff:ruby${{matrix.ruby-version}} /app/script/cibuild - name: Rubocop and Coverage run: docker run -e PUPPET_VERSION -e PUPPET_VERSIONS -e RSPEC_TEST -e RUBOCOP_TEST -e ENFORCE_COVERAGE octocatalog-diff:ruby${{matrix.ruby-version}} /app/script/cibuild if: matrix.ruby-version == '2.6' diff --git a/config/puppet-versions.json b/config/puppet-versions.json index 50f1b40b..cc716835 100644 --- a/config/puppet-versions.json +++ b/config/puppet-versions.json @@ -19,5 +19,11 @@ "maximum_version": "5.99.99", "additional_gems": [ ] + }, + { + "minimum_version": "6.0.0", + "maximum_version": "6.99.99", + "additional_gems": [ + ] } ] diff --git a/lib/octocatalog-diff/catalog-util/command.rb b/lib/octocatalog-diff/catalog-util/command.rb index edb8c398..18c936e7 100644 --- a/lib/octocatalog-diff/catalog-util/command.rb +++ b/lib/octocatalog-diff/catalog-util/command.rb @@ -54,9 +54,21 @@ def setup raise ArgumentError, 'Puppet binary was not supplied' if @puppet_binary.nil? raise Errno::ENOENT, "Puppet binary #{@puppet_binary} doesn't exist" unless File.file?(@puppet_binary) + puppet_version = Gem::Version.new(@options[:puppet_version]) + # Node to compile cmdline = [] - cmdline.concat ['master', '--compile', Shellwords.escape(@node)] + # The 'puppet master --compile' command was removed in Puppet 6.x and replaced in + # Puppet 6.5 with an identically functioning 'puppet catalog compile' command. + # From versions 6.0.0 until 6.5.0 there is no compatible invocation method. + if puppet_version < Gem::Version.new('6.0.0') + cmdline.concat ['master', '--compile', Shellwords.escape(@node)] + elsif puppet_version < Gem::Version.new('6.5.0') + raise OctocatalogDiff::Errors::PuppetVersionError, + 'Octocatalog-diff does not support Puppet versions >= 6.0.0 and < 6.5.0' + else + cmdline.concat ['catalog', 'compile', Shellwords.escape(@node)] + end # storeconfigs? if @options[:storeconfigs] @@ -93,11 +105,21 @@ def setup # Some typical options for puppet cmdline.concat %w( --no-daemonize - --no-ca --color=false - --config_version="/bin/echo catalogscript" ) + if puppet_version < Gem::Version.new('6.0.0') + # This config_version parameter causes an error when run with Puppet 6.x. Per + # the Puppet configuration settings docs, the below config_version argument + # may not actually be valid, but for backward compatibility's sake we'll keep it + # for the versions it has always worked with: + cmdline.concat ['--config_version="/bin/echo catalogscript"'] + + # The 'ca' configuration option was removed in Puppet 6, but we'll keep it + # for older versions: + cmdline.concat ['--no-ca'] + end + # Add environment - only make this variable if preserve_environments is used. # If preserve_environments is not used, the hard-coded 'production' here matches # up with the symlink created under the temporary directory structure. diff --git a/lib/octocatalog-diff/catalog/computed.rb b/lib/octocatalog-diff/catalog/computed.rb index a4d00fd8..302c2d05 100644 --- a/lib/octocatalog-diff/catalog/computed.rb +++ b/lib/octocatalog-diff/catalog/computed.rb @@ -147,7 +147,8 @@ def puppet_command_obj puppet_binary: @puppet_binary, fact_file: @builddir.fact_file, dir: @builddir.tempdir, - enc: @builddir.enc + enc: @builddir.enc, + puppet_version: puppet_version ) OctocatalogDiff::CatalogUtil::Command.new(command_opts) end diff --git a/spec/octocatalog-diff/fixtures/repos/default/modules/sshkeys_core/LICENSE b/spec/octocatalog-diff/fixtures/repos/default/modules/sshkeys_core/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/spec/octocatalog-diff/fixtures/repos/default/modules/sshkeys_core/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/spec/octocatalog-diff/fixtures/repos/default/modules/sshkeys_core/README.md b/spec/octocatalog-diff/fixtures/repos/default/modules/sshkeys_core/README.md new file mode 100644 index 00000000..91d46c76 --- /dev/null +++ b/spec/octocatalog-diff/fixtures/repos/default/modules/sshkeys_core/README.md @@ -0,0 +1,4 @@ +This is enough of puppetlabs sshkeys_core to allow to sample catalogs in this +repository to compile on Puppet 6. For the most recent version of sshkeys_core please go to: + +https://github.com/puppetlabs/puppetlabs-sshkeys_core diff --git a/spec/octocatalog-diff/fixtures/repos/default/modules/sshkeys_core/lib/puppet/provider/ssh_authorized_key/parsed.rb b/spec/octocatalog-diff/fixtures/repos/default/modules/sshkeys_core/lib/puppet/provider/ssh_authorized_key/parsed.rb new file mode 100644 index 00000000..b10066eb --- /dev/null +++ b/spec/octocatalog-diff/fixtures/repos/default/modules/sshkeys_core/lib/puppet/provider/ssh_authorized_key/parsed.rb @@ -0,0 +1,138 @@ +require 'puppet/provider/parsedfile' + +Puppet::Type.type(:ssh_authorized_key).provide( + :parsed, + parent: Puppet::Provider::ParsedFile, + filetype: :flat, + default_target: '', +) do + desc 'Parse and generate authorized_keys files for SSH.' + + text_line :comment, match: %r{^\s*#} + text_line :blank, match: %r{^\s*$} + + record_line :parsed, + fields: ['options', 'type', 'key', 'name'], + optional: ['options'], + rts: %r{^\s+}, + match: Puppet::Type.type(:ssh_authorized_key).keyline_regex, + post_parse: proc { |h| + h[:name] = '' if h[:name] == :absent + h[:options] ||= [:absent] + h[:options] = Puppet::Type::Ssh_authorized_key::ProviderParsed.parse_options(h[:options]) if h[:options].is_a? String + }, + pre_gen: proc { |h| + # if this name was generated, don't write it back to disk + h[:name] = '' if h[:unnamed] + h[:options] = [] if h[:options].include?(:absent) + h[:options] = h[:options].join(',') + } + + record_line :key_v1, + fields: ['options', 'bits', 'exponent', 'modulus', 'name'], + optional: ['options'], + rts: %r{^\s+}, + match: %r{^(?:(.+) )?(\d+) (\d+) (\d+)(?: (.+))?$} + + def dir_perm + 0o700 + end + + def file_perm + 0o600 + end + + def group_writable_perm + 0o020 + end + + def group_writable?(path) + path.stat.mode & group_writable_perm != 0 + end + + def trusted_path + # return if the parent directory does not exist + return false unless Puppet::FileSystem.dir_exist?(target) + path = Puppet::FileSystem.pathname(target).dirname + until path.dirname.root? + path = path.realpath if path.symlink? + # do not trust if path is world or group writable + if path.stat.uid != Process.euid || path.world_writable? || group_writable?(path) + Puppet.debug('Path untrusted, will attempt to write as the target user') + return false + end + path = path.dirname + end + Puppet.debug('Path trusted, writing the file as the current user') + end + + def flush + raise Puppet::Error, 'Cannot write SSH authorized keys without user' unless @resource.should(:user) + raise Puppet::Error, "User '#{@resource.should(:user)}' does not exist" unless Puppet::Util.uid(@resource.should(:user)) + # ParsedFile usually calls backup_target much later in the flush process, + # but our SUID makes that fail to open filebucket files for writing. + # Fortunately, there's already logic to make sure it only ever happens once, + # so calling it here suppresses the later attempt by our superclass's flush method. + self.class.backup_target(target) + + # attempt to create the file as the specified user if we're not dropping privileges + if @resource[:drop_privileges] + Puppet::Util::SUIDManager.asuser(@resource.should(:user)) do + unless Puppet::FileSystem.exist?(dir = File.dirname(target)) + Puppet.debug "Creating #{dir} as #{@resource.should(:user)}" + Dir.mkdir(dir, dir_perm) + end + super + + File.chmod(file_perm, target) + end + # to avoid race conditions when handling permissions as a privileged user + # (CVE-2011-3870) we use the trusted_path method to ensure the entire + # directory structure is "safe" to write in + else + raise Puppet::Error, 'drop_privileges is false but the target path is not trusted' unless trusted_path + super + + uid = Puppet::Util.uid(@resource.should(:user)) + gid = Puppet::Util.gid(@resource.should(:user)) + File.open(target) do |target| + target.chown(uid, gid) + target.chmod(file_perm) + end + end + end + + # Parse sshv2 option strings, which is a comma-separated list of + # either key="values" elements or bare-word elements + def self.parse_options(options) + result = [] + scanner = StringScanner.new(options) + until scanner.eos? + scanner.skip(%r{[ \t]*}) + # scan a long option + out = scanner.scan(%r{[-a-z0-9A-Z_]+=\".*?[^\\]\"}) || scanner.scan(%r{[-a-z0-9A-Z_]+}) + + # found an unscannable token, let's abort + break unless out + + result << out + + # eat a comma + scanner.skip(%r{[ \t]*,[ \t]*}) + end + result + end + + def self.prefetch_hook(records) + name_index = 0 + records.each do |record| + next unless record[:record_type] == :parsed && record[:name].empty? + record[:unnamed] = true + # Generate a unique ID for unnamed keys, in case they need purging. + # If you change this, you have to keep + # Puppet::Type::User#unknown_keys_in_file in sync! (PUP-3357) + record[:name] = "#{record[:target]}:unnamed-#{name_index += 1}" + Puppet.debug("generating name for on-disk ssh_authorized_key #{record[:key]}: #{record[:name]}") + end + end +end diff --git a/spec/octocatalog-diff/fixtures/repos/default/modules/sshkeys_core/lib/puppet/provider/sshkey/parsed.rb b/spec/octocatalog-diff/fixtures/repos/default/modules/sshkeys_core/lib/puppet/provider/sshkey/parsed.rb new file mode 100644 index 00000000..3ed0873d --- /dev/null +++ b/spec/octocatalog-diff/fixtures/repos/default/modules/sshkeys_core/lib/puppet/provider/sshkey/parsed.rb @@ -0,0 +1,58 @@ +require 'puppet/provider/parsedfile' + +Puppet::Type.type(:sshkey).provide( + :parsed, + parent: Puppet::Provider::ParsedFile, + filetype: :flat, +) do + desc 'Parse and generate host-wide known hosts files for SSH.' + + text_line :comment, match: %r{^#} + text_line :blank, match: %r{^\s*$} + + record_line :parsed, fields: ['name', 'type', 'key'], + post_parse: proc { |hash| + names = hash[:name].split(',', -1) + hash[:name] = names.shift + hash[:host_aliases] = names + }, + pre_gen: proc { |hash| + if hash[:host_aliases] + hash[:name] = [hash[:name], hash[:host_aliases]].flatten.join(',') + hash.delete(:host_aliases) + end + } + + # Make sure to use mode 644 if ssh_known_hosts is newly created + def self.default_mode + 0o644 + end + + def title + "#{property_hash[:name]}@#{property_hash[:type]}" + end + + def self.default_target + case Facter.value(:operatingsystem) + when 'Darwin' + # Versions 10.11 and up use /etc/ssh/ssh_known_hosts + version = Facter.value(:macosx_productversion_major) + if version + if Puppet::Util::Package.versioncmp(version, '10.11') >= 0 + '/etc/ssh/ssh_known_hosts' + else + '/etc/ssh_known_hosts' + end + else + '/etc/ssh_known_hosts' + end + else + '/etc/ssh/ssh_known_hosts' + end + end + + def self.resource_for_record(record, resources) + name = "#{record[:name]}@#{record[:type]}" + resources[name] + end +end diff --git a/spec/octocatalog-diff/fixtures/repos/default/modules/sshkeys_core/lib/puppet/type/ssh_authorized_key.rb b/spec/octocatalog-diff/fixtures/repos/default/modules/sshkeys_core/lib/puppet/type/ssh_authorized_key.rb new file mode 100644 index 00000000..953b1a60 --- /dev/null +++ b/spec/octocatalog-diff/fixtures/repos/default/modules/sshkeys_core/lib/puppet/type/ssh_authorized_key.rb @@ -0,0 +1,172 @@ +require 'puppet/parameter/boolean' + +module Puppet + Type.newtype(:ssh_authorized_key) do + @doc = "Manages SSH authorized keys. Currently only type 2 keys are supported. + + In their native habitat, SSH keys usually appear as a single long line, in + the format ` `. This resource type requires you + to split that line into several attributes. Thus, a key that appears in + your `~/.ssh/id_rsa.pub` file like this... + + ssh-rsa AAAAB3Nza[...]qXfdaQ== nick@magpie.example.com + + ...would translate to the following resource: + + ssh_authorized_key { 'nick@magpie.example.com': + ensure => present, + user => 'nick', + type => 'ssh-rsa', + key => 'AAAAB3Nza[...]qXfdaQ==', + } + + To ensure that only the currently approved keys are present, you can purge + unmanaged SSH keys on a per-user basis. Do this with the `user` resource + type's `purge_ssh_keys` attribute: + + user { 'nick': + ensure => present, + purge_ssh_keys => true, + } + + This will remove any keys in `~/.ssh/authorized_keys` that aren't being + managed with `ssh_authorized_key` resources. See the documentation of the + `user` type for more details. + + **Autorequires:** If Puppet is managing the user account in which this + SSH key should be installed, the `ssh_authorized_key` resource will autorequire + that user." + + ensurable + + newparam(:name) do + desc "The SSH key comment. This can be anything, and doesn't need to match + the original comment from the `.pub` file. + + Due to internal limitations, this must be unique across all user accounts; + if you want to specify one key for multiple users, you must use a different + comment for each instance." + + isnamevar + end + + newparam(:drop_privileges, boolean: true, parent: Puppet::Parameter::Boolean) do + desc "Whether to drop privileges when writing the key file. This is + useful for creating files in paths not writable by the target user. Note + the possible security implications of managing file ownership and + permissions as a privileged user." + + defaultto true + end + + newproperty(:type) do + desc 'The encryption type used.' + + newvalues :'ssh-dss', :'ssh-rsa', :'ecdsa-sha2-nistp256', :'ecdsa-sha2-nistp384', :'ecdsa-sha2-nistp521', :'ssh-ed25519', + :'sk-ecdsa-sha2-nistp256@openssh.com', :'sk-ssh-ed25519@openssh.com' + + aliasvalue(:dsa, :'ssh-dss') + aliasvalue(:ed25519, :'ssh-ed25519') + aliasvalue(:rsa, :'ssh-rsa') + aliasvalue(:'ecdsa-sk', :'sk-ecdsa-sha2-nistp256@openssh.com') + aliasvalue(:'ed25519-sk', :'sk-ssh-ed25519@openssh.com') + end + + newproperty(:key) do + desc "The public key itself; generally a long string of hex characters. The `key` + attribute may not contain whitespace. + + Make sure to omit the following in this attribute (and specify them in + other attributes): + + * Key headers, such as 'ssh-rsa' --- put these in the `type` attribute. + * Key identifiers / comments, such as 'joe@joescomputer.local' --- put these in + the `name` attribute/resource title." + + validate do |value| + raise Puppet::Error, _('Key must not contain whitespace: %{value}') % { value: value } if value =~ %r{\s} + end + end + + newproperty(:user) do + desc "The user account in which the SSH key should be installed. The resource + will autorequire this user if it is being managed as a `user` resource." + end + + newproperty(:target) do + desc "The absolute filename in which to store the SSH key. This + property is optional and should be used only in cases where keys + are stored in a non-standard location, for instance when not in + `~user/.ssh/authorized_keys`. The parent directory must be present + if the target is in a privileged path." + + defaultto :absent + + def should + return super if defined?(@should) && @should[0] != :absent + + return nil unless resource[:user] + + begin + return File.expand_path("~#{resource[:user]}/.ssh/authorized_keys") + rescue + Puppet.debug 'The required user is not yet present on the system' + return nil + end + end + + def insync?(is) + is == should + end + end + + newproperty(:options, array_matching: :all) do + desc "Key options; see sshd(8) for possible values. Multiple values + should be specified as an array. For example, you could use the + following to install a SSH CA that allows someone with the + 'superuser' principal to log in as root + + ssh_authorized_key { 'Company SSH CA': + ensure => present, + user => 'root', + type => 'ssh-ed25519', + key => 'AAAAC3NzaC[...]CeA5kG', + options => [ 'cert-authority', 'principals=\"superuser\"' ], + }" + + defaultto { :absent } + + validate do |value| + unless value == :absent || value =~ %r{^[-a-z0-9A-Z_]+(?:=\".*?\")?$} + raise( + Puppet::Error, + _("Option %{value} is not valid. A single option must either be of the form 'option' or 'option=\"value\"'. Multiple options must be provided as an array") % { value: value }, + ) + end + end + end + + autorequire(:user) do + should(:user) if should(:user) + end + + validate do + # Go ahead if target attribute is defined + return if @parameters[:target].shouldorig[0] != :absent + + # Go ahead if user attribute is defined + return if @parameters.include?(:user) + + # If neither target nor user is defined, this is an error + raise Puppet::Error, _("Attribute 'user' or 'target' is mandatory") + end + + # regular expression suitable for use by a ParsedFile based provider + REGEX = %r{^(?:(.+)\s+)?(ssh-dss|ssh-ed25519|ssh-rsa|ecdsa-sha2-nistp256| + ecdsa-sha2-nistp384|ecdsa-sha2-nistp521|ecdsa-sk|ed25519-sk| + sk-ecdsa-sha2-nistp256@openssh.com|sk-ssh-ed25519@openssh.com)\s+([^ ]+)\s*(.*)$}x + def self.keyline_regex + REGEX + end + end +end diff --git a/spec/octocatalog-diff/fixtures/repos/default/modules/sshkeys_core/lib/puppet/type/sshkey.rb b/spec/octocatalog-diff/fixtures/repos/default/modules/sshkeys_core/lib/puppet/type/sshkey.rb new file mode 100644 index 00000000..d45c059c --- /dev/null +++ b/spec/octocatalog-diff/fixtures/repos/default/modules/sshkeys_core/lib/puppet/type/sshkey.rb @@ -0,0 +1,116 @@ +module Puppet + Type.newtype(:sshkey) do + @doc = "Installs and manages ssh host keys. By default, this type will + install keys into `/etc/ssh/ssh_known_hosts`. To manage ssh keys in a + different `known_hosts` file, such as a user's personal `known_hosts`, + pass its path to the `target` parameter. See the `ssh_authorized_key` + type to manage authorized keys." + + ensurable + + def name + "#{self[:name]}@#{self[:type]}" + end + + def self.parameters_to_include + [:name, :type] + end + + def self.title_patterns + [ + [ + %r{^(.*?)@(.*)$}, + [ + [:name], + [:type], + ], + ], + [ + %r{^([^@]+)$}, + [ + [:name], + ], + ], + ] + end + + newparam(:type) do + desc 'The encryption type used. Probably ssh-dss or ssh-rsa.' + + isnamevar + + newvalues :'ssh-dss', :'ssh-ed25519', :'ssh-rsa', :'ecdsa-sha2-nistp256', :'ecdsa-sha2-nistp384', :'ecdsa-sha2-nistp521', + :'sk-ecdsa-sha2-nistp256@openssh.com', :'sk-ssh-ed25519@openssh.com' + + aliasvalue(:dsa, :'ssh-dss') + aliasvalue(:ed25519, :'ssh-ed25519') + aliasvalue(:rsa, :'ssh-rsa') + aliasvalue(:'ecdsa-sk', :'sk-ecdsa-sha2-nistp256@openssh.com') + aliasvalue(:'ed25519-sk', :'sk-ssh-ed25519@openssh.com') + end + + newproperty(:key) do + desc "The key itself; generally a long string of uuencoded characters. The `key` + attribute may not contain whitespace. + + Make sure to omit the following in this attribute (and specify them in + other attributes): + + * Key headers, such as 'ssh-rsa' --- put these in the `type` attribute. + * Key identifiers / comments, such as 'joescomputer.local' --- put these in + the `name` attribute/resource title." + end + + # FIXME: This should automagically check for aliases to the hosts, just + # to see if we can automatically glean any aliases. + newproperty(:host_aliases) do + desc 'Any aliases the host might have. Multiple values must be + specified as an array.' + + attr_accessor :meta + + def insync?(is) + is == @should + end + + # We actually want to return the whole array here, not just the first + # value. + def should + defined?(@should) ? @should : nil + end + + validate do |value| + if value =~ %r{\s} + raise Puppet::Error, _('Aliases cannot include whitespace') + end + if value =~ %r{,} + raise Puppet::Error, _('Aliases must be provided as an array, not a comma-separated list') + end + end + end + + newparam(:name) do + desc 'The host name that the key is associated with.' + + isnamevar + + validate do |value| + raise Puppet::Error, _('Resourcename cannot include whitespaces') if value =~ %r{\s} + raise Puppet::Error, _('No comma in resourcename allowed. If you want to specify aliases use the host_aliases property') if value.include?(',') + end + end + + newproperty(:target) do + desc "The file in which to store the ssh key. Only used by + the `parsed` provider." + + defaultto do + if @resource.class.defaultprovider.ancestors.include?(Puppet::Provider::ParsedFile) + @resource.class.defaultprovider.default_target + else + nil + end + end + end + end +end diff --git a/spec/octocatalog-diff/integration/reference_validation_spec.rb b/spec/octocatalog-diff/integration/reference_validation_spec.rb index 54db7fbc..843dfe75 100644 --- a/spec/octocatalog-diff/integration/reference_validation_spec.rb +++ b/spec/octocatalog-diff/integration/reference_validation_spec.rb @@ -126,7 +126,7 @@ def self.catalog_contains_resource(result, type, title) expect(@result.exception).to be_a_kind_of(OctocatalogDiff::Errors::CatalogError) end - if OctocatalogDiff::Spec.is_puppet5? + if OctocatalogDiff::Spec.major_version >= 5 it 'should pass through the error messages from Puppet' do msg = @result.exception.message expect(msg).to match(/Error: Could not find resource 'Exec\[subscribe target\]' in parameter 'subscribe'/) @@ -159,7 +159,7 @@ def self.catalog_contains_resource(result, type, title) expect(@result.exception).to be_a_kind_of(OctocatalogDiff::Errors::CatalogError) end - if OctocatalogDiff::Spec.is_puppet5? + if OctocatalogDiff::Spec.major_version >= 5 it 'should pass through the error messages from Puppet' do msg = @result.exception.message expect(msg).to match(/Error: Could not find resource 'Exec\[before target\]' in parameter 'before'/) @@ -187,7 +187,7 @@ def self.catalog_contains_resource(result, type, title) expect(@result.exception).to be_a_kind_of(OctocatalogDiff::Errors::CatalogError) end - if OctocatalogDiff::Spec.is_puppet5? + if OctocatalogDiff::Spec.major_version >= 5 it 'should pass through the error messages from Puppet' do msg = @result.exception.message expect(msg).to match(/Error: Could not find resource 'Test::Foo::Bar\[notify target\]' in parameter 'notify'/) @@ -215,7 +215,7 @@ def self.catalog_contains_resource(result, type, title) expect(@result.exception).to be_a_kind_of(OctocatalogDiff::Errors::CatalogError) end - if OctocatalogDiff::Spec.is_puppet5? + if OctocatalogDiff::Spec.major_version >= 5 it 'should pass through the error messages from Puppet' do msg = @result.exception.message expect(msg).to match(/Error: Could not find resource 'Exec\[require target\]' in parameter 'require'/) @@ -239,7 +239,7 @@ def self.catalog_contains_resource(result, type, title) @result = OctocatalogDiff::Spec.reference_validation_catalog('broken-subscribe', %w(before notify require)) end - if OctocatalogDiff::Spec.is_puppet5? + if OctocatalogDiff::Spec.major_version >= 5 it 'should raise CatalogError' do expect(@result.exception).to be_a_kind_of(OctocatalogDiff::Errors::CatalogError) end @@ -295,7 +295,7 @@ def self.catalog_contains_resource(result, type, title) expect(@result.exception).to be_a_kind_of(OctocatalogDiff::Errors::CatalogError) end - if OctocatalogDiff::Spec.is_puppet5? + if OctocatalogDiff::Spec.major_version >= 5 it 'should pass through the error messages from Puppet' do msg = @result.exception.message expect(msg).to match(/Error: Could not find resource 'Exec\[before alias target\]' in parameter 'before'/) diff --git a/spec/octocatalog-diff/support/httparty/ssl_test_server.rb b/spec/octocatalog-diff/support/httparty/ssl_test_server.rb index ed05f1d7..98a36447 100644 --- a/spec/octocatalog-diff/support/httparty/ssl_test_server.rb +++ b/spec/octocatalog-diff/support/httparty/ssl_test_server.rb @@ -44,6 +44,10 @@ def self.thread_main(tmpfile, options) ctx = OpenSSL::SSL::SSLContext.new ctx.cert = OpenSSL::X509::Certificate.new(options[:cert]) ctx.key = OpenSSL::PKey::RSA.new(options[:rsa_key]) + if Gem::Version.new(OpenSSL::VERSION) >= Gem::Version.new('2.1.0') + ctx.min_version = OpenSSL::SSL::SSL3_VERSION + ctx.max_version = OpenSSL::SSL::TLS1_2_VERSION + end ctx.verify_mode = if options[:client_verify] OpenSSL::SSL::VERIFY_PEER | OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT else diff --git a/spec/octocatalog-diff/tests/catalog-util/command_spec.rb b/spec/octocatalog-diff/tests/catalog-util/command_spec.rb index 1471898f..872fdd1e 100644 --- a/spec/octocatalog-diff/tests/catalog-util/command_spec.rb +++ b/spec/octocatalog-diff/tests/catalog-util/command_spec.rb @@ -3,6 +3,7 @@ require_relative '../spec_helper' require 'fileutils' require OctocatalogDiff::Spec.require_path('/catalog-util/command') +require OctocatalogDiff::Spec.require_path('/errors') describe OctocatalogDiff::CatalogUtil::Command do describe '#initialize' do @@ -73,6 +74,23 @@ expect { testobj.puppet_command }.to raise_error(Errno::ENOENT, /Puppet binary.*doesn't exist/) end + it 'should use "master --compile" when Puppet version is 5.x' do + testobj = OctocatalogDiff::CatalogUtil::Command.new(@default_opts.merge(puppet_version: '5.5.20')) + result = testobj.puppet_command + expect(result).to match(/master --compile/) + end + + it 'should use "catalog compile" when Puppet version is 6.x' do + testobj = OctocatalogDiff::CatalogUtil::Command.new(@default_opts.merge(puppet_version: '6.5.0')) + result = testobj.puppet_command + expect(result).to match(/catalog compile/) + end + + it 'should raise an error when Puppet version is 6.4' do + testobj = OctocatalogDiff::CatalogUtil::Command.new(@default_opts.merge(puppet_version: '6.4.0')) + expect { testobj.puppet_command }.to raise_error(OctocatalogDiff::Errors::PuppetVersionError, /does not support/) + end + it 'should include --storeconfigs and --storeconfigs_backend when storeconfigs is enabled' do testobj = OctocatalogDiff::CatalogUtil::Command.new(@default_opts.merge(storeconfigs: true)) result = testobj.puppet_command @@ -134,6 +152,18 @@ expect(result).to match(/--facts_terminus=facter/) end + it 'should include config_version when Puppet version < 6' do + testobj = OctocatalogDiff::CatalogUtil::Command.new(@default_opts.merge(puppet_version: '5.5.20')) + result = testobj.puppet_command + expect(result).to match(%r{--config_version="/bin/echo catalogscript"}) + end + + it 'should not include config_version when Puppet version >= 6' do + testobj = OctocatalogDiff::CatalogUtil::Command.new(@default_opts.merge(puppet_version: '6.18.0')) + result = testobj.puppet_command + expect(result).not_to match(/--config_version=/) + end + it 'should raise error when invalid facts terminus is specified' do testobj = OctocatalogDiff::CatalogUtil::Command.new(@default_opts.merge(facts_terminus: 'chicken')) expect { testobj.puppet_command }.to raise_error(ArgumentError, /Unrecognized facts_terminus setting/) diff --git a/spec/octocatalog-diff/tests/catalog/computed_spec.rb b/spec/octocatalog-diff/tests/catalog/computed_spec.rb index d7d00389..acf8e90c 100644 --- a/spec/octocatalog-diff/tests/catalog/computed_spec.rb +++ b/spec/octocatalog-diff/tests/catalog/computed_spec.rb @@ -70,7 +70,11 @@ expect(@logger_str.string).to match(%r{Symlinked.*environments/production ->.*/repos/default}) expect(@logger_str.string).to match(/Installed hiera.yaml from/) expect(@logger_str.string).to match(/Installed fact file at/) - expect(@logger_str.string).to match(/puppet master --compile rspec-node.github.net/) + if OctocatalogDiff::Spec.major_version >= 6 + expect(@logger_str.string).to match(/puppet catalog compile rspec-node.github.net/) + else + expect(@logger_str.string).to match(/puppet master --compile rspec-node.github.net/) + end expect(@logger_str.string).to match(/Catalog succeeded on try 1/) end end @@ -150,7 +154,11 @@ it 'should have the correct log messages' do expect(@logger_str.string).to match(%r{Symlinked.*environments/production ->.*/repos/failing-catalog}) expect(@logger_str.string).to match(/Installed fact file at/) - expect(@logger_str.string).to match(/puppet master --compile rspec-node.github.net/) + if OctocatalogDiff::Spec.major_version >= 6 + expect(@logger_str.string).to match(/puppet catalog compile rspec-node.github.net/) + else + expect(@logger_str.string).to match(/puppet master --compile rspec-node.github.net/) + end expect(@logger_str.string).to match(/Catalog failed on try 1/) end end diff --git a/spec/octocatalog-diff/tests/spec_helper.rb b/spec/octocatalog-diff/tests/spec_helper.rb index 01082989..b5f13c2b 100644 --- a/spec/octocatalog-diff/tests/spec_helper.rb +++ b/spec/octocatalog-diff/tests/spec_helper.rb @@ -288,9 +288,9 @@ def self.mock_puppetdb_fact_response(hostname) facts_in['values'].keys.map { |k| { 'name' => k, 'value' => facts_in['values'][k] } }.to_json end - # Determine if puppet version is Puppet 5 or not - def self.is_puppet5? - puppet_version && puppet_version >= '5.0.0' + # Helper functions to determine which major version of Puppet we are working with: + def self.major_version + puppet_version && puppet_version.split('.')[0].to_i end end end diff --git a/vendor/cache/concurrent-ruby-1.1.7.gem b/vendor/cache/concurrent-ruby-1.1.7.gem new file mode 100644 index 00000000..ae9b3702 Binary files /dev/null and b/vendor/cache/concurrent-ruby-1.1.7.gem differ diff --git a/vendor/cache/deep_merge-1.2.1.gem b/vendor/cache/deep_merge-1.2.1.gem new file mode 100644 index 00000000..ee9d198f Binary files /dev/null and b/vendor/cache/deep_merge-1.2.1.gem differ diff --git a/vendor/cache/hocon-1.3.1.gem b/vendor/cache/hocon-1.3.1.gem new file mode 100644 index 00000000..e474ff66 Binary files /dev/null and b/vendor/cache/hocon-1.3.1.gem differ diff --git a/vendor/cache/httpclient-2.8.3.gem b/vendor/cache/httpclient-2.8.3.gem new file mode 100644 index 00000000..9c19ad46 Binary files /dev/null and b/vendor/cache/httpclient-2.8.3.gem differ diff --git a/vendor/cache/puppet-6.18.0.gem b/vendor/cache/puppet-6.18.0.gem new file mode 100644 index 00000000..c3bcc148 Binary files /dev/null and b/vendor/cache/puppet-6.18.0.gem differ diff --git a/vendor/cache/puppet-resource_api-1.8.13.gem b/vendor/cache/puppet-resource_api-1.8.13.gem new file mode 100644 index 00000000..13f8c5ab Binary files /dev/null and b/vendor/cache/puppet-resource_api-1.8.13.gem differ diff --git a/vendor/cache/semantic_puppet-1.0.2.gem b/vendor/cache/semantic_puppet-1.0.2.gem new file mode 100644 index 00000000..305b4e61 Binary files /dev/null and b/vendor/cache/semantic_puppet-1.0.2.gem differ