(MODULES-1737) Add pw_hash() function

This commit is contained in:
Eli Young 2015-02-02 16:41:38 -08:00
parent c297bd80e3
commit 23be4020dd
4 changed files with 195 additions and 0 deletions

View 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].

View file

@ -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

View file

@ -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

View file

@ -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