diff --git a/lib/puppet/type/java_ks.rb b/lib/puppet/type/java_ks.rb index 366b9f7..7935eaf 100644 --- a/lib/puppet/type/java_ks.rb +++ b/lib/puppet/type/java_ks.rb @@ -1,243 +1,243 @@ # frozen_string_literal: true Puppet::Type.newtype(:java_ks) do @doc = 'Manages the entries in a java keystore, and uses composite namevars to accomplish the same alias spread across multiple target keystores.' ensurable do desc 'Has three states: present, absent, and latest. Latest will compare the on disk SHA1 fingerprint of the certificate to that in keytool to determine if insync? returns true or false. We redefine insync? for this parameter to accomplish this.' newvalue(:present) do provider.create end newvalue(:absent) do provider.destroy end newvalue(:latest) do if provider.exists? provider.update else provider.create end end def insync?(is) @should.each do |should| case should when :present return true if is == :present when :absent return true if is == :absent when :latest unless is == :absent current = provider.current.split('/') latest = provider.latest.split('/') return true if current.to_set.subset?(latest.to_set) end end end false end defaultto :present end newparam(:name) do desc 'The alias that is used to identify the entry in the keystore. This will be converted to lowercase.' isnamevar munge do |value| value.downcase end end newparam(:target) do desc 'Destination file for the keystore. This will autorequire the parent directory of the file.' isnamevar end newparam(:certificate) do desc 'A server certificate, followed by zero or more intermediate certificate authorities. All certificates will be placed in the keystore. This will autorequire the specified file.' isrequired end newparam(:storetype) do desc 'Optional storetype Valid options: , , ' newvalues(:jceks, :pkcs12, :jks) end newparam(:private_key) do desc 'If you want an application to be a server and encrypt traffic, you will need a private key. Private key entries in a keystore must be accompanied by a signed certificate for the keytool provider. This will autorequire the specified file.' end newparam(:private_key_type) do desc 'The type of the private key. Usually the private key is of type RSA key but it can also be an Elliptic Curve key (EC) or DSA. Valid options: , , . Defaults to ' newvalues(:rsa, :dsa, :ec) defaultto :rsa end newparam(:chain) do desc 'The intermediate certificate authorities, if they are to be taken from a file separate from the server certificate. This will autorequire the specified file.' end - newparam(:password) do + newproperty(:password) do desc 'The password used to protect the keystore. If private keys are subsequently also protected this password will be used to attempt unlocking. Must be six or more characters in length. Cannot be used together with :password_file, but you must pass at least one of these parameters.' munge do |value| value = value.unwrap if value.respond_to?(:unwrap) super(value) end validate do |value| value = value.unwrap if value.respond_to?(:unwrap) raise Puppet::Error, "password is #{value.length} characters long; must be 6 characters or greater in length" if value.length < 6 end end newparam(:password_file) do desc 'The path to a file containing the password used to protect the keystore. This cannot be used together with :password, but you must pass at least one of these parameters.' end newparam(:password_fail_reset) do desc "If the supplied password does not succeed in unlocking the keystore file, then delete the keystore file and create a new one. Default: false." newvalues(:true, :false) defaultto :false end newparam(:destkeypass) do desc 'The password used to protect the key in keystore.' munge do |value| value = value.unwrap if value.respond_to?(:unwrap) super(value) end validate do |value| value = value.unwrap if value.respond_to?(:unwrap) raise Puppet::Error, "destkeypass is #{value.length} characters long; must be of length 6 or greater" if value.length < 6 end end newparam(:trustcacerts) do desc "Certificate authorities aren't by default trusted so if you are adding a CA you need to set this to true. Defaults to :false." newvalues(:true, :false) defaultto :false end newparam(:path) do desc "The search path used for command (keytool, openssl) execution. Paths can be specified as an array or as a '#{File::PATH_SEPARATOR}' separated list." # Support both arrays and colon-separated fields. def value=(*values) @value = values.flatten.map { |val| val.split(File::PATH_SEPARATOR) }.flatten end end newparam(:keytool_timeout) do desc 'Timeout for the keytool command in seconds.' defaultto 120 end newparam(:source_password) do munge do |value| value = value.unwrap if value.respond_to?(:unwrap) super(value) end desc 'The source keystore password' end newparam(:source_alias) do desc 'The source certificate alias' end # Where we setup autorequires. autorequire(:file) do auto_requires = [] [:private_key, :certificate, :chain].each do |param| if @parameters.include?(param) auto_requires << @parameters[param].value end end if @parameters.include?(:target) auto_requires << ::File.dirname(@parameters[:target].value) end auto_requires end # Our title_patterns method for mapping titles to namevars for supporting # composite namevars. def self.title_patterns [ [ %r{^([^:]+)$}, [ [:name], ], ], [ %r{^(.*):([a-z]:(/|\\).*)$}i, [ [:name], [:target], ], ], [ %r{^(.*):(.*)$}, [ [:name], [:target], ], ], ] end validate do if value(:password) && value(:password_file) raise Puppet::Error, "You must pass either 'password' or 'password_file', not both." end unless value(:password) || value(:password_file) raise Puppet::Error, "You must pass one of 'password' or 'password_file'." end if value(:storetype) == :pkcs12 && value(:source_password).nil? fail "You must provide 'source_password' when using a 'pkcs12' storetype." # rubocop:disable Style/SignalException : Associated test fails if 'raise' is used end end end diff --git a/pdk.yaml b/pdk.yaml new file mode 100644 index 0000000..4bef4bd --- /dev/null +++ b/pdk.yaml @@ -0,0 +1,2 @@ +--- +ignore: [] diff --git a/spec/unit/puppet/type/java_ks_spec.rb b/spec/unit/puppet/type/java_ks_spec.rb index b8568ee..da52dba 100644 --- a/spec/unit/puppet/type/java_ks_spec.rb +++ b/spec/unit/puppet/type/java_ks_spec.rb @@ -1,252 +1,252 @@ # frozen_string_literal: true require 'spec_helper' describe Puppet::Type.type(:java_ks) do let(:temp_dir) do if Puppet.features.microsoft_windows? ENV['TEMP'] else '/tmp/' end end let(:provider_var) { class_double('provider', class: described_class.defaultprovider, clear: nil) } let(:app_example_com) do { title: "app.example.com:#{temp_dir}application.jks", name: 'app.example.com', target: "#{temp_dir}application.jks", password: 'puppet', destkeypass: 'keypass', certificate: "#{temp_dir}app.example.com.pem", private_key: "#{temp_dir}private/app.example.com.pem", private_key_type: 'rsa', storetype: 'jceks', provider: :keytool, } end let(:jks_resource) do app_example_com end before(:each) do allow(described_class.defaultprovider).to receive(:new).and_return(provider_var) end it 'defaults to being present' do expect(described_class.new(app_example_com)[:ensure]).to eq(:present) end describe 'when validating attributes' do - [:name, :target, :private_key, :private_key_type, :certificate, :password, :password_file, :trustcacerts, :destkeypass, :password_fail_reset, :source_password].each do |param| + [:name, :target, :private_key, :private_key_type, :certificate, :password_file, :trustcacerts, :destkeypass, :password_fail_reset, :source_password].each do |param| it "has a #{param} parameter" do expect(described_class.attrtype(param)).to eq(:param) end end - [:ensure].each do |prop| + [:ensure, :password].each do |prop| it "has a #{prop} property" do expect(described_class.attrtype(prop)).to eq(:property) end end end describe 'when validating attribute values' do [:present, :absent, :latest].each do |value| it "supports #{value} as a value to ensure" do described_class.new(jks_resource.merge(ensure: value)) end end it 'first half of title should map to name parameter' do jks = jks_resource.dup jks.delete(:name) expect(described_class.new(jks)[:name]).to eq(jks_resource[:name]) end it 'second half of title should map to target parameter when no target is supplied' do jks = jks_resource.dup jks.delete(:target) expect(described_class.new(jks)[:target]).to eq(jks_resource[:target]) end it 'second half of title should not map to target parameter when target is supplied #not to equal' do jks = jks_resource.dup jks[:target] = "#{temp_dir}some_other_app.jks" expect(described_class.new(jks)[:target]).not_to eq(jks_resource[:target]) end it 'second half of title should not map to target parameter when target is supplied #to equal' do jks = jks_resource.dup jks[:target] = "#{temp_dir}some_other_app.jks" expect(described_class.new(jks)[:target]).to eq("#{temp_dir}some_other_app.jks") end it 'title components should map to namevar parameters #name' do jks = jks_resource.dup jks.delete(:name) jks.delete(:target) expect(described_class.new(jks)[:name]).to eq(jks_resource[:name]) end it 'title components should map to namevar parameters #target' do jks = jks_resource.dup jks.delete(:name) jks.delete(:target) expect(described_class.new(jks)[:target]).to eq(jks_resource[:target]) end it 'downcases :name values' do jks = jks_resource.dup jks[:name] = 'APP.EXAMPLE.COM' expect(described_class.new(jks)[:name]).to eq(jks_resource[:name]) end it 'has :false value to :trustcacerts when parameter not provided' do expect(described_class.new(jks_resource)[:trustcacerts]).to eq(:false) end it 'has :rsa as the default value for :private_key_type' do expect(described_class.new(jks_resource)[:private_key_type]).to eq(:rsa) end it 'fails if :private_key_type is neither :rsa nor :ec nor :dsa' do jks = jks_resource.dup jks[:private_key_type] = 'nosuchkeytype' expect { described_class.new(jks) }.to raise_error(Puppet::Error) end it 'fails if both :password and :password_file are provided' do jks = jks_resource.dup jks[:password_file] = '/path/to/password_file' expect { described_class.new(jks) }.to raise_error(Puppet::Error, %r{You must pass either}) end it 'fails if neither :password or :password_file is provided' do jks = jks_resource.dup jks.delete(:password) expect { described_class.new(jks) }.to raise_error(Puppet::Error, %r{You must pass one of}) end it 'fails if :password is fewer than 6 characters' do jks = jks_resource.dup jks[:password] = 'aoeui' expect { described_class.new(jks) }.to raise_error(Puppet::Error, %r{6 characters}) end it 'fails if :destkeypass is fewer than 6 characters' do jks = jks_resource.dup jks[:destkeypass] = 'aoeui' expect { described_class.new(jks) }.to raise_error(Puppet::Error, %r{length 6}) end it 'has :false value to :password_fail_reset when parameter not provided' do expect(described_class.new(jks_resource)[:password_fail_reset]).to eq(:false) end it 'fails if :source_password is not provided for pkcs12 :storetype' do jks = jks_resource.dup jks[:storetype] = 'pkcs12' expect { described_class.new(jks) }.to raise_error(Puppet::Error, %r{You must provide 'source_password' when using a 'pkcs12' storetype}) end end describe 'when ensure is set to latest' do it 'insync? should return false if sha1 fingerprints do not match and state is :present' do jks = jks_resource.dup jks[:ensure] = :latest allow(provider_var).to receive(:latest).and_return('9B:8B:23:4C:6A:9A:08:F6:4E:B6:01:23:EA:5A:E7:8F:6A') allow(provider_var).to receive(:current).and_return('21:46:45:65:57:50:FE:2D:DA:7C:C8:57:D2:33:3A:B0:A6') expect(described_class.new(jks).property(:ensure)).not_to be_insync(:present) end it 'insync? should return false if state is :absent' do jks = jks_resource.dup jks[:ensure] = :latest expect(described_class.new(jks).property(:ensure)).not_to be_insync(:absent) end it 'insync? should return true if sha1 fingerprints match and state is :present' do jks = jks_resource.dup jks[:ensure] = :latest allow(provider_var).to receive(:latest).and_return('66:9B:8B:23:4C:6A:9A:08:F6:4E:B6:01:23:EA:5A') allow(provider_var).to receive(:current).and_return('66:9B:8B:23:4C:6A:9A:08:F6:4E:B6:01:23:EA:5A') expect(described_class.new(jks).property(:ensure)).to be_insync(:present) end it 'insync? should return true if subset of sha1 fingerprints match and state is :present' do jks = jks_resource.dup jks[:ensure] = :latest allow(provider_var).to receive(:latest).and_return('9B:8B:23:4C:6A:9A:08:F6:4E:B6:01:23:EA:5A:E7:8F:6A/66:9B:8B:23:4C:6A:9A:08:F6:4E:B6:01:23:EA:5A') allow(provider_var).to receive(:current).and_return('66:9B:8B:23:4C:6A:9A:08:F6:4E:B6:01:23:EA:5A') expect(described_class.new(jks).property(:ensure)).to be_insync(:present) end end describe 'when file resources are in the catalog' do let(:file_provider) { class_double('provider', class: Puppet::Type.type(:file).defaultprovider, clear: nil) } before(:each) do allow(Puppet::Type.type(:file).defaultprovider).to receive(:new).and_return(file_provider) end [:private_key, :certificate].each do |file| it "autorequires for #{file} #file" do test_jks = described_class.new(jks_resource) test_file = Puppet::Type.type(:file).new(title: jks_resource[file]) Puppet::Resource::Catalog.new :testing do |conf| [test_jks, test_file].each { |resource| conf.add_resource resource } end rel = test_jks.autorequire[0] expect(rel.source.ref).to eq(test_file.ref) end it "autorequires for #{file} #jks" do test_jks = described_class.new(jks_resource) test_file = Puppet::Type.type(:file).new(title: jks_resource[file]) Puppet::Resource::Catalog.new :testing do |conf| [test_jks, test_file].each { |resource| conf.add_resource resource } end rel = test_jks.autorequire[0] expect(rel.target.ref).to eq(test_jks.ref) end end it 'autorequires for the :target directory #file' do test_jks = described_class.new(jks_resource) test_file = Puppet::Type.type(:file).new(title: ::File.dirname(jks_resource[:target])) Puppet::Resource::Catalog.new :testing do |conf| [test_jks, test_file].each { |resource| conf.add_resource resource } end rel = test_jks.autorequire[0] expect(rel.source.ref).to eq(test_file.ref) end it 'autorequires for the :target directory #jks' do test_jks = described_class.new(jks_resource) test_file = Puppet::Type.type(:file).new(title: ::File.dirname(jks_resource[:target])) Puppet::Resource::Catalog.new :testing do |conf| [test_jks, test_file].each { |resource| conf.add_resource resource } end rel = test_jks.autorequire[0] expect(rel.target.ref).to eq(test_jks.ref) end end end