diff --git a/lib/puppet/face/certificate.rb b/lib/puppet/face/certificate.rb index 8019b6bea..97ab80484 100644 --- a/lib/puppet/face/certificate.rb +++ b/lib/puppet/face/certificate.rb @@ -1,119 +1,130 @@ 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) - host.generate_certificate_request - host.certificate_request.class.indirection.save(host.certificate_request) + + # 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)) 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 when_invoked do |name, options| host = Puppet::SSL::Host.new(name) host.desired_state = 'signed' Puppet::SSL::Host.indirection.save(host) 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 9291d7523..745f96b59 100755 --- a/spec/unit/face/certificate_spec.rb +++ b/spec/unit/face/certificate_spec.rb @@ -1,35 +1,116 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/face' require 'puppet/ssl/host' describe Puppet::Face[:certificate, '0.0.1'] do 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) 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 end