diff --git a/lib/puppet/face/certificate.rb b/lib/puppet/face/certificate.rb index 97ab80484..d116796ac 100644 --- a/lib/puppet/face/certificate.rb +++ b/lib/puppet/face/certificate.rb @@ -1,130 +1,148 @@ require 'puppet/indirector/face' require 'puppet/ssl/host' Puppet::Indirector::Face.define(:certificate, '0.0.1') do copyright "Puppet Labs", 2011 license "Apache 2 license; see COPYING" summary "Provide access to the CA for certificate management." description <<-EOT This subcommand interacts with a local or remote Puppet certificate authority. Currently, its behavior is not a full superset of `puppet cert`; specifically, it is unable to mimic puppet cert's "clean" option, and its "generate" action submits a CSR rather than creating a signed certificate. EOT option "--ca-location LOCATION" do required summary "Which certificate authority to use (local or remote)." description <<-EOT Whether to act on the local certificate authority or one provided by a remote puppet master. Allowed values are 'local' and 'remote.' This option is required. EOT before_action do |action, args, options| unless [:remote, :local, :only].include? options[:ca_location].to_sym raise ArgumentError, "Valid values for ca-location are 'remote', 'local', 'only'." end Puppet::SSL::Host.ca_location = options[:ca_location].to_sym end end action :generate do summary "Generate a new certificate signing request." arguments "" returns "Nothing." description <<-EOT Generates and submits a certificate signing request (CSR) for the specified host. This CSR will then have to be signed by a user with the proper authorization on the certificate authority. Puppet agent usually handles CSR submission automatically. This action is primarily useful for requesting certificates for individual users and external applications. EOT examples <<-EOT Request a certificate for "somenode" from the site's CA: $ puppet certificate generate somenode.puppetlabs.lan --ca-location remote EOT # Duplicate the option here explicitly to distinguish if it was passed arg # us vs. set in the config file. option "--dns-alt-names NAMES" do summary "Additional DNS names to add to the certificate request" description Puppet.settings.setting(:dns_alt_names).desc end when_invoked do |name, options| host = Puppet::SSL::Host.new(name) # If dns_alt_names are specified via the command line, we will always add # them. Otherwise, they will default to the config file setting iff this # cert is for the host we're running on. - host.generate_certificate_request(:dns_alt_names => options.delete(:dns_alt_names)) + host.generate_certificate_request(:dns_alt_names => options[:dns_alt_names]) end end action :list do summary "List all certificate signing requests." returns <<-EOT An array of #inspect output from CSR objects. This output is currently messy, but does contain the names of nodes requesting certificates. This action returns #inspect strings even when used from the Ruby API. EOT when_invoked do |options| Puppet::SSL::Host.indirection.search("*", { :for => :certificate_request, }).map { |h| h.inspect } end end action :sign do summary "Sign a certificate signing request for HOST." arguments "" returns <<-EOT A string that appears to be (but isn't) an x509 certificate. EOT examples <<-EOT Sign somenode.puppetlabs.lan's certificate: $ puppet certificate sign somenode.puppetlabs.lan --ca-location remote EOT + option("--[no-]allow-dns-alt-names") do + summary "Whether or not to accept DNS alt names in the certificate request" + end + when_invoked do |name, options| host = Puppet::SSL::Host.new(name) - host.desired_state = 'signed' - Puppet::SSL::Host.indirection.save(host) + if options[:ca_location] == :remote + if options[:allow_dns_alt_names] + raise ArgumentError, "--allow-dns-alt-names may not be specified with a remote CA" + end + + host.desired_state = 'signed' + Puppet::SSL::Host.indirection.save(host) + else + # We have to do this case manually because we need to specify + # allow_dns_alt_names. + unless ca = Puppet::SSL::CertificateAuthority.instance + raise ArgumentError, "This process is not configured as a certificate authority" + end + + ca.sign(name, options[:allow_dns_alt_names]) + end end end # Indirector action doc overrides find = get_action(:find) find.summary "Retrieve a certificate." find.arguments "" find.render_as = :s find.returns <<-EOT An x509 SSL certificate. Note that this action has a side effect of caching a copy of the certificate in Puppet's `ssldir`. EOT destroy = get_action(:destroy) destroy.summary "Delete a certificate." destroy.arguments "" destroy.returns "Nothing." destroy.description <<-EOT Deletes a certificate. This action currently only works on the local CA. EOT get_action(:search).summary "Invalid for this subcommand." get_action(:save).summary "Invalid for this subcommand." get_action(:save).description "Invalid for this subcommand." end diff --git a/spec/unit/face/certificate_spec.rb b/spec/unit/face/certificate_spec.rb index 745f96b59..97253186e 100755 --- a/spec/unit/face/certificate_spec.rb +++ b/spec/unit/face/certificate_spec.rb @@ -1,116 +1,202 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/face' require 'puppet/ssl/host' describe Puppet::Face[:certificate, '0.0.1'] do + include PuppetSpec::Files + + let(:ca) { Puppet::SSL::CertificateAuthority.instance } + + before :each do + Puppet[:confdir] = tmpdir('conf') + Puppet::SSL::CertificateAuthority.stubs(:ca?).returns true + + Puppet::SSL::Host.ca_location = :local + + # We can't cache the CA between tests, because each one has its own SSL dir. + ca = Puppet::SSL::CertificateAuthority.new + Puppet::SSL::CertificateAuthority.stubs(:new).returns ca + Puppet::SSL::CertificateAuthority.stubs(:instance).returns ca + end + it "should have a ca-location option" do subject.should be_option :ca_location end it "should set the ca location when invoked" do Puppet::SSL::Host.expects(:ca_location=).with(:local) - Puppet::SSL::Host.indirection.expects(:save) + ca.expects(:sign).with do |name,options| + name == "hello, friend" + end + subject.sign "hello, friend", :ca_location => :local end it "(#7059) should set the ca location when an inherited action is invoked" do Puppet::SSL::Host.expects(:ca_location=).with(:local) subject.indirection.expects(:find) subject.find "hello, friend", :ca_location => :local end it "should validate the option as required" do expect do subject.find 'hello, friend' end.to raise_exception ArgumentError, /required/i end it "should validate the option as a supported value" do expect do subject.find 'hello, friend', :ca_location => :foo end.to raise_exception ArgumentError, /valid values/i end describe "#generate" do - include PuppetSpec::Files - let(:options) { {:ca_location => 'local'} } let(:host) { Puppet::SSL::Host.new(hostname) } let(:csr) { host.certificate_request } - before :each do - Puppet[:confdir] = tmpdir('conf') - Puppet.settings.use(:main, :ca) - end - describe "for the current host" do let(:hostname) { Puppet[:certname] } it "should generate a CSR for this host" do subject.generate(hostname, options) csr.content.subject.to_s.should == "/CN=#{Puppet[:certname]}" csr.name.should == Puppet[:certname] end it "should add dns_alt_names from the global config if not otherwise specified" do Puppet[:dns_alt_names] = 'from,the,config' subject.generate(hostname, options) expected = %W[DNS:from DNS:the DNS:config DNS:#{hostname}] csr.subject_alt_names.should =~ expected end it "should add the provided dns_alt_names if they are specified" do Puppet[:dns_alt_names] = 'from,the,config' subject.generate(hostname, options.merge(:dns_alt_names => 'explicit,alt,names')) expected = %W[DNS:explicit DNS:alt DNS:names DNS:#{hostname}] csr.subject_alt_names.should =~ expected end end describe "for another host" do let(:hostname) { Puppet[:certname] + 'different' } it "should generate a CSR for the specified host" do subject.generate(hostname, options) csr.content.subject.to_s.should == "/CN=#{hostname}" csr.name.should == hostname end it "should fail if a CSR already exists for the host" do subject.generate(hostname, options) expect do subject.generate(hostname, options) end.to raise_error(RuntimeError, /#{hostname} already has a requested certificate; ignoring certificate request/) end it "should add not dns_alt_names from the config file" do Puppet[:dns_alt_names] = 'from,the,config' subject.generate(hostname, options) csr.subject_alt_names.should be_empty end it "should add the provided dns_alt_names if they are specified" do Puppet[:dns_alt_names] = 'from,the,config' subject.generate(hostname, options.merge(:dns_alt_names => 'explicit,alt,names')) expected = %W[DNS:explicit DNS:alt DNS:names DNS:#{hostname}] csr.subject_alt_names.should =~ expected end end end + + describe "#sign" do + let(:options) { {:ca_location => 'local'} } + let(:host) { Puppet::SSL::Host.new(hostname) } + let(:hostname) { "foobar" } + + it "should sign the certificate request if one is waiting" do + subject.generate(hostname, options) + + subject.sign(hostname, options) + + host.certificate_request.should be_nil + host.certificate.should be_a(Puppet::SSL::Certificate) + host.state.should == 'signed' + end + + it "should fail if there is no waiting certificate request" do + expect do + subject.sign(hostname, options) + end.to raise_error(ArgumentError, /Could not find certificate request for #{hostname}/) + end + + describe "when ca_location is local" do + describe "when the request has dns alt names" do + before :each do + subject.generate(hostname, options.merge(:dns_alt_names => 'some,alt,names')) + end + + it "should refuse to sign the request if allow_dns_alt_names is not set" do + expect do + subject.sign(hostname, options) + end.to raise_error(Puppet::SSL::CertificateAuthority::CertificateSigningError, + /CSR contained subject alternative names (.*), which are disallowed./) + + host.state.should == 'requested' + end + + it "should sign the request if allow_dns_alt_names is set" do + expect do + subject.sign(hostname, options.merge(:allow_dns_alt_names => true)) + end.not_to raise_error + + host.state.should == 'signed' + end + end + + describe "when the request has no dns alt names" do + before :each do + subject.generate(hostname, options) + end + + it "should sign the request if allow_dns_alt_names is set" do + expect { subject.sign(hostname, options.merge(:allow_dns_alt_names => true)) }.not_to raise_error + + host.state.should == 'signed' + end + + it "should sign the request if allow_dns_alt_names is not set" do + expect { subject.sign(hostname, options) }.not_to raise_error + + host.state.should == 'signed' + end + end + end + + describe "when ca_location is remote" do + let(:options) { {:ca_location => :remote} } + it "should fail if allow-dns-alt-names is specified" do + expect do + subject.sign(hostname, options.merge(:allow_dns_alt_names => true)) + end + end + end + end end