diff --git a/lib/puppet/indirector/key/file.rb b/lib/puppet/indirector/key/file.rb index 4536f8aa7..a413ccf63 100644 --- a/lib/puppet/indirector/key/file.rb +++ b/lib/puppet/indirector/key/file.rb @@ -1,42 +1,42 @@ require 'puppet/indirector/ssl_file' require 'puppet/ssl/key' class Puppet::SSL::Key::File < Puppet::Indirector::SslFile desc "Manage SSL private and public keys on disk." store_in :privatekeydir store_ca_at :cakey # Where should we store the public key? def public_key_path(name) if ca?(name) Puppet[:capub] else File.join(Puppet[:publickeydir], name.to_s + ".pem") end end # Remove the public key, in addition to the private key def destroy(request) super return unless FileTest.exist?(public_key_path(request.key)) begin File.unlink(public_key_path(request.key)) rescue => detail raise Puppet::Error, "Could not remove %s public key: %s" % [request.key, detail] end end # Save the public key, in addition to the private key. def save(request) super begin - File.open(public_key_path(request.key), "w") { |f| f.print request.instance.content.public_key.to_pem } + Puppet.settings.writesub(:publickeydir, public_key_path(request.key)) { |f| f.print request.instance.content.public_key.to_pem } rescue => detail - raise Puppet::Error, "Could not write %s: %s" % [key, detail] + raise Puppet::Error, "Could not write %s: %s" % [request.key, detail] end end end diff --git a/lib/puppet/indirector/ssl_file.rb b/lib/puppet/indirector/ssl_file.rb index 4119a656f..de7163700 100644 --- a/lib/puppet/indirector/ssl_file.rb +++ b/lib/puppet/indirector/ssl_file.rb @@ -1,172 +1,174 @@ require 'puppet/ssl' class Puppet::Indirector::SslFile < Puppet::Indirector::Terminus # Specify the directory in which multiple files are stored. def self.store_in(setting) @directory_setting = setting end # Specify a single file location for storing just one file. # This is used for things like the CRL. def self.store_at(setting) @file_setting = setting end # Specify where a specific ca file should be stored. def self.store_ca_at(setting) @ca_setting = setting end class << self attr_reader :directory_setting, :file_setting, :ca_setting end # The full path to where we should store our files. def self.collection_directory return nil unless directory_setting Puppet.settings[directory_setting] end # The full path to an individual file we would be managing. def self.file_location return nil unless file_setting Puppet.settings[file_setting] end # The full path to a ca file we would be managing. def self.ca_location return nil unless ca_setting Puppet.settings[ca_setting] end # We assume that all files named 'ca' are pointing to individual ca files, # rather than normal host files. It's a bit hackish, but all the other # solutions seemed even more hackish. def ca?(name) name == Puppet::SSL::Host.ca_name end def initialize Puppet.settings.use(:main, :ssl) (collection_directory || file_location) or raise Puppet::DevError, "No file or directory setting provided; terminus %s cannot function" % self.class.name end # Use a setting to determine our path. def path(name) if ca?(name) and ca_location ca_location elsif collection_directory File.join(collection_directory, name.to_s + ".pem") else file_location end end # Remove our file. def destroy(request) path = path(request.key) return false unless FileTest.exist?(path) Puppet.notice "Removing file %s %s at '%s'" % [model, request.key, path] begin File.unlink(path) rescue => detail raise Puppet::Error, "Could not remove %s: %s" % [request.key, detail] end end # Find the file on disk, returning an instance of the model. def find(request) path = path(request.key) return nil unless FileTest.exist?(path) or rename_files_with_uppercase(path) result = model.new(request.key) result.read(path) result end # Save our file to disk. def save(request) path = path(request.key) dir = File.dirname(path) raise Puppet::Error.new("Cannot save %s; parent directory %s does not exist" % [request.key, dir]) unless FileTest.directory?(dir) raise Puppet::Error.new("Cannot save %s; parent directory %s is not writable" % [request.key, dir]) unless FileTest.writable?(dir) write(request.key, path) { |f| f.print request.instance.to_s } end # Search for more than one file. At this point, it just returns # an instance for every file in the directory. def search(request) dir = collection_directory Dir.entries(dir).reject { |file| file !~ /\.pem$/ }.collect do |file| name = file.sub(/\.pem$/, '') result = model.new(name) result.read(File.join(dir, file)) result end end private # Demeterish pointers to class info. def collection_directory self.class.collection_directory end def file_location self.class.file_location end def ca_location self.class.ca_location end # A hack method to deal with files that exist with a different case. # Just renames it; doesn't read it in or anything. # LAK:NOTE This is a copy of the method in sslcertificates/support.rb, # which we'll be EOL'ing at some point. This method was added at 20080702 # and should be removed at some point. def rename_files_with_uppercase(file) dir, short = File.split(file) return nil unless FileTest.exist?(dir) raise ArgumentError, "Tried to fix SSL files to a file containing uppercase" unless short.downcase == short real_file = Dir.entries(dir).reject { |f| f =~ /^\./ }.find do |other| other.downcase == short end return nil unless real_file full_file = File.join(dir, real_file) Puppet.notice "Fixing case in %s; renaming to %s" % [full_file, file] File.rename(full_file, file) return true end # Yield a filehandle set up appropriately, either with our settings doing # the work or opening a filehandle manually. def write(name, path) if ca?(name) and ca_location Puppet.settings.write(self.class.ca_setting) { |f| yield f } elsif file_location Puppet.settings.write(self.class.file_setting) { |f| yield f } - else + elsif setting = self.class.directory_setting begin - File.open(path, "w") { |f| yield f } + Puppet.settings.writesub(setting, path) { |f| yield f } rescue => detail - raise Puppet::Error, "Could not write %s: %s" % [path, detail] + raise Puppet::Error, "Could not write %s to %s: %s" % [path, setting, detail] end + else + raise Puppet::DevError, "You must provide a setting to determine where the files are stored" end end end # LAK:NOTE This has to be at the end, because classes like SSL::Key use this # class, and this require statement loads those, which results in a load loop # and lots of failures. require 'puppet/ssl/host' diff --git a/spec/unit/indirector/key/file.rb b/spec/unit/indirector/key/file.rb index 8a1cb04bd..f365bfd60 100755 --- a/spec/unit/indirector/key/file.rb +++ b/spec/unit/indirector/key/file.rb @@ -1,104 +1,104 @@ #!/usr/bin/env ruby # # Created by Luke Kanies on 2008-3-7. # Copyright (c) 2007. All rights reserved. require File.dirname(__FILE__) + '/../../../spec_helper' require 'puppet/indirector/key/file' describe Puppet::SSL::Key::File do it "should have documentation" do Puppet::SSL::Key::File.doc.should be_instance_of(String) end it "should use the :privatekeydir as the collection directory" do Puppet.settings.expects(:value).with(:privatekeydir).returns "/key/dir" Puppet::SSL::Key::File.collection_directory.should == "/key/dir" end it "should store the ca key at the :cakey location" do Puppet.settings.stubs(:use) Puppet.settings.stubs(:value).returns "whatever" Puppet.settings.stubs(:value).with(:cakey).returns "/ca/key" file = Puppet::SSL::Key::File.new file.stubs(:ca?).returns true file.path("whatever").should == "/ca/key" end describe "when choosing the path for the public key" do it "should use the :capub setting location if the key is for the certificate authority" do Puppet.settings.stubs(:value).returns "/fake/dir" Puppet.settings.stubs(:value).with(:capub).returns "/ca/pubkey" Puppet.settings.stubs(:use) @searcher = Puppet::SSL::Key::File.new @searcher.stubs(:ca?).returns true @searcher.public_key_path("whatever").should == "/ca/pubkey" end it "should use the host name plus '.pem' in :publickeydir for normal hosts" do Puppet.settings.stubs(:value).with(:privatekeydir).returns "/private/key/dir" Puppet.settings.stubs(:value).with(:publickeydir).returns "/public/key/dir" Puppet.settings.stubs(:use) @searcher = Puppet::SSL::Key::File.new @searcher.stubs(:ca?).returns false @searcher.public_key_path("whatever").should == "/public/key/dir/whatever.pem" end end describe "when managing private keys" do before do @searcher = Puppet::SSL::Key::File.new @private_key_path = File.join("/fake/key/path") @public_key_path = File.join("/other/fake/key/path") @searcher.stubs(:public_key_path).returns @public_key_path @searcher.stubs(:path).returns @private_key_path FileTest.stubs(:directory?).returns true FileTest.stubs(:writable?).returns true @public_key = stub 'public_key' @real_key = stub 'sslkey', :public_key => @public_key @key = stub 'key', :name => "myname", :content => @real_key @request = stub 'request', :key => "myname", :instance => @key end it "should save the public key when saving the private key" do - File.stubs(:open).with(@private_key_path, "w") + Puppet.settings.stubs(:writesub) fh = mock 'filehandle' - File.expects(:open).with(@public_key_path, "w").yields fh + Puppet.settings.expects(:writesub).with(:publickeydir, @public_key_path).yields fh @public_key.expects(:to_pem).returns "my pem" fh.expects(:print).with "my pem" @searcher.save(@request) end it "should destroy the public key when destroying the private key" do File.stubs(:unlink).with(@private_key_path) FileTest.stubs(:exist?).with(@private_key_path).returns true FileTest.expects(:exist?).with(@public_key_path).returns true File.expects(:unlink).with(@public_key_path) @searcher.destroy(@request) end it "should not fail if the public key does not exist when deleting the private key" do File.stubs(:unlink).with(@private_key_path) FileTest.stubs(:exist?).with(@private_key_path).returns true FileTest.expects(:exist?).with(@public_key_path).returns false File.expects(:unlink).with(@public_key_path).never @searcher.destroy(@request) end end end diff --git a/spec/unit/indirector/ssl_file.rb b/spec/unit/indirector/ssl_file.rb index 89f682f38..559e2f98d 100755 --- a/spec/unit/indirector/ssl_file.rb +++ b/spec/unit/indirector/ssl_file.rb @@ -1,280 +1,280 @@ #!/usr/bin/env ruby # # Created by Luke Kanies on 2008-3-10. # Copyright (c) 2007. All rights reserved. require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/indirector/ssl_file' describe Puppet::Indirector::SslFile do before do @model = mock 'model' @indirection = stub 'indirection', :name => :testing, :model => @model Puppet::Indirector::Indirection.expects(:instance).with(:testing).returns(@indirection) @file_class = Class.new(Puppet::Indirector::SslFile) do def self.to_s "Testing::Mytype" end end @setting = :mydir @file_class.store_in @setting @path = "/my/directory" Puppet.settings.stubs(:value).returns "stubbed_setting" Puppet.settings.stubs(:value).with(@setting).returns(@path) Puppet.settings.stubs(:value).with(:trace).returns(false) end it "should use :main and :ssl upon initialization" do Puppet.settings.expects(:use).with(:main, :ssl) @file_class.new end it "should return a nil collection directory if no directory setting has been provided" do @file_class.store_in nil @file_class.collection_directory.should be_nil end it "should return a nil file location if no location has been provided" do @file_class.store_at nil @file_class.file_location.should be_nil end it "should fail if no store directory or file location has been set" do @file_class.store_in nil @file_class.store_at nil lambda { @file_class.new }.should raise_error(Puppet::DevError) end describe "when managing ssl files" do before do Puppet.settings.stubs(:use) @searcher = @file_class.new @cert = stub 'certificate', :name => "myname" @certpath = File.join(@path, "myname" + ".pem") @request = stub 'request', :key => @cert.name, :instance => @cert end it "should consider the file a ca file if the name is equal to what the SSL::Host class says is the CA name" do Puppet::SSL::Host.expects(:ca_name).returns "amaca" @searcher.should be_ca("amaca") end describe "when choosing the location for certificates" do it "should set them at the ca setting's path if a ca setting is available and the name resolves to the CA name" do @file_class.store_in nil @file_class.store_at :mysetting @file_class.store_ca_at :casetting Puppet.settings.stubs(:value).with(:casetting).returns "/ca/file" @searcher.expects(:ca?).with(@cert.name).returns true @searcher.path(@cert.name).should == "/ca/file" end it "should set them at the file location if a file setting is available" do @file_class.store_in nil @file_class.store_at :mysetting Puppet.settings.stubs(:value).with(:mysetting).returns "/some/file" @searcher.path(@cert.name).should == "/some/file" end it "should set them in the setting directory, with the certificate name plus '.pem', if a directory setting is available" do @searcher.path(@cert.name).should == @certpath end end describe "when finding certificates on disk" do describe "and no certificate is present" do before do # Stub things so the case management bits work. FileTest.stubs(:exist?).with(File.dirname(@certpath)).returns false FileTest.expects(:exist?).with(@certpath).returns false end it "should return nil" do @searcher.find(@request).should be_nil end end describe "and a certificate is present" do before do FileTest.expects(:exist?).with(@certpath).returns true end it "should return an instance of the model, which it should use to read the certificate" do cert = mock 'cert' model = mock 'model' @file_class.stubs(:model).returns model model.expects(:new).with("myname").returns cert cert.expects(:read).with(@certpath) @searcher.find(@request).should equal(cert) end end describe "and a certificate is present but has uppercase letters" do before do @request = stub 'request', :key => "myhost" end # This is kind of more an integration test; it's for #1382, until # the support for upper-case certs can be removed around mid-2009. it "should rename the existing file to the lower-case path" do @path = @searcher.path("myhost") FileTest.expects(:exist?).with(@path).returns(false) dir, file = File.split(@path) FileTest.expects(:exist?).with(dir).returns true Dir.expects(:entries).with(dir).returns [".", "..", "something.pem", file.upcase] File.expects(:rename).with(File.join(dir, file.upcase), @path) cert = mock 'cert' model = mock 'model' @searcher.stubs(:model).returns model @searcher.model.expects(:new).with("myhost").returns cert cert.expects(:read).with(@path) @searcher.find(@request) end end end describe "when saving certificates to disk" do before do FileTest.stubs(:directory?).returns true FileTest.stubs(:writable?).returns true end it "should fail if the directory is absent" do FileTest.expects(:directory?).with(File.dirname(@certpath)).returns false lambda { @searcher.save(@request) }.should raise_error(Puppet::Error) end it "should fail if the directory is not writeable" do FileTest.stubs(:directory?).returns true FileTest.expects(:writable?).with(File.dirname(@certpath)).returns false lambda { @searcher.save(@request) }.should raise_error(Puppet::Error) end it "should save to the path the output of converting the certificate to a string" do fh = mock 'filehandle' fh.expects(:print).with("mycert") @searcher.stubs(:write).yields fh @cert.expects(:to_s).returns "mycert" @searcher.save(@request) end describe "and a directory setting is set" do - it "should open the file in write mode" do + it "should use the Settings class to write the file" do @searcher.class.store_in @setting fh = mock 'filehandle' fh.stubs :print - File.expects(:open).with(@certpath, "w").yields(fh) + Puppet.settings.expects(:writesub).with(@setting, @certpath).yields fh @searcher.save(@request) end end describe "and a file location is set" do it "should use the filehandle provided by the Settings" do @searcher.class.store_at @setting fh = mock 'filehandle' fh.stubs :print Puppet.settings.expects(:write).with(@setting).yields fh @searcher.save(@request) end end describe "and the name is the CA name and a ca setting is set" do it "should use the filehandle provided by the Settings" do @searcher.class.store_at @setting @searcher.class.store_ca_at :castuff fh = mock 'filehandle' fh.stubs :print Puppet.settings.expects(:write).with(:castuff).yields fh @searcher.stubs(:ca?).returns true @searcher.save(@request) end end end describe "when destroying certificates" do describe "that do not exist" do before do FileTest.expects(:exist?).with(@certpath).returns false end it "should return false" do @searcher.destroy(@request).should be_false end end describe "that exist" do before do FileTest.expects(:exist?).with(@certpath).returns true end it "should unlink the certificate file" do File.expects(:unlink).with(@certpath) @searcher.destroy(@request) end it "should log that is removing the file" do File.stubs(:exist?).returns true File.stubs(:unlink) Puppet.expects(:notice) @searcher.destroy(@request) end end end describe "when searching for certificates" do before do @model = mock 'model' @file_class.stubs(:model).returns @model end it "should return a certificate instance for all files that exist" do Dir.expects(:entries).with(@path).returns %w{one.pem two.pem} one = stub 'one', :read => nil two = stub 'two', :read => nil @model.expects(:new).with("one").returns one @model.expects(:new).with("two").returns two @searcher.search(@request).should == [one, two] end it "should read each certificate in using the model's :read method" do Dir.expects(:entries).with(@path).returns %w{one.pem} one = stub 'one' one.expects(:read).with(File.join(@path, "one.pem")) @model.expects(:new).with("one").returns one @searcher.search(@request) end it "should skip any files that do not match /\.pem$/" do Dir.expects(:entries).with(@path).returns %w{. .. one.pem} one = stub 'one', :read => nil @model.expects(:new).with("one").returns one @searcher.search(@request) end end end end