diff --git a/lib/puppet/ssl/certificate_authority/interface.rb b/lib/puppet/ssl/certificate_authority/interface.rb index f73eff5f3..0f820e7aa 100644 --- a/lib/puppet/ssl/certificate_authority/interface.rb +++ b/lib/puppet/ssl/certificate_authority/interface.rb @@ -1,134 +1,178 @@ # This class is basically a hidden class that knows how to act # on the CA. It's only used by the 'puppetca' executable, and its # job is to provide a CLI-like interface to the CA class. module Puppet module SSL class CertificateAuthority class Interface INTERFACE_METHODS = [:destroy, :list, :revoke, :generate, :sign, :print, :verify, :fingerprint] class InterfaceError < ArgumentError; end attr_reader :method, :subjects, :digest # Actually perform the work. def apply(ca) unless subjects or method == :list raise ArgumentError, "You must provide hosts or :all when using #{method}" end begin return send(method, ca) if respond_to?(method) (subjects == :all ? ca.list : subjects).each do |host| ca.send(method, host) end rescue InterfaceError raise rescue => detail puts detail.backtrace if Puppet[:trace] Puppet.err "Could not call #{method}: #{detail}" end end def generate(ca) raise InterfaceError, "It makes no sense to generate all hosts; you must specify a list" if subjects == :all subjects.each do |host| ca.generate(host) end end def initialize(method, options) self.method = method self.subjects = options[:to] @digest = options[:digest] || :MD5 end # List the hosts. def list(ca) - unless subjects - puts ca.waiting?.join("\n") - return nil - end - signed = ca.list requests = ca.waiting? - if subjects == :all + case subjects + when :all hosts = [signed, requests].flatten - elsif subjects == :signed + when :signed hosts = signed.flatten + when nil + hosts = requests else hosts = subjects end + certs = {:signed => {}, :invalid => {}, :request => {}} + + return if hosts.empty? + hosts.uniq.sort.each do |host| - invalid = false begin ca.verify(host) unless requests.include?(host) rescue Puppet::SSL::CertificateAuthority::CertificateVerificationError => details - invalid = details.to_s + verify_error = details.to_s end - if not invalid and signed.include?(host) - puts "+ #{host} (#{ca.fingerprint(host, @digest)})" - elsif invalid - puts "- #{host} (#{ca.fingerprint(host, @digest)}) (#{invalid})" + + if verify_error + cert = Puppet::SSL::Certificate.indirection.find(host) + certs[:invalid][host] = [cert, verify_error] + elsif signed.include?(host) + cert = Puppet::SSL::Certificate.indirection.find(host) + certs[:signed][host] = cert else - puts "#{host} (#{ca.fingerprint(host, @digest)})" + req = Puppet::SSL::CertificateRequest.indirection.find(host) + certs[:request][host] = req end end + + names = certs.values.map(&:keys).flatten + + name_width = names.sort_by(&:length).last.length rescue 0 + + output = [:request, :signed, :invalid].map do |type| + next if certs[type].empty? + + certs[type].map do |host,info| + format_host(ca, host, type, info, name_width) + end + end.flatten.compact.sort.join("\n") + + puts output + end + + def format_host(ca, host, type, info, width) + certish, verify_error = info + alt_names = case type + when :signed + certish.alternate_names + when :request + (certish.subject_alt_names || []).map {|al| al.sub(/^DNS:/,'')} + else + [] + end + + alt_names.delete(host) + + alt_str = "(alt names: #{alt_names.join(', ')})" unless alt_names.empty? + + glyph = {:signed => '+', :request => ' ', :invalid => '-'}[type] + + name = host.ljust(width) + fingerprint = "(#{ca.fingerprint(host, @digest)})" + + explanation = "(#{verify_error})" if verify_error + + [glyph, name, fingerprint, alt_str, explanation].compact.join(' ') end # Set the method to apply. def method=(method) raise ArgumentError, "Invalid method #{method} to apply" unless INTERFACE_METHODS.include?(method) @method = method end # Print certificate information. def print(ca) (subjects == :all ? ca.list : subjects).each do |host| if value = ca.print(host) puts value else Puppet.err "Could not find certificate for #{host}" end end end # Print certificate information. def fingerprint(ca) (subjects == :all ? ca.list + ca.waiting?: subjects).each do |host| if value = ca.fingerprint(host, @digest) puts "#{host} #{value}" else Puppet.err "Could not find certificate for #{host}" end end end # Sign a given certificate. def sign(ca) list = subjects == :all ? ca.waiting? : subjects raise InterfaceError, "No waiting certificate requests to sign" if list.empty? list.each do |host| ca.sign(host) end end # Set the list of hosts we're operating on. Also supports keywords. def subjects=(value) unless value == :all or value == :signed or value.is_a?(Array) raise ArgumentError, "Subjects must be an array or :all; not #{value}" end value = nil if value.is_a?(Array) and value.empty? @subjects = value end end end end end diff --git a/spec/unit/ssl/certificate_authority/interface_spec.rb b/spec/unit/ssl/certificate_authority/interface_spec.rb index 46273ccee..7bff39914 100755 --- a/spec/unit/ssl/certificate_authority/interface_spec.rb +++ b/spec/unit/ssl/certificate_authority/interface_spec.rb @@ -1,332 +1,362 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/ssl/certificate_authority' shared_examples_for "a normal interface method" do it "should call the method on the CA for each host specified if an array was provided" do @ca.expects(@method).with("host1") @ca.expects(@method).with("host2") @applier = Puppet::SSL::CertificateAuthority::Interface.new(@method, :to => %w{host1 host2}) @applier.apply(@ca) end it "should call the method on the CA for all existing certificates if :all was provided" do @ca.expects(:list).returns %w{host1 host2} @ca.expects(@method).with("host1") @ca.expects(@method).with("host2") @applier = Puppet::SSL::CertificateAuthority::Interface.new(@method, :to => :all) @applier.apply(@ca) end end describe Puppet::SSL::CertificateAuthority::Interface do before do @class = Puppet::SSL::CertificateAuthority::Interface end describe "when initializing" do it "should set its method using its settor" do - @class.any_instance.expects(:method=).with(:generate) - @class.new(:generate, :to => :all) + instance = @class.new(:generate, :to => :all) + instance.method.should == :generate end it "should set its subjects using the settor" do - @class.any_instance.expects(:subjects=).with(:all) - @class.new(:generate, :to => :all) + instance = @class.new(:generate, :to => :all) + instance.subjects.should == :all end it "should set the digest if given" do interface = @class.new(:generate, :to => :all, :digest => :digest) interface.digest.should == :digest end it "should set the digest to md5 if none given" do interface = @class.new(:generate, :to => :all) interface.digest.should == :MD5 end end describe "when setting the method" do it "should set the method" do - @class.new(:generate, :to => :all).method.should == :generate + instance = @class.new(:generate, :to => :all) + instance.method = :list + + instance.method.should == :list end it "should fail if the method isn't a member of the INTERFACE_METHODS array" do - Puppet::SSL::CertificateAuthority::Interface::INTERFACE_METHODS.expects(:include?).with(:thing).returns false - - lambda { @class.new(:thing, :to => :all) }.should raise_error(ArgumentError) + lambda { @class.new(:thing, :to => :all) }.should raise_error(ArgumentError, /Invalid method thing to apply/) end end describe "when setting the subjects" do it "should set the subjects" do - @class.new(:generate, :to => :all).subjects.should == :all + instance = @class.new(:generate, :to => :all) + instance.subjects = :signed + + instance.subjects.should == :signed end - it "should fail if the subjects setting isn't :all or an array", :'fails_on_ruby_1.9.2' => true do - lambda { @class.new(:generate, "other") }.should raise_error(ArgumentError) + it "should fail if the subjects setting isn't :all or an array" do + lambda { @class.new(:generate, :to => "other") }.should raise_error(ArgumentError, /Subjects must be an array or :all; not other/) end end it "should have a method for triggering the application" do @class.new(:generate, :to => :all).should respond_to(:apply) end describe "when applying" do before do # We use a real object here, because :verify can't be stubbed, apparently. @ca = Object.new end it "should raise InterfaceErrors" do @applier = @class.new(:revoke, :to => :all) @ca.expects(:list).raises Puppet::SSL::CertificateAuthority::Interface::InterfaceError lambda { @applier.apply(@ca) }.should raise_error(Puppet::SSL::CertificateAuthority::Interface::InterfaceError) end it "should log non-Interface failures rather than failing" do @applier = @class.new(:revoke, :to => :all) @ca.expects(:list).raises ArgumentError Puppet.expects(:err) lambda { @applier.apply(@ca) }.should_not raise_error end describe "with an empty array specified and the method is not list" do it "should fail" do @applier = @class.new(:sign, :to => []) lambda { @applier.apply(@ca) }.should raise_error(ArgumentError) end end describe ":generate" do it "should fail if :all was specified" do @applier = @class.new(:generate, :to => :all) lambda { @applier.apply(@ca) }.should raise_error(ArgumentError) end it "should call :generate on the CA for each host specified" do @applier = @class.new(:generate, :to => %w{host1 host2}) @ca.expects(:generate).with("host1") @ca.expects(:generate).with("host2") @applier.apply(@ca) end end describe ":verify" do before { @method = :verify } #it_should_behave_like "a normal interface method" it "should call the method on the CA for each host specified if an array was provided" do # LAK:NOTE Mocha apparently doesn't allow you to mock :verify, but I'm confident this works in real life. end it "should call the method on the CA for all existing certificates if :all was provided" do # LAK:NOTE Mocha apparently doesn't allow you to mock :verify, but I'm confident this works in real life. end end describe ":destroy" do before { @method = :destroy } it_should_behave_like "a normal interface method" end describe ":revoke" do before { @method = :revoke } it_should_behave_like "a normal interface method" end describe ":sign" do describe "and an array of names was provided" do before do @applier = @class.new(:sign, :to => %w{host1 host2}) end it "should sign the specified waiting certificate requests" do @ca.expects(:sign).with("host1") @ca.expects(:sign).with("host2") @applier.apply(@ca) end end describe "and :all was provided" do it "should sign all waiting certificate requests" do @ca.stubs(:waiting?).returns(%w{cert1 cert2}) @ca.expects(:sign).with("cert1") @ca.expects(:sign).with("cert2") @applier = @class.new(:sign, :to => :all) @applier.apply(@ca) end it "should fail if there are no waiting certificate requests" do @ca.stubs(:waiting?).returns([]) @applier = @class.new(:sign, :to => :all) lambda { @applier.apply(@ca) }.should raise_error(Puppet::SSL::CertificateAuthority::Interface::InterfaceError) end end end describe ":list" do - describe "and an empty array was provided" do - it "should print a string containing all certificate requests" do - @ca.expects(:waiting?).returns %w{host1 host2} - @ca.stubs(:verify) + before :each do + certish = stub('certish', :alternate_names => [], :subject_alt_names => nil) + Puppet::SSL::Certificate.indirection.stubs(:find).returns certish + Puppet::SSL::CertificateRequest.indirection.stubs(:find).returns certish + + @ca.expects(:waiting?).returns %w{host1 host2 host3} + @ca.expects(:list).returns %w{host4 host5 host6} + @ca.stubs(:fingerprint).returns "fingerprint" + @ca.stubs(:verify) + end - @applier = @class.new(:list, :to => []) + describe "and an empty array was provided" do + it "should print all certificate requests" do + applier = @class.new(:list, :to => []) - @applier.expects(:puts).with "host1\nhost2" + applier.expects(:puts).with(<<-OUTPUT.chomp) + host1 (fingerprint) + host2 (fingerprint) + host3 (fingerprint) + OUTPUT - @applier.apply(@ca) + applier.apply(@ca) end end describe "and :all was provided" do it "should print a string containing all certificate requests and certificates" do - @ca.expects(:waiting?).returns %w{host1 host2} - @ca.expects(:list).returns %w{host3 host4} - @ca.stubs(:verify) - @ca.stubs(:fingerprint).returns "fingerprint" - @ca.expects(:verify).with("host3").raises(Puppet::SSL::CertificateAuthority::CertificateVerificationError.new(23), "certificate revoked") + @ca.stubs(:verify).with("host4").raises(Puppet::SSL::CertificateAuthority::CertificateVerificationError.new(23), "certificate revoked") - @applier = @class.new(:list, :to => :all) + applier = @class.new(:list, :to => :all) - @applier.expects(:puts).with "host1 (fingerprint)" - @applier.expects(:puts).with "host2 (fingerprint)" - @applier.expects(:puts).with "- host3 (fingerprint) (certificate revoked)" - @applier.expects(:puts).with "+ host4 (fingerprint)" + applier.expects(:puts).with(<<-OUTPUT.chomp) + host1 (fingerprint) + host2 (fingerprint) + host3 (fingerprint) ++ host5 (fingerprint) ++ host6 (fingerprint) +- host4 (fingerprint) (certificate revoked) + OUTPUT - @applier.apply(@ca) + applier.apply(@ca) end end describe "and :signed was provided" do it "should print a string containing all signed certificate requests and certificates" do - @ca.expects(:list).returns %w{host1 host2} + applier = @class.new(:list, :to => :signed) - @applier = @class.new(:list, :to => :signed) + applier.expects(:puts).with(<<-OUTPUT.chomp) ++ host4 (fingerprint) ++ host5 (fingerprint) ++ host6 (fingerprint) + OUTPUT - @applier.apply(@ca) + applier.apply(@ca) + end + + it "should include subject alt names if they are on the certificate request" do + request = stub 'request', :subject_alt_names => ["DNS:foo", "DNS:bar"] + Puppet::SSL::CertificateRequest.indirection.stubs(:find).returns(request) + + applier = @class.new(:list, :to => ['host1']) + + applier.expects(:puts).with(<<-OUTPUT.chomp) + host1 (fingerprint) (alt names: DNS:foo, DNS:bar) + OUTPUT + + applier.apply(@ca) end end describe "and an array of names was provided" do - it "should print a string of all named hosts that have a waiting request" do - @ca.expects(:waiting?).returns %w{host1 host2} - @ca.expects(:list).returns %w{host3 host4} - @ca.stubs(:fingerprint).returns "fingerprint" - @ca.stubs(:verify) + it "should print all named hosts" do + applier = @class.new(:list, :to => %w{host1 host2 host4 host5}) - @applier = @class.new(:list, :to => %w{host1 host2 host3 host4}) + applier.expects(:puts).with(<<-OUTPUT.chomp) + host1 (fingerprint) + host2 (fingerprint) ++ host4 (fingerprint) ++ host5 (fingerprint) + OUTPUT - @applier.expects(:puts).with "host1 (fingerprint)" - @applier.expects(:puts).with "host2 (fingerprint)" - @applier.expects(:puts).with "+ host3 (fingerprint)" - @applier.expects(:puts).with "+ host4 (fingerprint)" - - @applier.apply(@ca) + applier.apply(@ca) end end end describe ":print" do describe "and :all was provided" do it "should print all certificates" do @ca.expects(:list).returns %w{host1 host2} @applier = @class.new(:print, :to => :all) @ca.expects(:print).with("host1").returns "h1" @applier.expects(:puts).with "h1" @ca.expects(:print).with("host2").returns "h2" @applier.expects(:puts).with "h2" @applier.apply(@ca) end end describe "and an array of names was provided" do it "should print each named certificate if found" do @applier = @class.new(:print, :to => %w{host1 host2}) @ca.expects(:print).with("host1").returns "h1" @applier.expects(:puts).with "h1" @ca.expects(:print).with("host2").returns "h2" @applier.expects(:puts).with "h2" @applier.apply(@ca) end it "should log any named but not found certificates" do @applier = @class.new(:print, :to => %w{host1 host2}) @ca.expects(:print).with("host1").returns "h1" @applier.expects(:puts).with "h1" @ca.expects(:print).with("host2").returns nil Puppet.expects(:err).with { |msg| msg.include?("host2") } @applier.apply(@ca) end end end describe ":fingerprint" do it "should fingerprint with the set digest algorithm" do @applier = @class.new(:fingerprint, :to => %w{host1}, :digest => :digest) @ca.expects(:fingerprint).with("host1", :digest).returns "fingerprint1" @applier.expects(:puts).with "host1 fingerprint1" @applier.apply(@ca) end describe "and :all was provided" do it "should fingerprint all certificates (including waiting ones)" do @ca.expects(:list).returns %w{host1} @ca.expects(:waiting?).returns %w{host2} @applier = @class.new(:fingerprint, :to => :all) @ca.expects(:fingerprint).with("host1", :MD5).returns "fingerprint1" @applier.expects(:puts).with "host1 fingerprint1" @ca.expects(:fingerprint).with("host2", :MD5).returns "fingerprint2" @applier.expects(:puts).with "host2 fingerprint2" @applier.apply(@ca) end end describe "and an array of names was provided" do it "should print each named certificate if found" do @applier = @class.new(:fingerprint, :to => %w{host1 host2}) @ca.expects(:fingerprint).with("host1", :MD5).returns "fingerprint1" @applier.expects(:puts).with "host1 fingerprint1" @ca.expects(:fingerprint).with("host2", :MD5).returns "fingerprint2" @applier.expects(:puts).with "host2 fingerprint2" @applier.apply(@ca) end end end end end