diff --git a/lib/puppet/provider/ssh_authorized_key/parsed.rb b/lib/puppet/provider/ssh_authorized_key/parsed.rb index 5604ba32a..051fb63ef 100644 --- a/lib/puppet/provider/ssh_authorized_key/parsed.rb +++ b/lib/puppet/provider/ssh_authorized_key/parsed.rb @@ -1,135 +1,122 @@ 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 { |record| if record[:options].nil? record[:options] = [:absent] else record[:options] = Puppet::Type::Ssh_authorized_key::ProviderParsed.parse_options(record[:options]) end }, :pre_gen => proc { |record| if record[:options].include?(:absent) record[:options] = "" else record[:options] = record[:options].join(',') end } record_line :key_v1, :fields => %w{options bits exponent modulus name}, :optional => %w{options}, :rts => /^\s+/, :match => /^(?:(.+) )?(\d+) (\d+) (\d+)(?: (.+))?$/ - def prefetch - # This was done in the type class but path expansion was failing for - # not yet existing users, the only workaround I found was to move that - # in the provider. - @resource[:target] = target - - super - end - def target - if user - File.expand_path("~%s/.ssh/authorized_keys" % user) - elsif target = @resource.should(:target) - target - end + @resource.should(:target) end def user @resource.should(:user) end def dir_perm # Determine correct permission for created directory and file # we can afford more restrictive permissions when the user is known if target if user 0700 else 0755 end end end def file_perm if target if user 0600 else 0644 end end end def flush # As path expansion had to be moved in the provider, we cannot generate new file # resources and thus have to chown and chmod here. It smells hackish. # Create target's parent directory if nonexistant if target dir = File.dirname(target) if not File.exist? dir Puppet.debug("Creating directory %s which did not exist" % dir) Dir.mkdir(dir, dir_perm) end end # Generate the file super # Ensure correct permissions if target and user uid = Puppet::Util.uid(user) if uid File.chown(uid, nil, dir) File.chown(uid, nil, target) else raise Puppet::Error, "Specified user does not exist" end end if target File.chmod(file_perm, target) end 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 66cf3e733..997afb81e 100644 --- a/lib/puppet/type/ssh_authorized_key.rb +++ b/lib/puppet/type/ssh_authorized_key.rb @@ -1,72 +1,86 @@ module Puppet newtype(:ssh_authorized_key) do @doc = "Manages SSH authorized keys. Currently only type 2 keys are supported." ensurable newparam(:name) do desc "The SSH key comment." isnamevar 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." end newproperty(:user) do desc "The user account in which the SSH key should be installed." end newproperty(:target) do desc "The file in which to store the SSH key." + + defaultto :absent + + def should + if defined? @should and @should[0] != :absent + return super + end + + if user = resource[:user] + return File.expand_path("~%s/.ssh/authorized_keys" % user) + end + + return nil + 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 end autorequire(:user) do if should(:user) should(:user) end end validate do unless should(:target) or should(:user) raise Puppet::Error, "Attribute 'user' or 'target' is mandatory" end end end end diff --git a/spec/unit/provider/ssh_authorized_key/parsed.rb b/spec/unit/provider/ssh_authorized_key/parsed.rb index 73d623573..1e5d6be48 100755 --- a/spec/unit/provider/ssh_authorized_key/parsed.rb +++ b/spec/unit/provider/ssh_authorized_key/parsed.rb @@ -1,166 +1,146 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../../spec_helper' require 'puppettest' require 'puppettest/support/utils' require 'puppettest/fileparsing' provider_class = Puppet::Type.type(:ssh_authorized_key).provider(:parsed) describe provider_class do include PuppetTest include PuppetTest::FileParsing before :each do @sshauthkey_class = Puppet.type(:ssh_authorized_key) @provider = @sshauthkey_class.provider(:parsed) end after :each do @provider.initvars end def mkkey(args) fakeresource = fakeresource(:ssh_authorized_key, args[:name]) key = @provider.new(fakeresource) args.each do |p,v| key.send(p.to_s + "=", v) end return key end def genkey(key) @provider.filetype = :ram file = @provider.default_target key.flush text = @provider.target_object(file).read return text end it "should be able to parse each example" do fakedata("data/providers/ssh_authorized_key/parsed").each { |file| puts "Parsing %s" % file fakedataparse(file) } end it "should be able to generate a basic authorized_keys file" do key = mkkey({ :name => "Just Testing", :key => "AAAAfsfddsjldjgksdflgkjsfdlgkj", :type => "ssh-dss", :ensure => :present, :options => [:absent] }) 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 prefetch ~user/.ssh/authorized_keys when user is given" do - key = Puppet::Type.type(:ssh_authorized_key).create( - :name => "Test", - :key => "AA", - :type => "rsa", - :ensure => :present, - :user => "root") - prov = @provider.new key - - prov.prefetch - prov.target.should == File.expand_path("~root/.ssh/authorized_keys") - end - - it "should create destination dir" do - # No idea how to test the flush method - end - - it "should set correct default permissions" do - # No idea how to test the flush method - end - it "'s parse_options method should be able to parse options containing commas" 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 end end describe provider_class do before :each do @resource = stub("resource", :name => "foo") @resource.stubs(:[]).returns "foo" @provider = provider_class.new(@resource) 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 a user has been specified" do before :each do @resource.stubs(:should).with(:user).returns "nobody" - @resource.stubs(:should).with(:target).returns nil + target = File.expand_path("~nobody/.ssh/authorized_keys") + @resource.stubs(:should).with(:target).returns target end it "should create the directory" do Dir.expects(:mkdir).with(File.expand_path("~nobody/.ssh"), 0700) @provider.flush end it "should chown the directory to the user" do uid = Puppet::Util.uid("nobody") File.expects(:chown).with(uid, nil, File.expand_path("~nobody/.ssh")) @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.chmod(0600, File.expand_path("~nobody/.ssh/authorized_keys")) @provider.flush end end describe "and a target has been specified" do before :each do @resource.stubs(:should).with(:user).returns nil @resource.stubs(:should).with(:target).returns "/tmp/.ssh/authorized_keys" end it "should make the directory" do Dir.expects(:mkdir).with("/tmp/.ssh", 0755) @provider.flush end it "should chmod the key file to 0644" do File.expects(:chmod).with(0644, "/tmp/.ssh/authorized_keys") @provider.flush end end end end diff --git a/spec/unit/type/ssh_authorized_key.rb b/spec/unit/type/ssh_authorized_key.rb index 2cd5171c9..6d60ac2ef 100755 --- a/spec/unit/type/ssh_authorized_key.rb +++ b/spec/unit/type/ssh_authorized_key.rb @@ -1,106 +1,129 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../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::Node::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.namevar.should == :name end it "should have a :provider parameter" do @class.attrtype(:provider).should == :param end it "should have an ensure property" do @class.attrtype(:ensure).should == :property end it "should support :present as a value for :ensure" do proc { @class.create(:name => "whev", :ensure => :present, :user => "nobody") }.should_not raise_error end it "should support :absent as a value for :ensure" do proc { @class.create(: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.create(:name => "whev", :type => "ssh-dss", :user => "nobody") }.should_not raise_error end it "should support ssh-rsa as an type value" do proc { @class.create(:name => "whev", :type => "ssh-rsa", :user => "nobody") }.should_not raise_error end it "should support :dsa as an type value" do proc { @class.create(:name => "whev", :type => :dsa, :user => "nobody") }.should_not raise_error end it "should support :rsa as an type value" do proc { @class.create(:name => "whev", :type => :rsa, :user => "nobody") }.should_not raise_error end it "should not support values other than ssh-dss, ssh-rsa, dsa, rsa in the ssh_authorized_key_type" do proc { @class.create(:name => "whev", :type => :something) }.should raise_error(Puppet::Error) end it "should have an key property" do @class.attrtype(:key).should == :property end it "should have an user property" do @class.attrtype(:user).should == :property end it "should have an options property" do @class.attrtype(:options).should == :property end it "'s options property should return well formed string of arrays from is_to_s" do resource = @class.create(: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 "'s options property should return well formed string of arrays from is_to_s" do resource = @class.create(:name => "whev", :type => :rsa, :user => "nobody", :options => ["a","b","c"]) resource.property(:options).should_to_s(["a","b","c"]).should == "a,b,c" end it "should have a target property" do @class.attrtype(:target).should == :property end - it "should raise an error when neither user nor target is given" do - proc do - @class.create( - :name => "Test", - :key => "AAA", - :type => "ssh-rsa", - :ensure => :present) - end.should raise_error(Puppet::Error) + describe "when neither user nor target is specified" do + it "should raise an error" do + proc do + @class.create( + :name => "Test", + :key => "AAA", + :type => "ssh-rsa", + :ensure => :present) + end.should raise_error(Puppet::Error) + end + end + + describe "when both target and user are specified" do + it "should use target" do + resource = @class.create( + :name => "Test", + :user => "root", + :target => "/tmp/blah") + resource.should(:target).should == "/tmp/blah" + end + end + + + describe "when user is specified" do + it "should determine target" do + resource = @class.create( + :name => "Test", + :user => "root") + target = File.expand_path("~root/.ssh/authorized_keys") + resource.should(:target).should == target + end end after do @class.clear @catalog.clear end end