diff options
| author | Daniel Pittman <daniel@puppetlabs.com> | 2011-07-18 11:40:05 -0700 |
|---|---|---|
| committer | Daniel Pittman <daniel@puppetlabs.com> | 2011-07-20 14:50:07 -0700 |
| commit | c830ab01f31b3c6999953783a0ec1939bdbc25a9 (patch) | |
| tree | 06ab450fe66d2de7f7ec7f362238ac1ecf68b0ca | |
| parent | cc311ad3dcb70547d7249e7aec9f545672c3e8e2 (diff) | |
| download | puppet-c830ab01f31b3c6999953783a0ec1939bdbc25a9.tar.gz puppet-c830ab01f31b3c6999953783a0ec1939bdbc25a9.tar.xz puppet-c830ab01f31b3c6999953783a0ec1939bdbc25a9.zip | |
(#6789) Port SSL::CertificateAuthority::Interface to a Face
The Puppet::SSL::CertificateAuthority::Interface class was an early prototype
heading toward building out a system like Faces. Now that we have done that,
this changeset ports the early code to a new face.
Reviewed-By: Pieter van de Bruggen <pieter@puppetlabs.com>
| -rw-r--r-- | lib/puppet/application/ca.rb | 5 | ||||
| -rw-r--r-- | lib/puppet/face/ca.rb | 233 | ||||
| -rwxr-xr-x | spec/unit/face/ca_spec.rb | 355 |
3 files changed, 593 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/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 |
