diff --git a/lib/puppet/provider/ssh_authorized_key/parsed.rb b/lib/puppet/provider/ssh_authorized_key/parsed.rb index 6a3855c0e..81b1fbcfa 100644 --- a/lib/puppet/provider/ssh_authorized_key/parsed.rb +++ b/lib/puppet/provider/ssh_authorized_key/parsed.rb @@ -1,96 +1,90 @@ 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 => /^#/ text_line :blank, :match => /^\s+/ record_line :parsed, :fields => %w{options type key name}, :optional => %w{options}, :rts => /^\s+/, :match => /^(?:(.+) )?(ssh-dss|ssh-rsa) ([^ ]+) ?(.*)$/, :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| 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 target - @resource.should(:target) || File.expand_path("~#{@resource.should(:user)}/.ssh/authorized_keys") - rescue - raise Puppet::Error, "Target not defined and/or specified user does not exist yet" - end - def user uid = File.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 uid = Puppet::Util.uid(@resource.should(:user)) unless File.exist?(dir = File.dirname(target)) Puppet.debug "Creating #{dir}" Dir.mkdir(dir, dir_perm) File.chown(uid, nil, dir) end # 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 supresses the later attempt by our superclass's flush method. self.class.backup_target(target) Puppet::Util::SUIDManager.asuser(@resource.should(:user)) { super } File.chown(uid, nil, target) File.chmod(file_perm, target) end # parse sshv2 option strings, wich 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 end diff --git a/lib/puppet/type/ssh_authorized_key.rb b/lib/puppet/type/ssh_authorized_key.rb index 8338e2d64..170dc8383 100644 --- a/lib/puppet/type/ssh_authorized_key.rb +++ b/lib/puppet/type/ssh_authorized_key.rb @@ -1,102 +1,114 @@ module Puppet newtype(:ssh_authorized_key) do @doc = "Manages SSH authorized keys. Currently only type 2 keys are supported. **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 attribute is currently used as a system-wide primary key and therefore has to be unique." isnamevar + + validate do |value| + raise Puppet::Error, "Resourcename must not contain whitespace: #{value}" if value =~ /\s/ + end end newproperty(:type) do desc "The encryption type used: ssh-dss or ssh-rsa." newvalue("ssh-dss") newvalue("ssh-rsa") aliasvalue(:dsa, "ssh-dss") aliasvalue(:rsa, "ssh-rsa") end newproperty(:key) do desc "The key itself; generally a long string of hex digits." + + validate do |value| + raise Puppet::Error, "Key must not contain whitespace: #{value}" if value =~ /\s/ + end end newproperty(:user) do desc "The user account in which the SSH key should be installed. The resource will automatically depend on this user." end newproperty(:target) do desc "The absolute filename in which to store the SSH key. This property is optional and should only be used in cases where keys are stored in a non-standard location (i.e.` 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 def is_to_s(value) if value == :absent or value.include?(:absent) super else value.join(",") end end def should_to_s(value) if value == :absent or value.include?(:absent) super else value.join(",") end end + + validate do |value| + raise Puppet::Error, "Options must be provided as an array, not a comma separated list" if value != :absent and value.include?(',') + 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 end end 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 000000000..902f9ad22 --- /dev/null +++ b/spec/integration/provider/ssh_authorized_key_spec.rb @@ -0,0 +1,207 @@ +#!/usr/bin/env ruby + +require 'spec_helper' +require 'puppet/file_bucket/dipper' + +describe "ssh_authorized_key provider (integration)" do + include PuppetSpec::Files + + before :each do + @fake_userfile = tmpfile('authorized_keys.user') + @fake_rootfile = tmpfile('authorized_keys.root') + + # few testkeys generated with ssh-keygen + @sample_rsa_keys = [ + '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 + ] + @sample_dsa_keys = [ + 'AAAAB3NzaC1kc3MAAACBAOPck2O8MIDSqxPSnvENt6tzRrKJ5oOhB6Nc6oEcWm+VEH1gvuxdiRqwoMgRwyEf1yUd+UAcLw3a6Jn+EtFyEBN/5WF+4Tt4xTxZ0Pfik2Wc5uqHbQ2dkmOoXiAOYPiD3JUQ1Xwm/J0CgetjitoLfzAGdCNhMqguqAuHcVJ78ZZbAAAAFQCIBKFYZ+I18I+dtgteirXh+VVEEwAAAIEAs1yvQ/wnLLrRCM660pF4kBiw3D6dJfMdCXWQpn0hZmkBQSIzZv4Wuk3giei5luxscDxNc+y3CTXtnyG4Kt1Yi2sOdvhRI3rX8tD+ejn8GHazM05l5VIo9uu4AQPIE32iV63IqgApSBbJ6vDJW91oDH0J492WdLCar4BS/KE3cRwAAACBAN0uSDyJqYLRsfYcFn4HyVf6TJxQm1IcwEt6GcJVzgjri9VtW7FqY5iBqa9B9Zdh5XXAYJ0XLsWQCcrmMHM2XGHGpA4gL9VlCJ/0QvOcXxD2uK7IXwAVUA7g4V4bw8EVnFv2Flufozhsp+4soo1xiYc5jiFVHwVlk21sMhAtKAeF' # 1024 Bit + ] + + @sample_lines = [ + "ssh-rsa #{@sample_rsa_keys[1]} root@someotherhost", + "ssh-dss #{@sample_dsa_keys[0]} root@anywhere", + "ssh-rsa #{@sample_rsa_keys[2]} paul" + ] + + end + + after :each do + Puppet::Type::Ssh_authorized_key::ProviderParsed.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 =~ /^#|^$/ } + content.join("\n").should == 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 + + describe "when managing one resource" do + + before :each do + # We are not running as root so chown/chmod is not possible + File.stubs(:chown) + File.stubs(:chmod) + Puppet::Util::SUIDManager.stubs(:asuser).yields + end + + describe "with ensure set to absent" do + + before :each do + @example = 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(@example) + 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(@example) + check_fake_key(:root, @sample_lines) + end + + end + + describe "when ensure is present" do + + before :each do + @example = Puppet::Type.type(:ssh_authorized_key).new( + :name => 'root@hostname', + :type => :rsa, + :key => @sample_rsa_keys[0], + :target => @fake_rootfile, + :user => 'root', + :ensure => :present + ) + + # just a dummy so the parsedfile provider is aware + # of the user's authorized_keys file + @dummy = Puppet::Type.type(:ssh_authorized_key).new( + :name => 'dummy', + :target => @fake_userfile, + :user => 'nobody', + :ensure => :absent + ) + end + + it "should add the key if it is not present" do + create_fake_key(:root, @sample_lines) + run_in_catalog(@example) + 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(@example) + 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(@example) + 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(@example, @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(@example, @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 + @example[:options]=[ 'from="correct.domain.com"', 'no-port-forwarding', 'no-pty' ] + create_fake_key(:root, @sample_lines + [ "from=\"incorrect.domain.com\",no-port-forwarding,no-pty ssh-rsa #{@sample_rsa_keys[0]} root@hostname"]) + run_in_catalog(@example) + check_fake_key(:root, @sample_lines + [ "from=\"correct.domain.com\",no-port-forwarding,no-pty ssh-rsa #{@sample_rsa_keys[0]} root@hostname"] ) + end + + end + + end + + describe "when managing two resource" do + + before :each do + # We are not running as root so chown/chmod is not possible + File.stubs(:chown) + File.stubs(:chmod) + Puppet::Util::SUIDManager.stubs(:asuser).yields + @example_one = Puppet::Type.type(:ssh_authorized_key).new( + :name => 'root@hostname', + :type => :rsa, + :key => @sample_rsa_keys[0], + :target => @fake_rootfile, + :user => 'root', + :ensure => :present + ) + + @example_two = Puppet::Type.type(:ssh_authorized_key).new( + :name => 'user@hostname', + :key => @sample_rsa_keys[1], + :type => :rsa, + :target => @fake_userfile, + :user => 'nobody', + :ensure => :present + ) + 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(@example_one, @example_two) + 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/unit/provider/ssh_authorized_key/parsed_spec.rb b/spec/unit/provider/ssh_authorized_key/parsed_spec.rb index 69d29c674..bd5e55a9e 100755 --- a/spec/unit/provider/ssh_authorized_key/parsed_spec.rb +++ b/spec/unit/provider/ssh_authorized_key/parsed_spec.rb @@ -1,211 +1,200 @@ #!/usr/bin/env rspec require 'spec_helper' require 'shared_behaviours/all_parsedfile_providers' require 'puppet_spec/files' provider_class = Puppet::Type.type(:ssh_authorized_key).provider(:parsed) describe provider_class do include PuppetSpec::Files before :each do - @sshauthkey_class = Puppet::Type.type(:ssh_authorized_key) - @provider = @sshauthkey_class.provider(:parsed) @keyfile = tmpfile('authorized_keys') - @provider.any_instance.stubs(:target).returns @keyfile + @provider_class = provider_class + @provider_class.initvars + @provider_class.any_instance.stubs(:target).returns @keyfile @user = 'random_bob' Puppet::Util.stubs(:uid).with(@user).returns 12345 end - after :each do - @provider.initvars - end - def mkkey(args) args[:target] = @keyfile args[:user] = @user resource = Puppet::Type.type(:ssh_authorized_key).new(args) - key = @provider.new(resource) + key = @provider_class.new(resource) args.each do |p,v| key.send(p.to_s + "=", v) end key end def genkey(key) - @provider.stubs(:filetype).returns(Puppet::Util::FileType::FileTypeRam) + @provider_class.stubs(:filetype).returns(Puppet::Util::FileType::FileTypeRam) File.stubs(:chown) File.stubs(:chmod) Puppet::Util::SUIDManager.stubs(:asuser).yields key.flush - @provider.target_object(@keyfile).read + @provider_class.target_object(@keyfile).read end it_should_behave_like "all parsedfile providers", provider_class it "should be able to generate a basic authorized_keys file" do - key = mkkey(:name => "Just Testing", + key = mkkey(:name => "Just_Testing", :key => "AAAAfsfddsjldjgksdflgkjsfdlgkj", :type => "ssh-dss", :ensure => :present, :options => [:absent] ) - genkey(key).should == "ssh-dss AAAAfsfddsjldjgksdflgkjsfdlgkj Just Testing\n" + genkey(key).should == "ssh-dss AAAAfsfddsjldjgksdflgkjsfdlgkj Just_Testing\n" end it "should be able to generate a authorized_keys file with options" do key = mkkey(:name => "root@localhost", :key => "AAAAfsfddsjldjgksdflgkjsfdlgkj", :type => "ssh-rsa", :ensure => :present, :options => ['from="192.168.1.1"', "no-pty", "no-X11-forwarding"] ) genkey(key).should == "from=\"192.168.1.1\",no-pty,no-X11-forwarding ssh-rsa AAAAfsfddsjldjgksdflgkjsfdlgkj root@localhost\n" end it "should be able to parse options containing commas via its parse_options method" do options = %w{from="host1.reductlivelabs.com,host.reductivelabs.com" command="/usr/local/bin/run" ssh-pty} optionstr = options.join(", ") - @provider.parse_options(optionstr).should == options + @provider_class.parse_options(optionstr).should == options end it "should use '' as name for entries that lack a comment" do line = "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAut8aOSxenjOqF527dlsdHWV4MNoAsX14l9M297+SQXaQ5Z3BedIxZaoQthkDALlV/25A1COELrg9J2MqJNQc8Xe9XQOIkBQWWinUlD/BXwoOTWEy8C8zSZPHZ3getMMNhGTBO+q/O+qiJx3y5cA4MTbw2zSxukfWC87qWwcZ64UUlegIM056vPsdZWFclS9hsROVEa57YUMrehQ1EGxT4Z5j6zIopufGFiAPjZigq/vqgcAqhAKP6yu4/gwO6S9tatBeEjZ8fafvj1pmvvIplZeMr96gHE7xS3pEEQqnB3nd4RY7AF6j9kFixnsytAUO7STPh/M3pLiVQBN89TvWPQ==" - @provider.parse(line)[0][:name].should == "" + @provider_class.parse(line)[0][:name].should == "" end end describe provider_class do before :each do - @resource = stub("resource", :name => "foo") - @resource.stubs(:[]).returns "foo" - @resource.class.stubs(:key_attributes).returns( [:name] ) + @resource = Puppet::Type.type(:ssh_authorized_key).new(:name => "foo", :user => "random_bob") @provider = provider_class.new(@resource) provider_class.stubs(:filetype).returns(Puppet::Util::FileType::FileTypeRam) Puppet::Util::SUIDManager.stubs(:asuser).yields + + provider_class.initvars end describe "when flushing" do before :each do # Stub file and directory operations Dir.stubs(:mkdir) File.stubs(:chmod) File.stubs(:chown) end describe "and both a user and a target have been specified" do before :each do Puppet::Util.stubs(:uid).with("random_bob").returns 12345 - @resource.stubs(:should).with(:user).returns "random_bob" + @resource[:user] = "random_bob" target = "/tmp/.ssh_dir/place_to_put_authorized_keys" - @resource.stubs(:should).with(:target).returns target + @resource[:target] = target end it "should create the directory" do File.stubs(:exist?).with("/tmp/.ssh_dir").returns false Dir.expects(:mkdir).with("/tmp/.ssh_dir", 0700) @provider.flush end it "should chown the directory to the user" do uid = Puppet::Util.uid("random_bob") File.expects(:chown).with(uid, nil, "/tmp/.ssh_dir") @provider.flush end it "should chown the key file to the user" do uid = Puppet::Util.uid("random_bob") File.expects(:chown).with(uid, nil, "/tmp/.ssh_dir/place_to_put_authorized_keys") @provider.flush end it "should chmod the key file to 0600" do File.expects(:chmod).with(0600, "/tmp/.ssh_dir/place_to_put_authorized_keys") @provider.flush end end describe "and a user has been specified with no target" do before :each do - @resource.stubs(:should).with(:user).returns "nobody" - @resource.stubs(:should).with(:target).returns nil + @resource[:user] = "nobody" # # I'd like to use random_bob here and something like # # File.stubs(:expand_path).with("~random_bob/.ssh").returns "/users/r/random_bob/.ssh" # # but mocha objects strenuously to stubbing File.expand_path # so I'm left with using nobody. @dir = File.expand_path("~nobody/.ssh") end it "should create the directory if it doesn't exist" do File.stubs(:exist?).with(@dir).returns false Dir.expects(:mkdir).with(@dir,0700) @provider.flush end it "should not create or chown the directory if it already exist" do File.stubs(:exist?).with(@dir).returns false Dir.expects(:mkdir).never @provider.flush end it "should chown the directory to the user if it creates it" do File.stubs(:exist?).with(@dir).returns false Dir.stubs(:mkdir).with(@dir,0700) uid = Puppet::Util.uid("nobody") File.expects(:chown).with(uid, nil, @dir) @provider.flush end it "should not create or chown the directory if it already exist" do File.stubs(:exist?).with(@dir).returns false Dir.expects(:mkdir).never File.expects(:chown).never @provider.flush end it "should chown the key file to the user" do uid = Puppet::Util.uid("nobody") File.expects(:chown).with(uid, nil, File.expand_path("~nobody/.ssh/authorized_keys")) @provider.flush end it "should chmod the key file to 0600" do File.expects(:chmod).with(0600, File.expand_path("~nobody/.ssh/authorized_keys")) @provider.flush end end describe "and a target has been specified with no user" do - before :each do - @resource.stubs(:should).with(:user).returns nil - @resource.stubs(:should).with(:target).returns("/tmp/.ssh_dir/place_to_put_authorized_keys") - end - it "should raise an error" do + @resource = Puppet::Type.type(:ssh_authorized_key).new(:name => "foo", :target => "/tmp/.ssh_dir/place_to_put_authorized_keys") + @provider = provider_class.new(@resource) + proc { @provider.flush }.should raise_error end end describe "and a invalid user has been specified with no target" do - before :each do - @resource.stubs(:should).with(:user).returns "thisusershouldnotexist" - @resource.stubs(:should).with(:target).returns nil - end - it "should catch an exception and raise a Puppet error" do + @resource[:user] = "thisusershouldnotexist" + lambda { @provider.flush }.should raise_error(Puppet::Error) end end - end end diff --git a/spec/unit/type/ssh_authorized_key_spec.rb b/spec/unit/type/ssh_authorized_key_spec.rb index a5f167165..71b8a9ab0 100755 --- a/spec/unit/type/ssh_authorized_key_spec.rb +++ b/spec/unit/type/ssh_authorized_key_spec.rb @@ -1,151 +1,261 @@ #!/usr/bin/env rspec require 'spec_helper' ssh_authorized_key = Puppet::Type.type(:ssh_authorized_key) describe ssh_authorized_key do before do @class = Puppet::Type.type(:ssh_authorized_key) @provider_class = stub 'provider_class', :name => "fake", :suitable? => true, :supports_parameter? => true @class.stubs(:defaultprovider).returns(@provider_class) @class.stubs(:provider).returns(@provider_class) @provider = stub 'provider', :class => @provider_class, :file_path => "/tmp/whatever", :clear => nil @provider_class.stubs(:new).returns(@provider) @catalog = Puppet::Resource::Catalog.new end - it "should have a name parameter" do - @class.attrtype(:name).should == :param - end - it "should have :name be its namevar" do @class.key_attributes.should == [:name] end - it "should have a :provider parameter" do - @class.attrtype(:provider).should == :param - end + describe "when validating attributes" do - it "should have an ensure property" do - @class.attrtype(:ensure).should == :property - end + [:name, :provider].each do |param| + it "should have a #{param} parameter" do + @class.attrtype(param).should == :param + end + end - it "should support :present as a value for :ensure" do - proc { @class.new(:name => "whev", :ensure => :present, :user => "nobody") }.should_not raise_error - end + [:type, :key, :user, :target, :options, :ensure].each do |property| + it "should have a #{property} property" do + @class.attrtype(property).should == :property + end + end - it "should support :absent as a value for :ensure" do - proc { @class.new(:name => "whev", :ensure => :absent, :user => "nobody") }.should_not raise_error end - it "should have an type property" do - @class.attrtype(:type).should == :property - end - it "should support ssh-dss as an type value" do - proc { @class.new(:name => "whev", :type => "ssh-dss", :user => "nobody") }.should_not raise_error - end - it "should support ssh-rsa as an type value" do - proc { @class.new(:name => "whev", :type => "ssh-rsa", :user => "nobody") }.should_not raise_error - end - it "should support :dsa as an type value" do - proc { @class.new(:name => "whev", :type => :dsa, :user => "nobody") }.should_not raise_error - end - it "should support :rsa as an type value" do - proc { @class.new(:name => "whev", :type => :rsa, :user => "nobody") }.should_not raise_error - end + describe "when validating values" do - it "should not support values other than ssh-dss, ssh-rsa, dsa, rsa in the ssh_authorized_key_type" do - proc { @class.new(:name => "whev", :type => :something) }.should raise_error(Puppet::Error) - end + describe "for name" do - it "should have an key property" do - @class.attrtype(:key).should == :property - end + it "should support valid names" do + proc { @class.new(:name => "username", :ensure => :present, :user => "nobody") }.should_not raise_error + proc { @class.new(:name => "username@hostname", :ensure => :present, :user => "nobody") }.should_not raise_error + end - it "should have an user property" do - @class.attrtype(:user).should == :property - end + it "should not support whitespaces" do + proc { @class.new(:name => "my test", :ensure => :present, :user => "nobody") }.should raise_error(Puppet::Error,/Resourcename must not contain whitespace/) + proc { @class.new(:name => "my\ttest", :ensure => :present, :user => "nobody") }.should raise_error(Puppet::Error,/Resourcename must not contain whitespace/) + end - it "should have an options property" do - @class.attrtype(:options).should == :property - end + end - it "'s options property should return well formed string of arrays from is_to_s" do - resource = @class.new(:name => "whev", :type => :rsa, :user => "nobody", :options => ["a","b","c"]) + describe "for ensure" do - resource.property(:options).is_to_s(["a","b","c"]).should == "a,b,c" - end + it "should support :present" do + proc { @class.new(:name => "whev", :ensure => :present, :user => "nobody") }.should_not raise_error + end - it "'s options property should return well formed string of arrays from is_to_s" do - resource = @class.new(:name => "whev", :type => :rsa, :user => "nobody", :options => ["a","b","c"]) + it "should support :absent" do + proc { @class.new(:name => "whev", :ensure => :absent, :user => "nobody") }.should_not raise_error + end - resource.property(:options).should_to_s(["a","b","c"]).should == "a,b,c" - end + it "should not support other values" do + proc { @class.new(:name => "whev", :ensure => :foo, :user => "nobody") }.should raise_error(Puppet::Error, /Invalid value/) + end + + end + + describe "for type" do + + + it "should support ssh-dss" do + proc { @class.new(:name => "whev", :type => "ssh-dss", :user => "nobody") }.should_not raise_error + end + + it "should support ssh-rsa" do + proc { @class.new(:name => "whev", :type => "ssh-rsa", :user => "nobody") }.should_not raise_error + end + + it "should support :dsa" do + proc { @class.new(:name => "whev", :type => :dsa, :user => "nobody") }.should_not raise_error + end + + it "should support :rsa" do + proc { @class.new(:name => "whev", :type => :rsa, :user => "nobody") }.should_not raise_error + end + + it "should alias :rsa to :ssh-rsa" do + key = @class.new(:name => "whev", :type => :rsa, :user => "nobody") + key.should(:type).should == :'ssh-rsa' + end + + it "should alias :dsa to :ssh-dss" do + key = @class.new(:name => "whev", :type => :dsa, :user => "nobody") + key.should(:type).should == :'ssh-dss' + end + + it "should not support values other than ssh-dss, ssh-rsa, dsa, rsa" do + proc { @class.new(:name => "whev", :type => :something) }.should raise_error(Puppet::Error,/Invalid value/) + end + + end + + describe "for key" do + + it "should support a valid key like a 1024 bit rsa key" do + proc { @class.new(:name => "whev", :type => :rsa, :user => "nobody", :key => 'AAAAB3NzaC1yc2EAAAADAQABAAAAgQDCPfzW2ry7XvMc6E5Kj2e5fF/YofhKEvsNMUogR3PGL/HCIcBlsEjKisrY0aYgD8Ikp7ZidpXLbz5dBsmPy8hJiBWs5px9ZQrB/EOQAwXljvj69EyhEoGawmxQMtYw+OAIKHLJYRuk1QiHAMHLp5piqem8ZCV2mLb9AsJ6f7zUVw==')}.should_not raise_error + end + + it "should support a valid key like a 4096 bit rsa key" do + proc { @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==')}.should_not raise_error + end + + it "should support a valid key like a 1024 bit dsa key" do + proc { @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==')}.should_not raise_error + end + + it "should not support whitespaces" do + proc { @class.new(:name => "whev", :type => :rsa, :user => "nobody", :key => 'AAA FA==')}.should raise_error(Puppet::Error,/Key must not contain whitespace/) + end + + end + + describe "for options" do + + it "should support flags as options" do + proc { @class.new(:name => "whev", :type => :rsa, :user => "nobody", :options => 'cert-authority')}.should_not raise_error + proc { @class.new(:name => "whev", :type => :rsa, :user => "nobody", :options => 'no-port-forwarding')}.should_not raise_error + end + + it "should support key-value pairs as options" do + proc { @class.new(:name => "whev", :type => :rsa, :user => "nobody", :options => 'command="command"')}.should_not raise_error + end + + it "should support environments as options" do + proc { @class.new(:name => "whev", :type => :rsa, :user => "nobody", :options => 'environment="NAME=value"')}.should_not raise_error + end + + it "should support multiple options as an array" do + proc { @class.new(:name => "whev", :type => :rsa, :user => "nobody", :options => ['cert-authority','environment="NAME=value"'])}.should_not raise_error + end + + it "should not support a comma separated lists" do + proc { @class.new(:name => "whev", :type => :rsa, :user => "nobody", :options => 'cert-authority,no-port-forwarding')}.should raise_error(Puppet::Error, /must be provided as an array/) + end + + it "should use :absent as a default value" do + @class.new(:name => "whev", :type => :rsa, :user => "nobody").should(:options).should == [:absent] + end + + it "property should return well formed string of arrays from is_to_s" do + resource = @class.new(:name => "whev", :type => :rsa, :user => "nobody", :options => ["a","b","c"]) + resource.property(:options).is_to_s(["a","b","c"]).should == "a,b,c" + end + + it "property should return well formed string of arrays from is_to_s" do + resource = @class.new(:name => "whev", :type => :rsa, :user => "nobody", :options => ["a","b","c"]) + resource.property(:options).should_to_s(["a","b","c"]).should == "a,b,c" + end + + end + + describe "for user" do + + it "should support present users" do + proc { @class.new(:name => "whev", :type => :rsa, :user => "root") }.should_not raise_error + end + + it "should support absent users" do + proc { @class.new(:name => "whev", :type => :rsa, :user => "ihopeimabsent") }.should_not raise_error + end + + end + + describe "for target" do + + it "should support absolute paths" do + proc { @class.new(:name => "whev", :type => :rsa, :target => "/tmp/here") }.should_not raise_error + end + + it "should use the user's path if not explicitly specified" do + @class.new(:name => "whev", :user => 'root').should(:target).should == File.expand_path("~root/.ssh/authorized_keys") + end + + it "should not consider the user's path if explicitly specified" do + @class.new(:name => "whev", :user => 'root', :target => '/tmp/here').should(:target).should == '/tmp/here' + end + + it "should inform about an absent user" do + Puppet::Log.level = :debug + @class.new(:name => "whev", :user => 'idontexist').should(:target) + @logs.map(&:message).should include("The required user is not yet present on the system") + end + + end - it "should have a target property" do - @class.attrtype(:target).should == :property end describe "when neither user nor target is specified" do + it "should raise an error" do proc do - - @class.create( - + @class.new( :name => "Test", :key => "AAA", :type => "ssh-rsa", - :ensure => :present) - end.should raise_error(Puppet::Error) + end.should raise_error(Puppet::Error,/user.*or.*target.*mandatory/) end + end describe "when both target and user are specified" do - it "should use target" do - - resource = @class.create( + it "should use target" do + resource = @class.new( :name => "Test", :user => "root", - - :target => "/tmp/blah") + :target => "/tmp/blah" + ) resource.should(:target).should == "/tmp/blah" end + end describe "when user is specified" do - it "should determine target" do + it "should determine target" do resource = @class.create( - :name => "Test", - - :user => "root") + :user => "root" + ) target = File.expand_path("~root/.ssh/authorized_keys") resource.should(:target).should == target end # Bug #2124 - ssh_authorized_key always changes target if target is not defined it "should not raise spurious change events" do resource = @class.new(:name => "Test", :user => "root") target = File.expand_path("~root/.ssh/authorized_keys") resource.property(:target).safe_insync?(target).should == true end + end describe "when calling validate" do - it "should not crash on a non-existant user" do + it "should not crash on a non-existant user" do resource = @class.create( - :name => "Test", - - :user => "ihopesuchuserdoesnotexist") + :user => "ihopesuchuserdoesnotexist" + ) proc { resource.validate }.should_not raise_error end + end + end