diff options
37 files changed, 2380 insertions, 0 deletions
diff --git a/.fixtures.yml b/.fixtures.yml new file mode 100644 index 0000000..2296adb --- /dev/null +++ b/.fixtures.yml @@ -0,0 +1,6 @@ +# This file can be used to install module dependencies for unit testing +# See https://github.com/puppetlabs/puppetlabs_spec_helper#using-fixtures for details +--- +fixtures: + forge_modules: +# stdlib: "puppetlabs/stdlib" diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..543dd6a --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +*.rb eol=lf +*.erb eol=lf +*.pp eol=lf +*.sh eol=lf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..49bc2a4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +.*.sw[op] +.metadata +.yardoc +.yardwarns +*.iml +/.bundle/ +/.idea/ +/.vagrant/ +/coverage/ +/bin/ +/doc/ +/Gemfile.local +/Gemfile.lock +/junit/ +/log/ +/pkg/ +/spec/fixtures/manifests/ +/spec/fixtures/modules/ +/tmp/ +/vendor/ +/convert_report.txt +/update_report.txt +.DS_Store diff --git a/.pdkignore b/.pdkignore new file mode 100644 index 0000000..49bc2a4 --- /dev/null +++ b/.pdkignore @@ -0,0 +1,23 @@ +.*.sw[op] +.metadata +.yardoc +.yardwarns +*.iml +/.bundle/ +/.idea/ +/.vagrant/ +/coverage/ +/bin/ +/doc/ +/Gemfile.local +/Gemfile.lock +/junit/ +/log/ +/pkg/ +/spec/fixtures/manifests/ +/spec/fixtures/modules/ +/tmp/ +/vendor/ +/convert_report.txt +/update_report.txt +.DS_Store @@ -0,0 +1,2 @@ +--color +--format documentation diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..7ed6225 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,115 @@ +--- +require: rubocop-rspec +AllCops: + DisplayCopNames: true + TargetRubyVersion: '2.1' + Include: + - "./**/*.rb" + Exclude: + - bin/* + - ".vendor/**/*" + - "**/Gemfile" + - "**/Rakefile" + - pkg/**/* + - spec/fixtures/**/* + - vendor/**/* + - "**/Puppetfile" + - "**/Vagrantfile" + - "**/Guardfile" +Metrics/LineLength: + Description: People have wide screens, use them. + Max: 200 +RSpec/BeforeAfterAll: + Description: Beware of using after(:all) as it may cause state to leak between tests. + A necessary evil in acceptance testing. + Exclude: + - spec/acceptance/**/*.rb +RSpec/HookArgument: + Description: Prefer explicit :each argument, matching existing module's style + EnforcedStyle: each +Style/BlockDelimiters: + Description: Prefer braces for chaining. Mostly an aesthetical choice. Better to + be consistent then. + EnforcedStyle: braces_for_chaining +Style/ClassAndModuleChildren: + Description: Compact style reduces the required amount of indentation. + EnforcedStyle: compact +Style/EmptyElse: + Description: Enforce against empty else clauses, but allow `nil` for clarity. + EnforcedStyle: empty +Style/FormatString: + Description: Following the main puppet project's style, prefer the % format format. + EnforcedStyle: percent +Style/FormatStringToken: + Description: Following the main puppet project's style, prefer the simpler template + tokens over annotated ones. + EnforcedStyle: template +Style/Lambda: + Description: Prefer the keyword for easier discoverability. + EnforcedStyle: literal +Style/RegexpLiteral: + Description: Community preference. See https://github.com/voxpupuli/modulesync_config/issues/168 + EnforcedStyle: percent_r +Style/TernaryParentheses: + Description: Checks for use of parentheses around ternary conditions. Enforce parentheses + on complex expressions for better readability, but seriously consider breaking + it up. + EnforcedStyle: require_parentheses_when_complex +Style/TrailingCommaInArguments: + Description: Prefer always trailing comma on multiline argument lists. This makes + diffs, and re-ordering nicer. + EnforcedStyleForMultiline: comma +Style/TrailingCommaInLiteral: + Description: Prefer always trailing comma on multiline literals. This makes diffs, + and re-ordering nicer. + EnforcedStyleForMultiline: comma +Style/SymbolArray: + Description: Using percent style obscures symbolic intent of array's contents. + EnforcedStyle: brackets +RSpec/MessageSpies: + EnforcedStyle: receive +Style/Documentation: + Exclude: + - lib/puppet/parser/functions/**/* +Style/WordArray: + EnforcedStyle: brackets +Style/CollectionMethods: + Enabled: true +Style/MethodCalledOnDoEndBlock: + Enabled: true +Style/StringMethods: + Enabled: true +Layout/EndOfLine: + Enabled: false +Metrics/AbcSize: + Enabled: false +Metrics/BlockLength: + Enabled: false +Metrics/ClassLength: + Enabled: false +Metrics/CyclomaticComplexity: + Enabled: false +Metrics/MethodLength: + Enabled: false +Metrics/ModuleLength: + Enabled: false +Metrics/ParameterLists: + Enabled: false +Metrics/PerceivedComplexity: + Enabled: false +RSpec/DescribeClass: + Enabled: false +RSpec/ExampleLength: + Enabled: false +RSpec/MessageExpectation: + Enabled: false +RSpec/MultipleExpectations: + Enabled: false +RSpec/NestedGroups: + Enabled: false +Style/AsciiComments: + Enabled: false +Style/IfUnlessModifier: + Enabled: false +Style/SymbolProc: + Enabled: false diff --git a/.sync.yml b/.sync.yml new file mode 100644 index 0000000..d608181 --- /dev/null +++ b/.sync.yml @@ -0,0 +1,38 @@ +--- +Gemfile: + required: + ':system_tests': + - gem: 'puppet-module-posix-system-r#{minor_version}' + platforms: ruby + - gem: 'puppet-module-win-system-r#{minor_version}' + platforms: + - mswin + - mingw + - x64_mingw + - gem: beaker + version: '~> 3.13' + from_env: BEAKER_VERSION + - gem: beaker-abs + from_env: BEAKER_ABS_VERSION + version: '~> 0.1' + - gem: beaker-pe + - gem: beaker-hostgenerator + from_env: BEAKER_HOSTGENERATOR_VERSION + - gem: beaker-rspec + from_env: BEAKER_RSPEC_VERSION + ':development': + - gem: puppet-strings + +Rakefile: + requires: + - puppet-lint/tasks/puppet-lint + +#.rubocop.yml: +# default_configs: +# Layout/IndentHeredoc: +# Enabled: false +# RSpec/NamedSubject: +# Enabled: false + +.gitlab-ci.yml: + delete: true diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..81f77dd --- /dev/null +++ b/.travis.yml @@ -0,0 +1,44 @@ +--- +sudo: false +dist: trusty +language: ruby +cache: bundler +before_install: + - bundle -v + - rm -f Gemfile.lock + - gem update --system + - gem --version + - bundle -v +script: + - 'bundle exec rake $CHECK' +bundler_args: --without system_tests +rvm: + - 2.4.1 +env: + global: + - BEAKER_PUPPET_COLLECTION=puppet5 PUPPET_GEM_VERSION="~> 5.0" +matrix: + fast_finish: true + include: + - + env: CHECK="syntax lint metadata_lint check:symlinks check:git_ignore check:dot_underscore check:test_file rubocop" + - + env: CHECK=parallel_spec + - + env: PUPPET_GEM_VERSION="~> 4.0" CHECK=parallel_spec + rvm: 2.1.9 +branches: + only: + - master + - /^v\d/ +notifications: + email: false +deploy: + provider: puppetforge + user: puppet + password: + secure: "" + on: + tags: true + all_branches: true + condition: "$DEPLOY_TO_FORGE = yes" diff --git a/.yardopts b/.yardopts new file mode 100644 index 0000000..29c933b --- /dev/null +++ b/.yardopts @@ -0,0 +1 @@ +--markup markdown diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..4c954cd --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,11 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## Release 0.1.0 + +**Features** + +**Bugfixes** + +**Known Issues** @@ -0,0 +1,88 @@ +source ENV['GEM_SOURCE'] || 'https://rubygems.org' + +def location_for(place_or_version, fake_version = nil) + if place_or_version =~ %r{\A(git[:@][^#]*)#(.*)} + [fake_version, { git: Regexp.last_match(1), branch: Regexp.last_match(2), require: false }].compact + elsif place_or_version =~ %r{\Afile:\/\/(.*)} + ['>= 0', { path: File.expand_path(Regexp.last_match(1)), require: false }] + else + [place_or_version, { require: false }] + end +end + +def gem_type(place_or_version) + if place_or_version =~ %r{\Agit[:@]} + :git + elsif !place_or_version.nil? && place_or_version.start_with?('file:') + :file + else + :gem + end +end + +ruby_version_segments = Gem::Version.new(RUBY_VERSION.dup).segments +minor_version = ruby_version_segments[0..1].join('.') + +group :development do + gem "fast_gettext", '1.1.0', require: false if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new('2.1.0') + gem "fast_gettext", require: false if Gem::Version.new(RUBY_VERSION.dup) >= Gem::Version.new('2.1.0') + gem "json_pure", '<= 2.0.1', require: false if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new('2.0.0') + gem "json", '= 1.8.1', require: false if Gem::Version.new(RUBY_VERSION.dup) == Gem::Version.new('2.1.9') + gem "json", '<= 2.0.4', require: false if Gem::Version.new(RUBY_VERSION.dup) == Gem::Version.new('2.4.4') + gem "puppet-module-posix-default-r#{minor_version}", require: false, platforms: [:ruby] + gem "puppet-module-posix-dev-r#{minor_version}", require: false, platforms: [:ruby] + gem "puppet-module-win-default-r#{minor_version}", require: false, platforms: [:mswin, :mingw, :x64_mingw] + gem "puppet-module-win-dev-r#{minor_version}", require: false, platforms: [:mswin, :mingw, :x64_mingw] + gem "puppet-strings", require: false +end +group :system_tests do + gem "puppet-module-posix-system-r#{minor_version}", require: false, platforms: [:ruby] + gem "puppet-module-win-system-r#{minor_version}", require: false, platforms: [:mswin, :mingw, :x64_mingw] + gem "beaker", *location_for(ENV['BEAKER_VERSION'] || '~> 3.13') + gem "beaker-abs", *location_for(ENV['BEAKER_ABS_VERSION'] || '~> 0.1') + gem "beaker-pe", require: false + gem "beaker-hostgenerator" + gem "beaker-rspec" +end + +puppet_version = ENV['PUPPET_GEM_VERSION'] +puppet_type = gem_type(puppet_version) +facter_version = ENV['FACTER_GEM_VERSION'] +hiera_version = ENV['HIERA_GEM_VERSION'] + +gems = {} + +gems['puppet'] = location_for(puppet_version) + +# If facter or hiera versions have been specified via the environment +# variables + +gems['facter'] = location_for(facter_version) if facter_version +gems['hiera'] = location_for(hiera_version) if hiera_version + +if Gem.win_platform? && puppet_version =~ %r{^(file:///|git://)} + # If we're using a Puppet gem on Windows which handles its own win32-xxx gem + # dependencies (>= 3.5.0), set the maximum versions (see PUP-6445). + gems['win32-dir'] = ['<= 0.4.9', require: false] + gems['win32-eventlog'] = ['<= 0.6.5', require: false] + gems['win32-process'] = ['<= 0.7.5', require: false] + gems['win32-security'] = ['<= 0.2.5', require: false] + gems['win32-service'] = ['0.8.8', require: false] +end + +gems.each do |gem_name, gem_params| + gem gem_name, *gem_params +end + +# Evaluate Gemfile.local and ~/.gemfile if they exist +extra_gemfiles = [ + "#{__FILE__}.local", + File.join(Dir.home, '.gemfile'), +] + +extra_gemfiles.each do |gemfile| + if File.file?(gemfile) && File.readable?(gemfile) + eval(File.read(gemfile), binding) + end +end +# vim: syntax=ruby diff --git a/README.md b/README.md new file mode 100644 index 0000000..09b25ad --- /dev/null +++ b/README.md @@ -0,0 +1,95 @@ + +# sshkeys_core + +Welcome to your new module. A short overview of the generated parts can be found in the PDK documentation at https://puppet.com/pdk/latest/pdk_generating_modules.html . + +The README template below provides a starting point with details about what information to include in your README. + + + + + + + +#### Table of Contents + +1. [Description](#description) +2. [Setup - The basics of getting started with sshkeys_core](#setup) + * [What sshkeys_core affects](#what-sshkeys_core-affects) + * [Setup requirements](#setup-requirements) + * [Beginning with sshkeys_core](#beginning-with-sshkeys_core) +3. [Usage - Configuration options and additional functionality](#usage) +4. [Limitations - OS compatibility, etc.](#limitations) +5. [Development - Guide for contributing to the module](#development) + +## Description + +Briefly tell users why they might want to use your module. Explain what your module does and what kind of problems users can solve with it. + +This should be a fairly short description helps the user decide if your module is what they want. + + +## Setup + +### What sshkeys_core affects **OPTIONAL** + +If it's obvious what your module touches, you can skip this section. For example, folks can probably figure out that your mysql_instance module affects their MySQL instances. + +If there's more that they should know about, though, this is the place to mention: + +* Files, packages, services, or operations that the module will alter, impact, or execute. +* Dependencies that your module automatically installs. +* Warnings or other important notices. + +### Setup Requirements **OPTIONAL** + +If your module requires anything extra before setting up (pluginsync enabled, another module, etc.), mention it here. + +If your most recent release breaks compatibility or requires particular steps for upgrading, you might want to include an additional "Upgrading" section here. + +### Beginning with sshkeys_core + +The very basic steps needed for a user to get the module up and running. This can include setup steps, if necessary, or it can be an example of the most basic use of the module. + +## Usage + +Include usage examples for common use cases in the **Usage** section. Show your users how to use your module to solve problems, and be sure to include code examples. Include three to five examples of the most important or common tasks a user can accomplish with your module. Show users how to accomplish more complex tasks that involve different types, classes, and functions working in tandem. + +## Reference + +This section is deprecated. Instead, add reference information to your code as Puppet Strings comments, and then use Strings to generate a REFERENCE.md in your module. For details on how to add code comments and generate documentation with Strings, see the Puppet Strings [documentation](https://puppet.com/docs/puppet/latest/puppet_strings.html) and [style guide](https://puppet.com/docs/puppet/latest/puppet_strings_style.html) + +If you aren't ready to use Strings yet, manually create a REFERENCE.md in the root of your module directory and list out each of your module's classes, defined types, facts, functions, Puppet tasks, task plans, and resource types and providers, along with the parameters for each. + +For each element (class, defined type, function, and so on), list: + + * The data type, if applicable. + * A description of what the element does. + * Valid values, if the data type doesn't make it obvious. + * Default value, if any. + +For example: + +``` +### `pet::cat` + +#### Parameters + +##### `meow` + +Enables vocalization in your cat. Valid options: 'string'. + +Default: 'medium-loud'. +``` + +## Limitations + +In the Limitations section, list any incompatibilities, known issues, or other warnings. + +## Development + +In the Development section, tell other users the ground rules for contributing to your project and how they should submit their work. + +## Release Notes/Contributors/Etc. **Optional** + +If you aren't using changelog, put your release notes here (though you should consider using changelog). You can also add any additional sections you feel are necessary or important to include here. Please use the `## ` header. diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..ef5f698 --- /dev/null +++ b/Rakefile @@ -0,0 +1,7 @@ +require 'puppetlabs_spec_helper/rake_tasks' +require 'puppet-syntax/tasks/puppet-syntax' +require 'puppet_blacksmith/rake_tasks' if Bundler.rubygems.find_name('puppet-blacksmith').any? +require 'puppet-lint/tasks/puppet-lint' + +PuppetLint.configuration.send('disable_relative') + diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000..4a5b227 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,52 @@ +--- +version: 1.1.x.{build} +skip_commits: + message: /^\(?doc\)?.*/ +clone_depth: 10 +init: + - SET + - 'mkdir C:\ProgramData\PuppetLabs\code && exit 0' + - 'mkdir C:\ProgramData\PuppetLabs\facter && exit 0' + - 'mkdir C:\ProgramData\PuppetLabs\hiera && exit 0' + - 'mkdir C:\ProgramData\PuppetLabs\puppet\var && exit 0' +environment: + matrix: + - + RUBY_VERSION: 24-x64 + CHECK: syntax lint metadata_lint check:symlinks check:git_ignore check:dot_underscore check:test_file rubocop + - + PUPPET_GEM_VERSION: ~> 4.0 + RUBY_VERSION: 21 + CHECK: parallel_spec + - + PUPPET_GEM_VERSION: ~> 4.0 + RUBY_VERSION: 21-x64 + CHECK: parallel_spec + - + PUPPET_GEM_VERSION: ~> 5.0 + RUBY_VERSION: 24 + CHECK: parallel_spec + - + PUPPET_GEM_VERSION: ~> 5.0 + RUBY_VERSION: 24-x64 + CHECK: parallel_spec +matrix: + fast_finish: true +install: + - set PATH=C:\Ruby%RUBY_VERSION%\bin;%PATH% + - bundle install --jobs 4 --retry 2 --without system_tests + - type Gemfile.lock +build: off +test_script: + - bundle exec puppet -V + - ruby -v + - gem -v + - bundle -v + - bundle exec rake %CHECK% +notifications: + - provider: Email + to: + - nobody@nowhere.com + on_build_success: false + on_build_failure: false + on_build_status_changed: false diff --git a/lib/puppet/provider/ssh_authorized_key/parsed.rb b/lib/puppet/provider/ssh_authorized_key/parsed.rb new file mode 100644 index 0000000..f7ac9f7 --- /dev/null +++ b/lib/puppet/provider/ssh_authorized_key/parsed.rb @@ -0,0 +1,105 @@ +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 => /^\s*#/ + text_line :blank, :match => /^\s*$/ + + record_line :parsed, + :fields => %w{options type key name}, + :optional => %w{options}, + :rts => /^\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 => %w{options bits exponent modulus name}, + :optional => %w{options}, + :rts => /^\s+/, + :match => /^(?:(.+) )?(\d+) (\d+) (\d+)(?: (.+))?$/ + + def dir_perm + 0700 + end + + def file_perm + 0600 + end + + def user + uid = Puppet::FileSystem.stat(target).uid + Etc.getpwuid(uid).name + 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) + + 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 + 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) + while !scanner.eos? + scanner.skip(/[ \t]*/) + # scan a long option + if out = scanner.scan(/[-a-z0-9A-Z_]+=\".*?[^\\]\"/) or out = scanner.scan(/[-a-z0-9A-Z_]+/) + result << out + else + # found an unscannable token, let's abort + break + end + # eat a comma + scanner.skip(/[ \t]*,[ \t]*/) + end + result + end + + def self.prefetch_hook(records) + name_index = 0 + records.each do |record| + if 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 +end + diff --git a/lib/puppet/provider/sshkey/parsed.rb b/lib/puppet/provider/sshkey/parsed.rb new file mode 100644 index 0000000..1c42aeb --- /dev/null +++ b/lib/puppet/provider/sshkey/parsed.rb @@ -0,0 +1,50 @@ +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 => /^#/ + text_line :blank, :match => /^\s*$/ + + record_line :parsed, :fields => %w{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 + 0644 + 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 +end + diff --git a/lib/puppet/type/ssh_authorized_key.rb b/lib/puppet/type/ssh_authorized_key.rb new file mode 100644 index 0000000..c6ff5b6 --- /dev/null +++ b/lib/puppet/type/ssh_authorized_key.rb @@ -0,0 +1,143 @@ +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 `<TYPE> <KEY> <NAME/COMMENT>`. 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 + + newproperty(:type) do + desc "The encryption type used." + + newvalues :'ssh-dss', :'ssh-rsa', :'ecdsa-sha2-nistp256', :'ecdsa-sha2-nistp384', :'ecdsa-sha2-nistp521', :'ssh-ed25519' + + aliasvalue(:dsa, :'ssh-dss') + aliasvalue(:ed25519, :'ssh-ed25519') + aliasvalue(:rsa, :'ssh-rsa') + 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 =~ /\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`." + + defaultto :absent + + def should + return super if defined?(@should) and @should[0] != :absent + + return nil unless user = resource[:user] + + begin + return File.expand_path("~#{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." + + defaultto do :absent end + + validate do |value| + unless value == :absent or value =~ /^[-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 = /^(?:(.+)\s+)?(ssh-dss|ssh-ed25519|ssh-rsa|ecdsa-sha2-nistp256|ecdsa-sha2-nistp384|ecdsa-sha2-nistp521)\s+([^ ]+)\s*(.*)$/ + def self.keyline_regex + REGEX + end + end +end diff --git a/lib/puppet/type/sshkey.rb b/lib/puppet/type/sshkey.rb new file mode 100644 index 0000000..31e590b --- /dev/null +++ b/lib/puppet/type/sshkey.rb @@ -0,0 +1,83 @@ +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 + + newproperty(:type) do + desc "The encryption type used. Probably ssh-dss or ssh-rsa." + + newvalues :'ssh-dss', :'ssh-ed25519', :'ssh-rsa', :'ecdsa-sha2-nistp256', :'ecdsa-sha2-nistp384', :'ecdsa-sha2-nistp521' + + aliasvalue(:dsa, :'ssh-dss') + aliasvalue(:ed25519, :'ssh-ed25519') + aliasvalue(:rsa, :'ssh-rsa') + 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 =~ /\s/ + raise Puppet::Error, _("Aliases cannot include whitespace") + end + if value =~ /,/ + 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 =~ /\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 { if @resource.class.defaultprovider.ancestors.include?(Puppet::Provider::ParsedFile) + @resource.class.defaultprovider.default_target + else + nil + end + } + end + end +end diff --git a/metadata.json b/metadata.json new file mode 100644 index 0000000..27f9c52 --- /dev/null +++ b/metadata.json @@ -0,0 +1,90 @@ +{ + "name": "puppetlabs-sshkeys_core", + "version": "0.1.0", + "author": "Puppet Labs", + "summary": "", + "license": "Apache-2.0", + "source": "", + "dependencies": [ + + ], + "operatingsystem_support": [ + { + "operatingsystem": "CentOS", + "operatingsystemrelease": [ + "7" + ] + }, + { + "operatingsystem": "OracleLinux", + "operatingsystemrelease": [ + "7" + ] + }, + { + "operatingsystem": "RedHat", + "operatingsystemrelease": [ + "7" + ] + }, + { + "operatingsystem": "Scientific", + "operatingsystemrelease": [ + "7" + ] + }, + { + "operatingsystem": "Debian", + "operatingsystemrelease": [ + "8" + ] + }, + { + "operatingsystem": "Ubuntu", + "operatingsystemrelease": [ + "16.04" + ] + }, + { + "operatingsystem": "Fedora", + "operatingsystemrelease": [ + "25" + ] + }, + { + "operatingsystem": "Darwin", + "operatingsystemrelease": [ + "16" + ] + }, + { + "operatingsystem": "SLES", + "operatingsystemrelease": [ + "12" + ] + }, + { + "operatingsystem": "Solaris", + "operatingsystemrelease": [ + "11" + ] + }, + { + "operatingsystem": "windows", + "operatingsystemrelease": [ + "2008 R2", + "2012 R2", + "10" + ] + } + ], + "requirements": [ + { + "name": "puppet", + "version_requirement": ">= 4.7.0 < 6.0.0" + } + ], + "pdk-version": "1.5.0", + "template-url": "https://github.com/puppetlabs/pdk-templates.git", + "template-ref": "heads/master-0-g6654f6d" +} diff --git a/spec/acceptance/tests/resource/ssh_authorized_key/create.rb b/spec/acceptance/tests/resource/ssh_authorized_key/create.rb new file mode 100644 index 0000000..6b4c879 --- /dev/null +++ b/spec/acceptance/tests/resource/ssh_authorized_key/create.rb @@ -0,0 +1,39 @@ +test_name "should create an entry for an SSH authorized key" + +tag 'audit:medium', + 'audit:refactor', # Use block style `test_run` + 'audit:acceptance' # Could be done at the integration (or unit) layer though + # actual changing of resources could irreparably damage a + # host running this, or require special permissions. + +confine :except, :platform => ['windows'] + +auth_keys = '~/.ssh/authorized_keys' +name = "pl#{rand(999999).to_i}" + +agents.each do |agent| + teardown do + #(teardown) restore the #{auth_keys} file + on(agent, "mv /tmp/auth_keys #{auth_keys}", :acceptable_exit_codes => [0,1]) + end + + #------- SETUP -------# + step "(setup) backup #{auth_keys} file" + on(agent, "cp #{auth_keys} /tmp/auth_keys", :acceptable_exit_codes => [0,1]) + on(agent, "chown $LOGNAME #{auth_keys}") + + #------- TESTS -------# + step "create an authorized key entry with puppet (present)" + args = ['ensure=present', + "user=$LOGNAME", + "type='rsa'", + "key='mykey'", + ] + on(agent, puppet_resource('ssh_authorized_key', "#{name}", args)) + + step "verify entry in #{auth_keys}" + on(agent, "cat #{auth_keys}") do |res| + fail_test "didn't find the ssh_authorized_key for #{name}" unless stdout.include? "#{name}" + end + +end diff --git a/spec/acceptance/tests/resource/ssh_authorized_key/destroy.rb b/spec/acceptance/tests/resource/ssh_authorized_key/destroy.rb new file mode 100644 index 0000000..c80e967 --- /dev/null +++ b/spec/acceptance/tests/resource/ssh_authorized_key/destroy.rb @@ -0,0 +1,42 @@ +test_name "should delete an entry for an SSH authorized key" + +tag 'audit:medium', + 'audit:refactor', # Use block style `test_run` + 'audit:acceptance' # Could be done at the integration (or unit) layer though + # actual changing of resources could irreparably damage a + # host running this, or require special permissions. + +confine :except, :platform => ['windows'] + +auth_keys = '~/.ssh/authorized_keys' +name = "pl#{rand(999999).to_i}" + +agents.each do |agent| + teardown do + #(teardown) restore the #{auth_keys} file + on(agent, "mv /tmp/auth_keys #{auth_keys}", :acceptable_exit_codes => [0,1]) + end + + #------- SETUP -------# + step "(setup) backup #{auth_keys} file" + on(agent, "cp #{auth_keys} /tmp/auth_keys", :acceptable_exit_codes => [0,1]) + + step "(setup) create an authorized key in the #{auth_keys} file" + on(agent, "echo '' >> #{auth_keys} && echo 'ssh-rsa mykey #{name}' >> #{auth_keys}") + on(agent, "chown $LOGNAME #{auth_keys}") + + #------- TESTS -------# + step "delete an authorized key entry with puppet (absent)" + args = ['ensure=absent', + "user=$LOGNAME", + "type='rsa'", + "key='mykey'", + ] + on(agent, puppet_resource('ssh_authorized_key', "#{name}", args)) + + step "verify entry deleted from #{auth_keys}" + on(agent, "cat #{auth_keys}") do |res| + fail_test "found the ssh_authorized_key for #{name}" if stdout.include? "#{name}" + end + +end diff --git a/spec/acceptance/tests/resource/ssh_authorized_key/modify.rb b/spec/acceptance/tests/resource/ssh_authorized_key/modify.rb new file mode 100644 index 0000000..0a50c31 --- /dev/null +++ b/spec/acceptance/tests/resource/ssh_authorized_key/modify.rb @@ -0,0 +1,43 @@ +test_name "should update an entry for an SSH authorized key" + +tag 'audit:medium', + 'audit:refactor', # Use block style `test_run` + 'audit:acceptance' # Could be done at the integration (or unit) layer though + # actual changing of resources could irreparably damage a + # host running this, or require special permissions. + +confine :except, :platform => ['windows'] + +auth_keys = '~/.ssh/authorized_keys' +name = "pl#{rand(999999).to_i}" + +agents.each do |agent| + teardown do + #(teardown) restore the #{auth_keys} file + on(agent, "mv /tmp/auth_keys #{auth_keys}", :acceptable_exit_codes => [0,1]) + end + + #------- SETUP -------# + step "(setup) backup #{auth_keys} file" + on(agent, "cp #{auth_keys} /tmp/auth_keys", :acceptable_exit_codes => [0,1]) + + step "(setup) create an authorized key in the #{auth_keys} file" + on(agent, "echo '' >> #{auth_keys} && echo 'ssh-rsa mykey #{name}' >> #{auth_keys}") + on(agent, "chown $LOGNAME #{auth_keys}") + + #------- TESTS -------# + step "update an authorized key entry with puppet (present)" + args = ['ensure=present', + "user=$LOGNAME", + "type='rsa'", + "key='mynewshinykey'", + ] + on(agent, puppet_resource('ssh_authorized_key', "#{name}", args)) + + step "verify entry updated in #{auth_keys}" + on(agent, "cat #{auth_keys}") do |res| + fail_test "didn't find the updated key for #{name}" unless stdout.include? "mynewshinykey #{name}" + fail_test "Found old key mykey #{name}" if stdout.include? "mykey #{name}" + end + +end diff --git a/spec/acceptance/tests/resource/ssh_authorized_key/query.rb b/spec/acceptance/tests/resource/ssh_authorized_key/query.rb new file mode 100644 index 0000000..8caff85 --- /dev/null +++ b/spec/acceptance/tests/resource/ssh_authorized_key/query.rb @@ -0,0 +1,35 @@ +test_name "should be able to find an existing SSH authorized key" + +tag 'audit:medium', + 'audit:refactor', # Use block style `test_run` + 'audit:acceptance' # Could be done at the integration (or unit) layer though + # actual changing of resources could irreparably damage a + # host running this, or require special permissions. + +skip_test("This test is blocked by PUP-1605") + +confine :except, :platform => ['windows'] + +auth_keys = '~/.ssh/authorized_keys' +name = "pl#{rand(999999).to_i}" + +agents.each do |agent| + teardown do + #(teardown) restore the #{auth_keys} file + on(agent, "mv /tmp/auth_keys #{auth_keys}", :acceptable_exit_codes => [0,1]) + end + + #------- SETUP -------# + step "(setup) backup #{auth_keys} file" + on(agent, "cp #{auth_keys} /tmp/auth_keys", :acceptable_exit_codes => [0,1]) + + step "(setup) create an authorized key in the #{auth_keys} file" + on(agent, "echo '' >> #{auth_keys} && echo 'ssh-rsa mykey #{name}' >> #{auth_keys}") + + #------- TESTS -------# + step "verify SSH authorized key query with puppet" + on(agent, puppet_resource('ssh_authorized_key', "/#{name}")) do |res| + fail_test "Didn't find the ssh_authorized_key for #{name}" unless stdout.include? "#{name}" + end + +end diff --git a/spec/acceptance/tests/resource/sshkey/create.rb b/spec/acceptance/tests/resource/sshkey/create.rb new file mode 100644 index 0000000..4e75379 --- /dev/null +++ b/spec/acceptance/tests/resource/sshkey/create.rb @@ -0,0 +1,77 @@ +test_name "(PUP-5508) Should add an SSH key to the correct ssh_known_hosts file on OS X/macOS" do +# TestRail test case C93370 + +tag 'audit:medium', + 'audit:acceptance' # Could be done at the integration (or unit) layer though + # actual changing of resources could irreparably damage a + # host running this, or require special permissions. + +confine :to, :platform => /osx/ + +keyname = "pl#{rand(999999).to_i}" + +# FIXME: This is bletcherous +macos_version = fact_on(agent, "os.macosx.version.major") +if ["10.9","10.10"].include? macos_version + ssh_known_hosts = '/etc/ssh_known_hosts' +else + ssh_known_hosts = '/etc/ssh/ssh_known_hosts' +end + +teardown do + puts "Restore the #{ssh_known_hosts} file" + agents.each do |agent| + # Is it present? + rc = on(agent, "[ -e /tmp/ssh_known_hosts ]", + :accept_all_exit_codes => true) + if rc.exit_code == 0 + # It's present, so restore the original + on(agent, "mv -fv /tmp/ssh_known_hosts #{ssh_known_hosts}", + :accept_all_exit_codes => true) + else + # It's missing, which means there wasn't one to backup; just + # delete the one we laid down + on(agent, "rm -fv #{ssh_known_hosts}", + :accept_all_exit_codes => true) + end + end +end + +#------- SETUP -------# +step "Backup #{ssh_known_hosts} file, if present" do + # The 'cp' might fail because the source file doesn't exist + on(agent, "cp -fv #{ssh_known_hosts} /tmp/ssh_known_hosts", + :acceptable_exit_codes => [0,1]) +end + +#------- TESTS -------# +step 'Verify that the default file is empty or non-existent' do + # Is it even there? + rc = on(agent, "[ ! -e #{ssh_known_hosts} ]", + :acceptable_exit_codes => [0, 1]) + if rc.exit_code == 1 + # If it's there, it should be empty + on(agent, "cat #{ssh_known_hosts}") do |res| + fail_test "Default #{ssh_known_hosts} file not empty" \ + unless stdout.empty? + end + end +end + +step "Add an sshkey to the default file" do + args = [ + "ensure=present", + "key=how_about_the_key_of_c", + "type=ssh-rsa", + ] + on(agent, puppet_resource("sshkey", "#{keyname}", args)) +end + +step 'Verify the new entry in the default file' do + on(agent, "cat #{ssh_known_hosts}") do |rc| + fail_test "Didn't find the ssh_known_host entry for #{keyname}" \ + unless stdout.include? "#{keyname}" + end +end + +end diff --git a/spec/default_facts.yml b/spec/default_facts.yml new file mode 100644 index 0000000..3248be5 --- /dev/null +++ b/spec/default_facts.yml @@ -0,0 +1,8 @@ +# Use default_module_facts.yml for module specific facts. +# +# Facts specified here will override the values provided by rspec-puppet-facts. +--- +concat_basedir: "/tmp" +ipaddress: "172.16.254.254" +is_pe: false +macaddress: "AA:AA:AA:AA:AA:AA" diff --git a/spec/fixtures/integration/provider/sshkey/sample b/spec/fixtures/integration/provider/sshkey/sample new file mode 100644 index 0000000..840ed19 --- /dev/null +++ b/spec/fixtures/integration/provider/sshkey/sample @@ -0,0 +1,21 @@ +hosting2.planetargon.com,64.34.164.77 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAy+f2t52cDMrYkgEKQ6juqfMf/a0nDFry3JAzl+SAWQ0gTklVxNcVbfHx2pkZk66EBGQfrK33Bx1BflZ/iEDyiCwmzVtNba0X9A6ELYjB9WSkWdIqZCfPlKZMu9N//aZ6+3SDVuz/BVFsAVmtqQ4Let2QjOFiSIKXrtPqWvVT/MM= +kirby.madstop.com,192.168.0.5 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAw9iHuAa/wepHoUzWqsvhQvSkpE4K7agrdLOWHM9mvyRQ2X3HVq5GqzAvWu4J+f0FvcLPwA9tivpxt1oSt5MOtvDM6HoM+8m3P4daBp0nlNaYR8/vHCAmX6N3RyM8FWfp+VqWyux1SooQwxYxVFy86G78ApTqNsZ+p7cHmnBYqk0= +fedora1,192.168.0.51 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAyz1rKcApU4//j8CHYKexq4qnq2WVqLPrZYGnlij1t7FscLiDVKvBuRHVkfyTNIjAM/t7tM1Dj+FuD4iWziCmf7RO9q4wI5y/1zgCiSUegnZVSmH2yxnWGMdHGpXOkN3NXcpy6jylxyBo0M7T22PSezCxyUVfMclesjOEO1jETd0= +kirby ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAw9iHuAa/wepHoUzWqsvhQvSkpE4K7agrdLOWHM9mvyRQ2X3HVq5GqzAvWu4J+f0FvcLPwA9tivpxt1oSt5MOtvDM6HoM+8m3P4daBp0nlNaYR8/vHCAmX6N3RyM8FWfp+VqWyux1SooQwxYxVFy86G78ApTqNsZ+p7cHmnBYqk0= +sol10b,192.168.0.50 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAs37kiDwKxWqi6EfSdKwRaZXBwh2doOARRqZzyitBaPwESy26DwTx+xdQ2rwB4V2k1WIec+1f3bgTS2ArH75dQSPyba2HKqxaSRBd3Zh4z23+uUxpupEyoRdW1HolMOvuoceheSMsruiuYcuiyct41d4c/Qmr51Dv04Doi00k6Ws= +piratehaven.org,64.81.59.88 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEA7TYRRkle0wDNohZ0TNZN6R1Zp0svxgX+GJ9umI5yNM1bMxUTgeNRh5nIvZg1HgD1WRXQ57dSxxLzbvRyAqc245g6S8eWWCtenvOFLl0rOF5D3VxbQuw79sOe8/Ac8TC+c8RuWB7aaxpwL5Rv9xfDeazOtoKXj7+uwQW1PUmTaEM= +atalanta,192.168.0.10 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAysniuWiJH6OQLXl63XXcS1b/hP2lAgSz0IutjQ6ZUfBrt1BZ8udEgSh57w5QDLsZ1lNvND61u5cy6iDKXI5TIQY4DvUmsoFZhyr4iYJbtT/h6UJSyaZtEnA7ZMRjGhUIMOn+mjbj7Z3zmJMhxtImK3Xo3O2fJ1hnK4jsBwQPSLc= +pixie,192.168.0.9 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAzDp588CvET6w11LB2s/vPjc4tX9+u46iYJgNFfhzxrXYMVv4GF7d30IXB5+Hwyi2FOQIG1+h0kUXVGWEv64rAFBT7pD2KMFX0lcDERV4avqT0TRDIIA5OqFOhq9Ff+kOmHS2cB7eFyR5vqbN4ujOnJGTmru9dcqyL+2AcFekvh0= +culain,192.168.0.3 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAvJ/IORhQMdAsJ7LB1sI2LlWUHc7HPTCDiEgJ96ij3jFvqaIiYieRFaNkxbbk75mPkj3vIqWIgAsAtHmKX4wDikNG/gyjs4WM4cWFKtl2jiVhqpoxqqCaVxs6Ex+vpKuKhQR6SzFBFDlBZYP9an6DPu1msTLT8/hZH2WwswVmpyU= +cs312-server.een.orst.edu,128.193.40.66 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEA+t3hsOiyp1mztt013bLdJqIAFCo8lxlz86MYEPz/mADHzWLs3Xv7xpAUv/E8pRbhEOzXo84EddORRBXz6DgVyMah8+v/u3IOkpXuZI0Iu1n5hZyf2l5DGEyGecr3oqqjUdMuM9HeXFLnqXJI3hDE7civBtqf5AJSol+TCcipeE8= +freebsd1,192.168.0.52 ssh-dss AAAAB3NzaC1kc3MAAACBAJSiOyQhYlKAi0FDLKy42VzLDq6yJWXGXVCLSfgWyVx7QCq/3+W3C1dtHuAvjbypcjqqvsuGGITgQ1Y6B/+76n5d7FyQnj4SFZ5drOBn/TvslXhrS/Ok5KCcndfNAa+EyMnSZJ21jhoRjZftY4lmb4hy6fEF3RvjuOdf1qBN5FWpAAAAFQDcsWF0zELAW6YUlSjAyO0T0lfPbwAAAIAlOLdOd/WszzVaplCOnH5vF6LWfr6BosZKDkFi0mv6Ec636YGaj4AMxK8sRPusHv6sVByN17ntIJnLo2XD1SuoH28bZ0ZnPIdVnd0l1KqsOCuuow9LZYJUihezoUuYuTvij1jZdrwltuPNJTVLYtsZDnKE5plw+Tjzeb7ImjbXGwAAAIBT40olg3fxhRDiZECle0WK7GitgXCB3njs+4dba8VwveEJb9UuulMc1eR+zQiJR96IUBagC9NiLvUGS1IfiIHpT4FA8O/MK1W9SgxXB9d39Nk/9l8dH3U/fLnbC/hYVo8bN0or/mKxcxQMkdBwpPlWAbELRftod2BkkkvgfQdc+g== +192.168.0.2 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgwCZ+qxpMMRJ3otGsjaYeKTKf6tuCZyK1cD+ns9Eu7V0ZJLJ/LLMxduu7n4H/ufGI5rGV5axzgx8yZhjDRzsrGjLAQYsqlomMkf901YQI6UuieSA4MZa5MDkq/Jt6Vx1kEGTpkgrfw9kRMX5BngECt1QKY4xTgC7Ex+WlFvZwk+tRUT3 +openbsd1,192.168.0.54 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAvgRVrtJk0fBg9YsLf7rWR1X32ZjFcva5XBvenDlHruObaHzuGXyyr6iOCAEOc7eCZjlPBYrGZ2potqyk8HlBOHXr1cCBf49t4yAt8KgKswtzWlgdbU1UEwllSRVOpzqULAT0smv/jfaIZdvRoN1rLriqtpn4bQL//ZOHiyXCwdlJ+di8Mza2L8KZ5T75hwIFBhrgL12xfymRp3v+Iy21MjjzsF3pROIkZ7icitNqQF55U9vsHaA37vG8FepVkO10bYYP7IHPZaBPBMPx7qPyRgRDJahEUGBnkIkzwJHEmA+7YMiNTZu6MigezD+CIqY1xOi/eXZwObLwLKXo7eRmpw== +192.168.0.60 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAufcvE7s1eRwhUwMBfZ6uFNxkdSdzjSDEn3vjByOjG/eraNhnYAW3rxV7WIf2pEa6JSOMrE1mqsEL75xEtpXlzC949Ysz4+1OSHY52KonoFm/a+FbmbFp81TVuVPYaLoeWN27STiJh+puC5spkIZe0laqT1GU13M4gj6B+j3NLhU= +rh3a,192.168.0.55 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAufcvE7s1eRwhUwMBfZ6uFNxkdSdzjSDEn3vjByOjG/eraNhnYAW3rxV7WIf2pEa6JSOMrE1mqsEL75xEtpXlzC949Ysz4+1OSHY52KonoFm/a+FbmbFp81TVuVPYaLoeWN27STiJh+puC5spkIZe0laqT1GU13M4gj6B+j3NLhU= +tsetse,192.168.0.7 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAwCDGn82BEMSCfcE2LZKcwdAoyIBC+u2twVwWNRm3KzyrJMQ+RbTQo4AwGOdyr/QYh6KaxTKTSoDtiBLr132uenMQKwF47gCKMA1T47uiy+TBUehgOGwOxteSYP/pQRpKXFmhOppSPyDPQVq234XvANeJp0iT8ZKEhF2FsWTs6sM= +config.sage.org,131.106.3.205 ssh-dss AAAAB3NzaC1kc3MAAACBAL2akEcIfQsfm3zCd2hD6PgH3kWA/tqX/qbrLhL/ipX7iqK/y282GMClZQSQjc1YNi9virvLzb6u/gdZxicZ8K5O3FaJrULQJOZaP62SOHk5CUSHVnvpMCaCnbwB6gEHa2LeMWStcEfWW+g1CC2hzPJw16/S5GISGXbyanO02MnXAAAAFQDomwx/OCjTmmQljMTU5rgNn2E4gwAAAIBmtMSfcs2Tq5iFFKK5cahluv047vVNfXqYIAkeJniceL0Et16MKfuzadpq0H9ocxQYW/5Ir9nUULrdxBUN9LxNKq15/uWkJC9QCSh8PysgvFnjVZeCODua/dn6eZTZnY9DZ3S6v1pT8CP6uWr5fmZJ8FKJGrC3gYX4y1V1ZTCVewAAAIB6e7RCST6vkTS5rgn5wGbrfLK5ad+PW+2i66k77Zv1pjtfRz+0oejBjwJDPNVSc2YNEl7X0nEEMNjo/a5x8Ls+nVqhzJA+NXIwS1e/moKbXFGewW5HAtxd79gtInC8dEIO7hmnWnqF1dBkRHXg1YffYkHrMVJBxpzagw7nYa0BBQ== +rh3b,192.168.0.56 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAufcvE7s1eRwhUwMBfZ6uFNxkdSdzjSDEn3vjByOjG/eraNhnYAW3rxV7WIf2pEa6JSOMrE1mqsEL75xEtpXlzC949Ysz4+1OSHY52KonoFm/a+FbmbFp81TVuVPYaLoeWN27STiJh+puC5spkIZe0laqT1GU13M4gj6B+j3NLhU= +centos1,192.168.0.57 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEA0DXqYF+3Lf68GkWBAjjKBb6UITNnzm4wiDi/AGjv5+DoVXqDcqHvZ8rZFAMgUe1dVob4pWT2ZWLHW0gicoJCdr4UQbPXlWz1F62z8fo2PRRPlG6KN1wmF7pnyml8jr0wBX8lQZJsMqi4InGozf7wFHLH/7DNGRK3MD6tSp3Z4is= +doorstop.cafes.net,205.241.238.186 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEApJKeB9/bN5t55zLETHs0MVo/vEkfQ3EzY7178GKLI/yiOFmcn+NvUvUtCQK/xKpod813LBHCODxZPG1Kb0SjlaC/EkFEenb74LNu0o1qXa1GWh3wfaIm0JRNjXqPqxAWTlMs43O2HXwOwmLVhl7SSP3xtTw6h9gREbVKmfBaRdsRfzD0etfz1NCnmGh/1Sh9+j4eeS+IBtwoR5JVhZVhuofHCqs5HZ8gLDgfn8HXP7pMbLkx54cf1R/tmFmn9JGLdTPtEGcSIiu7414XSbfChSC83rGZCDPKHq7ZodiE8GpbWLBnyPXi2AYxTPM7aZMqitIHv3MWf5suV0q0WLGdnQ== +host.domain.com,host1.domain.com,192.168.0.1 dss thisismykey1 diff --git a/spec/fixtures/unit/provider/sshkey/parsed/sample b/spec/fixtures/unit/provider/sshkey/parsed/sample new file mode 100644 index 0000000..840ed19 --- /dev/null +++ b/spec/fixtures/unit/provider/sshkey/parsed/sample @@ -0,0 +1,21 @@ +hosting2.planetargon.com,64.34.164.77 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAy+f2t52cDMrYkgEKQ6juqfMf/a0nDFry3JAzl+SAWQ0gTklVxNcVbfHx2pkZk66EBGQfrK33Bx1BflZ/iEDyiCwmzVtNba0X9A6ELYjB9WSkWdIqZCfPlKZMu9N//aZ6+3SDVuz/BVFsAVmtqQ4Let2QjOFiSIKXrtPqWvVT/MM= +kirby.madstop.com,192.168.0.5 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAw9iHuAa/wepHoUzWqsvhQvSkpE4K7agrdLOWHM9mvyRQ2X3HVq5GqzAvWu4J+f0FvcLPwA9tivpxt1oSt5MOtvDM6HoM+8m3P4daBp0nlNaYR8/vHCAmX6N3RyM8FWfp+VqWyux1SooQwxYxVFy86G78ApTqNsZ+p7cHmnBYqk0= +fedora1,192.168.0.51 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAyz1rKcApU4//j8CHYKexq4qnq2WVqLPrZYGnlij1t7FscLiDVKvBuRHVkfyTNIjAM/t7tM1Dj+FuD4iWziCmf7RO9q4wI5y/1zgCiSUegnZVSmH2yxnWGMdHGpXOkN3NXcpy6jylxyBo0M7T22PSezCxyUVfMclesjOEO1jETd0= +kirby ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAw9iHuAa/wepHoUzWqsvhQvSkpE4K7agrdLOWHM9mvyRQ2X3HVq5GqzAvWu4J+f0FvcLPwA9tivpxt1oSt5MOtvDM6HoM+8m3P4daBp0nlNaYR8/vHCAmX6N3RyM8FWfp+VqWyux1SooQwxYxVFy86G78ApTqNsZ+p7cHmnBYqk0= +sol10b,192.168.0.50 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAs37kiDwKxWqi6EfSdKwRaZXBwh2doOARRqZzyitBaPwESy26DwTx+xdQ2rwB4V2k1WIec+1f3bgTS2ArH75dQSPyba2HKqxaSRBd3Zh4z23+uUxpupEyoRdW1HolMOvuoceheSMsruiuYcuiyct41d4c/Qmr51Dv04Doi00k6Ws= +piratehaven.org,64.81.59.88 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEA7TYRRkle0wDNohZ0TNZN6R1Zp0svxgX+GJ9umI5yNM1bMxUTgeNRh5nIvZg1HgD1WRXQ57dSxxLzbvRyAqc245g6S8eWWCtenvOFLl0rOF5D3VxbQuw79sOe8/Ac8TC+c8RuWB7aaxpwL5Rv9xfDeazOtoKXj7+uwQW1PUmTaEM= +atalanta,192.168.0.10 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAysniuWiJH6OQLXl63XXcS1b/hP2lAgSz0IutjQ6ZUfBrt1BZ8udEgSh57w5QDLsZ1lNvND61u5cy6iDKXI5TIQY4DvUmsoFZhyr4iYJbtT/h6UJSyaZtEnA7ZMRjGhUIMOn+mjbj7Z3zmJMhxtImK3Xo3O2fJ1hnK4jsBwQPSLc= +pixie,192.168.0.9 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAzDp588CvET6w11LB2s/vPjc4tX9+u46iYJgNFfhzxrXYMVv4GF7d30IXB5+Hwyi2FOQIG1+h0kUXVGWEv64rAFBT7pD2KMFX0lcDERV4avqT0TRDIIA5OqFOhq9Ff+kOmHS2cB7eFyR5vqbN4ujOnJGTmru9dcqyL+2AcFekvh0= +culain,192.168.0.3 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAvJ/IORhQMdAsJ7LB1sI2LlWUHc7HPTCDiEgJ96ij3jFvqaIiYieRFaNkxbbk75mPkj3vIqWIgAsAtHmKX4wDikNG/gyjs4WM4cWFKtl2jiVhqpoxqqCaVxs6Ex+vpKuKhQR6SzFBFDlBZYP9an6DPu1msTLT8/hZH2WwswVmpyU= +cs312-server.een.orst.edu,128.193.40.66 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEA+t3hsOiyp1mztt013bLdJqIAFCo8lxlz86MYEPz/mADHzWLs3Xv7xpAUv/E8pRbhEOzXo84EddORRBXz6DgVyMah8+v/u3IOkpXuZI0Iu1n5hZyf2l5DGEyGecr3oqqjUdMuM9HeXFLnqXJI3hDE7civBtqf5AJSol+TCcipeE8= +freebsd1,192.168.0.52 ssh-dss AAAAB3NzaC1kc3MAAACBAJSiOyQhYlKAi0FDLKy42VzLDq6yJWXGXVCLSfgWyVx7QCq/3+W3C1dtHuAvjbypcjqqvsuGGITgQ1Y6B/+76n5d7FyQnj4SFZ5drOBn/TvslXhrS/Ok5KCcndfNAa+EyMnSZJ21jhoRjZftY4lmb4hy6fEF3RvjuOdf1qBN5FWpAAAAFQDcsWF0zELAW6YUlSjAyO0T0lfPbwAAAIAlOLdOd/WszzVaplCOnH5vF6LWfr6BosZKDkFi0mv6Ec636YGaj4AMxK8sRPusHv6sVByN17ntIJnLo2XD1SuoH28bZ0ZnPIdVnd0l1KqsOCuuow9LZYJUihezoUuYuTvij1jZdrwltuPNJTVLYtsZDnKE5plw+Tjzeb7ImjbXGwAAAIBT40olg3fxhRDiZECle0WK7GitgXCB3njs+4dba8VwveEJb9UuulMc1eR+zQiJR96IUBagC9NiLvUGS1IfiIHpT4FA8O/MK1W9SgxXB9d39Nk/9l8dH3U/fLnbC/hYVo8bN0or/mKxcxQMkdBwpPlWAbELRftod2BkkkvgfQdc+g== +192.168.0.2 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgwCZ+qxpMMRJ3otGsjaYeKTKf6tuCZyK1cD+ns9Eu7V0ZJLJ/LLMxduu7n4H/ufGI5rGV5axzgx8yZhjDRzsrGjLAQYsqlomMkf901YQI6UuieSA4MZa5MDkq/Jt6Vx1kEGTpkgrfw9kRMX5BngECt1QKY4xTgC7Ex+WlFvZwk+tRUT3 +openbsd1,192.168.0.54 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAvgRVrtJk0fBg9YsLf7rWR1X32ZjFcva5XBvenDlHruObaHzuGXyyr6iOCAEOc7eCZjlPBYrGZ2potqyk8HlBOHXr1cCBf49t4yAt8KgKswtzWlgdbU1UEwllSRVOpzqULAT0smv/jfaIZdvRoN1rLriqtpn4bQL//ZOHiyXCwdlJ+di8Mza2L8KZ5T75hwIFBhrgL12xfymRp3v+Iy21MjjzsF3pROIkZ7icitNqQF55U9vsHaA37vG8FepVkO10bYYP7IHPZaBPBMPx7qPyRgRDJahEUGBnkIkzwJHEmA+7YMiNTZu6MigezD+CIqY1xOi/eXZwObLwLKXo7eRmpw== +192.168.0.60 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAufcvE7s1eRwhUwMBfZ6uFNxkdSdzjSDEn3vjByOjG/eraNhnYAW3rxV7WIf2pEa6JSOMrE1mqsEL75xEtpXlzC949Ysz4+1OSHY52KonoFm/a+FbmbFp81TVuVPYaLoeWN27STiJh+puC5spkIZe0laqT1GU13M4gj6B+j3NLhU= +rh3a,192.168.0.55 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAufcvE7s1eRwhUwMBfZ6uFNxkdSdzjSDEn3vjByOjG/eraNhnYAW3rxV7WIf2pEa6JSOMrE1mqsEL75xEtpXlzC949Ysz4+1OSHY52KonoFm/a+FbmbFp81TVuVPYaLoeWN27STiJh+puC5spkIZe0laqT1GU13M4gj6B+j3NLhU= +tsetse,192.168.0.7 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAwCDGn82BEMSCfcE2LZKcwdAoyIBC+u2twVwWNRm3KzyrJMQ+RbTQo4AwGOdyr/QYh6KaxTKTSoDtiBLr132uenMQKwF47gCKMA1T47uiy+TBUehgOGwOxteSYP/pQRpKXFmhOppSPyDPQVq234XvANeJp0iT8ZKEhF2FsWTs6sM= +config.sage.org,131.106.3.205 ssh-dss AAAAB3NzaC1kc3MAAACBAL2akEcIfQsfm3zCd2hD6PgH3kWA/tqX/qbrLhL/ipX7iqK/y282GMClZQSQjc1YNi9virvLzb6u/gdZxicZ8K5O3FaJrULQJOZaP62SOHk5CUSHVnvpMCaCnbwB6gEHa2LeMWStcEfWW+g1CC2hzPJw16/S5GISGXbyanO02MnXAAAAFQDomwx/OCjTmmQljMTU5rgNn2E4gwAAAIBmtMSfcs2Tq5iFFKK5cahluv047vVNfXqYIAkeJniceL0Et16MKfuzadpq0H9ocxQYW/5Ir9nUULrdxBUN9LxNKq15/uWkJC9QCSh8PysgvFnjVZeCODua/dn6eZTZnY9DZ3S6v1pT8CP6uWr5fmZJ8FKJGrC3gYX4y1V1ZTCVewAAAIB6e7RCST6vkTS5rgn5wGbrfLK5ad+PW+2i66k77Zv1pjtfRz+0oejBjwJDPNVSc2YNEl7X0nEEMNjo/a5x8Ls+nVqhzJA+NXIwS1e/moKbXFGewW5HAtxd79gtInC8dEIO7hmnWnqF1dBkRHXg1YffYkHrMVJBxpzagw7nYa0BBQ== +rh3b,192.168.0.56 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAufcvE7s1eRwhUwMBfZ6uFNxkdSdzjSDEn3vjByOjG/eraNhnYAW3rxV7WIf2pEa6JSOMrE1mqsEL75xEtpXlzC949Ysz4+1OSHY52KonoFm/a+FbmbFp81TVuVPYaLoeWN27STiJh+puC5spkIZe0laqT1GU13M4gj6B+j3NLhU= +centos1,192.168.0.57 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEA0DXqYF+3Lf68GkWBAjjKBb6UITNnzm4wiDi/AGjv5+DoVXqDcqHvZ8rZFAMgUe1dVob4pWT2ZWLHW0gicoJCdr4UQbPXlWz1F62z8fo2PRRPlG6KN1wmF7pnyml8jr0wBX8lQZJsMqi4InGozf7wFHLH/7DNGRK3MD6tSp3Z4is= +doorstop.cafes.net,205.241.238.186 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEApJKeB9/bN5t55zLETHs0MVo/vEkfQ3EzY7178GKLI/yiOFmcn+NvUvUtCQK/xKpod813LBHCODxZPG1Kb0SjlaC/EkFEenb74LNu0o1qXa1GWh3wfaIm0JRNjXqPqxAWTlMs43O2HXwOwmLVhl7SSP3xtTw6h9gREbVKmfBaRdsRfzD0etfz1NCnmGh/1Sh9+j4eeS+IBtwoR5JVhZVhuofHCqs5HZ8gLDgfn8HXP7pMbLkx54cf1R/tmFmn9JGLdTPtEGcSIiu7414XSbfChSC83rGZCDPKHq7ZodiE8GpbWLBnyPXi2AYxTPM7aZMqitIHv3MWf5suV0q0WLGdnQ== +host.domain.com,host1.domain.com,192.168.0.1 dss thisismykey1 diff --git a/spec/fixtures/unit/provider/sshkey/parsed/sample_with_blank_lines b/spec/fixtures/unit/provider/sshkey/parsed/sample_with_blank_lines new file mode 100644 index 0000000..02535df --- /dev/null +++ b/spec/fixtures/unit/provider/sshkey/parsed/sample_with_blank_lines @@ -0,0 +1,8 @@ +# A comment + +# ... and another after a blank line. The following line includes three whitespace characters. + +hosting2.planetargon.com,64.34.164.77 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAy+f2t52cDMrYkgEKQ6juqfMf/a0nDFry3JAzl+SAWQ0gTklVxNcVbfHx2pkZk66EBGQfrK33Bx1BflZ/ iEDyiCwmzVtNba0X9A6ELYjB9WSkWdIqZCfPlKZMu9N//aZ6+3SDVuz/BVFsAVmtqQ4Let2QjOFiSIKXrtPqWvVT/MM= + +will.isawesome.com,192.168.0.5 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAw9iHuAa/wepHoUzWqsvhQvSkpE4K7agrdLOWHM9mvyRQ2X3HVq5GqzAvWu4J+f0FvcLPwA9tivpxt1oSt5MOtvDM6HoM+8m3P4daBp0nlNaYR8/vHCAmX6N3RyM8FWfp+VqWyux1SooQwxYxVFy86G78ApTqNsZ+p7cHmnBYqk0= +will.isgreat.com,192.168.0.6 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAw9iHuAa/wepHoUzWqsvhQvSkpE4K7agrdLOWHM9mvyRQ2X3HVq5GqzAvWu4J+f0FvcLPwA9tivpxt1oSt5MOtvDM6HoM+8m3P4daBp0nlNaYR8/vHCAmX6N3RyM8FWfp+VqWyux1SooQwxYxVFy86G78ApTqNsZ+p7cHmnBasd= diff --git a/spec/integration/provider/ssh_authorized_key_spec.rb b/spec/integration/provider/ssh_authorized_key_spec.rb new file mode 100644 index 0000000..14af2de --- /dev/null +++ b/spec/integration/provider/ssh_authorized_key_spec.rb @@ -0,0 +1,219 @@ +#! /usr/bin/env ruby + +require 'spec_helper' +require 'puppet/file_bucket/dipper' + +describe Puppet::Type.type(:ssh_authorized_key).provider(:parsed), '(integration)', :unless => Puppet.features.microsoft_windows? do + include PuppetSpec::Files + + let :fake_userfile do + tmpfile('authorized_keys.user') + end + + let :fake_rootfile do + tmpfile('authorized_keys.root') + end + + let :sample_rsa_keys do + [ + 'AAAAB3NzaC1yc2EAAAADAQABAAAAgQCi18JBZOq10X3w4f67nVhO0O3s5Y1vHH4UgMSM3ZnQwbC5hjGyYSi9UULOoQQoQynI/a0I9NL423/Xk/XJVIKCHcS8q6V2Wmjd+fLNelOjxxoW6mbIytEt9rDvwgq3Mof3/m21L3t2byvegR00a+ikKbmInPmKwjeWZpexCIsHzQ==', # 1024 bit + 'AAAAB3NzaC1yc2EAAAADAQABAAAAgQDLClyvi3CsJw5Id6khZs2/+s11qOH4Gdp6iDioDsrIp0m8kSiPr71VGyQYAfPzzvHemHS7Xg0NkG1Kc8u9tRqBQfTvz7ubq0AT/g01+4P2hQ/soFkuwlUG/HVnnaYb6N0Qp5SHWvD5vBE2nFFQVpP5GrSctPtHSjzJq/i+6LYhmQ==', # 1024 bit + 'AAAAB3NzaC1yc2EAAAADAQABAAABAQDLygAO6txXkh9FNV8xSsBkATeqLbHzS7sFjGI3gt0Dx6q3LjyKwbhQ1RLf28kd5G6VWiXmClU/RtiPdUz8nrGuun++2mrxzrXrvpR9dq1lygLQ2wn2cI35dN5bjRMtXy3decs6HUhFo9MoNwX250rUWfdCyNPhGIp6OOfmjdy+UeLGNxq9wDx6i4bT5tVVSqVRtsEfw9+ICXchzl85QudjneVVpP+thriPZXfXA5eaGwAo/dmoKOIhUwF96gpdLqzNtrGQuxPbV80PTbGv9ZtAtTictxaDz8muXO7he9pXmchUpxUKtMFjHkL0FAZ9tRPmv3RA30sEr2fZ8+LKvnE50w0' #2048 Bit + ] + end + + let :sample_dsa_keys do + [ + 'AAAAB3NzaC1kc3MAAACBAOPck2O8MIDSqxPSnvENt6tzRrKJ5oOhB6Nc6oEcWm+VEH1gvuxdiRqwoMgRwyEf1yUd+UAcLw3a6Jn+EtFyEBN/5WF+4Tt4xTxZ0Pfik2Wc5uqHbQ2dkmOoXiAOYPiD3JUQ1Xwm/J0CgetjitoLfzAGdCNhMqguqAuHcVJ78ZZbAAAAFQCIBKFYZ+I18I+dtgteirXh+VVEEwAAAIEAs1yvQ/wnLLrRCM660pF4kBiw3D6dJfMdCXWQpn0hZmkBQSIzZv4Wuk3giei5luxscDxNc+y3CTXtnyG4Kt1Yi2sOdvhRI3rX8tD+ejn8GHazM05l5VIo9uu4AQPIE32iV63IqgApSBbJ6vDJW91oDH0J492WdLCar4BS/KE3cRwAAACBAN0uSDyJqYLRsfYcFn4HyVf6TJxQm1IcwEt6GcJVzgjri9VtW7FqY5iBqa9B9Zdh5XXAYJ0XLsWQCcrmMHM2XGHGpA4gL9VlCJ/0QvOcXxD2uK7IXwAVUA7g4V4bw8EVnFv2Flufozhsp+4soo1xiYc5jiFVHwVlk21sMhAtKAeF' # 1024 Bit + ] + end + + let :sample_lines do + [ + "ssh-rsa #{sample_rsa_keys[1]} root@someotherhost", + "ssh-dss #{sample_dsa_keys[0]} root@anywhere", + "ssh-rsa #{sample_rsa_keys[2]} paul", + "ssh-rsa #{sample_rsa_keys[2]} dummy" + ] + end + + let :dummy do + Puppet::Type.type(:ssh_authorized_key).new( + :name => 'dummy', + :target => fake_userfile, + :user => 'nobody', + :ensure => :absent + ) + end + + before :each do + File.stubs(:chown) + File.stubs(:chmod) + Puppet::Util::SUIDManager.stubs(:asuser).yields + end + + after :each do + described_class.clear # Work around bug #6628 + end + + def create_fake_key(username, content) + filename = (username == :root ? fake_rootfile : fake_userfile ) + File.open(filename, 'w') do |f| + content.each do |line| + f.puts line + end + end + end + + def check_fake_key(username, expected_content) + filename = (username == :root ? fake_rootfile : fake_userfile ) + content = File.readlines(filename).map(&:chomp).sort.reject{ |x| x =~ /^# HEADER:/ } + expect(content.join("\n")).to eq(expected_content.sort.join("\n")) + end + + def run_in_catalog(*resources) + Puppet::FileBucket::Dipper.any_instance.stubs(:backup) # Don't backup to the filebucket + catalog = Puppet::Resource::Catalog.new + catalog.host_config = false + resources.each do |resource| + resource.expects(:err).never + catalog.add_resource(resource) + end + catalog.apply + end + + it "should not complain about empty lines and comments" do + described_class.expects(:flush).never + sample = ['',sample_lines[0],' ',sample_lines[1],'# just a comment','#and another'] + create_fake_key(:user,sample) + run_in_catalog(dummy) + check_fake_key(:user, sample) + end + + it "should keep empty lines and comments when modifying a file" do + create_fake_key(:user, ['',sample_lines[0],' ',sample_lines[3],'# just a comment','#and another']) + run_in_catalog(dummy) + check_fake_key(:user, ['',sample_lines[0],' ','# just a comment','#and another']) + end + + describe "when managing one resource" do + + describe "with ensure set to absent" do + let :resource do + Puppet::Type.type(:ssh_authorized_key).new( + :name => 'root@hostname', + :type => :rsa, + :key => sample_rsa_keys[0], + :target => fake_rootfile, + :user => 'root', + :ensure => :absent + ) + end + + it "should not modify root's keyfile if resource is currently not present" do + create_fake_key(:root, sample_lines) + run_in_catalog(resource) + check_fake_key(:root, sample_lines) + end + + it "remove the key from root's keyfile if resource is currently present" do + create_fake_key(:root, sample_lines + ["ssh-rsa #{sample_rsa_keys[0]} root@hostname"]) + run_in_catalog(resource) + check_fake_key(:root, sample_lines) + end + end + + describe "when ensure is present" do + let :resource do + Puppet::Type.type(:ssh_authorized_key).new( + :name => 'root@hostname', + :type => :rsa, + :key => sample_rsa_keys[0], + :target => fake_rootfile, + :user => 'root', + :ensure => :present + ) + end + + # just a dummy so the parsedfile provider is aware + # of the user's authorized_keys file + + it "should add the key if it is not present" do + create_fake_key(:root, sample_lines) + run_in_catalog(resource) + check_fake_key(:root, sample_lines + ["ssh-rsa #{sample_rsa_keys[0]} root@hostname" ]) + end + + it "should modify the type if type is out of sync" do + create_fake_key(:root,sample_lines + [ "ssh-dss #{sample_rsa_keys[0]} root@hostname" ]) + run_in_catalog(resource) + check_fake_key(:root, sample_lines + [ "ssh-rsa #{sample_rsa_keys[0]} root@hostname" ]) + end + + it "should modify the key if key is out of sync" do + create_fake_key(:root,sample_lines + [ "ssh-rsa #{sample_rsa_keys[1]} root@hostname" ]) + run_in_catalog(resource) + check_fake_key(:root, sample_lines + [ "ssh-rsa #{sample_rsa_keys[0]} root@hostname" ]) + end + + it "should remove the key from old file if target is out of sync" do + create_fake_key(:user, [ sample_lines[0], "ssh-rsa #{sample_rsa_keys[0]} root@hostname" ]) + create_fake_key(:root, [ sample_lines[1], sample_lines[2] ]) + run_in_catalog(resource, dummy) + check_fake_key(:user, [ sample_lines[0] ]) + #check_fake_key(:root, [ sample_lines[1], sample_lines[2], "ssh-rsa #{sample_rsa_keys[0]} root@hostname" ]) + end + + it "should add the key to new file if target is out of sync" do + create_fake_key(:user, [ sample_lines[0], "ssh-rsa #{sample_rsa_keys[0]} root@hostname" ]) + create_fake_key(:root, [ sample_lines[1], sample_lines[2] ]) + run_in_catalog(resource, dummy) + #check_fake_key(:user, [ sample_lines[0] ]) + check_fake_key(:root, [ sample_lines[1], sample_lines[2], "ssh-rsa #{sample_rsa_keys[0]} root@hostname" ]) + end + + it "should modify options if options are out of sync" do + resource[:options]=[ 'from="*.domain1,host1.domain2"', 'no-port-forwarding', 'no-pty' ] + create_fake_key(:root, sample_lines + [ "from=\"*.false,*.false2\",no-port-forwarding,no-pty ssh-rsa #{sample_rsa_keys[0]} root@hostname"]) + run_in_catalog(resource) + check_fake_key(:root, sample_lines + [ "from=\"*.domain1,host1.domain2\",no-port-forwarding,no-pty ssh-rsa #{sample_rsa_keys[0]} root@hostname"] ) + end + end + end + + describe "when managing two resource" do + let :examples do + resources = [] + resources << Puppet::Type.type(:ssh_authorized_key).new( + :name => 'root@hostname', + :type => :rsa, + :key => sample_rsa_keys[0], + :target => fake_rootfile, + :user => 'root', + :ensure => :present + ) + resources << Puppet::Type.type(:ssh_authorized_key).new( + :name => 'user@hostname', + :key => sample_rsa_keys[1], + :type => :rsa, + :target => fake_userfile, + :user => 'nobody', + :ensure => :present + ) + resources + end + + describe "and both keys are absent" do + before :each do + create_fake_key(:root, sample_lines) + create_fake_key(:user, sample_lines) + end + + it "should add both keys" do + run_in_catalog(*examples) + check_fake_key(:root, sample_lines + [ "ssh-rsa #{sample_rsa_keys[0]} root@hostname" ]) + check_fake_key(:user, sample_lines + [ "ssh-rsa #{sample_rsa_keys[1]} user@hostname" ]) + end + end + end +end diff --git a/spec/integration/provider/sshkey_spec.rb b/spec/integration/provider/sshkey_spec.rb new file mode 100644 index 0000000..f461460 --- /dev/null +++ b/spec/integration/provider/sshkey_spec.rb @@ -0,0 +1,159 @@ +#!/usr/bin/env ruby + +require 'spec_helper' +require 'puppet/file_bucket/dipper' +require 'puppet_spec/files' +require 'puppet_spec/compiler' + +describe Puppet::Type.type(:sshkey).provider(:parsed), '(integration)', + :unless => Puppet.features.microsoft_windows? do + include PuppetSpec::Files + include PuppetSpec::Compiler + + before :each do + # Don't backup to filebucket + Puppet::FileBucket::Dipper.any_instance.stubs(:backup) + # We don't want to execute anything + described_class.stubs(:filetype). + returns Puppet::Util::FileType::FileTypeFlat + + @sshkey_file = tmpfile('sshkey_integration_specs') + FileUtils.cp(my_fixture('sample'), @sshkey_file) + end + + after :each do + # sshkey provider class + described_class.clear + end + + let(:type_under_test) { 'sshkey' } + + describe "when managing a ssh known hosts file it..." do + + let(:super_unique) { "my.super.unique.host" } + it "should create a new known_hosts file with mode 0644" do + target = tmpfile('ssh_known_hosts') + manifest = "#{type_under_test} { '#{super_unique}': + ensure => 'present', + type => 'rsa', + key => 'TESTKEY', + target => '#{target}' }" + apply_with_error_check(manifest) + expect_file_mode(target, "644") + end + + it "should create an SSH host key entry (ensure present)" do + manifest = "#{type_under_test} { '#{super_unique}': + ensure => 'present', + type => 'rsa', + key => 'mykey', + target => '#{@sshkey_file}' }" + apply_with_error_check(manifest) + expect(File.read(@sshkey_file)).to match(/#{super_unique}.*mykey/) + end + + let(:sshkey_name) { 'kirby.madstop.com' } + it "should delete an entry for an SSH host key" do + manifest = "#{type_under_test} { '#{sshkey_name}': + ensure => 'absent', + target => '#{@sshkey_file}' }" + apply_with_error_check(manifest) + expect(File.read(@sshkey_file)).not_to match(/#{sshkey_name}.*Yqk0=/) + end + + it "should update an entry for an SSH host key" do + manifest = "#{type_under_test} { '#{sshkey_name}': + ensure => 'present', + type => 'rsa', + key => 'mynewshinykey', + target => '#{@sshkey_file}' }" + apply_with_error_check(manifest) + expect(File.read(@sshkey_file)).to match(/#{sshkey_name}.*mynewshinykey/) + expect(File.read(@sshkey_file)).not_to match(/#{sshkey_name}.*Yqk0=/) + end + + # test all key types + types = ["ssh-dss", "dsa", + "ssh-ed25519", "ed25519", + "ssh-rsa", "rsa", + "ecdsa-sha2-nistp256", + "ecdsa-sha2-nistp384", + "ecdsa-sha2-nistp521"] + # these types are treated as aliases for sshkey <ahem> type + # so they are populated as the *values* below + aliases = {"dsa" => "ssh-dss", + "ed25519" => "ssh-ed25519", + "rsa" => "ssh-rsa"} + types.each do |type| + it "should update an entry with #{type} type" do + manifest = "#{type_under_test} { '#{sshkey_name}': + ensure => 'present', + type => '#{type}', + key => 'mynewshinykey', + target => '#{@sshkey_file}' }" + + apply_with_error_check(manifest) + if aliases.has_key?(type) + full_type = aliases[type] + expect(File.read(@sshkey_file)). + to match(/#{sshkey_name}.*#{full_type}.*mynew/) + else + expect(File.read(@sshkey_file)). + to match(/#{sshkey_name}.*#{type}.*mynew/) + end + end + end + + # test unknown key type fails + let(:invalid_type) { 'ssh-er0ck' } + it "should raise an error with an unknown type" do + manifest = "#{type_under_test} { '#{sshkey_name}': + ensure => 'present', + type => '#{invalid_type}', + key => 'mynewshinykey', + target => '#{@sshkey_file}' }" + expect { + apply_compiled_manifest(manifest) + }.to raise_error(Puppet::ResourceError, /Invalid value "#{invalid_type}"/) + end + + #single host_alias + let(:host_alias) { 'r0ckdata.com' } + it "should update an entry with new host_alias" do + manifest = "#{type_under_test} { '#{sshkey_name}': + ensure => 'present', + host_aliases => '#{host_alias}', + target => '#{@sshkey_file}' }" + apply_with_error_check(manifest) + expect(File.read(@sshkey_file)).to match(/#{sshkey_name},#{host_alias}\s/) + expect(File.read(@sshkey_file)).not_to match(/#{sshkey_name}\s/) + end + + #array host_alias + let(:host_aliases) { "r0ckdata.com,erict.net" } + it "should update an entry with new host_alias" do + manifest = "#{type_under_test} { '#{sshkey_name}': + ensure => 'present', + host_aliases => '#{host_alias}', + target => '#{@sshkey_file}' }" + apply_with_error_check(manifest) + expect(File.read(@sshkey_file)).to match(/#{sshkey_name},#{host_alias}\s/) + expect(File.read(@sshkey_file)).not_to match(/#{sshkey_name}\s/) + end + + #puppet resource sshkey + it "should fetch an entry from resources" do + @resource_app = Puppet::Application[:resource] + @resource_app.preinit + @resource_app.command_line.stubs(:args). + returns([type_under_test, sshkey_name, "target=#{@sshkey_file}"]) + + @resource_app.expects(:puts).with do |args| + expect(args).to match(/#{sshkey_name}/) + end + @resource_app.main + end + + end + +end diff --git a/spec/lib/puppet_spec/compiler.rb b/spec/lib/puppet_spec/compiler.rb new file mode 100644 index 0000000..8964a26 --- /dev/null +++ b/spec/lib/puppet_spec/compiler.rb @@ -0,0 +1,112 @@ +module PuppetSpec::Compiler + module_function + + def compile_to_catalog(string, node = Puppet::Node.new('test')) + Puppet[:code] = string + # see lib/puppet/indirector/catalog/compiler.rb#filter + Puppet::Parser::Compiler.compile(node).filter { |r| r.virtual? } + end + + # Does not removed virtual resources in compiled catalog (i.e. keeps unrealized) + def compile_to_catalog_unfiltered(string, node = Puppet::Node.new('test')) + Puppet[:code] = string + # see lib/puppet/indirector/catalog/compiler.rb#filter + Puppet::Parser::Compiler.compile(node) + end + + def compile_to_ral(manifest, node = Puppet::Node.new('test')) + catalog = compile_to_catalog(manifest, node) + ral = catalog.to_ral + ral.finalize + ral + end + + def compile_to_relationship_graph(manifest, prioritizer = Puppet::Graph::SequentialPrioritizer.new) + ral = compile_to_ral(manifest) + graph = Puppet::Graph::RelationshipGraph.new(prioritizer) + graph.populate_from(ral) + graph + end + + def apply_compiled_manifest(manifest, prioritizer = Puppet::Graph::SequentialPrioritizer.new) + catalog = compile_to_ral(manifest) + if block_given? + catalog.resources.each { |res| yield res } + end + transaction = Puppet::Transaction.new(catalog, + Puppet::Transaction::Report.new, + prioritizer) + transaction.evaluate + transaction.report.finalize_report + + transaction + end + + def apply_with_error_check(manifest) + apply_compiled_manifest(manifest) do |res| + res.expects(:err).never + end + end + + def order_resources_traversed_in(relationships) + order_seen = [] + relationships.traverse { |resource| order_seen << resource.ref } + order_seen + end + + def collect_notices(code, node = Puppet::Node.new('foonode')) + Puppet[:code] = code + compiler = Puppet::Parser::Compiler.new(node) + node.environment.check_for_reparse + logs = [] + Puppet::Util::Log.with_destination(Puppet::Test::LogCollector.new(logs)) do + yield(compiler) + end + logs = logs.select { |log| log.level == :notice }.map { |log| log.message } + logs + end + + def eval_and_collect_notices(code, node = Puppet::Node.new('foonode'), topscope_vars = {}) + collect_notices(code, node) do |compiler| + unless topscope_vars.empty? + scope = compiler.topscope + topscope_vars.each {|k,v| scope.setvar(k, v) } + end + if block_given? + compiler.compile do |catalog| + yield(compiler.topscope, catalog) + catalog + end + else + compiler.compile + end + end + end + + # Compiles a catalog, and if source is given evaluates it and returns its result. + # The catalog is returned if no source is given. + # Topscope variables are set before compilation + # Uses a created node 'testnode' if none is given. + # (Parameters given by name) + # + def evaluate(code: 'undef', source: nil, node: Puppet::Node.new('testnode'), variables: {}) + source_location = caller[0] + Puppet[:code] = code + compiler = Puppet::Parser::Compiler.new(node) + unless variables.empty? + scope = compiler.topscope + variables.each {|k,v| scope.setvar(k, v) } + end + + if source.nil? + compiler.compile + # see lib/puppet/indirector/catalog/compiler.rb#filter + return compiler.filter { |r| r.virtual? } + end + + # evaluate given source is the context of the compiled state and return its result + compiler.compile do |catalog | + Puppet::Pops::Parser::EvaluatingParser.singleton.evaluate_string(compiler.topscope, source, source_location) + end + end +end diff --git a/spec/lib/puppet_spec/files.rb b/spec/lib/puppet_spec/files.rb new file mode 100644 index 0000000..a6529f6 --- /dev/null +++ b/spec/lib/puppet_spec/files.rb @@ -0,0 +1,126 @@ +require 'fileutils' +require 'tempfile' +require 'tmpdir' +require 'pathname' + +# A support module for testing files. +module PuppetSpec::Files + @global_tempfiles = [] + + def self.cleanup + until @global_tempfiles.empty? + path = @global_tempfiles.pop + begin + Dir.unstub(:entries) + FileUtils.rm_rf path, secure: true + rescue Errno::ENOENT # rubocop:disable Lint/HandleExceptions + # nothing to do + end + end + end + + def make_absolute(path) + PuppetSpec::Files.make_absolute(path) + end + + def self.make_absolute(path) + path = File.expand_path(path) + path[0] = 'c' if Puppet.features.microsoft_windows? + path + end + + def tmpfile(name, dir = nil) + PuppetSpec::Files.tmpfile(name, dir) + end + + def self.tmpfile(name, dir = nil) + # Generate a temporary file, just for the name... + source = dir ? Tempfile.new(name, dir) : Tempfile.new(name) + path = Puppet::FileSystem.expand_path(source.path.encode(Encoding::UTF_8)) + source.close! + + record_tmp(File.expand_path(path)) + + path + end + + def file_containing(name, contents) + PuppetSpec::Files.file_containing(name, contents) + end + + def self.file_containing(name, contents) + file = tmpfile(name) + File.open(file, 'wb') { |f| f.write(contents) } + file + end + + def script_containing(name, contents) + PuppetSpec::Files.script_containing(name, contents) + end + + def self.script_containing(name, contents) + file = tmpfile(name) + if Puppet.features.microsoft_windows? + file += '.bat' + text = contents[:windows] + else + text = contents[:posix] + end + File.open(file, 'wb') { |f| f.write(text) } + Puppet::FileSystem.chmod(0o755, file) + file + end + + def tmpdir(name) + PuppetSpec::Files.tmpdir(name) + end + + def self.tmpdir(name) + dir = Puppet::FileSystem.expand_path(Dir.mktmpdir(name).encode!(Encoding::UTF_8)) + + record_tmp(dir) + + dir + end + + def dir_containing(name, contents_hash) + PuppetSpec::Files.dir_containing(name, contents_hash) + end + + def self.dir_containing(name, contents_hash) + dir_contained_in(tmpdir(name), contents_hash) + end + + def dir_contained_in(dir, contents_hash) + PuppetSpec::Files.dir_contained_in(dir, contents_hash) + end + + def self.dir_contained_in(dir, contents_hash) + contents_hash.each do |k, v| + if v.is_a?(Hash) + Dir.mkdir(tmp = File.join(dir, k)) + dir_contained_in(tmp, v) + else + file = File.join(dir, k) + File.open(file, 'wb') { |f| f.write(v) } + end + end + dir + end + + def self.record_tmp(tmp) + # ...record it for cleanup, + @global_tempfiles ||= [] + @global_tempfiles << tmp + end + + def expect_file_mode(file, mode) + actual_mode = '%o' % Puppet::FileSystem.stat(file).mode + target_mode = if Puppet.features.microsoft_windows? + mode + else + '10' + '%04i' % mode.to_i + end + expect(actual_mode).to eq(target_mode) + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..e69d11d --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,45 @@ + +require 'puppetlabs_spec_helper/module_spec_helper' +require 'rspec-puppet-facts' + +begin + require 'spec_helper_local' if File.file?(File.join(File.dirname(__FILE__), 'spec_helper_local.rb')) +rescue LoadError => loaderror + warn "Could not require spec_helper_local: #{loaderror.message}" +end + +include RspecPuppetFacts + +default_facts = { + puppetversion: Puppet.version, + facterversion: Facter.version, +} + +default_facts_path = File.expand_path(File.join(File.dirname(__FILE__), 'default_facts.yml')) +default_module_facts_path = File.expand_path(File.join(File.dirname(__FILE__), 'default_module_facts.yml')) + +if File.exist?(default_facts_path) && File.readable?(default_facts_path) + default_facts.merge!(YAML.safe_load(File.read(default_facts_path))) +end + +if File.exist?(default_module_facts_path) && File.readable?(default_module_facts_path) + default_facts.merge!(YAML.safe_load(File.read(default_module_facts_path))) +end + +RSpec.configure do |c| + c.default_facts = default_facts + c.before :each do + # set to strictest setting for testing + # by default Puppet runs at warning level + Puppet.settings[:strict] = :warning + end +end + +def ensure_module_defined(module_name) + module_name.split('::').reduce(Object) do |last_module, next_module| + last_module.const_set(next_module, Module.new) unless last_module.const_defined?(next_module) + last_module.const_get(next_module) + end +end + +# 'spec_overrides' from sync.yml will appear below this line diff --git a/spec/spec_helper_local.rb b/spec/spec_helper_local.rb new file mode 100644 index 0000000..fc786a6 --- /dev/null +++ b/spec/spec_helper_local.rb @@ -0,0 +1,17 @@ +dir = File.expand_path(File.dirname(__FILE__)) +$LOAD_PATH.unshift File.join(dir, 'lib') + +# Container for various Puppet-specific RSpec helpers. +module PuppetSpec +end + +require 'puppet_spec/files' + +RSpec.configure do |config| + config.before :each do |test| + base = PuppetSpec::Files.tmpdir('tmp_settings') + Puppet[:vardir] = File.join(base, 'var') + + FileUtils.mkdir_p Puppet[:statedir] + end +end
\ No newline at end of file diff --git a/spec/unit/provider/sshkey/parsed_spec.rb b/spec/unit/provider/sshkey/parsed_spec.rb new file mode 100644 index 0000000..38aa7f7 --- /dev/null +++ b/spec/unit/provider/sshkey/parsed_spec.rb @@ -0,0 +1,93 @@ +#! /usr/bin/env ruby +require 'spec_helper' + +describe "sshkey parsed provider" do + let :type do Puppet::Type.type(:sshkey) end + let :provider do type.provider(:parsed) end + subject { provider } + + after :each do + subject.clear + end + + def key + 'AAAAB3NzaC1yc2EAAAABIwAAAQEAzwHhxXvIrtfIwrudFqc8yQcIfMudrgpnuh1F3AV6d2BrLgu/yQE7W5UyJMUjfj427sQudRwKW45O0Jsnr33F4mUw+GIMlAAmp9g24/OcrTiB8ZUKIjoPy/cO4coxGi8/NECtRzpD/ZUPFh6OEpyOwJPMb7/EC2Az6Otw4StHdXUYw22zHazBcPFnv6zCgPx1hA7QlQDWTu4YcL0WmTYQCtMUb3FUqrcFtzGDD0ytosgwSd+JyN5vj5UwIABjnNOHPZ62EY1OFixnfqX/+dUwrFSs5tPgBF/KkC6R7tmbUfnBON6RrGEmu+ajOTOLy23qUZB4CQ53V7nyAWhzqSK+hw==' + end + + it "should parse the name from the first field" do + expect(subject.parse_line('test ssh-rsa '+key)[:name]).to eq("test") + end + + it "should parse the first component of the first field as the name" do + expect(subject.parse_line('test,alias ssh-rsa '+key)[:name]).to eq("test") + end + + it "should parse host_aliases from the remaining components of the first field" do + expect(subject.parse_line('test,alias ssh-rsa '+key)[:host_aliases]).to eq(["alias"]) + end + + it "should parse multiple host_aliases" do + expect(subject.parse_line('test,alias1,alias2,alias3 ssh-rsa '+key)[:host_aliases]).to eq(["alias1","alias2","alias3"]) + end + + it "should not drop an empty host_alias" do + expect(subject.parse_line('test,alias, ssh-rsa '+key)[:host_aliases]).to eq(["alias",""]) + end + + it "should recognise when there are no host aliases" do + expect(subject.parse_line('test ssh-rsa '+key)[:host_aliases]).to eq([]) + end + + context "with the sample file" do + ['sample', 'sample_with_blank_lines'].each do |sample_file| + let :fixture do my_fixture(sample_file) end + before :each do subject.stubs(:default_target).returns(fixture) end + + it "should parse to records on prefetch" do + expect(subject.target_records(fixture)).to be_empty + subject.prefetch + + records = subject.target_records(fixture) + expect(records).to be_an Array + expect(records).to be_all {|x| expect(x).to be_an Hash } + end + + it "should reconstitute the file from records" do + subject.prefetch + records = subject.target_records(fixture) + text = subject.to_file(records).gsub(/^# HEADER.+\n/, '') + + oldlines = File.readlines(fixture).map(&:chomp) + newlines = text.chomp.split("\n") + expect(oldlines.length).to eq(newlines.length) + + oldlines.zip(newlines).each do |old, new| + expect(old.gsub(/\s+/, '')).to eq(new.gsub(/\s+/, '')) + end + end + end + end + + context 'default ssh_known_hosts target path' do + ['9.10', '9.11', '10.10'].each do |version| + it 'should be `/etc/ssh_known_hosts` when OSX version 10.10 or older`' do + Facter.expects(:value).with(:operatingsystem).returns('Darwin') + Facter.expects(:value).with(:macosx_productversion_major).returns(version) + expect(subject.default_target).to eq('/etc/ssh_known_hosts') + end + end + + ['10.11', '10.13', '11.0', '11.11'].each do |version| + it 'should be `/etc/ssh/ssh_known_hosts` when OSX version 10.11 or newer`' do + Facter.expects(:value).with(:operatingsystem).returns('Darwin') + Facter.expects(:value).with(:macosx_productversion_major).returns(version) + expect(subject.default_target).to eq('/etc/ssh/ssh_known_hosts') + end + end + + it 'should be `/etc/ssh/ssh_known_hosts` on other operating systems' do + Facter.expects(:value).with(:operatingsystem).returns('RedHat') + expect(subject.default_target).to eq('/etc/ssh/ssh_known_hosts') + end + end +end diff --git a/spec/unit/type/ssh_authorized_key_spec.rb b/spec/unit/type/ssh_authorized_key_spec.rb new file mode 100644 index 0000000..ae93667 --- /dev/null +++ b/spec/unit/type/ssh_authorized_key_spec.rb @@ -0,0 +1,258 @@ +#! /usr/bin/env ruby +require 'spec_helper' + + +describe Puppet::Type.type(:ssh_authorized_key), :unless => Puppet.features.microsoft_windows? do + include PuppetSpec::Files + + before do + provider_class = stub 'provider_class', :name => "fake", :suitable? => true, :supports_parameter? => true + described_class.stubs(:defaultprovider).returns(provider_class) + described_class.stubs(:provider).returns(provider_class) + + provider = stub 'provider', :class => provider_class, :file_path => make_absolute("/tmp/whatever"), :clear => nil + provider_class.stubs(:new).returns(provider) + end + + it "has :name as its namevar" do + expect(described_class.key_attributes).to eq [:name] + end + + describe "when validating attributes" do + + [:name, :provider].each do |param| + it "has a #{param} parameter" do + expect(described_class.attrtype(param)).to eq :param + end + end + + [:type, :key, :user, :target, :options, :ensure].each do |property| + it "has a #{property} property" do + expect(described_class.attrtype(property)).to eq :property + end + end + + end + + describe "when validating values" do + + describe "for name" do + + it "supports valid names" do + described_class.new(:name => "username", :ensure => :present, :user => "nobody") + described_class.new(:name => "username@hostname", :ensure => :present, :user => "nobody") + end + + it "supports whitespace" do + described_class.new(:name => "my test", :ensure => :present, :user => "nobody") + end + + end + + describe "for ensure" do + + it "supports :present" do + described_class.new(:name => "whev", :ensure => :present, :user => "nobody") + end + + it "supports :absent" do + described_class.new(:name => "whev", :ensure => :absent, :user => "nobody") + end + + it "nots support other values" do + expect { described_class.new(:name => "whev", :ensure => :foo, :user => "nobody") }.to raise_error(Puppet::Error, /Invalid value/) + end + + end + + describe "for type" do + + [ + :'ssh-dss', :dsa, + :'ssh-rsa', :rsa, + :'ecdsa-sha2-nistp256', + :'ecdsa-sha2-nistp384', + :'ecdsa-sha2-nistp521', + :ed25519, :'ssh-ed25519', + ].each do |keytype| + it "supports #{keytype}" do + described_class.new(:name => "whev", :type => keytype, :user => "nobody") + end + end + + it "aliases :rsa to :ssh-rsa" do + key = described_class.new(:name => "whev", :type => :rsa, :user => "nobody") + expect(key.should(:type)).to eq :'ssh-rsa' + end + + it "aliases :dsa to :ssh-dss" do + key = described_class.new(:name => "whev", :type => :dsa, :user => "nobody") + expect(key.should(:type)).to eq :'ssh-dss' + end + + it "doesn't support values other than ssh-dss, ssh-rsa, dsa, rsa" do + expect { described_class.new(:name => "whev", :type => :something) }.to raise_error(Puppet::Error,/Invalid value/) + end + + end + + describe "for key" do + + it "supports a valid key like a 1024 bit rsa key" do + expect { described_class.new(:name => "whev", :type => :rsa, :user => "nobody", :key => 'AAAAB3NzaC1yc2EAAAADAQABAAAAgQDCPfzW2ry7XvMc6E5Kj2e5fF/YofhKEvsNMUogR3PGL/HCIcBlsEjKisrY0aYgD8Ikp7ZidpXLbz5dBsmPy8hJiBWs5px9ZQrB/EOQAwXljvj69EyhEoGawmxQMtYw+OAIKHLJYRuk1QiHAMHLp5piqem8ZCV2mLb9AsJ6f7zUVw==')}.to_not raise_error + end + + it "supports a valid key like a 4096 bit rsa key" do + expect { described_class.new(:name => "whev", :type => :rsa, :user => "nobody", :key => 'AAAAB3NzaC1yc2EAAAADAQABAAACAQDEY4pZFyzSfRc9wVWI3DfkgT/EL033UZm/7x1M+d+lBD00qcpkZ6CPT7lD3Z+vylQlJ5S8Wcw6C5Smt6okZWY2WXA9RCjNJMIHQbJAzwuQwgnwU/1VMy9YPp0tNVslg0sUUgpXb13WW4mYhwxyGmIVLJnUrjrQmIFhtfHsJAH8ZVqCWaxKgzUoC/YIu1u1ScH93lEdoBPLlwm6J0aiM7KWXRb7Oq1nEDZtug1zpX5lhgkQWrs0BwceqpUbY+n9sqeHU5e7DCyX/yEIzoPRW2fe2Gx1Iq6JKM/5NNlFfaW8rGxh3Z3S1NpzPHTRjw8js3IeGiV+OPFoaTtM1LsWgPDSBlzIdyTbSQR7gKh0qWYCNV/7qILEfa0yIFB5wIo4667iSPZw2pNgESVtenm8uXyoJdk8iWQ4mecdoposV/znknNb2GPgH+n/2vme4btZ0Sl1A6rev22GQjVgbWOn8zaDglJ2vgCN1UAwmq41RXprPxENGeLnWQppTnibhsngu0VFllZR5kvSIMlekLRSOFLFt92vfd+tk9hZIiKm9exxcbVCGGQPsf6dZ27rTOmg0xM2Sm4J6RRKuz79HQgA4Eg18+bqRP7j/itb89DmtXEtoZFAsEJw8IgIfeGGDtHTkfAlAC92mtK8byeaxGq57XCTKbO/r5gcOMElZHy1AcB8kw==')}.to_not raise_error + end + + it "supports a valid key like a 1024 bit dsa key" do + expect { described_class.new(:name => "whev", :type => :dsa, :user => "nobody", :key => 'AAAAB3NzaC1kc3MAAACBAI80iR78QCgpO4WabVqHHdEDigOjUEHwIjYHIubR/7u7DYrXY+e+TUmZ0CVGkiwB/0yLHK5dix3Y/bpj8ZiWCIhFeunnXccOdE4rq5sT2V3l1p6WP33RpyVYbLmeuHHl5VQ1CecMlca24nHhKpfh6TO/FIwkMjghHBfJIhXK+0w/AAAAFQDYzLupuMY5uz+GVrcP+Kgd8YqMmwAAAIB3SVN71whLWjFPNTqGyyIlMy50624UfNOaH4REwO+Of3wm/cE6eP8n75vzTwQGBpJX3BPaBGW1S1Zp/DpTOxhCSAwZzAwyf4WgW7YyAOdxN3EwTDJZeyiyjWMAOjW9/AOWt9gtKg0kqaylbMHD4kfiIhBzo31ZY81twUzAfN7angAAAIBfva8sTSDUGKsWWIXkdbVdvM4X14K4gFdy0ZJVzaVOtZ6alysW6UQypnsl6jfnbKvsZ0tFgvcX/CPyqNY/gMR9lyh/TCZ4XQcbqeqYPuceGehz+jL5vArfqsW2fJYFzgCcklmr/VxtP5h6J/T0c9YcDgc/xIfWdZAlznOnphI/FA==')}.to_not raise_error + end + + it "doesn't support whitespaces" do + expect { described_class.new(:name => "whev", :type => :rsa, :user => "nobody", :key => 'AAA FA==')}.to raise_error(Puppet::Error,/Key must not contain whitespace/) + end + + end + + describe "for options" do + + it "supports flags as options" do + expect { described_class.new(:name => "whev", :type => :rsa, :user => "nobody", :options => 'cert-authority')}.to_not raise_error + expect { described_class.new(:name => "whev", :type => :rsa, :user => "nobody", :options => 'no-port-forwarding')}.to_not raise_error + end + + it "supports key-value pairs as options" do + expect { described_class.new(:name => "whev", :type => :rsa, :user => "nobody", :options => 'command="command"')}.to_not raise_error + end + + it "supports key-value pairs where value consist of multiple items" do + expect { described_class.new(:name => "whev", :type => :rsa, :user => "nobody", :options => 'from="*.domain1,host1.domain2"')}.to_not raise_error + end + + it "supports environments as options" do + expect { described_class.new(:name => "whev", :type => :rsa, :user => "nobody", :options => 'environment="NAME=value"')}.to_not raise_error + end + + it "supports multiple options as an array" do + expect { described_class.new(:name => "whev", :type => :rsa, :user => "nobody", :options => ['cert-authority','environment="NAME=value"'])}.to_not raise_error + end + + it "doesn't support a comma separated list" do + expect { described_class.new(:name => "whev", :type => :rsa, :user => "nobody", :options => 'cert-authority,no-port-forwarding')}.to raise_error(Puppet::Error, /must be provided as an array/) + end + + it "uses :absent as a default value" do + expect(described_class.new(:name => "whev", :type => :rsa, :user => "nobody").should(:options)).to eq [:absent] + end + + it "property should return well formed string of arrays from is_to_s" do + resource = described_class.new(:name => "whev", :type => :rsa, :user => "nobody", :options => ["a","b","c"]) + expect(resource.property(:options).is_to_s(["a","b","c"])).to eq "['a', 'b', 'c']" + end + + it "property should return well formed string of arrays from should_to_s" do + resource = described_class.new(:name => "whev", :type => :rsa, :user => "nobody", :options => ["a","b","c"]) + expect(resource.property(:options).should_to_s(["a","b","c"])).to eq "['a', 'b', 'c']" + end + + end + + describe "for user" do + + it "supports present users" do + described_class.new(:name => "whev", :type => :rsa, :user => "root") + end + + it "supports absent users" do + described_class.new(:name => "whev", :type => :rsa, :user => "ihopeimabsent") + end + + end + + describe "for target" do + + it "supports absolute paths" do + described_class.new(:name => "whev", :type => :rsa, :target => "/tmp/here") + end + + it "uses the user's path if not explicitly specified" do + expect(described_class.new(:name => "whev", :user => 'root').should(:target)).to eq File.expand_path("~root/.ssh/authorized_keys") + end + + it "doesn't consider the user's path if explicitly specified" do + expect(described_class.new(:name => "whev", :user => 'root', :target => '/tmp/here').should(:target)).to eq '/tmp/here' + end + + it "informs about an absent user" do + Puppet::Log.level = :debug + described_class.new(:name => "whev", :user => 'idontexist').should(:target) + expect(@logs.map(&:message)).to include("The required user is not yet present on the system") + end + + end + + end + + describe "when neither user nor target is specified" do + + it "raises an error" do + expect do + described_class.new( + :name => "Test", + :key => "AAA", + :type => "ssh-rsa", + :ensure => :present) + end.to raise_error(Puppet::Error,/user.*or.*target.*mandatory/) + end + + end + + describe "when both target and user are specified" do + + it "uses target" do + resource = described_class.new( + :name => "Test", + :user => "root", + :target => "/tmp/blah" + ) + expect(resource.should(:target)).to eq "/tmp/blah" + end + + end + + + describe "when user is specified" do + + it "determines target" do + resource = described_class.new( + :name => "Test", + :user => "root" + ) + target = File.expand_path("~root/.ssh/authorized_keys") + expect(resource.should(:target)).to eq target + end + + # Bug #2124 - ssh_authorized_key always changes target if target is not defined + it "doesn't raise spurious change events" do + resource = described_class.new(:name => "Test", :user => "root") + target = File.expand_path("~root/.ssh/authorized_keys") + expect(resource.property(:target).safe_insync?(target)).to eq true + end + + end + + describe "when calling validate" do + + it "doesn't crash on a non-existent user" do + resource = described_class.new( + :name => "Test", + :user => "ihopesuchuserdoesnotexist" + ) + resource.validate + end + + end + +end diff --git a/spec/unit/type/sshkey_spec.rb b/spec/unit/type/sshkey_spec.rb new file mode 100644 index 0000000..d16e595 --- /dev/null +++ b/spec/unit/type/sshkey_spec.rb @@ -0,0 +1,77 @@ +#! /usr/bin/env ruby +require 'spec_helper' + + +describe Puppet::Type.type(:sshkey) do + + it "uses :name as its namevar" do + expect(described_class.key_attributes).to eq [:name] + end + + describe "when validating attributes" do + [:name, :provider].each do |param| + it "has a #{param} parameter" do + expect(described_class.attrtype(param)).to eq :param + end + end + + [:host_aliases, :ensure, :key, :type].each do |property| + it "has a #{property} property" do + expect(described_class.attrtype(property)).to eq :property + end + end + end + + describe "when validating values" do + + [ + :'ssh-dss', :dsa, + :'ssh-rsa', :rsa, + :'ecdsa-sha2-nistp256', + :'ecdsa-sha2-nistp384', + :'ecdsa-sha2-nistp521', + :'ssh-ed25519', :ed25519, + ].each do |keytype| + it "supports #{keytype} as a type value" do + described_class.new(:name => "foo", :type => keytype) + end + end + + it "aliases :rsa to :ssh-rsa" do + key = described_class.new(:name => "foo", :type => :rsa) + expect(key.should(:type)).to eq :'ssh-rsa' + end + + it "aliases :dsa to :ssh-dss" do + key = described_class.new(:name => "foo", :type => :dsa) + expect(key.should(:type)).to eq :'ssh-dss' + end + + it "doesn't support values other than ssh-dss, ssh-rsa, dsa, rsa for type" do + expect { + described_class.new(:name => "whev", :type => :'ssh-dsa') + }.to raise_error(Puppet::Error, /Invalid value.*ssh-dsa/) + end + + it "accepts one host_alias" do + described_class.new(:name => "foo", :host_aliases => 'foo.bar.tld') + end + + it "accepts multiple host_aliases as an array" do + described_class.new(:name => "foo", :host_aliases => ['foo.bar.tld','10.0.9.9']) + end + + it "doesn't accept spaces in any host_alias" do + expect { + described_class.new(:name => "foo", :host_aliases => ['foo.bar.tld','foo bar']) + }.to raise_error(Puppet::Error, /cannot include whitespace/) + end + + it "doesn't accept aliases in the resourcename" do + expect { + described_class.new(:name => 'host,host.domain,ip') + }.to raise_error(Puppet::Error, /No comma in resourcename/) + end + + end +end |