]> gitweb.fluxo.info Git - puppet-stdlib.git/commitdiff
(MODULES-1737) Add pw_hash() function
authorEli Young <elyscape@gmail.com>
Tue, 3 Feb 2015 00:41:38 +0000 (16:41 -0800)
committerEli Young <elyscape@gmail.com>
Mon, 6 Apr 2015 00:21:17 +0000 (17:21 -0700)
README.markdown
lib/puppet/parser/functions/pw_hash.rb [new file with mode: 0644]
spec/acceptance/pw_hash_spec.rb [new file with mode: 0644]
spec/functions/pw_hash_spec.rb [new file with mode: 0644]

index 293a81aca0335552dd90bb686b92845c5f76cc90..d6be689bcb5e41dabc9c7f1d6283e6a64e20c3de 100644 (file)
@@ -500,6 +500,24 @@ Calling the class or definition from outside the current module will fail. For e
 
   *Type*: statement
 
+#### `pw_hash`
+
+Hashes a password using the crypt function. Provides a hash usable on most POSIX systems.
+
+The first argument to this function is the password to hash. If it is undef or an empty string, this function returns undef.
+
+The second argument to this function is which type of hash to use. It will be converted into the appropriate crypt(3) hash specifier. Valid hash types are:
+
+|Hash type            |Specifier|
+|---------------------|---------|
+|MD5                  |1        |
+|SHA-256              |5        |
+|SHA-512 (recommended)|6        |
+
+The third argument to this function is the salt to use.
+
+Note: this uses the Puppet Master's implementation of crypt(3). If your environment contains several different operating systems, ensure that they are compatible before using this function.
+
 #### `range`
 
 When given range in the form of '(start, stop)', `range` extrapolates a range as an array. For example, `range("0", "9")` returns [0,1,2,3,4,5,6,7,8,9]. Zero-padded strings are converted to integers automatically, so `range("00", "09")` returns [0,1,2,3,4,5,6,7,8,9].
