summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDaniel Pittman <daniel@puppetlabs.com>2011-07-20 14:50:30 -0700
committerDaniel Pittman <daniel@puppetlabs.com>2011-07-20 14:50:30 -0700
commitda504b3a0004666e9014ffbd0bf108acf07f9dce (patch)
tree06ab450fe66d2de7f7ec7f362238ac1ecf68b0ca
parent72abe6ce7192bba0b295a8a83483668d21050624 (diff)
parentc830ab01f31b3c6999953783a0ec1939bdbc25a9 (diff)
downloadpuppet-da504b3a0004666e9014ffbd0bf108acf07f9dce.tar.gz
puppet-da504b3a0004666e9014ffbd0bf108acf07f9dce.tar.xz
puppet-da504b3a0004666e9014ffbd0bf108acf07f9dce.zip
Merge branch 'feature/2.7.x/6789-port-ca-interface-to-faces' into 2.7.x
-rw-r--r--lib/puppet/application/ca.rb5
-rw-r--r--lib/puppet/face/ca.rb233
-rw-r--r--lib/puppet/ssl/inventory.rb2
-rwxr-xr-xspec/unit/face/ca_spec.rb355
4 files changed, 595 insertions, 0 deletions
diff --git a/lib/puppet/application/ca.rb b/lib/puppet/application/ca.rb
new file mode 100644
index 000000000..d1ec2502e
--- /dev/null
+++ b/lib/puppet/application/ca.rb
@@ -0,0 +1,5 @@
+require 'puppet/application/face_base'
+
+class Puppet::Application::Ca < Puppet::Application::FaceBase
+ run_mode :master
+end
diff --git a/lib/puppet/face/ca.rb b/lib/puppet/face/ca.rb
new file mode 100644
index 000000000..e643530f0
--- /dev/null
+++ b/lib/puppet/face/ca.rb
@@ -0,0 +1,233 @@
+require 'puppet/face'
+
+Puppet::Face.define(:ca, '0.1.0') do
+ copyright "Puppet Labs", 2011
+ license "Apache 2 license; see COPYING"
+
+ summary "Local Puppet Certificate Authority management."
+
+ description <<TEXT
+This provides local management of the Puppet Certificate Authority.
+
+You can use this subcommand to sign outstanding certificate requests, list
+and manage local certificates, and inspect the state of the CA.
+TEXT
+
+ action :list do
+ summary "List certificates and/or certificate requests."
+
+ description <<-end
+This will list the current certificates and certificate signing requests
+in the Puppet CA. You will also get the fingerprint, and any certificate
+verification failure reported.
+ end
+
+ option "--[no-]all" do
+ summary "Include all certificates and requests."
+ end
+
+ option "--[no-]pending" do
+ summary "Include pending certificate signing requests."
+ end
+
+ option "--[no-]signed" do
+ summary "Include signed certificates."
+ end
+
+ option "--subject PATTERN" do
+ summary "Only list if the subject matches PATTERN."
+
+ description <<TEXT
+Only include certificates or requests where subject matches PATTERN.
+
+PATTERN is interpreted as a regular expression, allowing complex
+filtering of the content.
+TEXT
+ end
+
+ when_invoked do |options|
+ raise "Not a CA" unless Puppet::SSL::CertificateAuthority.ca?
+ unless ca = Puppet::SSL::CertificateAuthority.instance
+ raise "Unable to fetch the CA"
+ end
+
+ pattern = options[:subject].nil? ? nil :
+ Regexp.new(options[:subject], Regexp::IGNORECASE)
+
+ pending = options[:pending].nil? ? options[:all] : options[:pending]
+ signed = options[:signed].nil? ? options[:all] : options[:signed]
+
+ # By default we list pending, so if nothing at all was requested...
+ unless pending or signed then pending = true end
+
+ hosts = []
+
+ pending and hosts += ca.waiting?
+ signed and hosts += ca.list
+
+ pattern and hosts = hosts.select {|hostname| pattern.match hostname }
+
+ hosts.sort.map {|host| Puppet::SSL::Host.new(host) }
+ end
+
+ when_rendering :console do |hosts|
+ unless ca = Puppet::SSL::CertificateAuthority.instance
+ raise "Unable to fetch the CA"
+ end
+
+ length = hosts.map{|x| x.name.length }.max + 1
+
+ hosts.map do |host|
+ name = host.name.ljust(length)
+ if host.certificate_request then
+ " #{name} (#{host.certificate_request.fingerprint})"
+ else
+ begin
+ ca.verify(host.certificate)
+ "+ #{name} (#{host.certificate.fingerprint})"
+ rescue Puppet::SSL::CertificateAuthority::CertificateVerificationError => e
+ "- #{name} (#{host.certificate.fingerprint}) (#{e.to_s})"
+ end
+ end
+ end.join("\n")
+ end
+ end
+
+ action :destroy do
+ when_invoked do |host, options|
+ raise "Not a CA" unless Puppet::SSL::CertificateAuthority.ca?
+ unless ca = Puppet::SSL::CertificateAuthority.instance
+ raise "Unable to fetch the CA"
+ end
+
+ ca.destroy host
+ end
+ end
+
+ action :revoke do
+ when_invoked do |host, options|
+ raise "Not a CA" unless Puppet::SSL::CertificateAuthority.ca?
+ unless ca = Puppet::SSL::CertificateAuthority.instance
+ raise "Unable to fetch the CA"
+ end
+
+ begin
+ ca.revoke host
+ rescue ArgumentError => e
+ # This is a bit naff, but it makes the behaviour consistent with the
+ # destroy action. The underlying tools could be nicer for that sort
+ # of thing; they have fairly inconsistent reporting of failures.
+ raise unless e.to_s =~ /Could not find a serial number for /
+ "Nothing was revoked"
+ end
+ end
+ end
+
+ action :generate do
+ when_invoked do |host, options|
+ raise "Not a CA" unless Puppet::SSL::CertificateAuthority.ca?
+ unless ca = Puppet::SSL::CertificateAuthority.instance
+ raise "Unable to fetch the CA"
+ end
+
+ begin
+ ca.generate host
+ rescue RuntimeError => e
+ if e.to_s =~ /already has a requested certificate/
+ "#{host} already has a certificate request; use sign instead"
+ else
+ raise
+ end
+ rescue ArgumentError => e
+ if e.to_s =~ /A Certificate already exists for /
+ "#{host} already has a certificate"
+ else
+ raise
+ end
+ end
+ end
+ end
+
+ action :sign do
+ when_invoked do |host, options|
+ raise "Not a CA" unless Puppet::SSL::CertificateAuthority.ca?
+ unless ca = Puppet::SSL::CertificateAuthority.instance
+ raise "Unable to fetch the CA"
+ end
+
+ begin
+ ca.sign host
+ rescue ArgumentError => e
+ if e.to_s =~ /Could not find certificate request/
+ e.to_s
+ else
+ raise
+ end
+ end
+ end
+ end
+
+ action :print do
+ when_invoked do |host, options|
+ raise "Not a CA" unless Puppet::SSL::CertificateAuthority.ca?
+ unless ca = Puppet::SSL::CertificateAuthority.instance
+ raise "Unable to fetch the CA"
+ end
+
+ ca.print host
+ end
+ end
+
+ action :fingerprint do
+ option "--digest ALGORITHM" do
+ summary "The hash algorithm to use when displaying the fingerprint"
+ end
+
+ when_invoked do |host, options|
+ raise "Not a CA" unless Puppet::SSL::CertificateAuthority.ca?
+ unless ca = Puppet::SSL::CertificateAuthority.instance
+ raise "Unable to fetch the CA"
+ end
+
+ begin
+ # I want the default from the CA, not to duplicate it, but passing
+ # 'nil' explicitly means that we don't get that. This works...
+ if options.has_key? :digest
+ ca.fingerprint host, options[:digest]
+ else
+ ca.fingerprint host
+ end
+ rescue ArgumentError => e
+ raise unless e.to_s =~ /Could not find a certificate or csr for/
+ nil
+ end
+ end
+ end
+
+ action :verify do
+ when_invoked do |host, options|
+ raise "Not a CA" unless Puppet::SSL::CertificateAuthority.ca?
+ unless ca = Puppet::SSL::CertificateAuthority.instance
+ raise "Unable to fetch the CA"
+ end
+
+ begin
+ ca.verify host
+ { :host => host, :valid => true }
+ rescue ArgumentError => e
+ raise unless e.to_s =~ /Could not find a certificate for/
+ { :host => host, :valid => false, :error => e.to_s }
+ rescue Puppet::SSL::CertificateAuthority::CertificateVerificationError => e
+ { :host => host, :valid => false, :error => e.to_s }
+ end
+ end
+
+ when_rendering :console do |value|
+ if value[:valid]
+ nil
+ else
+ "Could not verify #{value[:host]}: #{value[:error]}"
+ end
+ end
+ end
+end
diff --git a/lib/puppet/ssl/inventory.rb b/lib/puppet/ssl/inventory.rb
index e094da100..c210fdc35 100644
--- a/lib/puppet/ssl/inventory.rb
+++ b/lib/puppet/ssl/inventory.rb
@@ -48,5 +48,7 @@ class Puppet::SSL::Inventory
return Integer($1)
end
+
+ return nil
end
end
diff --git a/spec/unit/face/ca_spec.rb b/spec/unit/face/ca_spec.rb
new file mode 100755
index 000000000..84769605f
--- /dev/null
+++ b/spec/unit/face/ca_spec.rb
@@ -0,0 +1,355 @@
+#!/usr/bin/env rspec
+require 'spec_helper'
+require 'puppet/face'
+
+describe Puppet::Face[:ca, '0.1.0'] do
+ include PuppetSpec::Files
+
+ before :each do
+ Puppet.run_mode.stubs(:master?).returns(true)
+ Puppet[:ca] = true
+ Puppet[:ssldir] = tmpdir("face-ca-ssldir")
+
+ Puppet::SSL::Host.ca_location = :only
+ Puppet[:certificate_revocation] = true
+
+ # This is way more intimate than I want to be with the implementation, but
+ # there doesn't seem any other way to test this. --daniel 2011-07-18
+ Puppet::SSL::CertificateAuthority.stubs(:instance).returns(
+ # ...and this actually does the directory creation, etc.
+ Puppet::SSL::CertificateAuthority.new
+ )
+ end
+
+ def make_certs(csr_names, crt_names)
+ Array(csr_names).map do |name|
+ Puppet::SSL::Host.new(name).generate_certificate_request
+ end
+
+ Array(crt_names).map do |name|
+ Puppet::SSL::Host.new(name).generate
+ end
+ end
+
+ context "#verify" do
+ let :action do Puppet::Face[:ca, '0.1.0'].get_action(:verify) end
+
+ it "should not explode if there is no certificate" do
+ expect {
+ subject.verify('random-host').should == {
+ :host => 'random-host', :valid => false,
+ :error => 'Could not find a certificate for random-host'
+ }
+ }.should_not raise_error
+ end
+
+ it "should not explode if there is only a CSR" do
+ make_certs('random-host', [])
+ expect {
+ subject.verify('random-host').should == {
+ :host => 'random-host', :valid => false,
+ :error => 'Could not find a certificate for random-host'
+ }
+ }.should_not raise_error
+ end
+
+ it "should verify a signed certificate" do
+ make_certs([], 'random-host')
+ subject.verify('random-host').should == {
+ :host => 'random-host', :valid => true
+ }
+ end
+
+ it "should not verify a revoked certificate" do
+ make_certs([], 'random-host')
+ subject.revoke('random-host')
+
+ expect {
+ subject.verify('random-host').should == {
+ :host => 'random-host', :valid => false,
+ :error => 'certificate revoked'
+ }
+ }.should_not raise_error
+ end
+
+ it "should verify a revoked certificate if CRL use was turned off" do
+ make_certs([], 'random-host')
+ subject.revoke('random-host')
+
+ Puppet[:certificate_revocation] = false
+ subject.verify('random-host').should == {
+ :host => 'random-host', :valid => true
+ }
+ end
+ end
+
+ context "#fingerprint" do
+ let :action do Puppet::Face[:ca, '0.1.0'].get_action(:fingerprint) end
+
+ it "should have a 'digest' option" do
+ action.should be_option :digest
+ end
+
+ it "should not explode if there is no certificate" do
+ expect {
+ subject.fingerprint('random-host').should be_nil
+ }.should_not raise_error
+ end
+
+ it "should fingerprint a CSR" do
+ make_certs('random-host', [])
+ expect {
+ subject.fingerprint('random-host').should =~ /^[0-9A-F:]+$/
+ }.should_not raise_error
+ end
+
+ it "should fingerprint a certificate" do
+ make_certs([], 'random-host')
+ subject.fingerprint('random-host').should =~ /^[0-9A-F:]+$/
+ end
+
+ %w{md5 MD5 sha1 ShA1 SHA1 RIPEMD160 sha256 sha512}.each do |digest|
+ it "should fingerprint with #{digest.inspect}" do
+ make_certs([], 'random-host')
+ subject.fingerprint('random-host', :digest => digest).should =~ /^[0-9A-F:]+$/
+ end
+
+ it "should fingerprint with #{digest.to_sym} as a symbol" do
+ make_certs([], 'random-host')
+ subject.fingerprint('random-host', :digest => digest.to_sym).
+ should =~ /^[0-9A-F:]+$/
+ end
+ end
+ end
+
+ context "#print" do
+ let :action do Puppet::Face[:ca, '0.1.0'].get_action(:print) end
+
+ it "should not explode if there is no certificate" do
+ expect {
+ subject.print('random-host').should be_nil
+ }.should_not raise_error
+ end
+
+ it "should return nothing if there is only a CSR" do
+ make_certs('random-host', [])
+ expect {
+ subject.print('random-host').should be_nil
+ }.should_not raise_error
+ end
+
+ it "should return the certificate content if there is a cert" do
+ make_certs([], 'random-host')
+ text = subject.print('random-host')
+ text.should be_an_instance_of String
+ text.should =~ /^Certificate:/
+ text.should =~ /Issuer: CN=Puppet CA: /
+ text.should =~ /Subject: CN=random-host$/
+ end
+ end
+
+ context "#sign" do
+ let :action do Puppet::Face[:ca, '0.1.0'].get_action(:sign) end
+
+ it "should not explode if there is no CSR" do
+ expect {
+ subject.sign('random-host').
+ should == 'Could not find certificate request for random-host'
+ }.should_not raise_error
+ end
+
+ it "should not explode if there is a signed cert" do
+ make_certs([], 'random-host')
+ expect {
+ subject.sign('random-host').
+ should == 'Could not find certificate request for random-host'
+ }.should_not raise_error
+ end
+
+ it "should sign a CSR if one exists" do
+ make_certs('random-host', [])
+ subject.sign('random-host').should be_an_instance_of Puppet::SSL::Certificate
+
+ list = subject.list(:signed => true)
+ list.count.should == 1
+ list.first.name.should == 'random-host'
+ end
+ end
+
+ context "#generate" do
+ let :action do Puppet::Face[:ca, '0.1.0'].get_action(:generate) end
+
+ it "should generate a certificate if requested" do
+ subject.list(:all => true).should == []
+
+ subject.generate('random-host')
+
+ list = subject.list(:signed => true)
+ list.count.should == 1
+ list.first.name.should == 'random-host'
+ end
+
+ it "should not explode if a CSR with that name already exists" do
+ make_certs('random-host', [])
+ expect {
+ subject.generate('random-host').should =~ /already has a certificate request/
+ }.should_not raise_error
+ end
+
+ it "should not explode if the certificate with that name already exists" do
+ make_certs([], 'random-host')
+ expect {
+ subject.generate('random-host').should =~ /already has a certificate/
+ }.should_not raise_error
+ end
+ end
+
+ context "#revoke" do
+ let :action do Puppet::Face[:ca, '0.1.0'].get_action(:revoke) end
+
+ it "should not explode when asked to revoke something that doesn't exist" do
+ expect { subject.revoke('nonesuch') }.should_not raise_error
+ end
+
+ it "should let the user know what went wrong" do
+ subject.revoke('nonesuch').should == 'Nothing was revoked'
+ end
+
+ it "should revoke a certificate" do
+ make_certs([], 'random-host')
+ found = subject.list(:all => true, :subject => 'random-host')
+ subject.get_action(:list).when_rendering(:console).call(found).
+ should =~ /^\+ random-host/
+
+ subject.revoke('random-host')
+
+ found = subject.list(:all => true, :subject => 'random-host')
+ subject.get_action(:list).when_rendering(:console).call(found).
+ should =~ /^- random-host \([:0-9A-F]+\) \(certificate revoked\)/
+ end
+ end
+
+ context "#destroy" do
+ let :action do Puppet::Face[:ca, '0.1.0'].get_action(:destroy) end
+
+ it "should not explode when asked to delete something that doesn't exist" do
+ expect { subject.destroy('nonesuch') }.should_not raise_error
+ end
+
+ it "should let the user know if nothing was deleted" do
+ subject.destroy('nonesuch').should == "Nothing was deleted"
+ end
+
+ it "should destroy a CSR, if we have one" do
+ make_certs('random-host', [])
+ subject.list(:pending => true, :subject => 'random-host').should_not == []
+
+ subject.destroy('random-host')
+
+ subject.list(:pending => true, :subject => 'random-host').should == []
+ end
+
+ it "should destroy a certificate, if we have one" do
+ make_certs([], 'random-host')
+ subject.list(:signed => true, :subject => 'random-host').should_not == []
+
+ subject.destroy('random-host')
+
+ subject.list(:signed => true, :subject => 'random-host').should == []
+ end
+
+ it "should tell the user something was deleted" do
+ make_certs([], 'random-host')
+ subject.list(:signed => true, :subject => 'random-host').should_not == []
+ subject.destroy('random-host').
+ should == "Deleted for random-host: Puppet::SSL::Certificate, Puppet::SSL::Key"
+ end
+ end
+
+ context "#list" do
+ let :action do Puppet::Face[:ca, '0.1.0'].get_action(:list) end
+
+ context "options" do
+ subject { Puppet::Face[:ca, '0.1.0'].get_action(:list) }
+ it { should be_option :pending }
+ it { should be_option :signed }
+ it { should be_option :all }
+ it { should be_option :subject }
+ end
+
+ context "with no hosts in CA" do
+ [:pending, :signed, :all].each do |type|
+ it "should return nothing for #{type}" do
+ subject.list(type => true).should == []
+ end
+
+ it "should not fail when a matcher is passed" do
+ expect {
+ subject.list(type => true, :subject => '.').should == []
+ }.should_not raise_error
+ end
+ end
+ end
+
+ context "with some hosts" do
+ csr_names = (1..3).map {|n| "csr-#{n}" }
+ crt_names = (1..3).map {|n| "crt-#{n}" }
+ all_names = csr_names + crt_names
+
+ {
+ {} => csr_names,
+ { :pending => true } => csr_names,
+
+ { :signed => true } => crt_names,
+
+ { :all => true } => all_names,
+ { :pending => true, :signed => true } => all_names,
+ }.each do |input, expect|
+ it "should map #{input.inspect} to #{expect.inspect}" do
+ make_certs(csr_names, crt_names)
+ subject.list(input).map(&:name).should =~ expect
+ end
+
+ ['', '.', '2', 'none'].each do |pattern|
+ filtered = expect.select {|x| Regexp.new(pattern).match(x) }
+
+ it "should filter all hosts matching #{pattern.inspect} to #{filtered.inspect}" do
+ make_certs(csr_names, crt_names)
+ subject.list(input.merge :subject => pattern).map(&:name).should =~ filtered
+ end
+ end
+ end
+
+ context "when_rendering :console" do
+ { [["csr1.local"], []] => '^ csr1.local ',
+ [[], ["crt1.local"]] => '^\+ crt1.local ',
+ [["csr2"], ["crt2"]] => ['^ csr2 ', '^\+ crt2 ']
+ }.each do |input, pattern|
+ it "should render #{input.inspect} to match #{pattern.inspect}" do
+ make_certs(*input)
+ text = action.when_rendering(:console).call(subject.list(:all => true))
+ Array(pattern).each do |item|
+ text.should =~ Regexp.new(item)
+ end
+ end
+ end
+ end
+ end
+ end
+
+ actions = %w{destroy list revoke generate sign print verify fingerprint}
+ actions.each do |action|
+ it { should be_action action }
+ it "should fail #{action} when not a CA" do
+ Puppet[:ca] = false
+ expect {
+ case subject.method(action).arity
+ when -1 then subject.send(action)
+ when -2 then subject.send(action, 'dummy')
+ else
+ raise "#{action} has arity #{subject.method(action).arity}"
+ end
+ }.should raise_error(/Not a CA/)
+ end
+ end
+end