diff --git a/lib/puppet/parser/functions/pw_hash.rb b/lib/puppet/parser/functions/pw_hash.rb
new file mode 100644 (file)
index 0000000..ad3e393
--- /dev/null
@@ -0,0 +1,56 @@
+Puppet::Parser::Functions::newfunction(
+  :pw_hash,
+  :type => :rvalue,
+  :arity => 3,
+  :doc => "Hashes a password using the crypt function. Provides a hash
+  usable on most POSIX systems.
+
+  The first argument to this function is the password to hash. If it is
+  undef or an empty string, this function returns undef.
+
+  The second argument to this function is which type of hash to use. It
+  will be converted into the appropriate crypt(3) hash specifier. Valid
+  hash types are:
+
+  |Hash type            |Specifier|
+  |---------------------|---------|
+  |MD5                  |1        |
+  |SHA-256              |5        |
+  |SHA-512 (recommended)|6        |
+
+  The third argument to this function is the salt to use.
+
+  Note: this uses the Puppet Master's implementation of crypt(3). If your
+  environment contains several different operating systems, ensure that they
+  are compatible before using this function.") do |args|
+    raise ArgumentError, "pw_hash(): wrong number of arguments (#{args.size} for 3)" if args.size != 3
+    raise ArgumentError, "pw_hash(): first argument must be a string" unless args[0].is_a? String or args[0].nil?
+    raise ArgumentError, "pw_hash(): second argument must be a string" unless args[1].is_a? String
+    hashes = { 'md5'     => '1',
+               'sha-256' => '5',
+               'sha-512' => '6' }
+    hash_type = hashes[args[1].downcase]
+    raise ArgumentError, "pw_hash(): #{args[1]} is not a valid hash type" if hash_type.nil?
+    raise ArgumentError, "pw_hash(): third argument must be a string" unless args[2].is_a? String
+    raise ArgumentError, "pw_hash(): third argument must not be empty" if args[2].empty?
+    raise ArgumentError, "pw_hash(): characters in salt must be in the set [a-zA-Z0-9./]" unless args[2].match(/\A[a-zA-Z0-9.\/]+\z/)
+
+    password = args[0]
+    return nil if password.nil? or password.empty?
+
+    # handle weak implementations of String#crypt
+    if 'test'.crypt('$1$1') != '$1$1$Bp8CU9Oujr9SSEw53WV6G.'
+      # JRuby < 1.7.17
+      if RUBY_PLATFORM == 'java'
+        # override String#crypt for password variable
+        def password.crypt(salt)
+          # puppetserver bundles Apache Commons Codec
+          org.apache.commons.codec.digest.Crypt.crypt(self.to_java_bytes, salt)
+        end
+      else
+        # MS Windows and other systems that don't support enhanced salts
+        raise Puppet::ParseError, 'system does not support enhanced salts'
+      end
+    end
+    password.crypt("$#{hash_type}$#{args[2]}")
+end
diff --git a/spec/acceptance/pw_hash_spec.rb b/spec/acceptance/pw_hash_spec.rb
new file mode 100644 (file)
index 0000000..4768975
--- /dev/null
@@ -0,0 +1,34 @@
+#! /usr/bin/env ruby -S rspec
+require 'spec_helper_acceptance'
+
+# Windows and OS X do not have useful implementations of crypt(3)
+describe 'pw_hash function', :unless => (UNSUPPORTED_PLATFORMS + ['windows', 'Darwin']).include?(fact('operatingsystem')) do
+  describe 'success' do
+    it 'hashes passwords' do
+      pp = <<-EOS
+      $o = pw_hash('password', 6, 'salt')
+      notice(inline_template('pw_hash is <%= @o.inspect %>'))
+      EOS
+
+      apply_manifest(pp, :catch_failures => true) do |r|
+        expect(r.stdout).to match(/pw_hash is "\$6\$salt\$IxDD3jeSOb5eB1CX5LBsqZFVkJdido3OUILO5Ifz5iwMuTS4XMS130MTSuDDl3aCI6WouIL9AjRbLCelDCy\.g\."/)
+      end
+    end
+
+    it 'returns nil if no password is provided' do
+      pp = <<-EOS
+      $o = pw_hash('', 6, 'salt')
+      notice(inline_template('pw_hash is <%= @o.inspect %>'))
+      EOS
+
+      apply_manifest(pp, :catch_failures => true) do |r|
+        expect(r.stdout).to match(/pw_hash is ""/)
+      end
+    end
+  end
+  describe 'failure' do
+    it 'handles less than three arguments'
+    it 'handles more than three arguments'
+    it 'handles non strings'
+  end
+end
diff --git a/spec/functions/pw_hash_spec.rb b/spec/functions/pw_hash_spec.rb
new file mode 100644 (file)
index 0000000..01a1105
--- /dev/null
@@ -0,0 +1,87 @@
+#! /usr/bin/env ruby -S rspec
+require 'spec_helper'
+
+describe "the pw_hash function" do
+  let(:scope) { PuppetlabsSpec::PuppetInternals.scope }
+
+  it "should exist" do
+    expect(Puppet::Parser::Functions.function("pw_hash")).to eq("function_pw_hash")
+  end
+
+  it "should raise an ArgumentError if there are less than 3 arguments" do
+    expect { scope.function_pw_hash([]) }.to( raise_error(ArgumentError, /[Ww]rong number of arguments/) )
+    expect { scope.function_pw_hash(['password']) }.to( raise_error(ArgumentError, /[Ww]rong number of arguments/) )
+    expect { scope.function_pw_hash(['password', 'sha-512']) }.to( raise_error(ArgumentError, /[Ww]rong number of arguments/) )
+  end
+
+  it "should raise an ArgumentError if there are more than 3 arguments" do
+    expect { scope.function_pw_hash(['password', 'sha-512', 'salt', 5]) }.to( raise_error(ArgumentError, /[Ww]rong number of arguments/) )
+  end
+
+  it "should raise an ArgumentError if the first argument is not a string" do
+    expect { scope.function_pw_hash([['password'], 'sha-512', 'salt']) }.to( raise_error(ArgumentError, /first argument must be a string/) )
+    # in Puppet 3, numbers are passed as strings, so we can't test that
+  end
+
+  it "should return nil if the first argument is empty" do
+    expect(scope.function_pw_hash(['', 'sha-512', 'salt'])).to eq(nil)
+  end
+
+  it "should return nil if the first argument is undef" do
+    expect(scope.function_pw_hash([nil, 'sha-512', 'salt'])).to eq(nil)
+  end
+
+  it "should raise an ArgumentError if the second argument is an invalid hash type" do
+    expect { scope.function_pw_hash(['', 'invalid', 'salt']) }.to( raise_error(ArgumentError, /not a valid hash type/) )
+  end
+
+  it "should raise an ArgumentError if the second argument is not a string" do
+    expect { scope.function_pw_hash(['', [], 'salt']) }.to( raise_error(ArgumentError, /second argument must be a string/) )
+  end
+
+  it "should raise an ArgumentError if the third argument is not a string" do
+    expect { scope.function_pw_hash(['password', 'sha-512', ['salt']]) }.to( raise_error(ArgumentError, /third argument must be a string/) )
+    # in Puppet 3, numbers are passed as strings, so we can't test that
+  end
+
+  it "should raise an ArgumentError if the third argument is empty" do
+    expect { scope.function_pw_hash(['password', 'sha-512', '']) }.to( raise_error(ArgumentError, /third argument must not be empty/) )
+  end
+
+  it "should raise an ArgumentError if the third argument has invalid characters" do
+    expect { scope.function_pw_hash(['password', 'sha-512', '%']) }.to( raise_error(ArgumentError, /characters in salt must be in the set/) )
+  end
+
+  it "should fail on platforms with weak implementations of String#crypt" do
+    String.any_instance.expects(:crypt).with('$1$1').returns('$1SoNol0Ye6Xk')
+    expect { scope.function_pw_hash(['password', 'sha-512', 'salt']) }.to( raise_error(Puppet::ParseError, /system does not support enhanced salts/) )
+  end
+
+  it "should return a hashed password" do
+    result = scope.function_pw_hash(['password', 'sha-512', 'salt'])
+    expect(result).to eql('$6$salt$IxDD3jeSOb5eB1CX5LBsqZFVkJdido3OUILO5Ifz5iwMuTS4XMS130MTSuDDl3aCI6WouIL9AjRbLCelDCy.g.')
+  end
+
+  it "should use the specified salt" do
+    result = scope.function_pw_hash(['password', 'sha-512', 'salt'])
+    expect(result).to match('salt')
+  end
+
+  it "should use the specified hash type" do
+    resultmd5 = scope.function_pw_hash(['password', 'md5', 'salt'])
+    resultsha256 = scope.function_pw_hash(['password', 'sha-256', 'salt'])
+    resultsha512 = scope.function_pw_hash(['password', 'sha-512', 'salt'])
+
+    expect(resultmd5).to eql('$1$salt$qJH7.N4xYta3aEG/dfqo/0')
+    expect(resultsha256).to eql('$5$salt$Gcm6FsVtF/Qa77ZKD.iwsJlCVPY0XSMgLJL0Hnww/c1')
+    expect(resultsha512).to eql('$6$salt$IxDD3jeSOb5eB1CX5LBsqZFVkJdido3OUILO5Ifz5iwMuTS4XMS130MTSuDDl3aCI6WouIL9AjRbLCelDCy.g.')
+  end
+
+  it "should generate a valid hash" do
+    password_hash = scope.function_pw_hash(['password', 'sha-512', 'salt'])
+
+    hash_parts = password_hash.match(%r{\A\$(.*)\$([a-zA-Z0-9./]+)\$([a-zA-Z0-9./]+)\z})
+
+    expect(hash_parts).not_to eql(nil)
+  end
+end